なぜブログを移行したか
以前からブログは運用していた。でも、書く体験に不満があった。自分のドメインで、自分の好きなように、もっとシンプルに書きたかった。MDXのファイルを1つ置けば記事になる。それだけの仕組みが欲しかった。
今回、ブログ基盤をNext.js 16 + MDX + Tailwind CSS v4でゼロから作り直した。「書くことに集中する」ためのインフラとして再設計し、記事を書く体験を最大限シンプルに保ちつつ、読む側にとっては高速で美しいページを提供する。そのバランスを技術でどう実現したかを書く。
ディレクトリ構成
.
├── app/ # Next.js App Router
│ └── [locale]/
│ └── blog/
│ └── [slug]/
│ ├── page.tsx # Blog post page (RSC)
│ └── opengraph-image.tsx # OG image (build-time)
├── content/blog/ # Blog posts (MDX files)
│ └── hello-world/
│ ├── ja.mdx # Japanese version
│ └── en.mdx # English version
├── components/blog/ # Blog-specific components
├── lib/
│ ├── blog.ts # Post metadata, pagination
│ ├── mdx.ts # MDX compilation pipeline
│ └── image-upload.ts # Image optimization pipeline
└── public/images/blog/ # Local image fallback (dev)ポイントはcontent/blog/ディレクトリ。各記事はslug名/のディレクトリにja.mdxとen.mdxを置くだけ。ファイルシステムがそのままCMSになる。データベースはない。
MDXという選択
MDXはMarkdownにJSXを埋め込める形式だ。frontmatterにメタデータを書き、本文をMarkdownで書く。それだけで記事が完成する。
title: "ブログを移行しました"
description: "ブログ基盤を作り直した話"
date: "2026-03-16"
tags: ["blog", "nextjs"]
published: truepublished: falseにすれば下書き状態。デプロイしても公開されない。シンプルなフラグ1つでコントロールできる。
コードブロックはShikiでビルド時にハイライトされる。ランタイムでJavaScriptは一切動かない。ライトモードとダークモードの両テーマを事前生成する。
const prettyCodeOptions: PrettyCodeOptions = {
theme: {
dark: "github-dark-dimmed",
light: "github-light",
},
keepBackground: false,
};数式もKaTeXで対応している。
画像パイプライン: コピペだけで完結する
ブログを書くとき、画像の扱いは最も面倒な作業の一つだ。スクリーンショットを撮る → リサイズする → WebPに変換する → 適切なディレクトリに配置する → Markdownにパスを書く。この手順を全て自動化した。
エディタのテキストエリアに画像をCtrl+Vでペーストするだけ。あとは自動で以下が実行される:
- クリップボードから画像ファイルを抽出
- プレースホルダー(
![uploading...]())をカーソル位置に挿入 - Sharpで最適化(WebP変換、最大幅1920px、品質75%)
- Cloudflare R2にアップロード(開発環境はローカル保存)
- プレースホルダーを実際のMarkdown画像構文に置換
export async function optimizeImage(buffer: Buffer) {
const image = sharp(buffer);
const metadata = await image.metadata();
const needsResize = metadata.width !== undefined && metadata.width > 1920;
const pipeline = needsResize ? image.resize({ width: 1920 }) : image;
return pipeline
.webp({ quality: 75, effort: 4 })
.toBuffer({ resolveWithObject: true });
}セキュリティ面では、MIMEタイプのチェックだけでなくSharpでマジックバイトを検証し、偽装ファイルを弾く。ファイルサイズは3MBに制限。アップロードは並列2件まで。
書き手が意識するのは「Ctrl+V」だけ。それ以外は全てシステムが処理する。
SSG: ゼロランタイムの静的サイト
このブログは完全な静的サイトだ。generateStaticParamsで全記事のページをビルド時に生成する。
export async function generateStaticParams() {
const slugs = getPostSlugs();
const params: { locale: string; slug: string }[] = [];
for (const slug of slugs) {
for (const locale of ["ja", "en"]) {
if (fs.existsSync(`content/blog/${slug}/${locale}.mdx`)) {
params.push({ locale, slug });
}
}
}
return params;
}MDXのコンパイル、Shikiによるシンタックスハイライト、KaTeXの数式レンダリング—すべてがビルド時に完了する。ユーザーのブラウザではReact Server Componentsで生成された静的HTMLが表示されるだけ。
ISR(Incremental Static Regeneration)は使っていない。記事を更新したらデプロイする。それでいい。シンプルさが正義だ。
OGP画像の自動生成
SNSでシェアされたときのOG画像も、全記事に対してビルド時に自動生成される。Next.jsのImageResponse(Satoriベース)を使う。
export default async function OGImage({ params }) {
const { locale, slug } = await params;
const post = getPost(slug, locale as Locale);
return new ImageResponse(
<div style={{
background: "linear-gradient(135deg, #0f172a, #1e293b)",
// title, description, site name, date...
}}>
{post.meta.title}
</div>,
{ width: 1200, height: 630 }
);
}記事のタイトル・説明文・日付がフロントマターから読み取られ、1200x630pxのPNG画像として出力される。Figmaでデザインする必要はない。記事を書けばOGPも勝手にできる。
加えて、TechArticle・BreadcrumbList・FAQの構造化データ(JSON-LD)も自動出力される。SEOのためにやることは、フロントマターを丁寧に書くことだけ。
デザインシステム
色やフォントを個別に指定することは禁じている。全てCSS変数とTailwind CSS v4のユーティリティクラスで統一する。
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--primary: oklch(0.546 0.245 262.881);
--muted: oklch(0.97 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
}フォントはNoto Sans JP(日本語)とInter(欧文)の組み合わせ。日本語の組版にはword-break: auto-phraseとline-break: strictを適用し、自然な改行を実現している。
ダークモード対応はCSSカスタムプロパティの切り替えだけ。コンポーネント側でダークモードを意識するコードは一行も書いていない。
多言語対応(i18n)
next-intlで日本語と英語の2言語に対応した。記事ごとにja.mdxとen.mdxを配置するだけで、URLは自動的に/ja/blog/slugと/en/blog/slugになる。
サイトマップにはhreflang属性が自動付与され、Google検索に各言語の存在を通知する。canonicalURLも言語ごとに適切に設定される。
まとめ
今回の移行で実現したかったのは「書くことへの摩擦をゼロにする」ことだ。
- MDXファイルを1つ書けば記事になる
- 画像はコピペするだけで最適化・配信される
- OGPもSEOもビルド時に自動生成
- デザインはテーマ変数で一貫管理
- 多言語はファイルを追加するだけ
以前のブログで感じていた不満を全て解消できた。技術は手段であって目的ではない。でも、適切な技術選定が「書く」という行為の障壁を限りなくゼロに近づけてくれる。移行して良かったと思えるブログ基盤ができた。