第1章:なぜ作り直したのか
「動いているものを触るな」というのは、プログラミングの世界では半ば格言のように言われ続けている。
それでも私は、18年以上運営してきたゴルフブログのフロントエンドを全部作り直すことにした。
きっかけは小さな不満の積み重ねだった。
このブログはGDOブログからスタートし、容量オーバーを機にレンタルサーバへ移行してMovable Typeを導入。記事数が増えて処理が重くなったところで2014年にWordPressに乗り換え、スキン(テーマ)もいろいろ変えながら運営してきた。デザインの素人なりに工夫を重ね、最終的に「比較的軽い」と評判のCocoonに落ち着いた。
Cocoonは優秀だと思う。設定だけでシンプルながら見栄えのするサイトが作れる。でも「それなり」の壁を超えようとした瞬間に壁にぶち当たる。コース紹介ページを作ろうとしたらテーマの構造に縛られてレイアウトが思い通りにならない。コメント欄を変えようとしたらCSSと格闘する羽目になる。月別アーカイブのUIを改善しようとしたらプラグインの独自仕様が邪魔をする。
UIを本気で改善するなら、Webプログラムの素人である私がそれなりに時間をかけるしかない。ただ、いつか素人でも過去の資産を無駄にせず自由にUIを構築できる仕組みができるはずだ——そう思いながら、ずっと待っていた。
その「いつか」が来たと気づいたのは、3週間ほど前にClaude Codeを使い始めたときだ。
「WordPressで入力したデータをAIに解釈させて、新たに静的なページとして作れば、デザインも自由自在になるし、速度もアップするのでは?」
これがいわゆる「ヘッドレスCMS」と呼ばれる構成だ。Claude Codeに相談すると、即座に以下の流れを提案してきた。
WordPressで書く → REST APIで渡す → Next.jsで表示する
書く道具はWordPressのまま。読者が見る画面だけ、Next.jsで全部作り直す。
実質3日間でほぼ置き換えることができた。細かいバグやUIの調整はこれから続くが、希望していた動作はほぼ実装されている。
結果から先に言うと、モバイルのPageSpeed InsightsのスコアはWordPress時代の71から98に上がった。LCPは5.3秒(赤判定)から2秒と大幅改善し、デスクトップは満点の100になった。
SEOスコアが下がっているのは、移行期間中に検索エンジンに誤ってインデックスされないよう「noindex」を設定していたためで、本番切り替え後に解除済み。


