なぜ作り直したのか(Why)

正直に言うと、壊れかけていました。
このブログは2020年に作ったもので、フレームワークはNuxt 2、microCMSからビルド時に記事を引いて、ロリポップの共用サーバーに静的ファイルとして吐き出すJamstack構成でした。当時としては悪くない選択で、実際6年動き続けてくれました。

問題は、その6年のあいだ、AIもいない時代に自分の手で機能を継ぎ足し継ぎ足ししてきたことです。目次の自動生成、タグの自動配色、OGP画像の合成、パスワード付き記事、埋め込み対応。思いつくたびに設定ファイルへ関数を足していった結果、nuxt.config.jsは自分でも全体を把握しきれない有機的な塊になっていました。

余談ですが、この「継ぎ足してきた」記録は、実はこのブログ自身に残っています。「ブログ開発」というタグを辿ると、機能を作るたびに書き残した製作日記がずらりと並びます。
tag:ブログ開発 | EMC2NARY「ブログ開発」のタグが付いた記事 27件utautattaro.blog
そして今回の刷新で気づいたのですが、この製作日記の山が、そのまま当時のADR(Architecture Decision Record/なぜその設計にしたのかの意思決定記録)として機能しました。「なぜこのタグ機能はこんな作りなのか」「このパスワード実装は何を意図していたのか」——作り直しの過程でその判断根拠を知りたくなったとき、答えは6年前の自分がブログに書き残していたのです。当時はただ楽しくて書いていた製作日記が、6年後の自分への申し送り書になっていた。これは、書き続けてきてよかったと素直に思えた瞬間でした。

しかしこの積み上げ続けたブログシステムは次第に壊れはじめました。CIがコケたりコケなかったり、コケていないのにindex.htmlだけ生成されずトップが403になったり、生き物のような劣化の仕方をしてくるようになりました。決定的だったのは土台のNuxt 2そのものがサポート終了(EOL)を迎えていたことです。もう乗り続けられない船でした。

そうして刷新することを決意し、基盤をNuxt 2からAstroへ載せ替えました。思想は変えていません。ビルド時にすべてを解決し、ブラウザにはできるだけ何も送らない。 むしろその思想を、6年前より徹底できるようになりました。

Nuxtへの感謝を語りたい

……と、載せ替えの話をあっさり書いてしまいましたが、本題に入る前にNuxtへの思いをまとめておこうと思います。
Nuxtは自分にとって転換点になるようなフレームワークでした。Nuxtはフロントエンドフレームワーク初心者の私にはわかりやすくとても良い入口となったので、非常によいフレームワークでした。
私がJavaScriptのフレームワークというものをまともに触れるようになったのは、Nuxtのおかげでした。Vueの気持ちよさを、SSRやSSGという考え方を、asyncDataでデータを流し込む感覚を、ぜんぶNuxtから教わりました。このブログの6年間は、そのままNuxtと過ごした6年間です。深夜に一人で機能を足しては動いた瞬間に小さくガッツポーズする、あの時間の相棒は、いつもNuxtでした。

今回離れる決断をしたのは、Nuxtが悪くなったからではまったくありません。私が2に留まり続けてしまっただけで、Nuxtはとうに3へ、その先へと進化しています。むしろ、6年前の私に「これで作れ」と手を差し出してくれたのがNuxtだったからこそ、いまこうして作り直せるだけの力がついた。そういう意味で、この刷新はNuxtからの卒業でもあります。
——さて、餞(はなむけ)はここまでにして、新しい土台で何ができるようになったのか、実演つきで紹介します。

どう作ったか(How)

今回の刷新は、実装のほとんどをAIエージェント(Claude Code)に任せ、私は設計と意思決定に専念する、という進め方をしました。これが想像以上にうまくいったので、やり方そのものを技術者向けに書き残しておきます。丸投げでもなければ、AIに使われるでもない、ちょうどいい二人三脚の形が見えた気がしています。

原則:いきなり作らせない

最初にやったのは、コードを一行も書かせず、仕様書を一枚書くことでした。
リポジトリのルートに CLAUDE.md を置き、そこにこの刷新のすべての原則を書きました。URLは旧サイトと完全互換にすることを絶対条件にする、味のある実装は思想ごと保存する、既存フィールドは切替完了まで壊さない——そういう「守るべき境界」を先に固めました。
この一枚が、以後のすべての判断の拠り所になりました。AIが何かを提案してきたとき、私が却下するとき、その根拠は常にこの仕様書にありました。逆に言えば、ここに書き忘れたことは後でブレます。設計とは、実装の前に「何を守るか」を言語化する作業なのだと、改めて実感しました。

