gist みたいに、ディレクトリを1ページにした #

ある技術記事に対して解説を併記したり、実行可能なコードをそのまま埋め込んだりしたい場合、従来の「1ファイル=1ページ」という構造では限界を感じていました。そこで、ディレクトリ単位でページを生成する仕組みを検討し、今回実装に至りました(Issue #58)。

Gistが複数のファイルを1ページで表示するように、このブログでもディレクトリを1つのページとして扱えるようにしました。

設計のポイント #

posts/2026-10-22-bundle.md/ や docs/guide.md/ といった *.md ディレクトリを「バンドル」として扱い、posts/2026-10-22-bundle.md(ファイル)と同じURL(例: /posts/bundle、/docs/guide)でアクセスできるようにしています。少し違和感があるかもしれませんが、この設計にはいくつかの利点があります。

  • **URL空間の増加なし:** 既存の一覧表示、日付ナビゲーション、前後記事リンク、OGP設定がそのまま機能します。
  • **名前の衝突防止:** ファイルシステムの特性により、foo.md(ファイル)と foo.md(ディレクトリ)は共存できません。これにより「1URL=1ページ」が保証され、スラッグの一意性を個別に管理する必要がありません。
  • **明確な意図表明:** 当初は index.md を起点とする案も検討しましたが、ディレクトリ名自体が「これはバンドルである」という意思表示になるため、曖昧さが排除されます。

ページ構成は以下の通りです。

  • **メインコンテンツ:** README.md が最優先されます。存在しない場合は、ファイル名の辞書順で最初のファイルが使用されます。これがページの本文と、タイトル・概要・前後記事情報を提供します。
  • **サブコンテンツ:** 残りのファイルは、ファイル名の見出し付きセクションとして下部に追加されます。00- などの接頭辞で表示順を制御できます。
  • **非Markdownファイル:** .go などのファイルは <pre> ブロックでそのまま表示されます。
  • **ダウンロード機能:** 「Download .md」はメインコンテンツのみを対象とし、個別のファイルは <ページ>/<ファイル名> で直接取得できます(.md は text/markdown、その他は text/plain)。
  • **階層制限:** サブディレクトリはサポートせず、検出時にエラーとなります。

実際の動作は /posts/bundle-demo で確認できます。デモ用の posts/2026-10-22-bundle-demo.md/ がそのまま公開されています。

実装と検証 #

Go版とMoonBit版の両方に同様の変更を加えました。Go側では LoadPosts/LoadDocs がディレクトリ内のファイルを読み込み、ビューがメインコンテンツの後にサブセクションを追加。ルーターに /posts/{slug}/{member} と /docs/ の個別ファイル解決機能を追加しました。MoonBit側では embed.sh が *.md ディレクトリの中身も収集し、load_posts/load_docs が dir/name のペアをバンドルとして構築。ルーティングとビューはGo版と同様の構成です。

検証は従来通りの make compare 相当の手順で実施。GoサーバーとMoonBitのWasmをNode.jsのアダプター経由で起動し、バンドルページ、個別ファイルのraw取得、404エラー、既存ページを含む15パスを比較。結果、差分は0件でした。

今後の展望 #

本格的に「blog for ai」を構築し、それとは別に「blog for human」を立ち上げようかと考えています。

改めて気づくのは、自分が求める機能と一般的なニーズとの間にズレがあることです。例えば、Gistのように1記事に複数ファイルを配置できる機能は自分にとって高優先度ですが、画像表示やシンタックスハイライトはそれほど重要ではありません。逆に、Markdownのダウンロード機能やセクションIDの付与、前後記事リンクは重視しています。

今回のバンドル機能も、この延長線上にあります。読み物としての完成度よりも、テキストをシンプルに束ねて保存できることを優先しました。