Next.js 16・Velite・MDX でバイリンガル技術ブログを構築する
目次
はじめに
開発者ブログの構成はどれも似通いがちだ。Next.js + Markdown、Contentlayer を入れて、シンタックスハイライトを足して完成 — そんなパターンが多い。shinyaz.com を作るにあたって、もう少し自分の意見を反映したものにしたかった。日本語と英語を対等に扱うバイリンガルブログで、CMS やデータベースなしの完全静的サイト、そして高速であること。
この記事では、主要なアーキテクチャ上の判断と、構築を通じて学んだことを紹介する。まずは技術スタックの全体像から。
| カテゴリ | 技術 |
|---|---|
| フレームワーク | Next.js 16 (App Router) |
| スタイリング | Tailwind CSS v4 |
| コンテンツ管理 | MDX + Velite |
| コードハイライト | Shiki + rehype-pretty-code |
| 数式レンダリング | remark-math + rehype-katex |
| ダークモード | next-themes |
| PWA | Serwist |
| テスト | Vitest + Playwright |
| デプロイ | Vercel |
Velite を選んだ理由
最大の判断はコンテンツパイプラインだった。MDX + Next.js なら Contentlayer が定番だが、実質的にメンテナンスが止まっている。Velite は同様の開発体験 — 型安全なコンテンツコレクション、ビルド時処理 — を提供しつつ、活発にメンテナンスされており柔軟性も高い。
パイプラインはシンプルだ。MDX ファイルをプリビルドステップで Velite に通し、型付き JSON を Next.js が直接インポートする。
content/posts/{en,ja}/*.mdx
→ Velite (prebuild)
→ .velite/ (静的データ)
→ Next.js (App Router)MDX の処理は3つのプラグインを連鎖させている。見出しアンカー用の rehype-slug、数式レンダリング用の remark-math + rehype-katex、そしてシンタックスハイライト用の rehype-pretty-code(Shiki、ライト/ダーク両対応)。この構成の良い点は、すべてがビルド時に解決されること — コンテンツレンダリングのためにクライアントサイド JavaScript は一切不要だ。
i18n ライブラリなしのバイリンガル対応
next-intl や react-i18next は意図的に避けた。固定の UI 文字列しかない静的ブログでは、メリットに対して複雑さが大きすぎる。代わりに、すべての翻訳を単一の TypeScript 辞書に集約している。
const dictionaries = {
ja: {
site: { name: "@shinyaz", description: "..." },
nav: { home: "ホーム", blog: "ブログ", ... },
// ...
},
en: {
site: { name: "@shinyaz", description: "..." },
nav: { home: "Home", blog: "Blog", ... },
// ...
},
};すべてのルートは /[locale]/ 配下に配置し、コンテンツファイルもディレクトリで分割(content/posts/ja/、content/posts/en/)。ヘッダーの言語切替ボタンで同じページのまま日英を切り替えられ、ロケールプレフィックスなしの URL はブラウザの Accept-Language ヘッダーに基づいて自動リダイレクトされる。
トレードオフは明白で、この方法は何十言語にもスケールしない。ただし2言語なら、シンプルで型安全、ランタイムコストもゼロだ。
ビジュアルデザイン: モノトーン・IBM Plex・ダークモード
デザインはコンテンツの邪魔をしないことを最優先にした。モノトーンの配色、十分な余白、そして IBM Plex フォントファミリー — 本文に Sans、コードに Mono、日本語に Sans JP を採用。日本語フォントはラテン文字版よりかなり大きいため、preload: false で遅延読み込みしている。
Tailwind CSS v4 の CSS ファーストな設定がテーミングをクリーンにしてくれた。カラートークンはすべて CSS カスタムプロパティとして定義し、:root と .dark で切り替える。next-themes がトグルを管理し、コードブロックも Shiki のデュアルテーマ(github-light / github-dark)でテーマに自動追従する。
function greet(name: string): string {
return `Hello, ${name}! Welcome to @shinyaz.`;
}
console.log(greet("World"));コンテンツ表現力
MDX + Shiki + KaTeX のスタックにより、リッチなコンテンツがそのまま書ける。
複数言語のシンタックスハイライトとファイル名表示:
def fibonacci(n: int) -> list[int]:
"""Generate Fibonacci sequence up to n terms."""
if n <= 0:
return []
fib = [0, 1]
for _ in range(2, n):
fib.append(fib[-1] + fib[-2])
return fib[:n]
print(fibonacci(10))LaTeX 記法の数式も完全対応。インライン: 。ブロック:
各記事にはスクロールスパイ付きの目次が自動生成され、タイトル・説明・カテゴリ・タグを横断する AND 検索対応のクライアントサイド検索も搭載している。
見つけてもらうために: SEO・フィード・PWA
個人ブログでも、見つけてもらう仕組みは思った以上に重要だ。いくつかの領域に投資した。
SEO — JSON-LD 構造化データ(WebSite、BlogPosting、BreadcrumbList)、Open Graph / Twitter Card メタタグ、翻訳ペアの hreflang / canonical マークアップ、動的サイトマップ。いずれも定番だが、バイリンガルサイトで正しく実装するにはロケール処理に細心の注意が必要だった。
RSS / Atom フィード — ロケールごとに RSS 2.0 と Atom 1.0 を自動生成(各最新20件)。RSS はブログをフォローする最良の手段だと今でも思っているので、これは必須だった。
PWA — Serwist がサービスワーカーを提供し、ビルド時プリキャッシュとランタイムキャッシュ戦略で動作する。オフラインフォールバックページにより、接続がなくてもブログはグレースフルに動作する。
セキュリティヘッダー — CSP、HSTS(max-age 2年)、X-Content-Type-Options、X-Frame-Options、制限的な Permissions-Policy をすべてのルートに適用している。
まとめ
ブログをゼロから構築するのは最も効率的な道ではないが、最も学びの多い道の一つだ。同じようなスタックを検討している人に向けて、いくつかポイントを挙げる。
- Velite は Contentlayer の有力な代替 — 活発にメンテナンスされ、柔軟で、型安全なコンテンツコレクションが App Router とうまく噛み合う。
- i18n ライブラリは必ずしも要らない — 少数言語の静的コンテンツなら、TypeScript 辞書のほうがシンプルでランタイムコストもゼロ。
- Tailwind CSS v4 の CSS ファーストはテーミングと相性が良い — CSS カスタムプロパティでテーマトークンを定義すれば、別途設定ファイルが不要になる。
- ビルド時処理への投資が効く — シンタックスハイライト、数式レンダリング、コンテンツクエリをビルド時に解決するほど、クライアントに送る JavaScript は減る。
- テストは初日から — Vitest のユニット/コンポーネントテストと Playwright の E2E テストが、リグレッションを本番到達前に捕捉してくれる。