フェーズを切る

そのうえで、作業を6つのフェーズに分けました。

  1. 読解と棚卸し(実装なし)
  2. 骨格(Astro+microCMS接続、URL互換でページ生成)
  3. デザイン実装
  4. 検索・OGP生成・表示制御
  5. パスワード記事・プレビュー
  6. CI/CD・URL互換の全数検証・切替


肝は、Phase 1で一行も実装させなかったことです。まず旧コードを全部読ませて、機能一覧・スキーマ・独自処理を棚卸しさせる。ここで各フェーズの完了時には必ず私が確認を入れ、承認してから次へ進む、というリズムを徹底しました。

「読む」精度に驚いた

このやり方でいちばん驚いたのが、AIの「読む」フェーズの精度でした。
6年分の設定ファイルを棚卸しさせると、自分でも忘れていた機能や、実装と仕様がズレている箇所まで発掘されてきました。たとえば、ある一覧ページの並び順が仕様と食い違っていたのですが、これは私自身が把握していなかった挙動でした。

さらに感心したのは、古い実装の意図まで復元してきたことです。パスワード付き記事の表示挙動には、一見すると不可解な非対称性(あるページには出るが別のページには出ない)があったのですが、AIはコードだけでなく、私が昔microCMSの管理画面に書き残した説明文まで拾い上げて、「この非対称性は意図されたものですね」と判断してきました。過去の自分が残したメモが、6年後にAIを介して効いた瞬間でした。

前座で「製作日記がそのままADRになった」と書きましたが、これはまさにその実演です。私が数日前に書いていた仕様書(CLAUDE.md)自体も、いま読み返せば今回の刷新のADRになっています。何を守り、何を捨て、なぜそう決めたのか——判断の記録を残しながら進めることが、AIとの協業では特に効くのだと思います。

作る前に、地雷を探させる

もうひとつ徹底したのが、実装の前に必ず実データを調べさせるという作法です。

具体例を挙げます。「本文中に貼られたURLをカードに変換する機能」を作るとき、いきなり実装させるのではなく、まず全記事を走査して該当するURLが何件あるか、どんなパターンがあるかを調べさせました。結果、577件の候補URLが見つかり、そのうち約460件はカード化でき、残りはリンク切れなどで素のリンクにフォールバックする、という見積もりが実装前に立ちました。

数式(LaTeX)対応のときも同じです。本文に数式デリミタを導入する前に、既存の454記事にそのデリミタが偶然出現していないかを全走査させました。もし「$100」のような表記が数式と誤爆するなら、デリミタ設計から変える必要があるからです。調査の結果、誤爆リスクのある出現はごくわずかで、しかもすべてコードブロックの中(=除外対象)だと分かり、安心して実装に入れました。

「作ってから直す」のではなく「作る前に地雷を探す」。この一手間を挟むだけで、手戻りが劇的に減りました。453件の記事という実データが相手だと、想像で設計するのは危険で、必ずデータに聞く。これはAIがいるからこそ、低コストで徹底できるようになった作法です。

私がやったこと

こうして振り返ると、AIに任せたのは「実装」で、私が担ったのは「判断」でした。

境界を引くこと。「その味は殺すな」と指示すること。提案に対して「それは違う」と却下すること。実データの調査結果を見て設計を決めること。実装から解放されたぶん、意思決定の密度はいつもより明らかに高くなりました。

たとえば、タグの色を決めるあの力技のロジック(文字コードを色に変換する例のアレ)を、AIは当然のように「今風のきれいな方式に書き換えましょうか」と提案してきました。私はそれを却下し、「ロジックごとそのまま移植しろ、これは思想だ」と指示しました。こういう、効率だけでは測れない判断こそが、人間の仕事として残るのだと思います。

AIとの協業は、人間が楽をする話ではありませんでした。むしろ、人間が「何を大事にするか」を一つひとつ言葉にして突きつけられる、けっこう緊張感のある数日間でした。そしてそれは、とても良い体験でした。

何ができるようになったか(What)

