5 月 5 日、夕方 17 時。1 本のテスト記事を書き上げた直後、私たちはまだ何の 公開インフラも持っていませんでした。
ブログを公開する CMS も、画像生成のパイプラインも、CTA を仕込む構造もない。それを「今日中に 20 記事 + 60 画像を本番公開できる状態にする」と決めました。
15 時間後の翌朝。あなたが今読んでいるこのブログそのものが、その夜の成果です。本記事では、Next.js × MDX × AI 協業で構築した自社ブログシステムの 設計・実装・つまづき を実装記録として残します。
完成したもの — 数字で並べる
抽象的な設計思想を語る前に、まず実物を数字で示します。
| 項目 | 値 |
|---|---|
| 公開記事数 | 20 本(5/5 公開 1 + 5/6〜5/24 予約 19) |
| 自動生成インフォグラフィック | 60 枚(カバー + 本文 2 枚 × 20 記事) |
| インフラ月額 | 0 円(Vercel 無料枠 + Resend 無料枠) |
| デプロイ手段 | vercel --prod 1 行 |
| 投稿手段 | MDX ファイルを置くだけ |
| 平均ページ表示速度(warm キャッシュ) | 49〜59ms |
| ローカル dev → 本番デプロイの差 | dev 11,640ms → 本番 49ms(約 240 倍 高速) |
技術スタック:
- Next.js 16(App Router、Webpack 強制 / Turbopack は日本語パスでパニック)
- MDX + gray-matter(frontmatter 解析、ファイルシステム読込)
- Satori + resvg-js(JSX → SVG → PNG のコード生成画像)
- Resend(送信メール)
- Vercel(ホスティング + CDN + SSL 自動発行)
- Claude Code(実装パートナー)
設計思想 — 「ファイルを書く」だけで終わる
ブログを運用するうえで、書く側と表示側の摩擦が一番のコストです。
WordPress なら GUI で書けますが、AI に書かせるには API 連携が必要。Notion CMS なら API は便利ですが、毎回同期するレイテンシがある。Headless CMS(microCMS / Strapi)も同じ問題があります。
結論として、src/content/insights/ ファイルを git に置くだけで公開される方式にしました。AI(Claude Code)に「このファイルを書け」と指示するだけで完結します。

💡 KEY TAKEAWAYS
ブログ CMS の選定で迷ったら、まず「誰がコンテンツを書くか」を決めてください。AI 主体ならファイルベース MDX、手動主体なら GUI 中心の Notion / WordPress。当方は AI 主体なので MDX 一択でした。
7 つの落とし穴 — 実装中に転んだ場所
ここからが本題です。15 時間で 20 記事 + 60 画像を本番化するまでに、7 箇所で立ち止まりました。それぞれ、転んだ理由と突破方法を共有します。
落とし穴 1: gray-matter の date は string ではなく Date オブジェクト
予約投稿(公開日まで非表示)を実装したとき、なぜか すべての記事が消える 現象が発生しました。
原因は frontmatter の date 解析。YAML で date: 2026-05-05 と書くと、gray-matter は JavaScript Date オブジェクト として返します。文字列化すると "Tue May 05 2026 09:00:00 GMT+0900" のようになり、"2026-05-05" との単純比較が壊れます。
``js`
// "Tue May 05..." > "2026-05-05" → 辞書順で true("T" > "2")
// → 全記事が「未来日付」扱いでフィルタされる
突破: 読み込み時に JST に揃えてから ISO YYYY-MM-DD に正規化するヘルパーを 1 つ追加。本番環境のタイムゾーンが UTC である件もここで吸収しました(Vercel は UTC、JST との 9 時間ズレで日付が 1 日ズレるのは別の罠)。
落とし穴 2: Reveal アニメーションで本文が永久に opacity 0
framer-motion の useInView を使ったフェードイン演出で、JS 失敗時や IO 不発火時に文字が完全に消える 問題が発生しました。
特に複合的な事故が起きたのは、本文中間に CTA バナーを差し込む機能を追加した瞬間。
それまで
の中身は子 div 1 つでしたが、CTA 挿入で前半 + バナー + 後半の 3 つの子 div に分割。Animator の document.querySelector(".article-body > div") は最初の 1 つしか取らないため、後半の文字が永久に opacity 0 のまま で表示されませんでした。
突破: 設計を反転しました。デフォルト opacity 1、JS が走った時だけ
.ab-pending クラスを付けてフェード演出を有効化。「壊れても文字は読める」を最優先に。
落とし穴 3: 画像 AI でインフォグラフィックは作れない
最初は Codex(画像生成 AI)でインフォグラフィックを作ろうとしました。プロンプトを 60 個書いて、画像 AI に投げる。
結果は壊滅的でした。
私が書いたプロンプトの中身(CSS 仕様・カラーコード・レイアウト指示)が、そのまま画像内のテキストとして配置されたのです。「
#ffffff / #c8c8d0」「縦長フローチャート的にステップ 01→02→03→04」という 人間向けの仕様書 を、AI が「画像に書き込む文字」と解釈しました。
根本問題は、画像 AI が「正確なテキスト・数字・レイアウト」が苦手という技術的限界です。マーケ用インフォグラフィックには相性が悪い。
突破: コード生成方式に切り替え。Satori(JSX → SVG)+ resvg-js(SVG → PNG) で 4 種類のテンプレ(cover / flow / grid / comparison)を実装。JSON で「データ + テンプレ種別」を指定すれば、コマンド一発で 60 枚を 30 秒で生成できる構造に。

コード生成インフォグラフィックの 4 テンプレート
落とし穴 4: Satori が calc() を解釈しない
JSX で書いた
width: "calc(33.333% - 12px)" が 無視される ことに気づいたのは、出力が縦 1 列に並んだ時です。
Satori は CSS の完全実装ではなく、サブセットのみ対応しています。
calc() はサポート外。background-clip: text、box-decoration-break、mask-image も非対応です。
突破: JS 側でピクセル計算してから渡す。
`ts
const totalW = 1040;
const gap = 16;
const cardW = Math.floor((totalW - gap * 2) / 3); // 3 列幅を実数で
`
事前に Satori の CSS サポート表を確認すべきでした。動作検証フェーズをテンプレ実装より先に置くのが教訓です。
落とし穴 5: ブロックエディタが server component で動かない
WordPress 風の編集 UI を求める声に応えて BlockNote を導入。1 行目で
window is not defined で SSR がクラッシュしました。
ブロックエディタ系(BlockNote / TipTap / Lexical)は クライアント専用 です。Next.js 16 では
ssr: false を server component から指定できないため、間にクライアントラッパーを挟む 必要があります。
`ts
// editor-loader.tsx (client)
"use client";
import dynamic from "next/dynamic";
export const Editor = dynamic(() => import("./editor"), { ssr: false });
// page.tsx (server) — server から Editor を import
import { Editor } from "./editor-loader";
`
Next.js 15 までと挙動が変わっている部分。AGENTS.md に「Next.js 16 は破壊的変更が入っているので、書く前に docs を読め」と書いておいたのが効きました。
落とし穴 6: markdown のテーブルが描画されない
「読みやすくするため表を入れて」と書いた MDX を本番で確認したら、
| col | がそのままテキスト として表示されていました。
私が書いた最小 markdown レンダラに テーブルパーサが実装されていなかった ためです。書く前に renderMdx の対応構文を確認すべきでした。
突破:
|---|---| の区切り行を検知して 