ただし、そこに至るまでにはサーバーのセキュリティ設定によるブロック、通信エラー、プラグインの独自仕様、ドメイン切り替えの落とし穴など、想定外の問題がいくつも待ち構えていた。
その作業を通じて気づいたことがある。AIに頼りながら、AIを100%信用しない。間違いに気づいてディレクションする。 それが今の人間の役割なのだと。
その作業を記録して記事にすることにした。
第2章:ヘッドレスCMSとは何か
まず「ヘッドレスCMS」という言葉を整理しておく。
CMSとは Content Management System の略で、WordPressはその代表格だ。記事を書いて、画像をアップロードして、公開する。その仕組み全体がCMSである。
通常のWordPressは「書く機能」と「見せる機能」が一体になっている。テーマが見た目を担当し、サーバー上でHTMLを生成して、ブラウザに届ける。シンプルで強力な仕組みだが、表示の自由度はテーマの範囲内に限られる。
ヘッドレスCMSは、この2つを分離する。
【従来のWordPress】
WordPress(書く+見せる)→ ブラウザ
【ヘッドレス構成】
WordPress(書くだけ)→ データ配信 → Next.js(見せるだけ)→ ブラウザ
「ヘッドレス(headless)」とは、頭がない、つまり表示部分を持たないという意味だ。WordPressはデータを保管・管理するだけに徹し、見た目の構築は別のフレームワークが担う。
この構成の何がいいか。
書く作業は変わらない。 WordPress管理画面から記事を投稿する手順はこれまでと同じだ。プラグインも使える。カテゴリもタグも運用はそのまま。
見せる部分は完全に自由になる。
Next.jsはReactベースのフレームワークで、HTMLとCSSとJavaScriptを自分で書く。テーマの制約はゼロ。思い通りのレイアウトを、思い通りのコードで実現できる。
パフォーマンスが劇的に上がる。 Next.jsはビルド時に静的なHTMLをあらかじめ生成しておく。WordPressのようにアクセスのたびにデータベースへ問い合わせる必要がない。加えてVercelは世界中に配信拠点を持っており、日本から近い場所からコンテンツを届けてくれる。物理的に遠いサーバーでも表示が速い理由はこれだ。
この構成はいま急速に広まっている。WordPress公式がNext.js用のスターターキットを提供しているほど、組み合わせとしては定番になりつつある。
私の場合、具体的な構成はこうなった。
| 役割 | 使ったもの |
|---|---|
| CMS(記事管理) | WordPress(レンタルサーバー ConoHa WING) |
| フロントエンド | Next.js 15 + TypeScript + Tailwind CSS |
| ホスティング | Vercel |
| 翻訳 | Claude API(Anthropic) |
ConoHaはレンタルサーバーサービスで、もともとWordPressを動かしていた場所だ。今回の構成でも引き続きWordPressのデータを保管する場所として使う。新しく追加したのがVercelで、Next.jsのアプリをここで動かす。
WordPressとNext.jsの連携には「WordPress REST API」を使う。WordPressには標準で記事データをJSON形式で返すAPIが付いており、Next.jsはそれを呼び出して記事を取得・表示する。
次章からは、この構成を実際に構築していく中で直面した問題を順番に紹介していく。最初の壁は、レンタルサーバーのセキュリティ機能だった。
第3章:最初の壁―WAFとURLの罠
実装を始めてすぐ、最初の壁にぶつかった。
ローカル(自分のPC)でNext.jsを起動して確認すると記事が取得できるのに、Vercelにデプロイすると「記事の取得に失敗しました」というエラーが出る。
原因はConoHaのWAF(Web Application Firewall)だった。
WAFとはサーバーへの不正アクセスを防ぐセキュリティ機能だ。ConoHaのWAFはデフォルトで「海外からのWordPress REST APIアクセスをブロック」する設定になっていた。自分のPCからアクセスするときは日本のIPなので問題ないが、Vercelのサーバーは米国バージニアにある。海外IPからのAPIアクセスとして遮断されていたのだ。
対処は.htaccessに1行追加するだけだった。
SetEnvIf Request_URI ".*" AllowForeignRestApi
これでWAFがREST APIへの海外アクセスを許可するようになった。
第4章:ContentViewsプラグインとの格闘
データ取得の問題を解決すると、今度は記事の中身が正しく表示されないケースが出てきた。
原因はWordPressのプラグイン「ContentViews」だった。旧サイトでは「ContentViews」を多用していた。記事一覧をグリッドやリスト形式でおしゃれに表示できるプラグインで、多くの記事のなかに埋め込んでいた。
問題は、このプラグインが独自の記法(ショートコード)で記事一覧を生成していることだ。
[pt_view id="abc123"]
このような記法がWordPress側の記事本文に含まれているのだが、Next.jsでそのまま表示しても何も起きない。WordPress側でHTMLに変換されたものを取得しても、JavaScript依存のコンポーネントが動かない。
解決策は「WordPress側にカスタムAPIを作り、Next.js側で呼び出してレンダリングする」というものだった。
WordPress側のfunctions.phpに新しいAPIエンドポイントgolf-bk/v1/view/{id}を追加し、「このビューIDに対応する記事一覧を返す」機能を実装した。Next.js側はショートコードのIDを検出したときにこのAPIを叩き、返ってきたデータを独自のUIでレンダリングする。
さらに技術的にハマったのが「ネストしたDIV構造の置換」だ。
ContentViewsのHTMLには<div id="pt-cv-view-abc123">...</div>というコンテナが含まれており、その中に複雑な入れ子のDIVが何層も続く。このコンテナ全体を独自のグリッドUIに置き換えたかったのだが、正規表現では入れ子の終端を正確に見つけられない。
解決策は「スタック」を使うアルゴリズムだった。<divが現れるたびにカウントを増やし、</div>が現れるたびに減らし、0になった時点が対応する閉じタグだと特定できる。シンプルだが確実な方法だ。
第5章:コメント機能とCORSの罠
コメント投稿機能を実装したとき、またしても見えない壁にぶつかった。
フォームから送信するとブラウザのコンソールに「CORS error」や「401 Unauthorized」が表示されてコメントが投稿できない。
CORS(Cross-Origin Resource Sharing)は、異なるドメイン間のリクエストを制限するブラウザのセキュリティ機能だ。golf-bk.com(フロントエンド)からwp.golf-bk.com(WordPress)に直接POSTリクエストを送ると、異なるドメイン間の通信としてブロックされる。
解決策は「Next.js側にプロキシを作る」ことだ。
【NG】ブラウザ → WordPress(セキュリティでブロック)
【OK】ブラウザ → Next.js(中継) → WordPress
Next.js側に中継用の受け口を作り、フォームのデータをいったんここで受け取ってからサーバー側でWordPressに転送する。サーバー同士の通信にはブラウザのセキュリティ制約がかからないため、これで解決できた。
第6章:Claude APIで4か国語に自動翻訳する
Next.jsで作り直すにあたって、せっかくなら以前はできなかった機能も追加したかった。その一つが多言語対応だ。
ゴルフの記事は海外の読者にも需要がある。当サイトは海外からのアクセスも一定数ある。ブラウザの翻訳機能などで内容を理解してもらっているのだと思うが、最初から多言語化できているほうがいいなと思っていた。以前もいろいろとチャレンジしたが、PC向けブラウザは問題なかったがiOSのSafariの問題で完全に多言語化することはできてなかった。そこで翻訳もClaude APIを使った自動翻訳すればよいんじゃないかと思い付いた。
URLで言語を切り替える設計
対応言語は英語・中国語・スペイン語・フランス語の4言語。URLの先頭に言語コードを付けるシンプルな設計にした。
/posts/179100 → 日本語(オリジナル)
/en/posts/179100 → 英語
/zh/posts/179100 → 中国語
/es/posts/179100 → スペイン語
/fr/posts/179100 → フランス語
翻訳コストをほぼゼロにするキャッシュ設計
自動翻訳で最大の懸念はコストだ。記事数が多いブログでアクセスのたびに翻訳APIを呼んでいたら、あっという間に費用が膨らむ。
解決策は「一度翻訳したら永続的に保存する」設計だ。同じ記事・同じ言語の翻訳は2回目以降APIを呼ばない。記事を更新した場合だけ、専用の操作でキャッシュを削除して再翻訳する。翻訳APIの呼び出し回数を最小限に抑えながら多言語対応を実現できた。
HTMLを壊さずに翻訳する
翻訳で工夫したのは、記事本文がHTMLであることだ。リンクや画像タグが含まれた状態で翻訳させると、タグの中身まで翻訳されてリンクが壊れることがある。
AIに「翻訳して」と頼むだけだと、タグの中の属性まで翻訳してしまったり、タグ構造が崩れたりすることがある。
そこでAIへの指示に「HTMLタグの属性値は翻訳しない」「タグ構造を変えない」という制約を明示した。実際の指示文は日本語より英語の方が精度が上がるため英語で記述している。
Rules:
- Preserve ALL HTML tags, attributes, class names, and URLs exactly as-is
- Only translate the visible text content
- Do not add any explanation or markdown code blocks
- Return only the translated HTML
しかし、API経由でAIを使い翻訳をすると時間がかかる。10秒以上かかるので一番最初にアクセスしたユーザーはかなり待つ必要があるので翻訳速度は今後の課題である。また中国語、スペイン語、フランス語も対応完了したが、無駄にAPIトークンを消費させたくないので現状は英語のみ対応にしておいた。
第7章:WordPressのHTMLをレスポンシブ対応させる
Next.jsでWordPressの記事を表示すると、PCでは問題ないのにスマートフォンで崩れる箇所が複数出てきた。原因はいずれも「WordPressが生成するHTML」と「Next.jsのCSS」の間の衝突だった。
画像が画面からはみ出す問題
WordPressが出力するHTMLには、width="533"のような幅を固定する属性が画像に付いている。
<img src="..." width="533" height="400">
CSSでmax-width: 100%を指定しても、HTMLの属性指定のほうが優先されてしまい、スマートフォンの画面幅を無視して固定サイズで表示される。
CSSだけでは解決できないため、サーバーサイドでHTMLを書き換えた。記事本文を表示する前にすべての<img>タグからwidthとheight属性を取り除き、CSSでmax-width: 100%; height: autoを付与する処理を追加した。
テーブルのスクロールが死ぬ問題
記事本文を囲む要素にoverflow-x: hiddenを設定していたところ、その内側にあるテーブルが横スクロールできなくなった。横に長いテーブルがはみ出した状態で固定されてしまう。
overflow-x: hiddenを内側の要素ではなく外側の要素に移動することで解決した。
ダークモードで文字が消える問題
Next.jsはデフォルトでダークモード(画面を暗くする表示設定)のCSSが有効になっているが、WordPressの記事本文はダークモードを考慮していない。夜間にスマートフォンでダークモードをONにすると、白背景に白文字になって記事が読めなくなった。
記事コンテンツ部分はWordPressから来たHTMLなので制御できない。そのため、Next.js側のダークモードCSSを無効化し、常にライトモードで表示するように変更した。
Cocoonのブログカードが崩れる問題
記事内に埋め込んだ「ブログカード(他の記事へのリンクをカード形式で表示するもの)」がスマートフォンで崩れていた。カードの左端に200px固定のサムネイル画像があり、残りのテキスト幅が数pxしかないという状態だ。
WordPressのHTMLには手を加えたくないため、CSSのみで解決した。スマートフォンではサムネイルを120pxに縮小し、カード下部の「ドメイン名・日付」表示はposition: absoluteを使ってカード全幅の2行目に移動した。HTMLに触れずレイアウトだけ変える典型的なCSSテクニックもAIが実行してくれた。
これらのスマートフォン対応に共通していたのは「WordPressが出力するHTMLはNext.jsの想定外」という事実だ。長年の運用で積み重なった様々な指定が混在している。それをいかに壊さずに整えるか、AIに相談しながら解決していった。
第8章:本番切り替えで起きたこと
新サイトをVercelにnew.golf-bk.comとして公開し、動作確認が取れたところで本番への切り替えを行った。
手順は「DNSのAレコードをVercelのIPアドレスに変更する」だ。ConoHaのDNS管理画面で、golf-bk.comのAレコードの向け先をVercelが指定するIPアドレスに変更した。
変更前にDNSのTTL(設定が世界中に伝わる時間)を300秒に短縮しておいた。これにより変更が短時間で反映されるようになり、問題があったときに素早く元に戻せる状態に切り替え作業中だけ設定した。
「DEPLOYMENT_NOT_FOUND」エラー
DNS変更を行ったところ、golf-bk.comにアクセスすると「DEPLOYMENT_NOT_FOUND」というエラーが表示された。
これは私自身が作業前に懸念していた問題だった。「golf-bk.comをVercelプロジェクトに登録する必要があるのでは?」と確認したのだが、Claude Codeは「DNS変更だけで問題ない」と答えた。しかし実際には登録作業が必要で、それを実行していなかった。
Vercelでドメインを使うには2つの作業が必要だ。
- DNSのAレコードをVercelのIPアドレスに向ける
- Vercelのプロジェクトにそのドメインを登録する(
vercel domains add golf-bk.com)
1だけやっても、Vercelはそのドメインへのリクエストをどのプロジェクトに向ければいいかわからないため、エラーになる。2を実行してはじめて紐付けが完成する。
コマンドを実行してドメインを登録したところ、エラーは解消された。
WordPressの内部リンク問題
切り替え後、記事内のリンクの一部が旧サイトに飛んでしまう問題も発生した。WordPressの記事本文の中にある内部リンクが、まだWordPressのURL(https://golf-bk.com/wordpress/2023/07/...形式)になっていたのだ。
これはNext.js側でHTMLを書き換えることで対処した。記事を表示する前に本文HTML内のWordPressリンクをスキャンし、Next.jsのルート(/wp-pages/...形式)に書き換える処理を追加した。
英語ページの内部リンク問題
さらに、英語翻訳ページ(/en/posts/...)の記事内リンクが、言語プレフィックスなしの/posts/...に飛ぶ問題も見つかった。英語ページから内部リンクをクリックすると日本語ページに遷移してしまう。
これも同様にHTMLを書き換えて対処した。翻訳ページ表示時に/posts/を/en/posts/に、/wp-pages/を/en/wp-pages/に書き換える処理を追加した。
旧URLへのリダイレクト
以前golf-bk.com/wordpressでアクセスしていた読者のために、このURLにアクセスしたときに新しいトップページへリダイレクトする設定も追加した。
第9章:作り直してどうなったか
本番切り替え後にパフォーマンスを計測した。
PageSpeed Insights(Googleの計測ツール)
| 指標 | 新サイト(モバイル) | 旧サイト(モバイル) | 新サイト(デスクトップ) | 旧サイト(デスクトップ) |
|---|---|---|---|---|
| パフォーマンス | 95 | 71 | 100 | 90 |
| ユーザー補助 | 96 | 89 | 95 | 89 |
| おすすめの方法 | 100 | 92 | 100 | 95 |
| LCP | 2秒 | 5.3秒(赤) | 0.9秒 | 1.0秒 |
モバイルのパフォーマンススコアが71から95に改善した。LCPは5.3秒(赤判定)から1秒と大幅改善し、デスクトップは満点100点になった。
WebPageTest(より詳細な計測ツール)
| 指標 | 旧サイト US | 新サイト US | 旧サイト Tokyo | 新サイト Tokyo |
|---|---|---|---|---|
| FCP(最初の表示) | 3.198秒 | 1.18秒 | 2.489秒 | 0.84秒 |
| LCP(主要コンテンツ表示) | 3.321秒 | 1.787秒 | 2.795秒 | 1.216秒 |
| TTFB(サーバー応答速度) | 2.16秒 | 0.31秒 | 1.77秒 | 0.35秒 |
| ページ容量 | 2MB | 611KB | 1MB | 610KB |
| リクエスト数 | 129件 | 67件 | 96件 | 67件 |
FCPは米国から接続で-63%、東京から接続で-66%改善した。ページ容量は米国から接続で約70%削減された。旧サイトにあったJavaScriptの処理待ち時間はゼロになった。
面白いのはTokyoのTTFBが0.35秒という数値だ。新サイトのサーバーは米国バージニアにあるにもかかわらず、日本のサーバーを使っている旧サイト(1.77秒)より速い。これはVercelのエッジキャッシュが東京のノードからデータを配信できているためだと思う。
第10章:AIと作業して気づいたこと
3日間で18年物のWordPressブログをNext.jsに作り直した。パフォーマンスは劇的に改善し、やりたかったUIも自由に実装できるようになった。技術的な目標はほぼ達成できたと言っていい。
ただ、この作業を通じて一番印象に残ったのは、技術的な話ではなかった。
AIは迷走する
サイトの本番移行が完了した後、日本語の投稿が公開されたら自動で英語の翻訳ページも生成する仕組みを実装しようとした。
Claude Codeは複雑な実装を2回試みたが、どちらも動かなかった。するとAIはエラーの原因をさらに深く分析し始め、技術的な説明を延々と繰り広げた。解決策は見えないまま、議論だけが深みにはまっていった。
そこで私が言ったのはこれだけだ。
「ページ公開後に、普通に英語ページのURLにアクセスすればいいのでは?」
翻訳は「英語ページに初めてアクセスされたとき」に実行される仕組みがすでにある。ならばWordPressの投稿完了のタイミングで、そのURLに内部リクエストを投げるだけでいい。1行のコードで解決した。
AIは「技術的に正しい手段」を探しすぎる。人間は「目的」から逆算できる。
AIは「大丈夫」と答えがち
第8章で触れたドメイン登録の件がわかりやすい例だ。私が「Vercelへのドメイン登録が必要では?」と事前に確認したとき、AIは「DNS変更だけで問題ない」と答えた。実際には登録が必要で、問題が起きてから対処することになった。
AIは「一般的にはこうすれば大丈夫」という知識は持っている。しかし「今この環境が具体的にどういう状態か」は自分から確認しに行かない。だからAIが「大丈夫」と言っても、それは現状確認済みの保証ではなく、一般論としての推測にすぎない。
「確認してから進めて」と一言添えるだけで、この種のミスはかなり減る。
デザインは仕様で動く
もう一つ気づいたことがある。「シンプルにして」「見やすくして」といった指示では、AIは無難なアウトプットしか出さない。AIにセンスがないのではなく、仕様がないと動けないのだ。
参考にしたいサイトのURL、使いたい色、余白の感覚——これらを具体的に渡すとアウトプットの質は大きく変わる。自分のセンスを磨いて「指示の解像度」を上げるほうが早い、というのはわかっている。ただ怠け者なので、AIのセンスが上がるのを待つことにした。
そのためデザインはまだ納得のいくレベルではない。ただ「いつでも自由に変えられる土壌」は整った。それだけで十分だと思っている。
AIは便利なパートナー
上記の限界を踏まえても、AIとの共同作業は非常に効率的だった。TypeScriptのエラーを瞬時に読み解き、ライブラリのAPIを調べ、コードを書いてくれる。自分一人では1ヶ月以上かかったはずの作業が3日で終わった。
重要なのは「AIを使いこなす側のスキル」だ。何を作りたいか明確に伝える、確認すべきことを確認する、出てきた結果を自分で検証する。この基本的な姿勢があれば、AIは強力なツールになる。
WordPress + Next.js + VercelのヘッドレスCMS構成は、長年WordPressを使ってきてデザインに限界を感じている人に向いている。WordPressの資産を捨てずにフロントエンドだけを刷新できるのが最大のメリットだ。最初の壁(WAF、CORS、プラグイン依存)を越えるまでに手間はかかるが、越えてしまえば自由度は格段に上がる。


コメント