今回の刷新にあたって、既存機能はこれまで通り問題なく使えるように踏襲しつつ、これまで追加で欲しいと思っていても実装までできていなかった機能や、アーキテクチャ刷新によって一気にできるようになった機能など、欲しい追加機能をいくつか追加しました。

数式が書けるようになりました(LaTeX対応)

経営管理ドメインに属してから、色々なモデルを数式で書くことが増えてきたため、ずっとやりたかった機能です。
本文にLaTeX記法で数式を書くと、綺麗に組版されます。
たとえばインラインで E=mc2E = mc^2 と書けますし、円周率 π3.14159\pi \approx 3.14159 のように文章へ自然に溶け込ませることもできます。
独立した式なら、こう組まれます。
n=11n2=π26 \sum_{n=1}^{\infty} \frac{1}{n^2} = \frac{\pi^2}{6} itΨ=H^Ψ=(22m2+V)Ψ i\hbar\frac{\partial}{\partial t}\Psi = \hat{H}\Psi = \left(-\frac{\hbar^2}{2m}\nabla^2 + V\right)\Psi ここで一つ、地味ですがこだわった点があります。数式のデリミタに、LaTeX標準でよく使われる $あえて採用しませんでした$ は「100ドル」の $100 や、コード例に出てくるシェル変数 $PATH と誤爆します。そこで独自に開始・終了のマーカーを予約し、その内側だけを数式として扱う方式にしました。おかげで、地の文にドル記号を書いても数式と誤認されません。
そして重要なのは、この数式の組版がブラウザ側ではなくビルド時に行われていることです。ページを開いた時点で、数式はもうHTMLとして出来上がっています。読む人の端末で計算が走ることはありません。これは後述する他の機能にも共通する、このブログの一貫した思想です。

リンクカードが安定しました

本文にURLを貼ると、タイトルや説明つきのカードになります。この機能自体は旧ブログにもありました。
FDEとSESは何が違うのか——その正体は、モンスターハンターである#仕事🖥️#開発👨‍💻#キャリア🧭#モンハン🐲
たとえば、上のように前回の記事のURLを貼ると、こういうカードが出ます。
何が変わったかというと、安定性です。旧実装は、記事が表示されるたびにサーバー(PHP)がリンク先のサイトへOGP情報を取りに行っていました。そのため、相手のサイトが遅かったり落ちていたりすると、こちらの表示までそれに引きずられて不安定になっていました。

新実装では、リンク先の情報取得をビルド前に一度だけ行い、結果をリポジトリ内にキャッシュします。表示時はそのキャッシュを読むだけなので、完全に静的で、相手サイトの状態に一切左右されません。取得に失敗したURLは、素のリンクとして表示されるようフォールバックします。
「表示のたびに外部へ問い合わせる」から「ビルド時に一度だけ取得して焼き込む」へ。考え方は数式とまったく同じです。

検索が速くなりました

旧実装の検索は、検索するたびにMicroCMSのAPIを叩いていました。正直、かなり遅かったです。
新実装では、ビルド時に静的な検索インデックスを生成する方式(Pagefind)に変えました。サーバーへの問い合わせが一切なく、ブラウザ内で即座に結果が返ります。

実際に試せます。たとえば「VOXELCANVAS」で検索した結果はこちらです。
検索 | EMC2NARYEMC2NARYの記事を検索できます。utautattaro.blog
URLに ?q=キーワード を付けるだけで検索結果に直接飛べるので、よかったら好きな言葉で試してみてください。体感で、旧実装とは別物になっているはずです。

OGP画像が壊れなくなりました

SNSでこの記事をシェアすると出てくる、あの画像の話です。
旧実装は、外部の画像合成サービスにタイトル文字列を渡して画像を作っていました。この方式は、タイトルに記号が含まれると壊れることがあり、外部サービスの都合にも左右されました。
新実装では、記事ごとのOGP画像を自前でビルド時に生成し、静的にホストしています。仕組みはシンプルで、各記事の画像は /og/{記事のID}.png というURLに置かれています。
たとえばこの画像は、ある記事のために生成されたOGP画像そのものです。

OGPのレンダリング時に組版がきれいに出るような工夫もしています。また当ブログのメタデータ付きタグをうまいこと出したいので、タグも表示することにしました。タグのアイコンも適切に表示されており、らしさが出ているんじゃないかなと思います。
外部サービスに依存しないので、記号だらけのタイトルでも壊れませんし、サービス終了に怯える必要もなくなりました。

