いま読んでいるこのブログは、自作の静的サイトジェネレーター (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 つあります。

  1. 学びの場が欲しかった。このブログ自体が学習アウトプットの場です。Rust とクリーンアーキテクチャを「動くものを作りながら」身につける題材として、SSG はちょうどいい大きさでした。仕様が枯れていて、ゴール (静的ファイルを吐く) が明確だからです。
  2. 中身を全部把握していたかった。既製ツールはテンプレート記法もビルドの流れもブラックボックスになりがちです。自作なら、Markdown が HTML になるまでの全工程を自分で説明できます。
  3. 欲しい機能を自分の手で足せる。後述するマガジン (連載) や自動サムネイル生成のような「ちょっと特殊な機能」を、設定ファイルの制約に悩まず素直に実装できます。

逆に言えば、ただブログを公開したいだけなら自作は割に合いません。ここでは 作る過程そのものが目的 です。

全体像: 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 を丸ごと別実装に置き換えても、domainapplication は無傷です。設計の詳しい話は別記事に譲りますが、SSG はこの考え方の練習台として素直でした。

技術スタック

infrastructure で使っている主なライブラリです。標準ライブラリに近い、小さく枯れたものを選んでいます。

用途 クレート
Markdown → HTML (GFM 対応) comrak
HTML テンプレート (型安全) askama
構文ハイライト syntect
プレビュー用 HTTP サーバー tiny_http
CLI 引数のパース clap
フロントマター (YAML) のパース serde_yaml

askama はテンプレートを コンパイル時に Rust の型として検査 してくれるのが効きます。テンプレートの変数名を間違えれば cargo build が落ちるので、壊れた HTML を吐く前に気づけます。

ビルドの流れ

ntea build を実行すると、おおまかに次の順で処理が進みます。

  1. content/posts/<category>/<slug>.md を走査し、フロントマター (YAML) と本文に分解する。
  2. 本文の Markdown を comrak で HTML に変換し、コードブロックを syntect でハイライトする。
  3. タグ・カテゴリ・マガジン・関連記事・ページネーションといった一覧構造を組み立てる。
  4. Askama テンプレートに流し込んで各ページの HTML を生成する。
  5. 検索インデックス (search-index.json)・RSS (feed.xml)・サムネイル画像を生成する。
  6. すべてを 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/ 一式を吐くまでを自前で握っている。