[{"data":1,"prerenderedAt":675},["ShallowReactive",2],{"content-/docpage-auto-title-display":3,"all-pages-for-dir":673,"og-image-/docpage-auto-title-display":674},{"id":4,"title":5,"body":6,"category":656,"description":657,"extension":658,"meta":659,"navigation":343,"ogImage":660,"path":661,"project_name":662,"published":663,"publishedAt":664,"seo":665,"stem":666,"tags":667,"todo":660,"unpublished":663,"updatedAt":660,"__hash__":672},"pages/2026-06/2026-06-29/docpage-auto-title-display.md","Nuxt Content の記事タイトルが本文に出ない問題を DocPage 側の自動分岐で解消した",{"type":7,"value":8,"toc":647},"minimark",[9,14,27,34,44,47,72,79,83,90,112,126,458,461,514,518,522,531,534,550,553,567,571,588,603,606,627,630,636,643],[10,11,13],"h2",{"id":12},"引き金は-ai-体験格差の引用記事","引き金は AI 体験格差の引用記事",[15,16,17,18,22,23,26],"p",{},"朝、マーク・アンドリーセンが「最先端モデルを触っていない人は AI を遅れた像のまま見続ける」と語る引用を持ち込んで、その日の ",[19,20,21],"code",{},"2026-06-29/"," ディレクトリに ",[19,24,25],{},"ai-experience-gap-frontier-models.md"," として保存させた。honda-sakubun スキルで読点と修飾語順をひと通り整え、公開記事として置いた。",[15,28,29,30,33],{},"保存後、ローカルの dev サーバで開いて読み直したとき、画面の一番上から本文が始まっていることに気づいた。frontmatter に書いた ",[19,31,32],{},"title"," がブラウザのタブ名にしか出ていない。記事の中身を読み始めたつもりが、何の記事を読んでいるのか分からないまま段落が立ち上がる。",[15,35,36,37,40,41,43],{},"このサイトには ",[19,38,39],{},"\u003Ch1>"," を手書きで埋めている記事と、frontmatter の ",[19,42,32],{}," だけで済ませている記事が混ざっていた。後者の側だけ、画面上から見出しが消えていた。",[10,45,46],{"id":46},"なぜ今まで気づかなかったか",[15,48,49,52,53,56,57,60,61,64,65,67,68,71],{},[19,50,51],{},"DocPage.vue"," を覗くと、",[19,54,55],{},"props.doc.title"," は ",[19,58,59],{},"useSeoMeta"," 経由で ",[19,62,63],{},"\u003Ctitle>"," タグと OG 画像メタデータにしか流れていなかった。本文の ",[19,66,39],{}," レンダリングは ",[19,69,70],{},"\u003CContentRenderer>"," 任せで、AST に h1 ノードがあればそれが描画され、無ければ画面には何も出ない。",[15,73,74,75,78],{},"過去にこのサイトで記事を書き始めた頃は、本文の冒頭に ",[19,76,77],{},"# タイトル"," を手書きで入れる癖が残っていた。最近の記事はそれを省略して frontmatter だけで済ませている。古い記事は表示が崩れず、新しい記事は表紙のない本のように始まる。読み手としての違和感を、書き手としての慣れが上書きしていた。",[10,80,82],{"id":81},"設計の分かれ道コンポーザブル化かdocpage-直書きか","設計の分かれ道：コンポーザブル化か、DocPage 直書きか",[15,84,85,86,89],{},"最初に浮かんだ案は「タイトル自動補完ロジックを ",[19,87,88],{},"composables/useDocTitle.ts"," に切り出す」だった。コンポーザブル化しておけば将来 takken 配下の独自レイアウトページや、新しいページタイプからも呼べる。",[15,91,92,93,95,96,99,100,103,104,107,108,111],{},"ただ、実際にレンダリングを担っているコンポーネントは ",[19,94,51],{}," 1 箇所しかない。",[19,97,98],{},"/takken/"," 配下も別 ",[19,101,102],{},".vue"," ではなく ",[19,105,106],{},"DocPage"," の中で ",[19,109,110],{},"path.startsWith(\"/takken/\")"," 分岐で出し分けている。再利用先が無い純粋関数をコンポーザブルに昇格させても、追跡対象のファイルが 1 つ増えるだけで割に合わない。",[15,113,114,115,117,118,121,122,125],{},"最終的に、判定ロジックを ",[19,116,51],{}," の ",[19,119,120],{},"\u003Cscript setup>"," 内に ",[19,123,124],{},"computed"," 2 本で閉じ込めた。",[127,128,133],"pre",{"className":129,"code":130,"language":131,"meta":132,"style":132},"language-ts shiki shiki-themes vitesse-light vitesse-light","const hasH1InBody = computed(() => {\n  const body = props.doc.body as { children?: Array\u003C{ tag?: string }> } | undefined;\n  const children = body?.children;\n  if (!Array.isArray(children)) return false;\n  return children.some(node => node?.tag === \"h1\");\n});\n\nconst showAutoTitle = computed(() => {\n  if (!props.doc.title) return false;\n  if (props.doc.path?.startsWith(\"/takken/\")) return false; // takken は独自レイアウト\n  return !hasH1InBody.value;\n});\n","ts","",[19,134,135,165,234,254,289,332,338,345,363,392,436,453],{"__ignoreMap":132},[136,137,140,144,148,152,156,159,162],"span",{"class":138,"line":139},"line",1,[136,141,143],{"class":142},"stQ0i","const ",[136,145,147],{"class":146},"s4oTP","hasH1InBody",[136,149,151],{"class":150},"shFtX"," =",[136,153,155],{"class":154},"senZ8"," computed",[136,157,158],{"class":150},"(()",[136,160,161],{"class":150}," =>",[136,163,164],{"class":150}," {\n",[136,166,168,171,174,176,179,182,185,187,189,193,196,199,202,205,209,212,215,217,219,222,225,228,231],{"class":138,"line":167},2,[136,169,170],{"class":142},"  const ",[136,172,173],{"class":146},"body",[136,175,151],{"class":150},[136,177,178],{"class":146}," props",[136,180,181],{"class":150},".",[136,183,184],{"class":146},"doc",[136,186,181],{"class":150},[136,188,173],{"class":146},[136,190,192],{"class":191},"sHkkW"," as",[136,194,195],{"class":150}," {",[136,197,198],{"class":146}," children",[136,200,201],{"class":142},"?",[136,203,204],{"class":150},": ",[136,206,208],{"class":207},"sSkh3","Array",[136,210,211],{"class":150},"\u003C{ ",[136,213,214],{"class":146},"tag",[136,216,201],{"class":142},[136,218,204],{"class":150},[136,220,221],{"class":207},"string",[136,223,224],{"class":150}," }> }",[136,226,227],{"class":150}," |",[136,229,230],{"class":142}," undefined",[136,232,233],{"class":150},";\n",[136,235,237,239,242,244,247,250,252],{"class":138,"line":236},3,[136,238,170],{"class":142},[136,240,241],{"class":146},"children",[136,243,151],{"class":150},[136,245,246],{"class":146}," body",[136,248,249],{"class":150},"?.",[136,251,241],{"class":146},[136,253,233],{"class":150},[136,255,257,260,263,266,268,270,273,276,278,281,284,287],{"class":138,"line":256},4,[136,258,259],{"class":191},"  if",[136,261,262],{"class":150}," (",[136,264,265],{"class":142},"!",[136,267,208],{"class":146},[136,269,181],{"class":150},[136,271,272],{"class":154},"isArray",[136,274,275],{"class":150},"(",[136,277,241],{"class":146},[136,279,280],{"class":150},"))",[136,282,283],{"class":191}," return",[136,285,286],{"class":191}," false",[136,288,233],{"class":150},[136,290,292,295,297,299,302,304,307,309,312,314,316,319,323,327,329],{"class":138,"line":291},5,[136,293,294],{"class":191},"  return",[136,296,198],{"class":146},[136,298,181],{"class":150},[136,300,301],{"class":154},"some",[136,303,275],{"class":150},[136,305,306],{"class":146},"node",[136,308,161],{"class":150},[136,310,311],{"class":146}," node",[136,313,249],{"class":150},[136,315,214],{"class":146},[136,317,318],{"class":142}," === ",[136,320,322],{"class":321},"sMJiu","\"",[136,324,326],{"class":325},"sdGka","h1",[136,328,322],{"class":321},[136,330,331],{"class":150},");\n",[136,333,335],{"class":138,"line":334},6,[136,336,337],{"class":150},"});\n",[136,339,341],{"class":138,"line":340},7,[136,342,344],{"emptyLinePlaceholder":343},true,"\n",[136,346,348,350,353,355,357,359,361],{"class":138,"line":347},8,[136,349,143],{"class":142},[136,351,352],{"class":146},"showAutoTitle",[136,354,151],{"class":150},[136,356,155],{"class":154},[136,358,158],{"class":150},[136,360,161],{"class":150},[136,362,164],{"class":150},[136,364,366,368,370,372,375,377,379,381,383,386,388,390],{"class":138,"line":365},9,[136,367,259],{"class":191},[136,369,262],{"class":150},[136,371,265],{"class":142},[136,373,374],{"class":146},"props",[136,376,181],{"class":150},[136,378,184],{"class":146},[136,380,181],{"class":150},[136,382,32],{"class":146},[136,384,385],{"class":150},")",[136,387,283],{"class":191},[136,389,286],{"class":191},[136,391,233],{"class":150},[136,393,395,397,399,401,403,405,407,410,412,415,417,419,421,423,425,427,429,432],{"class":138,"line":394},10,[136,396,259],{"class":191},[136,398,262],{"class":150},[136,400,374],{"class":146},[136,402,181],{"class":150},[136,404,184],{"class":146},[136,406,181],{"class":150},[136,408,409],{"class":146},"path",[136,411,249],{"class":150},[136,413,414],{"class":154},"startsWith",[136,416,275],{"class":150},[136,418,322],{"class":321},[136,420,98],{"class":325},[136,422,322],{"class":321},[136,424,280],{"class":150},[136,426,283],{"class":191},[136,428,286],{"class":191},[136,430,431],{"class":150},";",[136,433,435],{"class":434},"sxvE3"," // takken は独自レイアウト\n",[136,437,439,441,444,446,448,451],{"class":138,"line":438},11,[136,440,294],{"class":191},[136,442,443],{"class":142}," !",[136,445,147],{"class":146},[136,447,181],{"class":150},[136,449,450],{"class":146},"value",[136,452,233],{"class":150},[136,454,456],{"class":138,"line":455},12,[136,457,337],{"class":150},[15,459,460],{},"テンプレ側は素直に 1 行追加するだけ。",[127,462,466],{"className":463,"code":464,"language":465,"meta":132,"style":132},"language-html shiki shiki-themes vitesse-light vitesse-light","\u003Ch1 v-if=\"showAutoTitle\" class=\"doc__title\">{{ doc.title }}\u003C/h1>\n","html",[19,467,468],{"__ignoreMap":132},[136,469,470,473,475,478,481,483,485,487,490,492,494,497,499,502,506,509,511],{"class":138,"line":139},[136,471,472],{"class":150},"\u003C",[136,474,326],{"class":191},[136,476,477],{"class":146}," v-if",[136,479,480],{"class":150},"=",[136,482,322],{"class":321},[136,484,352],{"class":325},[136,486,322],{"class":321},[136,488,489],{"class":146}," class",[136,491,480],{"class":150},[136,493,322],{"class":321},[136,495,496],{"class":325},"doc__title",[136,498,322],{"class":321},[136,500,501],{"class":150},">",[136,503,505],{"class":504},"sG7-3","{{ doc.title }}",[136,507,508],{"class":150},"\u003C/",[136,510,326],{"class":191},[136,512,513],{"class":150},">\n",[10,515,517],{"id":516},"なぜ","なぜ「",[326,519,521],{"id":520},"有無で分岐するのか"," 有無」で分岐するのか",[15,523,524,525,527,528,530],{},"frontmatter から無条件に ",[19,526,39],{}," を差し込むと、手書きで ",[19,529,77],{}," を入れていた古い記事は h1 が二重になる。SEO 上も「ページ内 h1 は 1 個」が安全側で、機械学習で SERP を組むクローラが混乱する余地を残したくない。",[15,532,533],{},"選択肢は 3 つあった。",[535,536,537,544,547],"ol",{},[538,539,540,541,543],"li",{},"既存記事を全件 grep して手書き ",[19,542,39],{}," を消す（書き手の意図を破壊する）",[538,545,546],{},"frontmatter にフラグを足して書き手に選ばせる（書き手の認知負荷を増やす）",[538,548,549],{},"本文 AST に h1 があるかをコンポーネント側で見て自動分岐する",[15,551,552],{},"3 を選んだ理由は、書き手は「タイトルを画面に出したい」しか考えていないからだ。表示の重複は描画側の責務であって、書き手に判定材料を持たせる話ではない。",[15,554,555,558,559,562,563,566],{},[19,556,557],{},"@nuxt/content"," がパース後に渡してくる ",[19,560,561],{},"doc.body.children"," は AST の浅い配列で、",[19,564,565],{},"children.some(node => node?.tag === \"h1\")"," だけで先頭付近の h1 が拾える。深い入れ子は h1 にならないのでこれで足りる。",[10,568,570],{"id":569},"ssr-と-csr-で同じ-ast-を見る","SSR と CSR で同じ AST を見る",[15,572,573,574,577,578,580,581,584,585,587],{},"SSR と CSR で参照ソースを変えると hydration mismatch が起きる。",[19,575,576],{},"props.doc.body"," は SSR の payload にも CSR の hydration 後にも同じ AST が乗っているので、両側で ",[19,579,147],{}," の戻り値が一致する。",[19,582,583],{},"onMounted"," 内で DOM を querySelector で走査して ",[19,586,39],{}," を探す案も浮かんだが、SSR 時に判定できず初回描画でタイトルが出ない/出るが瞬く挙動になる。AST 側を 1 ソースに固定して回避した。",[15,589,590,592,593,595,596,598,599,602],{},[19,591,98],{}," 配下だけは別件で ",[19,594,106],{}," 内に独自レイアウト分岐があり、そこでタイトル領域を別途持っているので ",[19,597,352],{}," から明示除外した。条件式に書いた ",[19,600,601],{},"path?.startsWith(\"/takken/\")"," は、将来 takken 側の構造が変わったときに grep で引っかかる場所にしておきたかった。",[10,604,605],{"id":605},"やってよかったこと",[607,608,609,612,618],"ul",{},[538,610,611],{},"引用記事を読み返したときに、ようやく「何の記事を読んでいるか」が見出しで分かるようになった",[538,613,614,615,617],{},"既存記事の手書き ",[19,616,39],{}," をひとつも壊さずに済んだ",[538,619,620,621,623,624,626],{},"ロジックを ",[19,622,51],{}," の上から 250 行付近に隣接させたので、後から ",[19,625,184],{}," 周りを触る人が判定意図を読み落としにくい",[10,628,629],{"id":629},"振り返り",[15,631,632,633,635],{},"「コンポーザブルに切り出すべきか」を毎回反射で考えると、再利用先のない関数を量産しがちになる。今回は判定対象が 1 コンポーネントに閉じていて、",[19,634,120],{}," の上から数十行で完結する規模だったので、外に出さずに済ませた。",[15,637,638,639,642],{},"書き手の意図を壊さない自動化は、書き手が考えるべきことを 1 つ減らす方向で設計する。フラグを増やす方に逃げると、書き手側に「毎回 ",[19,640,641],{},"display_title: true"," を付け忘れない」というルールが生まれて、半年後の自分が確実に踏む。AST を見て描画側で吸収する判断のほうが、書き手の脳内モデルを守れる。",[644,645,646],"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 .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 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 .sxvE3, html code.shiki .sxvE3{--shiki-default:#A0ADA0;--shiki-dark:#A0ADA0}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);}html pre.shiki code .sG7-3, html code.shiki .sG7-3{--shiki-default:#393A34;--shiki-dark:#393A34}",{"title":132,"searchDepth":167,"depth":167,"links":648},[649,650,651,652,653,654,655],{"id":12,"depth":167,"text":13},{"id":46,"depth":167,"text":46},{"id":81,"depth":167,"text":82},{"id":516,"depth":167,"text":517},{"id":569,"depth":167,"text":570},{"id":605,"depth":167,"text":605},{"id":629,"depth":167,"text":629},"dev","frontmatter の title が画面に出ず読みづらかった問題を、DocPage.vue 側で本文 AST の h1 有無を見て自動補完するロジックで潰した。単一描画点に閉じ込めた判断の記録。","md",{},null,"/docpage-auto-title-display","mdx-playground",false,"2026-06-29T00:00:00.000Z",{"title":5,"description":657},"2026-06/2026-06-29/docpage-auto-title-display",[668,669,670,671],"Nuxt","Vue","コンポーネント設計","DX","QR1HxD-ngVlEgCPy6ajRA3Yl0nHzx-DUXtYyHszb7JM",[],"https://log.eurekapu.com/og/blog/docpage-auto-title-display.png?v=2026-06-29T00%3A00%3A00.000Z&title=Nuxt%20Content%20%E3%81%AE%E8%A8%98%E4%BA%8B%E3%82%BF%E3%82%A4%E3%83%88%E3%83%AB%E3%81%8C%E6%9C%AC%E6%96%87%E3%81%AB%E5%87%BA%E3%81%AA%E3%81%84%E5%95%8F%E9%A1%8C%E3%82%92%20DocPage%20%E5%81%B4%E3%81%AE%E8%87%AA%E5%8B%95%E5%88%86%E5%B2%90%E3%81%A7%E8%A7%A3%E6%B6%88%E3%81%97%E3%81%9F&author=Kei%20Komatsu&sig=1937f7a856652ddc",1782885016885]