読みやすさを調整しました

これは主観的な話ですが、本文の一行の長さと行間を、日本語が読みやすい値に調整しました。欧文向けフレームワークのデフォルト値のまま日本語を流し込んでいた6年間からの、ささやかな卒業です。いま読んでいるこの行の詰まり具合が、その結果です。

プレビューできるようになりました

これは完全な新機能です。下書きを、本番とまったく同じ見た目で確認してから公開できるようになりました。地味なうえ私にしか見えない画面で恐縮ですが、書く体験がいちばん変わったのは、実はここかもしれません。この記事も、プレビューで見た目を確かめながら書いています。

変えなかったもの——6年分の"変な仕組み"たち

ここまで新しくなった点を紹介してきましたが、逆に、あえてそのまま受け継いだものもあります。むしろ、こちらのほうがこのブログの個性かもしれません。普通のブログシステムには、まずない仕組みたちです。

タグの色は、タグ名の文字列そのものから機械的に決まります。ハッシュもカラーパレットも持たず、タグ名の文字コードを変換して色にしているだけの力技です。おかげで同じタグは常に同じ色になり、しかも先頭の文字が同じタグは自然と近い色になります。今風のやり方に書き換えることもできましたが、この味のある実装は、ロジックごとそのまま移植しました。理屈より、こういう変なこだわりが自分は好きです。

タグにメタデータを持たせる仕組みも残しました。このブログのタグは、ただの分類ラベルではなく、それぞれに説明文やアイコン、テーマ画像を持てるコンテンツになっています。自作だからこそ、こういう拡張が自由にできます。

記事内で任意のJavaScriptを実行する機能も健在です。これは講義や解説でこのブログを使うときに、記事の中で動くデモをそのまま見せたくて作ったものです。もちろん本文と実行コードのエスケープ境界はきちんと分離して、安全に作り直しました。

新しい機能を足すことと同じくらい、こういう"変な仕組み"を殺さずに引き継ぐことに気を配りました。刷新とは、過去を捨てることではないと思うからです。

なぜ、Cloudflareではなくロリポップなのか

技術的な選択の話をひとつ。
今回の構成を考えるとき、静的サイトのホスティング先として、正直Cloudflareのような今風のプラットフォームに全部乗せてしまうのが一番モダンで、ラクな選択でした。実際、下書きプレビューの仕組みだけはCloudflareの力を借りて実装しており、その際にブログ本体もできてしまって、あれ?これでいいんじゃね?という場面に遭遇しました。

それでも、本体の公開先はロリポップ!の共用サーバーのまま据え置きました。
理由は2つあります。ひとつは実利的な話で、パスワード付き記事の認証に、どうしてもサーバー側で動く処理(PHP)が必要だったこと。ロリポップならPHPが普通に動くので、publicにphpファイルを置いておくだけでデプロイするとサーバーコードがすぐデプロイでき、この仕組みを素直に実装できます。
もうひとつは、正直に言えば、愛です。
ロリポップは、私がまだ何も作れなかった頃に、月数百円で「サーバーというもの」を触らせてくれた場所でした。FTPでファイルを上げるとページが世界に公開される、あの魔法みたいな体験を最初に授けてくれたのが、この共用サーバーです。今ならもっと高機能な選択肢がありますが、それでも自分を育ててくれた場所で動かし続けたいという気持ちでそのまま選択しました。
モダンさだけで技術を選ばない。それも、自分でシステムを持つことの自由だと思っています。

そして、URLは何も変わっていません

最後に、一番地味で一番大事な点を。
これだけ中身を作り替えておきながら、URL構造は旧サイトと完全に一致させてあります。 過去記事のリンクも、どこかに貼られたブックマークも、検索エンジンの評価も、何ひとつ切れていないはずです。

次回:なぜブログシステムを自作し続けるのか

さて、ここまで技術の話をしてきました。速くなった、壊れなくなった、便利になった。それはそれで事実なのですが……。
noteもあります。はてなブログもあります。Zennもあります。無料で、高機能で、メンテナンスも要らない優れたプラットフォームがいくらでもある時代に、なぜ私はわざわざ自分でブログシステムを作り、6年も運用し、あまつさえ数日かけて作り直したりしているのでしょうか。

その答えは、技術の話ではありません。次回、「なぜ私はブログシステムを作り続けるのか」を書きます。こちらが、たぶん本題です。