ターミナル風ブログをReact Router v7 + Cloudflare Pagesで構築した
React Router v7のプリレンダリング機能を使い、ターミナル風UIのブログをCloudflare Pagesにデプロイした。Tailwind CSS v4のprose互換問題やreact-markdown v10の破壊的変更への対処法も含む。
ターミナル風ブログをReact Router v7 + Cloudflare Pagesで構築した
動機
個人ブログのデザインをターミナルエディタ風にしたかった。GitHubのダークテーマをベースに、$ ls -la ./posts/ のようなプロンプト表示で記事一覧を表示し、コードブロックにはAtom One Darkテーマのシンタックスハイライトを入れる。
技術スタックはReact Router v7(SSR + プリレンダリング)、Tailwind CSS v4、Cloudflare Pages。
プリレンダリングで完全静的サイト化
React Router v7には prerender() 関数がある。ビルド時にMarkdownファイルを走査してすべてのスラッグを収集し、各ページのHTMLを事前生成する。
typescript// react-router.config.ts export default { ssr: true, async prerender() { const postsDir = join(process.cwd(), "content/posts"); const files = await getMarkdownFiles(postsDir); const slugs = await Promise.all( files.map(async (file) => { const raw = await readFile(file, "utf8"); const { data } = matter(raw); return data.slug || getSlugFromPath(file); }) ); return ["/", ...slugs.map((s) => `/${s}`)]; }, } satisfies Config;
これで build/client/ 以下に各記事のHTMLと .data ファイルが生成される。Cloudflare Pagesにはこのディレクトリをそのまま wrangler pages deploy するだけ。サーバーサイドランタイム不要。
Tailwind CSS v4でprose variantが効かない問題
Tailwind CSS v4 + @tailwindcss/typography の組み合わせで、prose variant クラスが効かなかった。
tsx// これが効かない <div className="prose prose-h2:text-[#58a6ff] prose-p:text-[#c9d1d9]">
v4では @plugin "@tailwindcss/typography" で読み込むが、prose variant の一部が機能しない。解決策はreact-markdownのカスタムコンポーネントで各HTML要素に直接クラスを適用すること。
tsxcomponents={{ h2({ children, node, ...props }) { return ( <h2 className="text-xl font-bold text-[#58a6ff] font-mono border-b border-[#21262d] pb-2 mt-12 mb-6" {...props} > {children} </h2> ); }, // h3, p, strong, em, table, td, th... すべて同様 }}
ポイントは node を分割代入で除外すること。{...props} でスプレッドすると node オブジェクトがDOM属性として node="[object Object]" と出力され、ハイドレーションエラーの原因になる。
react-markdown v10のインライン検出
react-markdown v10で inline propが削除された。バッククォートで囲んだインラインコード(`gh`など)がすべてブロックコードとしてレンダリングされる問題が起きた。
回避策はASTノードのプロパティで判別する。
tsxcode({ node, className, children, ...props }) { const match = /language-(\w+)/.exec(className || ""); const language = match ? match[1] : ""; const isInline = !node?.properties?.className && node?.position && !language; if (!isInline) { return <SyntaxHighlighter ... />; } return <code className="px-1.5 py-0.5 bg-[#161b22] text-[#f0883e] rounded text-sm" {...props}>{children}</code>; }
className がなく language もなければインラインコードとして扱う。
プリレンダリングサイトのクライアントサイドナビゲーション問題
React Router v7のプリレンダリングで生成した静的サイトをCloudflare Pagesにデプロイすると、直接URLアクセスは問題ないが、一覧ページからのリンククリック(クライアントサイドナビゲーション)でエラーが出た。
No routes matched location "/article-slug"
Failed to fetch manifest patches SyntaxError: Unexpected token '<'
原因は、クライアントサイドルーターが動的ルートパターン(:slug)を正しくマッチできないこと。.data ファイルのフェッチ時にCloudflare PagesがHTMLを返してJSONパースに失敗する。
解決策は Link コンポーネントに reloadDocument を指定して、フルページロードで遷移させる。
tsx<Link to={`/${post.slug}`} reloadDocument className="block group">
プリレンダリング済みHTMLを直接サーブするので、SPAルーティングは不要。ページ遷移のオーバーヘッドも体感上問題ない。
まとめ
React Router v7のプリレンダリングは、Markdownベースのブログに適している。ビルド時にNode.jsのfs APIでファイルを読み、静的HTMLを生成し、Cloudflare Pagesに置くだけで完結する。Tailwind CSS v4やreact-markdown v10の破壊的変更には注意が必要だが、カスタムコンポーネントレンダラーで対処できる。