このサイトのブログ機能を作った。CMSは入れていない。content/blog/.mdxファイルを置いて、ビルド時にNext.jsが読み込む。それだけの構成だ。

なぜCMSを使わなかったか

理由は単純で、記事を書く頻度がまだ読めないから。月に1本書くかもしれないし、しばらく何も書かないかもしれない。そのくらいの頻度でCMSの管理画面を維持するコストは見合わない。

ファイルさえ置けば記事になる。エディタで書いて、コミットして、デプロイする。この単純さを最初は優先したい。

static exportとMDXレンダリングの組み合わせ

このサイトはnext.config.tsoutput: "export"を指定している。Cloudflare Pagesに静的ファイルとしてデプロイするためだ。ここで問題になるのが、MDXをどう読み込むか。

next-mdx-remoteにはApp Router向けのnext-mdx-remote/rscというエントリがある。これはServer Componentとしてビルド時に(正確にはgenerateStaticParamsで列挙された各ページのプリレンダリング時に)MDXをコンパイルしてHTMLに変換してくれる。動的なrevalidateやAPIルートには依存しないので、output: "export"と素直に共存する。

実装はfrontmatterのパースとMDX本体のレンダリングを分けた。frontmatterはgray-matterで取り出し、src/lib/posts.tsに集約している。本文は<MDXRemote source={post.content} />にそのまま渡すだけ。

const { data, content } = matter(rawMdxFile);
// data.title, data.publishedAt などがfrontmatter
// contentがMDX本文の生テキスト

draft記事の扱い

frontmatterにdraft: trueを書くと、一覧・詳細・トップページのどこにも出ない。仕組みはgetPublishedPosts()というヘルパーでdraftをフィルタしているだけなのだが、地味に重要なのは詳細ページのgenerateStaticParamsもdraftを除外している点だ。

一覧に出さないだけでは足りない。静的エクスポートである以上、ページが生成されてさえいなければURLを直接叩いてもアクセスできない。これが一番安全な「非公開」の形だと思う。

コピーボタンだけ自作した

シンタックスハイライトはまだ入れていない(Panda Syntaxの導入は別タスクにしてある)。ただしコードブロックのコピーボタンだけは先に作った。理由は単純で、技術記事を書く以上コピーできないコードブロックはそれだけでストレスになるからだ。

MDXのpre要素をClient Componentで差し替えて、textContentをクリップボードに書き込むだけの小さな実装にしている。ハイライトが後から入っても、この構造は変えずに済むはずだ。

まだ記事は少ないが、書く場所ができたのでここからは書くだけになった。