いま読んでいるこのブログは、自作の静的サイトジェネレーター (SSG) がビルドしています。Markdown で書いた記事を HTML へ変換し、public/ に吐き出すだけのツールです。同じことは Hugo や Zola でもできますが、あえて Rust で一から作りました。この記事では「なぜ作ったか」と「どう作ったか」を紹介します。
SSG とは何か
SSG (Static Site Generator) は、Markdown などのソースから HTML・CSS・JS の静的ファイル一式を生成するツールです。閲覧時にサーバーで処理する必要がなく、生成済みのファイルをそのまま配信するだけ。だから速く、安く、壊れにくい。WordPress のような動的サイトと違い、データベースもアプリケーションサーバーもいりません。
このブログも、ビルドで吐いた public/ の中身を S3 + CloudFront に置いているだけです (その手順は「S3 + CloudFront で自作 SSG ブログをデプロイする」にまとめました)。
なぜ既製の SSG ではなく自作したのか
実用性だけを見れば、Hugo や Zola を使うほうが賢い選択です。それでも自作したのは、理由が 3 つあります。
- 学びの場が欲しかった。このブログ自体が学習アウトプットの場です。Rust とクリーンアーキテクチャを「動くものを作りながら」身につける題材として、SSG はちょうどいい大きさでした。仕様が枯れていて、ゴール (静的ファイルを吐く) が明確だからです。
- 中身を全部把握していたかった。既製ツールはテンプレート記法もビルドの流れもブラックボックスになりがちです。自作なら、Markdown が HTML になるまでの全工程を自分で説明できます。
- 欲しい機能を自分の手で足せる。後述するマガジン (連載) や自動サムネイル生成のような「ちょっと特殊な機能」を、設定ファイルの制約に悩まず素直に実装できます。
逆に言えば、ただブログを公開したいだけなら自作は割に合いません。ここでは 作る過程そのものが目的 です。
全体像: 4 レイヤのクリーンアーキテクチャ
このSSGは、クリーンアーキテクチャに沿って Cargo workspace を 4 つのクレートに分けています。クリーンアーキテクチャとは、ビジネスの中心ロジックを外側の都合 (ファイル形式・ライブラリ・I/O) から切り離す設計方針です。鍵は 依存の向きを「外側 → 内側」の一方向に固定する ことにあります。
cli ──→ application ──→ domain
↑
infrastructure
| クレート | 役割 | 外部依存 |
|---|---|---|
domain |
記事・タグ・マガジンなどのエンティティと値オブジェクト | なし |
application |
ユースケースとポート (trait で定義する抽象) | domain のみ |
infrastructure |
ポートの具体実装 (Markdown 変換・テンプレート・I/O・検索・フィード) | 各種ライブラリ |
cli |
コマンドの受け口 (バイナリ ntea) |
すべて束ねる |
ポイントは domain が 外部ライブラリを一切知らない ことです。「記事とは何か」「slug とは何か」という概念だけが置かれ、Markdown ライブラリが comrak だろうと別物だろうと、ここは影響を受けません。Markdown を HTML に変換する処理は application が ポート (trait) として「こういう入出力の変換器が欲しい」とだけ宣言し、その実体を infrastructure が差し込みます。
この向きを守ると、ライブラリの差し替えが内側に波及しません。infrastructure を丸ごと別実装に置き換えても、domain と application は無傷です。設計の詳しい話は別記事に譲りますが、SSG はこの考え方の練習台として素直でした。
技術スタック
infrastructure で使っている主なライブラリです。標準ライブラリに近い、小さく枯れたものを選んでいます。
| 用途 | クレート |
|---|---|
| Markdown → HTML (GFM 対応) | comrak |
| HTML テンプレート (型安全) | askama |
| 構文ハイライト | syntect |
| プレビュー用 HTTP サーバー | tiny_http |
| CLI 引数のパース | clap |
| フロントマター (YAML) のパース | serde_yaml |
askama はテンプレートを コンパイル時に Rust の型として検査 してくれるのが効きます。テンプレートの変数名を間違えれば cargo build が落ちるので、壊れた HTML を吐く前に気づけます。
ビルドの流れ
ntea build を実行すると、おおまかに次の順で処理が進みます。
content/posts/<category>/<slug>.mdを走査し、フロントマター (YAML) と本文に分解する。- 本文の Markdown を comrak で HTML に変換し、コードブロックを syntect でハイライトする。
- タグ・カテゴリ・マガジン・関連記事・ページネーションといった一覧構造を組み立てる。
- Askama テンプレートに流し込んで各ページの HTML を生成する。
- 検索インデックス (
search-index.json)・RSS (feed.xml)・サムネイル画像を生成する。 - すべてを
public/に書き出す。assets/はそのままコピー。
cargo run -p cli -- build # public/ を生成 (draft は除外)
cargo run -p cli -- serve # 下書き込みでビルドしてローカル配信
カテゴリはディレクトリ名がそのままキーになります。content/posts/programming/foo.md なら、カテゴリは programming、公開 URL は /programming/foo/。フロントマターにカテゴリを書かせない (置き場所で決まる) のは、設定の重複を避けるための割り切りです。
実装した機能
「紹介と振り返り」なので、現時点でできることを並べておきます。
- Markdown 変換 (GFM)・フロントマター解析
- カテゴリ・タグ別の一覧 (ページネーション付き)
- マガジン — 読む順番が決まった連載リスト
- クライアントサイド全文検索 (生成した JSON を引く)
- 構文ハイライト・ダークモード切替・一覧のリスト/カード切替
- RSS フィード・おすすめ記事 (関連度順)・人気記事 (手動キュレーション)
- サムネイル省略時に決定的な SVG を自動生成
- 下書き制御 (
draft: trueは本番ビルドから除外)
なかでもマガジンと自動サムネイル生成は、既製ツールだとプラグインや設定で消耗しがちな部分です。自作なら magazines.toml を読んで前後ナビを差し込むだけ、と素直に書けました。
作って分かったこと
- 小さく枯れた題材は設計の練習に向く。SSG はゴールが明確で副作用が少ないので、依存方向やポート/アダプタの理屈を「動かしながら」確かめられました。
- 型がドキュメントになる。Askama のコンパイル時検査のように、Rust の型システムが「壊れた状態」を事前に弾いてくれる場面が多く、テンプレートやフロントマターの取り違えで悩む時間が減りました。
- 自作は手段であって近道ではない。公開するだけなら既製ツールが速い。それでも、中身を全部説明できる道具を一つ持っておく価値はありました。
まとめ
- このブログは Rust 製の自作 SSG が生成している。実用面では Hugo / Zola で十分だが、学びと把握とカスタマイズを目的に一から作った。
- 設計はクリーンアーキテクチャの 4 レイヤ構成で、依存方向を「外側 → 内側」に固定し、
domainを外部ライブラリから切り離している。 - comrak・Askama・syntect・tiny_http など小さく枯れたライブラリを組み合わせ、Markdown から
public/一式を吐くまでを自前で握っている。