• #Vue
  • #Nuxt4
  • #レイアウト
  • #SSG
  • #Cloudflare
  • #テキスト教材
  • #並列処理
開発eurekapu-nuxt4メモ

3600行のドラフトMDを11個のVueページに変換し、stepsレイアウトを汎用化した記録

前日に生成した会計テキスト教材のドラフト(10チャプター・計3600行のマークダウン)をVueページに変換した。Cloudflareにデプロイしてブラウザのタブを開いた瞬間、全ページが空白で返ってきた。そこから原因を掘り、レイアウトを汎用化し、ビルドエラーを潰し、トップページへの導線を足すまで、朝から夕方までキーボードを叩き続けた。

ドラフトが消えた: process.cwd()の罠

ローカルでは全チャプターが表示されていた。Cloudflareにデプロイして教材ページを開くと、コンテンツ領域に何も描画されない。DevToolsのNetworkタブを見ると、APIレスポンスが空のJSONを返していた。

コードを追うと process.cwd() + '/memo/' でファイルシステムを直接読んでいる箇所が見つかった。ローカルのNode.jsでは memo/ ディレクトリが存在するから読める。Cloudflare Workers上ではそのディレクトリ自体が存在しない。fs.readFileSync が例外を投げ、catchされて空レスポンスになっていた。

対処は、ドラフトMDをVueページに変換してNuxtのルーティングに乗せること。ファイルシステムへの依存を断てば、ビルド時に静的HTMLとして吐き出される。

レイアウト選択: Miller Columnを捨ててstepsに絞る

最初に頭に浮かんだのはMiller Column(3ペインの階層ナビ)だった。macOSのFinderのようにコース → チャプター → セクションと掘り下げていくUI。プロトタイプを脳内で組み立てている途中で手が止まった。教材の構造は単純な線形進行で、読者は「次へ」「前へ」で進むだけ。3ペインを用意しても2ペインしか使わない。

既存のCF精算表コースがstepsレイアウトを使っていた。左サイドバーにチャプター一覧、右にコンテンツ本文。教材にもこのパターンがそのまま嵌まる。新しいレイアウトを1から書く代わりに、stepsレイアウトを汎用化する方向に切り替えた。

stepsレイアウトの汎用化: ハードコードを設定テーブルに変える

従来のstepsレイアウトはCF精算表専用にハードコードされていた。サイドバーのリンク一覧やタイトルがコンポーネント内にべた書きで、別のコースを追加しようとすると丸ごとコピーするしかなかった。

変更後は sidebarConfigs オブジェクトにコース設定をまとめた。ルートパスのプレフィックス(/cf-worksheet//beppyo-textbook/)に基づいて、サイドバーの設定を自動で切り替える。

const sidebarConfigs: Record<string, SidebarConfig> = {
  '/cf-worksheet/': {
    title: 'CF精算表',
    items: cfWorksheetItems,
  },
  '/beppyo-textbook/': {
    title: 'テキスト教材',
    items: beppyoTextbookItems,
  },
}

// 現在のルートパスからconfigを自動選択
const currentConfig = computed(() => {
  const path = route.path
  for (const [prefix, config] of Object.entries(sidebarConfigs)) {
    if (path.startsWith(prefix)) return config
  }
  return null
})

新しいコースを追加するときは、sidebarConfigs に1エントリ追加するだけで済む。レイアウトコンポーネントには一切触らない。

全11ファイルのレイアウト統一

もともと教材用に beppyo という専用レイアウトを作りかけていた。しかしstepsレイアウトを汎用化した時点で、専用レイアウトを維持する理由がなくなった。Ch0(イントロダクション)からCh9まで、加えてインデックスページの計11ファイルを definePageMeta({ layout: 'steps' }) に統一した。

beppyo レイアウトのファイルは削除した。同じ機能が2箇所に存在する状態を放置すると、次に触る人(将来の自分)が「どちらが正しいのか」で手が止まる。

SVGパス解決: 25ファイルをpublic/にコピーする

レイアウト統一後にビルドを走らせると、SVGの参照エラーが25件出てビルドが止まった。各チャプターのVueテンプレート内で <img src="/images/beppyo/ch3-flow.svg"> のように参照していたファイルが、ビルド時にどこにも見つからない。

原因は、SVGファイルがプロジェクトの memo/ 配下にしか存在しなかったこと。Nuxtの静的ビルドでは public/ ディレクトリに置かないと /images/... のパスで配信されない。

最初はNitroのビルドフックで memo/ から public/ へ自動コピーする仕組みを考えた。しかしコピー元のパスが章ごとにバラバラで、フックのロジックが複雑になりそうだった。結局、25枚のプレースホルダーSVGを手動で public/images/beppyo/ にコピーした。明示的に public/ に置く方が、次にビルドしたとき何が含まれているか一目で分かる。ビルドが通った。

並列サブエージェントでCh0-Ch9を分担作成

Vueページへの変換は10チャプター分ある。1つずつ手で書き換えていたら日が暮れる。サブエージェントを並列に走らせ、Ch0-Ch2、Ch3-Ch5、Ch6-Ch7、Ch8-Ch9の4グループに分けて同時に変換した。

各サブエージェントには以下を渡した:

  • 変換元のマークダウンファイル
  • stepsレイアウトの使い方(definePageMeta の記述)
  • SVGの参照パス規則(/images/beppyo/ プレフィックス)
  • コンポーネント構造のテンプレート

4グループが出力したVueファイルをマージし、微修正を加えてビルド確認。ここでSVGパスの不一致が数か所見つかったが、grepで洗い出して一括修正した。

トップページに「テキスト教材」セクションを追加

全チャプターのページが揃ったのにトップページからたどり着けないことに気づいた。トップページのコースカード一覧に「テキスト教材」カードを1枚追加し、チャプター数と「別表四・五を図解で学ぶ」という一行説明を載せた。これでトップ → 教材インデックス → 各チャプターへの導線が繋がった。

振り返り

前日の仕込みが効いた

1日で片付いた最大の理由は、前日にドラフトMD 3600行を生成し終えていたこと。今日は「何を書くか」で悩む時間がゼロで、「どう変換するか」だけに集中できた。素材の準備と形式の変換を別の日に分けたことで、各工程で頭のスイッチを切り替えずに済んだ。

stepsレイアウトの汎用化が今後に効く

CF精算表専用だったレイアウトが、sidebarConfigs に1エントリ追加するだけで新コースに対応できる形になった。次にコースを増やすとき、レイアウトファイルを触る必要がない。ここを今日やっておいたことで、将来の作業が「設定の追加」で完結する。

「ローカルで動く、本番で動かない」のパターン

process.cwd() でファイルを読む方式は、ローカルのNode.jsサーバーではそのまま動く。しかしCloudflare Workersのようなエッジランタイムではファイルシステムが存在しない。SVGパスの問題も同根で、public/ に明示的に配置しないとビルドに含まれない。この2つのトラブルが同じ日に起きたことで、「SSGでは全てのアセットがビルド時に解決される必要がある」というルールが体に染みた。