朝6時半に前日の積み残しmemoを開いて「takken 49ページのHTMLが0件になってる」と気づいた瞬間、心臓が落ちる音がした。前日の事故からの復旧、そこから本来やりたかったPhase 2(lessons数百ページの外部化)へ。1日で6セッションをまたいで走り、Worker bundleを2.909 MiB(margin 91 KiB / Red)から2.744 MiB(margin 262 KiB / Green)まで削った記録。
朝イチ: Phase 1の復旧
引き継ぎmemoには「Phase 1完了、takken 49ページがpublic/content/takken/*.htmlに存在」と書いてあったのに、実態は0件だった。前日のブランチ事故で消えた状態のままmainに入っていた。
ブランチを切ってfeatからgit restore --source=で1ファイルずつ取り込み直す方針に切り替えた。git checkout <branch> -- <path> 形式が権限プロンプトに弾かれたので、git restore形式に変えたら通った。app/data/topics.tsのdraft: true設定やapp/plugins/auth.server.tsまで取り込み忘れていたことに気づいて拾い直す。
pnpm buildでWorker 2.909 MiB、wrangler pages dev上で49件SSR smoke testが全件PASS。PR #21をsquash mergeして本番反映。ここでようやくPhase 1が「本当に」終わった。
朝の時点で気づいたこと: memoの「完了」を信じる前に、ディスク上のファイル数を必ず確認する。memoは未来の自分への手紙だが、嘘をついていることがある。
Phase 2着手: lessons 307ファイルの棚卸し
新セッションに引き継いで Phase 2 開始。タグ pre-plan-d-phase2-20260617 を切ってブランチ作成。lessons/配下を数えたら 307ファイル。Phase 2計画書には「23件を外部化」と書いてあったが、現実の規模はもっと大きい。
classify-lessons.mjsを書いて分類した。最初は判定シグナルが過剰で「A判定(外部化可能)=0件」になった。useHeadがSEO設定だから本文hydrationに無関係なのに検出していた、:bindもカスタムcomponentのprops渡しまで拾っていた。判定ロジックを精緻化したらA=267 / B=34 / C=6になった。
ところがintro-to-accounting 11件を1件中身確認したら、全てBookkeepingMillerViewerLoader単体のページだった。つまり「動的viewerをロードするだけ」のプレースホルダで実質B。同様の判定漏れがあるはず、と疑って再分類したら純粋A=23件まで絞られた。
ここでCodexにレビューを投げたら致命的指摘3点が返ってきた。「23件で進めるのは危険。beppyo 9件 + 基盤修正パイロットに縮小すべき」。スコープを縮めることに方針転換。
A基盤修正 + B beppyo 9件の外部HTML化
A-1〜A-8で converter スクリプトに以下を追加した。
extractMetaにuseHead対応を追加(useSeoMeta形式しか拾えてなかった)- 再帰readdirで
lessons/beppyo/ch1のような full slug を出力できるように --excludeフィルタでinteractive-test.vueのようなB分類ファイルを converter から除外--slug-prefixで出力先のパス階層を制御
B-1〜B-5で beppyo 9件を本変換、[...slug].vue の動的ルートを作成、旧 vue を git rm。pnpm buildしたらWorker 2.853 MiB(Phase 1完了時 2.909 MiB → 56 KiB削減)。
検証フェーズでハマったのが draft middleware。beppyo は draft: true 設定なので admin 認証が要る。wrangler pages dev で403が返ってきて、middlewareを一時的にearly-returnさせる patch を入れて9件全部の SSR HTML を確認 → revert、という遠回り。Cloudflare Workers では process.env がランタイムで参照できないので bypass フラグも効かなかった。
dev環境でhydration mismatchを目視で拾う
ユーザーから「ローカルで表示確認してくれ、port 3200で立ってる」と言われて Chrome DevTools MCP で見たら、hydration mismatchを発見した。
SSRはclass="layout-steps"で出ているのに、ブラウザのDOMではclass="layout"になっていた。LessonBreadcrumbを確認したらSSR/CSR分岐ロジックがあるわけでもない。別ページに移動して再確認したらmismatchが消えていた。dev server がまだ新ファイルを認識していなかっただけだった。
ch1からch9まで巡回してconsole error 0を確認。画面の数字を見て違和感を拾う係は人間、修正を回す係はAIという構図が今日もハマった。
deploy.ps1にWorker bundle計測ロジックを追加
ユーザーから「KiBがどんどん減っていくのが目標なんで、デプロイ時にちゃんとログに出力してください」と言われて、deploy.ps1にMeasure-WorkerBundle関数を追加した。
# gzip 圧縮後サイズで Worker bundle を計測
$gzipBytes = Measure-WorkerGzipBytes
$marginKiB = (3MB - $gzipBytes) / 1KB
# Red ≤ 100 KiB / Yellow 100-200 / Green ≥ 200
ゾーンの閾値を Red / Yellow / Green で色分けした。Cloudflareの3.0 MiB上限に対する余裕で判定する。さらに deploy 時刻と margin を worker-bundle-history.csv に追記して、減っていく履歴を残すようにした。
ここでPowerShell 5.1がBOMなしUTF-8の日本語コメントをCP932で誤読してparse崩壊させる罠を踏んだ。全角括弧()が原因だった。コメントをASCIIに書き直してPARSE OK復旧。日本語の罠は油断するとここでも刺さる。
JournalExample 147件のClientOnly+Lazy化(Phase 2.5)
beppyo 9件で-56 KiB稼いだあと、次の打ち手はJournalExample(1882行の巨大component)を使っているzaimu-suuchi-case100の128件 + bookkeeping-3kyu-notesの19件 = 147件。
最初はLazyJournalExampleに書き換えただけで build したらWorker bundle が逆に+3 KiB。Lazy属性はクライアントbundle分割にしか効かず、NitroはSSR用にcomponentをbundleするから Worker bundle には効かない、と仮説通り。
次に<ClientOnly>でSSR自体から除外する形にラップした。
<!-- before -->
<JournalExample :example="exampleData" />
<!-- after: SSRから完全に外す -->
<ClientOnly>
<LazyJournalExample :example="exampleData" />
</ClientOnly>
これが効いた。Worker bundle margin: 151.6 → 232.4 KiB(+80.8 KiB、Greenゾーンへ)。147件を一括変換するNodeスクリプトを書いて回した。
Phase 3: MillerViewer 73件のClientOnly+Lazy化
午後の新セッションでPhase 3。MillerViewer(1564行のcomponent)を使うlessonsページが73件あった(handoffには29件と書いてあったが過小カウントだった)。
パイロット3件(軽量・標準・重量代表)でSSR HTMLサイズを計測したら、47.8 KB → 9.6 KB(-80%)、67.8 KB → 15.6 KB(-77%)。SEO関連のhead(title/description/og:*)は完全に維持されていることをdiffで確認した。消えた本文は Miller Column の章タイトル一覧と TheaterViewer の本文・画像caption。これらはSEOには寄与しないので落としても問題ない。
70件を一括変換するスクリプトを書いて回し、build → Worker 2.773 → 2.759 MiB(margin 247 KiB)。PR #26をsquash merge + デプロイ成功。
その後 B(ClosingTransferExample 1件)と E(terms.vue / privacy.vue の外部HTML化)をまとめて PR #27 でmerge。terms/privacyはuseContent + v-htmlで配信する形に書き換えて、専用のpublic/content/pages/を作った。最終的にWorker 2.744 MiB / margin 262 KiB。
16時台: G (E2E) / H (Dependabot) / C調査
夕方の新セッションで残タスク3件に着手。
H (Dependabot): esbuild override が <=0.24.2 までしか対象にしてなかったのを更新、[email protected] を消して 8.21.0 に上げた。alerts 3件解消。
G (E2E テスト全件fail): 原因は chrome-headless-shell.exe 未インストールだった。npx playwright install で解決。さらに dev server の Pinia SSR エラーと、トップページのカード数が 5→7 に増えていた期待値ズレを修正。takken の SVG modal テストは data-modal-bound="1" 属性の付与を待つように修正。最終的に44 passed、1 skipped、0 failed。PR #28作成。
C (章ナビ manifest 化 + bk-3kyu-notes 外部HTML化): 設計まで進めて未着手のまま次セッションへ。
セッション切り替えと引き継ぎmemoの運用
今日は1日で6セッション切り替えた。handoff memoは v1 → v11 まで版を上げた。MCPが不調になったり、Opus 4.7の長セッション後半で思考精度が落ちるのを避けるため、/clearで区切りを入れている。
引き継ぎmemoの書き方で効いたこと:
- **§4.5「v2修正版手順」**のように、最新版の場所を必ず明示する
- 次セッション用プロンプトを memo の末尾に書いておき、コピペで起動できる形にする
- Codexレビューの結論だけ反映して、レビュー本文は memo に書かない(次セッションがノイズに引きずられる)
朝のユーザー指示「Codexのレビュー内容を入れる必要ありますか?反映した結果を計画書に書けばいいんじゃないですか」が、引き継ぎの本質を突いていた。
学び・気づき
- memoの「完了」を信じる前にディスクを確認する。前日のmemoが「49件存在」と書いてあっても、ブランチ事故で消えていることがある。
ls public/content/takken/*.html | wc -lを打つのは5秒 - Lazy 属性はクライアントbundle分割専用、Worker bundleには効かない。SSRから外すには
<ClientOnly>が必要 - SEOに寄与しない本文(Miller Column の章タイトル、画像caption など)はSSRから外してよい。
useContentMetaが<script setup>直下で動くのでhead系は維持される - PowerShell 5.1はBOMなしUTF-8の全角括弧でparse崩壊する。コメントもASCIIで書く方が安全
- deploy時の数値をログに残すと、減っていく実感が湧いて続けられる。Red/Yellow/Greenの色分けは精神衛生に効く
- 長セッションは区切る。Opus 4.7は後半で思考努力が増える傾向があるので、PR 1〜2本ごとに
/clearを入れる
次セッションへの引き継ぎ
C (章ナビ manifest 化 + bk-3kyu-notes 外部HTML化) が未着手で残っている。import.meta.globで章ナビのリンクを有効/無効にしているのを manifest ファイルに置き換える設計までは終わっている。
Worker bundleは margin 262 KiB の Green ゾーンで運用余裕が出たので、bundle削減の優先度は下げてOK。次は「Bundle = 認証・課金・対話的ロジックだけ。それ以外は全部分離」というv8で明文化した設計原則に沿って、認証系の分離設計に入る予定。