このサイトのブログ機能を作った。CMSは入れていない。content/blog/に.mdxファイルを置いて、ビルド時にNext.jsが読み込む。それだけの構成だ。
なぜCMSを使わなかったか
理由は単純で、記事を書く頻度がまだ読めないから。月に1本書くかもしれないし、しばらく何も書かないかもしれない。そのくらいの頻度でCMSの管理画面を維持するコストは見合わない。
ファイルさえ置けば記事になる。エディタで書いて、コミットして、デプロイする。この単純さを最初は優先したい。
static exportとMDXレンダリングの組み合わせ
このサイトはnext.config.tsでoutput: "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をクリップボードに書き込むだけの小さな実装にしている。ハイライトが後から入っても、この構造は変えずに済むはずだ。
まだ記事は少ないが、書く場所ができたのでここからは書くだけになった。