[{"data":1,"prerenderedAt":562},["ShallowReactive",2],{"content-/beppyo-vue-conversion":3,"all-pages-for-dir":560,"og-image-/beppyo-vue-conversion":561},{"id":4,"title":5,"body":6,"category":540,"description":541,"extension":542,"meta":543,"navigation":232,"path":544,"project_name":545,"published":546,"publishedAt":547,"seo":548,"stem":549,"tags":550,"todo":557,"unpublished":546,"updatedAt":558,"__hash__":559},"pages/2026-04/2026-04-22/beppyo-vue-conversion.md","3600行のドラフトMDを11個のVueページに変換し、stepsレイアウトを汎用化した記録",{"type":7,"value":8,"toc":526},"minimark",[9,13,17,22,25,41,44,48,51,54,58,61,76,380,386,389,400,405,409,416,430,446,450,453,456,480,483,487,490,493,497,500,503,509,513,522],[10,11,5],"h1",{"id":12},"_3600行のドラフトmdを11個のvueページに変換しstepsレイアウトを汎用化した記録",[14,15,16],"p",{},"前日に生成した会計テキスト教材のドラフト（10チャプター・計3600行のマークダウン）をVueページに変換した。Cloudflareにデプロイしてブラウザのタブを開いた瞬間、全ページが空白で返ってきた。そこから原因を掘り、レイアウトを汎用化し、ビルドエラーを潰し、トップページへの導線を足すまで、朝から夕方までキーボードを叩き続けた。",[18,19,21],"h2",{"id":20},"ドラフトが消えた-processcwdの罠","ドラフトが消えた: process.cwd()の罠",[14,23,24],{},"ローカルでは全チャプターが表示されていた。Cloudflareにデプロイして教材ページを開くと、コンテンツ領域に何も描画されない。DevToolsのNetworkタブを見ると、APIレスポンスが空のJSONを返していた。",[14,26,27,28,32,33,36,37,40],{},"コードを追うと ",[29,30,31],"code",{},"process.cwd() + '/memo/'"," でファイルシステムを直接読んでいる箇所が見つかった。ローカルのNode.jsでは ",[29,34,35],{},"memo/"," ディレクトリが存在するから読める。Cloudflare Workers上ではそのディレクトリ自体が存在しない。",[29,38,39],{},"fs.readFileSync"," が例外を投げ、catchされて空レスポンスになっていた。",[14,42,43],{},"対処は、ドラフトMDをVueページに変換してNuxtのルーティングに乗せること。ファイルシステムへの依存を断てば、ビルド時に静的HTMLとして吐き出される。",[18,45,47],{"id":46},"レイアウト選択-miller-columnを捨ててstepsに絞る","レイアウト選択: Miller Columnを捨ててstepsに絞る",[14,49,50],{},"最初に頭に浮かんだのはMiller Column（3ペインの階層ナビ）だった。macOSのFinderのようにコース → チャプター → セクションと掘り下げていくUI。プロトタイプを脳内で組み立てている途中で手が止まった。教材の構造は単純な線形進行で、読者は「次へ」「前へ」で進むだけ。3ペインを用意しても2ペインしか使わない。",[14,52,53],{},"既存のCF精算表コースがstepsレイアウトを使っていた。左サイドバーにチャプター一覧、右にコンテンツ本文。教材にもこのパターンがそのまま嵌まる。新しいレイアウトを1から書く代わりに、stepsレイアウトを汎用化する方向に切り替えた。",[18,55,57],{"id":56},"stepsレイアウトの汎用化-ハードコードを設定テーブルに変える","stepsレイアウトの汎用化: ハードコードを設定テーブルに変える",[14,59,60],{},"従来のstepsレイアウトはCF精算表専用にハードコードされていた。サイドバーのリンク一覧やタイトルがコンポーネント内にべた書きで、別のコースを追加しようとすると丸ごとコピーするしかなかった。",[14,62,63,64,67,68,71,72,75],{},"変更後は ",[29,65,66],{},"sidebarConfigs"," オブジェクトにコース設定をまとめた。ルートパスのプレフィックス（",[29,69,70],{},"/cf-worksheet/"," や ",[29,73,74],{},"/beppyo-textbook/","）に基づいて、サイドバーの設定を自動で切り替える。",[77,78,83],"pre",{"className":79,"code":80,"language":81,"meta":82,"style":82},"language-typescript shiki shiki-themes vitesse-light vitesse-light","const sidebarConfigs: Record\u003Cstring, SidebarConfig> = {\n  '/cf-worksheet/': {\n    title: 'CF精算表',\n    items: cfWorksheetItems,\n  },\n  '/beppyo-textbook/': {\n    title: 'テキスト教材',\n    items: beppyoTextbookItems,\n  },\n}\n\n// 現在のルートパスからconfigを自動選択\nconst currentConfig = computed(() => {\n  const path = route.path\n  for (const [prefix, config] of Object.entries(sidebarConfigs)) {\n    if (path.startsWith(prefix)) return config\n  }\n  return null\n})\n","typescript","",[29,84,85,123,139,158,171,177,188,204,216,221,227,234,241,264,284,332,359,365,374],{"__ignoreMap":82},[86,87,90,94,97,101,105,108,111,114,117,120],"span",{"class":88,"line":89},"line",1,[86,91,93],{"class":92},"stQ0i","const ",[86,95,66],{"class":96},"s4oTP",[86,98,100],{"class":99},"shFtX",": ",[86,102,104],{"class":103},"sSkh3","Record",[86,106,107],{"class":99},"\u003C",[86,109,110],{"class":103},"string",[86,112,113],{"class":99},", ",[86,115,116],{"class":103},"SidebarConfig",[86,118,119],{"class":99},"> =",[86,121,122],{"class":99}," {\n",[86,124,126,130,133,136],{"class":88,"line":125},2,[86,127,129],{"class":128},"sMJiu","  '",[86,131,70],{"class":132},"sdGka",[86,134,135],{"class":128},"'",[86,137,138],{"class":99},": {\n",[86,140,142,146,148,150,153,155],{"class":88,"line":141},3,[86,143,145],{"class":144},"sz8Xr","    title",[86,147,100],{"class":99},[86,149,135],{"class":128},[86,151,152],{"class":132},"CF精算表",[86,154,135],{"class":128},[86,156,157],{"class":99},",\n",[86,159,161,164,166,169],{"class":88,"line":160},4,[86,162,163],{"class":144},"    items",[86,165,100],{"class":99},[86,167,168],{"class":96},"cfWorksheetItems",[86,170,157],{"class":99},[86,172,174],{"class":88,"line":173},5,[86,175,176],{"class":99},"  },\n",[86,178,180,182,184,186],{"class":88,"line":179},6,[86,181,129],{"class":128},[86,183,74],{"class":132},[86,185,135],{"class":128},[86,187,138],{"class":99},[86,189,191,193,195,197,200,202],{"class":88,"line":190},7,[86,192,145],{"class":144},[86,194,100],{"class":99},[86,196,135],{"class":128},[86,198,199],{"class":132},"テキスト教材",[86,201,135],{"class":128},[86,203,157],{"class":99},[86,205,207,209,211,214],{"class":88,"line":206},8,[86,208,163],{"class":144},[86,210,100],{"class":99},[86,212,213],{"class":96},"beppyoTextbookItems",[86,215,157],{"class":99},[86,217,219],{"class":88,"line":218},9,[86,220,176],{"class":99},[86,222,224],{"class":88,"line":223},10,[86,225,226],{"class":99},"}\n",[86,228,230],{"class":88,"line":229},11,[86,231,233],{"emptyLinePlaceholder":232},true,"\n",[86,235,237],{"class":88,"line":236},12,[86,238,240],{"class":239},"sxvE3","// 現在のルートパスからconfigを自動選択\n",[86,242,244,246,249,252,256,259,262],{"class":88,"line":243},13,[86,245,93],{"class":92},[86,247,248],{"class":96},"currentConfig",[86,250,251],{"class":99}," =",[86,253,255],{"class":254},"senZ8"," computed",[86,257,258],{"class":99},"(()",[86,260,261],{"class":99}," =>",[86,263,122],{"class":99},[86,265,267,270,273,275,278,281],{"class":88,"line":266},14,[86,268,269],{"class":92},"  const ",[86,271,272],{"class":96},"path",[86,274,251],{"class":99},[86,276,277],{"class":96}," route",[86,279,280],{"class":99},".",[86,282,283],{"class":96},"path\n",[86,285,287,291,294,296,299,302,305,308,311,314,317,319,322,325,327,330],{"class":88,"line":286},15,[86,288,290],{"class":289},"sHkkW","  for",[86,292,293],{"class":99}," (",[86,295,93],{"class":92},[86,297,298],{"class":99},"[",[86,300,301],{"class":96},"prefix",[86,303,304],{"class":99},",",[86,306,307],{"class":96}," config",[86,309,310],{"class":99},"]",[86,312,313],{"class":92}," of ",[86,315,316],{"class":96},"Object",[86,318,280],{"class":99},[86,320,321],{"class":254},"entries",[86,323,324],{"class":99},"(",[86,326,66],{"class":96},[86,328,329],{"class":99},"))",[86,331,122],{"class":99},[86,333,335,338,340,342,344,347,349,351,353,356],{"class":88,"line":334},16,[86,336,337],{"class":289},"    if",[86,339,293],{"class":99},[86,341,272],{"class":96},[86,343,280],{"class":99},[86,345,346],{"class":254},"startsWith",[86,348,324],{"class":99},[86,350,301],{"class":96},[86,352,329],{"class":99},[86,354,355],{"class":289}," return",[86,357,358],{"class":96}," config\n",[86,360,362],{"class":88,"line":361},17,[86,363,364],{"class":99},"  }\n",[86,366,368,371],{"class":88,"line":367},18,[86,369,370],{"class":289},"  return",[86,372,373],{"class":92}," null\n",[86,375,377],{"class":88,"line":376},19,[86,378,379],{"class":99},"})\n",[14,381,382,383,385],{},"新しいコースを追加するときは、",[29,384,66],{}," に1エントリ追加するだけで済む。レイアウトコンポーネントには一切触らない。",[18,387,388],{"id":388},"全11ファイルのレイアウト統一",[14,390,391,392,395,396,399],{},"もともと教材用に ",[29,393,394],{},"beppyo"," という専用レイアウトを作りかけていた。しかしstepsレイアウトを汎用化した時点で、専用レイアウトを維持する理由がなくなった。Ch0（イントロダクション）からCh9まで、加えてインデックスページの計11ファイルを ",[29,397,398],{},"definePageMeta({ layout: 'steps' })"," に統一した。",[14,401,402,404],{},[29,403,394],{}," レイアウトのファイルは削除した。同じ機能が2箇所に存在する状態を放置すると、次に触る人（将来の自分）が「どちらが正しいのか」で手が止まる。",[18,406,408],{"id":407},"svgパス解決-25ファイルをpublicにコピーする","SVGパス解決: 25ファイルをpublic/にコピーする",[14,410,411,412,415],{},"レイアウト統一後にビルドを走らせると、SVGの参照エラーが25件出てビルドが止まった。各チャプターのVueテンプレート内で ",[29,413,414],{},"\u003Cimg src=\"/images/beppyo/ch3-flow.svg\">"," のように参照していたファイルが、ビルド時にどこにも見つからない。",[14,417,418,419,421,422,425,426,429],{},"原因は、SVGファイルがプロジェクトの ",[29,420,35],{}," 配下にしか存在しなかったこと。Nuxtの静的ビルドでは ",[29,423,424],{},"public/"," ディレクトリに置かないと ",[29,427,428],{},"/images/..."," のパスで配信されない。",[14,431,432,433,435,436,438,439,442,443,445],{},"最初はNitroのビルドフックで ",[29,434,35],{}," から ",[29,437,424],{}," へ自動コピーする仕組みを考えた。しかしコピー元のパスが章ごとにバラバラで、フックのロジックが複雑になりそうだった。結局、25枚のプレースホルダーSVGを手動で ",[29,440,441],{},"public/images/beppyo/"," にコピーした。明示的に ",[29,444,424],{}," に置く方が、次にビルドしたとき何が含まれているか一目で分かる。ビルドが通った。",[18,447,449],{"id":448},"並列サブエージェントでch0-ch9を分担作成","並列サブエージェントでCh0-Ch9を分担作成",[14,451,452],{},"Vueページへの変換は10チャプター分ある。1つずつ手で書き換えていたら日が暮れる。サブエージェントを並列に走らせ、Ch0-Ch2、Ch3-Ch5、Ch6-Ch7、Ch8-Ch9の4グループに分けて同時に変換した。",[14,454,455],{},"各サブエージェントには以下を渡した:",[457,458,459,463,470,477],"ul",{},[460,461,462],"li",{},"変換元のマークダウンファイル",[460,464,465,466,469],{},"stepsレイアウトの使い方（",[29,467,468],{},"definePageMeta"," の記述）",[460,471,472,473,476],{},"SVGの参照パス規則（",[29,474,475],{},"/images/beppyo/"," プレフィックス）",[460,478,479],{},"コンポーネント構造のテンプレート",[14,481,482],{},"4グループが出力したVueファイルをマージし、微修正を加えてビルド確認。ここでSVGパスの不一致が数か所見つかったが、grepで洗い出して一括修正した。",[18,484,486],{"id":485},"トップページにテキスト教材セクションを追加","トップページに「テキスト教材」セクションを追加",[14,488,489],{},"全チャプターのページが揃ったのにトップページからたどり着けないことに気づいた。トップページのコースカード一覧に「テキスト教材」カードを1枚追加し、チャプター数と「別表四・五を図解で学ぶ」という一行説明を載せた。これでトップ → 教材インデックス → 各チャプターへの導線が繋がった。",[18,491,492],{"id":492},"振り返り",[494,495,496],"h3",{"id":496},"前日の仕込みが効いた",[14,498,499],{},"1日で片付いた最大の理由は、前日にドラフトMD 3600行を生成し終えていたこと。今日は「何を書くか」で悩む時間がゼロで、「どう変換するか」だけに集中できた。素材の準備と形式の変換を別の日に分けたことで、各工程で頭のスイッチを切り替えずに済んだ。",[494,501,502],{"id":502},"stepsレイアウトの汎用化が今後に効く",[14,504,505,506,508],{},"CF精算表専用だったレイアウトが、",[29,507,66],{}," に1エントリ追加するだけで新コースに対応できる形になった。次にコースを増やすとき、レイアウトファイルを触る必要がない。ここを今日やっておいたことで、将来の作業が「設定の追加」で完結する。",[494,510,512],{"id":511},"ローカルで動く本番で動かないのパターン","「ローカルで動く、本番で動かない」のパターン",[14,514,515,518,519,521],{},[29,516,517],{},"process.cwd()"," でファイルを読む方式は、ローカルのNode.jsサーバーではそのまま動く。しかしCloudflare Workersのようなエッジランタイムではファイルシステムが存在しない。SVGパスの問題も同根で、",[29,520,424],{}," に明示的に配置しないとビルドに含まれない。この2つのトラブルが同じ日に起きたことで、「SSGでは全てのアセットがビルド時に解決される必要がある」というルールが体に染みた。",[523,524,525],"style",{},"html pre.shiki code .stQ0i, html code.shiki .stQ0i{--shiki-default:#AB5959;--shiki-dark:#AB5959}html pre.shiki code .s4oTP, html code.shiki .s4oTP{--shiki-default:#B07D48;--shiki-dark:#B07D48}html pre.shiki code .shFtX, html code.shiki .shFtX{--shiki-default:#999999;--shiki-dark:#999999}html pre.shiki code .sSkh3, html code.shiki .sSkh3{--shiki-default:#2E8F82;--shiki-dark:#2E8F82}html pre.shiki code .sMJiu, html code.shiki .sMJiu{--shiki-default:#B5695977;--shiki-dark:#B5695977}html pre.shiki code .sdGka, html code.shiki .sdGka{--shiki-default:#B56959;--shiki-dark:#B56959}html pre.shiki code .sz8Xr, html code.shiki .sz8Xr{--shiki-default:#998418;--shiki-dark:#998418}html pre.shiki code .sxvE3, html code.shiki .sxvE3{--shiki-default:#A0ADA0;--shiki-dark:#A0ADA0}html pre.shiki code .senZ8, html code.shiki .senZ8{--shiki-default:#59873A;--shiki-dark:#59873A}html pre.shiki code .sHkkW, html code.shiki .sHkkW{--shiki-default:#1E754F;--shiki-dark:#1E754F}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":82,"searchDepth":125,"depth":125,"links":527},[528,529,530,531,532,533,534,535],{"id":20,"depth":125,"text":21},{"id":46,"depth":125,"text":47},{"id":56,"depth":125,"text":57},{"id":388,"depth":125,"text":388},{"id":407,"depth":125,"text":408},{"id":448,"depth":125,"text":449},{"id":485,"depth":125,"text":486},{"id":492,"depth":125,"text":492,"children":536},[537,538,539],{"id":496,"depth":141,"text":496},{"id":502,"depth":141,"text":502},{"id":511,"depth":141,"text":512},"dev","会計テキスト教材10章分のマークダウンをVueページへ変換。ドラフトがCloudflareで表示されない原因を突き止め、レイアウトをstepsに統一し、SVGパス解決やトップページ追加まで一気に進めた1日の記録","md",{},"/beppyo-vue-conversion","eurekapu-nuxt4",false,"2026-04-22T00:00:00.000Z",{"title":5,"description":541},"2026-04/2026-04-22/beppyo-vue-conversion",[551,552,553,554,555,199,556],"Vue","Nuxt4","レイアウト","SSG","Cloudflare","並列処理","memo",null,"UTblXYLkTw9tgR_MQdqsVRiDbLi9ua33GJfBvdn69jQ",[],"https://log.eurekapu.com/og/blog/beppyo-vue-conversion.png?v=2026-04-22T00%3A00%3A00.000Z&title=3600%E8%A1%8C%E3%81%AE%E3%83%89%E3%83%A9%E3%83%95%E3%83%88MD%E3%82%9211%E5%80%8B%E3%81%AEVue%E3%83%9A%E3%83%BC%E3%82%B8%E3%81%AB%E5%A4%89%E6%8F%9B%E3%81%97%E3%80%81steps%E3%83%AC%E3%82%A4%E3%82%A2%E3%82%A6%E3%83%88%E3%82%92%E6%B1%8E%E7%94%A8%E5%8C%96%E3%81%97%E3%81%9F%E8%A8%98%E9%8C%B2&author=Kei%20Komatsu&sig=04c16612f0095b17",1780786054103]