@shinyaz

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
PWASerwist
テストVitest + Playwright
デプロイVercel

Velite を選んだ理由

最大の判断はコンテンツパイプラインだった。MDX + Next.js なら Contentlayer が定番だが、実質的にメンテナンスが止まっている。Velite は同様の開発体験 — 型安全なコンテンツコレクション、ビルド時処理 — を提供しつつ、活発にメンテナンスされており柔軟性も高い。

パイプラインはシンプルだ。MDX ファイルをプリビルドステップで Velite に通し、型付き JSON を Next.js が直接インポートする。

Output
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-intlreact-i18next は意図的に避けた。固定の UI 文字列しかない静的ブログでは、メリットに対して複雑さが大きすぎる。代わりに、すべての翻訳を単一の TypeScript 辞書に集約している。

src/lib/i18n.ts
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)でテーマに自動追従する。

example.ts
function greet(name: string): string {
  return `Hello, ${name}! Welcome to @shinyaz.`;
}
 
console.log(greet("World"));

コンテンツ表現力

MDX + Shiki + KaTeX のスタックにより、リッチなコンテンツがそのまま書ける。

複数言語のシンタックスハイライトとファイル名表示:

fibonacci.py
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 記法の数式も完全対応。インライン: E=mc2E = mc^2。ブロック:

ex2dx=π\int_{-\infty}^{\infty} e^{-x^2} dx = \sqrt{\pi} x=b±b24ac2ax = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}

各記事にはスクロールスパイ付きの目次が自動生成され、タイトル・説明・カテゴリ・タグを横断する AND 検索対応のクライアントサイド検索も搭載している。

見つけてもらうために: SEO・フィード・PWA

個人ブログでも、見つけてもらう仕組みは思った以上に重要だ。いくつかの領域に投資した。

SEO — JSON-LD 構造化データ(WebSiteBlogPostingBreadcrumbList)、Open Graph / Twitter Card メタタグ、翻訳ペアの hreflang / canonical マークアップ、動的サイトマップ。いずれも定番だが、バイリンガルサイトで正しく実装するにはロケール処理に細心の注意が必要だった。

RSS / Atom フィード — ロケールごとに RSS 2.0 と Atom 1.0 を自動生成(各最新20件)。RSS はブログをフォローする最良の手段だと今でも思っているので、これは必須だった。

PWASerwist がサービスワーカーを提供し、ビルド時プリキャッシュとランタイムキャッシュ戦略で動作する。オフラインフォールバックページにより、接続がなくてもブログはグレースフルに動作する。

セキュリティヘッダー — 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 テストが、リグレッションを本番到達前に捕捉してくれる。

共有する

田原 慎也

田原 慎也

ソリューションアーキテクト @ AWS

AWS ソリューションアーキテクトとして金融業界のお客様を中心に技術支援をしており、クラウドアーキテクチャや AI/ML に関する学びをこのサイトで発信しています。このサイトの内容は個人の見解であり、所属企業の公式な意見や見解を代表するものではありません。

関連記事