このサイトは長らく WordPress で動く技術ブログでした。それを先日、Next.js の静的サイトに作り直しました。57記事のURLは一本も変えていません。切り替えのダウンタイムは、DNSの浸透時間だけです。

同じことを考えている方の参考になるかもしれないので、何を考えて、どう作り、どこでつまずいたかを記録しておきます。

なぜ作り直したのか

理由はふたつあります。

ひとつは、ブログの役目が変わったことです。このサイトはもともと「WordPress の使い方」を発信する技術ブログで、検索流入がすべてでした。しかし AI が当たり前になった今、ハウツー記事は検索される前に答えが出てしまいます。PVはピーク時の数分の一まで落ちていました。これは記事の質の問題というより、構造の変化だと考えています。

もうひとつは、サイトに求める役割が変わったことです。いま自分に必要なのは「情報を発信するメディア」ではなく、「何をつくってきたかを見せる場所」でした。そこでブログを主役から降ろし、ポートフォリオを主役にしました。記事は消さずに、付録として残すことにしています。

移行の絶対条件:URLを一本も変えない

方針転換はしても、10年分の記事とその被リンクは資産です。そこで移行の絶対条件をひとつだけ決めました。

既存記事のURL /[category]/[slug]/ を絶対に変えない。

移行前に WP REST API で全記事を調査したところ、記事は57本、実際に使われているURLパターンは /wordpress//xserver//column/ の3種類だけでした。管理画面上にはもっと多くのカテゴリがありましたが、URLに現れるのは親カテゴリのみ。この調査で「守るべきもの」が57本×3パターンに確定しました。移行の見通しは、ここで一気に良くなります。

CMSをやめた

当初はヘッドレス構成(WordPress をサブドメインに移して REST API でつなぐ)を考えていて、設計も一度は固めました。しかし途中でやめています。

理由は単純で、このサイトの更新頻度に、動くCMSは要らないからです。記事は年に数本、実績の追加も年に数件。そのために WordPress 本体・プラグイン・PHP・データベースを維持し続けるのは、割に合いません。

代わりに選んだのが、記事をリポジトリ内の MDX ファイルとして持つ構成です。

content/
├ posts/
│   ├ wordpress/[slug].mdx   → /wordpress/[slug]/
│   ├ xserver/[slug].mdx     → /xserver/[slug]/
│   └ column/[slug].mdx      → /column/[slug]/
└ works/[slug].mdx           → /works/[slug]/

ポイントは、ファイル構造がそのままURL構造になることです。content/posts/wordpress/foo.mdx は必ず /wordpress/foo/ になります。URL互換をルーティングのロジックで頑張るのではなく、構造で保証する形です。存在しないURLは dynamicParams: false で即404になります。カテゴリ判別のような条件分岐は、どこにも書いていません。

技術構成は次のとおりです。

  • Next.js(App Router)+ TypeScript、全ページをビルド時に静的生成して Vercel で配信
  • MDX の処理は gray-matter + next-mdx-remote/rsc + rehype-pretty-code
  • frontmatter は zod でバリデーション(ビルド時に不正なデータを弾く)
  • お問い合わせフォームだけ API Route(サーバーレス関数)で送信

もうひとつの副産物として、この構成は移行のダウンタイム問題を消してくれました。ヘッドレス構成だと WordPress を移設した瞬間から新サイト公開まで記事が404になる時間帯が生まれますが、静的生成なら旧サイトを一切触らずに新サイトを完成させ、最後にDNSを切り替えるだけで済みます。実際、切り替え作業はDNSレコードの変更のみでした。

57記事の移行は、スクリプト一発……ではなかった

記事の移行は、WP REST API から全記事を取得して Markdown に変換するスクリプトを書きました。画像も約500枚、すべてローカルに退避しています。旧サイトと同じドメインを使う以上、切り替えた瞬間に旧サーバー上の画像は参照できなくなるためです。

一発でうまくいったかというと、そんなことはありません。変換後の全ページを実際に巡回して見つかった問題が3つあります。

  1. 表の崩壊。 thead の無いテーブルが変換されずテキスト化していました。31記事が該当。
  2. コードブロックの言語欠落。 シンタックスハイライトのために付いていたクラス形式が変換で拾えず、100箇所以上が言語不明のコードになっていました。
  3. 目次アンカーのリンク切れ。 旧プラグインが生成していたアンカーIDと、新しく生成されるIDが一致しませんでした。

いずれもスクリプトを修正して再変換しています。最後に「WPが持っていた記事URL」と「生成ファイルから導かれるURL」を全件突き合わせて、不一致ゼロを機械的に確認しました。移行の成否を目視ではなく検証スクリプトで証明できるようにしたのは、あとから効いてきます。切り替え後の本番確認も、同じスクリプトを本番URLに向けるだけで済みました。

パフォーマンスで一番効いたのは、意外にもフォントだった

リニューアル後の Lighthouse はモバイルで Performance 95〜97です。ただし、最初からこの数字だったわけではありません。

一番大きかったのは日本語Webフォントです。日本語フォントはグリフ数が膨大で、分割配信されるとはいえ、モバイルのスコアを確実に削っていきます。検討の末、日本語はシステムフォント(ヒラギノ/游ゴシック)に任せ、Webフォントは欧文の見出し用だけに絞りました。このサイトの顔は英字の大きなタイポグラフィなので、そこさえ守れれば世界観は崩れません。この一手で、記事ページのスコアは60点台から90点台に跳ねました。

もうひとつ、演出には明確な線を引いています。トップと Works にはスクロール演出や WebGL の星空を入れていますが、記事ページには演出のライブラリを一切読み込ませていません。読み物のページは、読むための速度と静けさを最優先にしています。

つまずきの記録

移行全体でつまずいた点も、恥を忍んで書いておきます。

  • CSS変数の定義方法を誤り、Webフォントが一切適用されていませんでした。 ビルドは通りますし、macOS では見た目がそれらしく出るので気づきにくい問題です。実ブラウザでフォントのリクエストが飛んでいないことから発覚しました。「ビルドが通る」と「動いている」は別物です。
  • DNSの切り替えでメールを巻き込みかけました。 MXレコードがドメイン自身(のAレコード)を参照する構成だったので、そのままAレコードを差し替えるとメールの配送先ごと変わってしまいます。先にメール用のサブドメインへMXを逃がしてから、Webを切り替えました。ドメイン直下でメールを運用している方は、切り替え前にMXの参照先を必ず確認することをおすすめします。
  • プレビューでは確認できないものがあります。 OGP画像やcanonicalは本番ドメイン基準で出力されるため、切り替え前の環境では「未設定に見える」のです。仕組みを理解していないと、直前で慌てることになります(慌てました)。

移行してどうか

まだ日が浅いですが、いま感じていることを書いておきます。

運用は圧倒的に軽くなりました。WordPress の更新もプラグインの脆弱性対応もありません。記事を書くことはテキストファイルを書くことになり、サイトの変更はすべて git の履歴に残ります。壊れる要素が、ほとんどありません。

一方で、スマホからさっと記事を直す、といった手軽さは失いました。ただ、それで困るほどの更新頻度がないことは、この10年の自分が証明しています。

ブログとしてのこのサイトは役目を終えましたが、記事は全部残っています。そしてこれからも、こうして気が向いたときに書き足していくつもりです。この記事が、その一本目になります。