[{"data":1,"prerenderedAt":1806},["ShallowReactive",2],{"content-/markdown-copy-feature":3,"all-pages-for-dir":1785,"related-/markdown-copy-feature":1786,"og-image-/markdown-copy-feature":1805},{"id":4,"title":5,"body":6,"category":1770,"concepts":1770,"description":1771,"extension":629,"meta":1772,"navigation":466,"ogImage":1770,"path":1773,"project_name":1770,"published":1774,"publishedAt":1775,"seo":1776,"source":1770,"stem":1777,"tags":1778,"todo":1770,"unpublished":1774,"updatedAt":1775,"__hash__":1784},"pages/2025-12/2025-12-31/markdown-copy-feature.md","ブログ記事のマークダウンコピー機能を実装",{"type":7,"value":8,"toc":1752},"minimark",[9,12,16,20,23,34,37,107,110,115,296,302,324,328,781,786,812,816,1217,1221,1602,1605,1611,1614,1628,1631,1634,1638,1644,1664,1670,1674,1713,1717,1725,1735,1749],[10,11,5],"h1",{"id":5},[13,14,15],"h2",{"id":15},"概要",[17,18,19],"p",{},"ブログページにPC表示用の「共有」ボタンと「MD（マークダウンコピー）」ボタンを追加した。",[13,21,22],{"id":22},"アーキテクチャ",[24,25,30],"pre",{"className":26,"code":28,"language":29},[27],"language-text","┌─────────────────────────────────────────────────────────────┐\n│                    ビルド時                                  │\n│  content/*.md → nuxt.config.ts → dist/_raw/*.md            │\n│                  (frontmatter除去)                          │\n└─────────────────────────────────────────────────────────────┘\n                              ↓\n┌─────────────────────────────────────────────────────────────┐\n│                    実行時                                    │\n│  本番環境: fetch('/_raw/...md') → 静的ファイル              │\n│  開発環境: $fetch('/api/markdown/...') → Node.js API        │\n└─────────────────────────────────────────────────────────────┘\n","text",[31,32,28],"code",{"__ignoreMap":33},"",[13,35,36],{"id":36},"実装ファイル構成",[38,39,40,53],"table",{},[41,42,43],"thead",{},[44,45,46,50],"tr",{},[47,48,49],"th",{},"ファイル",[47,51,52],{},"役割",[54,55,56,67,77,87,97],"tbody",{},[44,57,58,64],{},[59,60,61],"td",{},[31,62,63],{},"lib/markdown.ts",[59,65,66],{},"frontmatter除去ユーティリティ（共通）",[44,68,69,74],{},[59,70,71],{},[31,72,73],{},"nuxt.config.ts",[59,75,76],{},"ビルド時にマークダウンを静的ファイルとしてコピー",[44,78,79,84],{},[59,80,81],{},[31,82,83],{},"server/api/markdown/[...path].get.ts",[59,85,86],{},"開発環境用API（本番では使用不可）",[44,88,89,94],{},[59,90,91],{},[31,92,93],{},"app/components/DocPage.vue",[59,95,96],{},"コピーボタンUI・ロジック",[44,98,99,104],{},[59,100,101],{},[31,102,103],{},"tests/markdown.test.ts",[59,105,106],{},"ユニットテスト（14テスト）",[13,108,109],{"id":109},"詳細実装",[111,112,114],"h3",{"id":113},"_1-frontmatter除去ユーティリティlibmarkdownts","1. frontmatter除去ユーティリティ（lib/markdown.ts）",[24,116,120],{"className":117,"code":118,"language":119,"meta":33,"style":33},"language-typescript shiki shiki-themes vitesse-light vitesse-light","export function removeFrontmatter(content: string): string {\n  // frontmatterは --- で囲まれた部分（YAMLフォーマット）\n  // - 開始タグ: コンテンツの先頭で \"---\" + 改行\n  // - 終了タグ: 改行の後に \"---\"（行頭に来る）\n  const frontmatterRegex = /^---\\r?\\n[\\s\\S]*?\\r?\\n---(?:\\r?\\n)?/;\n  return content.replace(frontmatterRegex, '').trim();\n}\n","typescript",[31,121,122,163,170,176,182,256,290],{"__ignoreMap":33},[123,124,127,131,135,139,143,147,150,154,157,160],"span",{"class":125,"line":126},"line",1,[123,128,130],{"class":129},"sHkkW","export",[123,132,134],{"class":133},"stQ0i"," function",[123,136,138],{"class":137},"senZ8"," removeFrontmatter",[123,140,142],{"class":141},"shFtX","(",[123,144,146],{"class":145},"s4oTP","content",[123,148,149],{"class":141},": ",[123,151,153],{"class":152},"sSkh3","string",[123,155,156],{"class":141},"):",[123,158,159],{"class":152}," string",[123,161,162],{"class":141}," {\n",[123,164,166],{"class":125,"line":165},2,[123,167,169],{"class":168},"sxvE3","  // frontmatterは --- で囲まれた部分（YAMLフォーマット）\n",[123,171,173],{"class":125,"line":172},3,[123,174,175],{"class":168},"  // - 開始タグ: コンテンツの先頭で \"---\" + 改行\n",[123,177,179],{"class":125,"line":178},4,[123,180,181],{"class":168},"  // - 終了タグ: 改行の後に \"---\"（行頭に来る）\n",[123,183,185,188,191,194,198,201,205,209,213,216,219,222,225,228,230,232,234,236,239,241,243,245,248,250,253],{"class":125,"line":184},5,[123,186,187],{"class":133},"  const ",[123,189,190],{"class":145},"frontmatterRegex",[123,192,193],{"class":141}," =",[123,195,197],{"class":196},"sMJiu"," /",[123,199,200],{"class":129},"^",[123,202,204],{"class":203},"sLkVR","---",[123,206,208],{"class":207},"she3A","\\r",[123,210,212],{"class":211},"sM54T","?",[123,214,215],{"class":207},"\\n",[123,217,218],{"class":141},"[",[123,220,221],{"class":207},"\\s\\S",[123,223,224],{"class":141},"]",[123,226,227],{"class":211},"*?",[123,229,208],{"class":207},[123,231,212],{"class":211},[123,233,215],{"class":207},[123,235,204],{"class":203},[123,237,238],{"class":141},"(?:",[123,240,208],{"class":207},[123,242,212],{"class":211},[123,244,215],{"class":207},[123,246,247],{"class":141},")",[123,249,212],{"class":211},[123,251,252],{"class":196},"/",[123,254,255],{"class":141},";\n",[123,257,259,262,265,268,271,273,275,278,281,284,287],{"class":125,"line":258},6,[123,260,261],{"class":129},"  return",[123,263,264],{"class":145}," content",[123,266,267],{"class":141},".",[123,269,270],{"class":137},"replace",[123,272,142],{"class":141},[123,274,190],{"class":145},[123,276,277],{"class":141},",",[123,279,280],{"class":196}," ''",[123,282,283],{"class":141},").",[123,285,286],{"class":137},"trim",[123,288,289],{"class":141},"();\n",[123,291,293],{"class":125,"line":292},7,[123,294,295],{"class":141},"}\n",[17,297,298],{},[299,300,301],"strong",{},"テスト済みエッジケース:",[303,304,305,309,315,318,321],"ul",{},[306,307,308],"li",{},"CRLF/LF両対応",[306,310,311,312,314],{},"frontmatter内に",[31,313,204],{},"を含む値",[306,316,317],{},"閉じタグ後に改行なしで本文開始",[306,319,320],{},"frontmatterがないコンテンツ",[306,322,323],{},"閉じタグが行頭にない場合（マッチしない）",[111,325,327],{"id":326},"_2-ビルド時の静的ファイル生成nuxtconfigts","2. ビルド時の静的ファイル生成（nuxt.config.ts）",[24,329,331],{"className":117,"code":330,"language":119,"meta":33,"style":33},"hooks: {\n  'nitro:build:public-assets': async (nitro) => {\n    const contentDir = resolve(__dirname, 'content');\n    const publicDir = resolve(nitro.options.output.publicDir);\n    const rawContentDir = join(publicDir, '_raw');\n\n    async function copyMarkdownFiles(dir: string, relativePath = '') {\n      const entries = await readdir(dir, { withFileTypes: true });\n\n      for (const entry of entries) {\n        if (entry.isDirectory()) {\n          await copyMarkdownFiles(sourcePath, currentRelativePath);\n        } else if (/\\.md$/i.test(entry.name)) {\n          const content = await readFile(sourcePath, 'utf-8');\n          const cleanedContent = removeFrontmatter(content);\n          await writeFile(targetPath, cleanedContent, 'utf-8');\n        }\n      }\n    }\n\n    await copyMarkdownFiles(contentDir);\n  }\n}\n",[31,332,333,343,374,404,435,462,468,500,538,543,567,587,607,657,687,705,733,739,745,751,756,770,776],{"__ignoreMap":33},[123,334,335,338,341],{"class":125,"line":126},[123,336,337],{"class":137},"hooks",[123,339,340],{"class":141},":",[123,342,162],{"class":141},[123,344,345,348,352,355,358,361,364,367,369,372],{"class":125,"line":165},[123,346,347],{"class":196},"  '",[123,349,351],{"class":350},"sdGka","nitro:build:public-assets",[123,353,354],{"class":196},"'",[123,356,149],{"class":357},"sG7-3",[123,359,360],{"class":133},"async",[123,362,363],{"class":141}," (",[123,365,366],{"class":145},"nitro",[123,368,247],{"class":141},[123,370,371],{"class":141}," =>",[123,373,162],{"class":141},[123,375,376,379,382,384,387,389,392,394,397,399,401],{"class":125,"line":172},[123,377,378],{"class":133},"    const ",[123,380,381],{"class":145},"contentDir",[123,383,193],{"class":141},[123,385,386],{"class":137}," resolve",[123,388,142],{"class":141},[123,390,391],{"class":145},"__dirname",[123,393,277],{"class":141},[123,395,396],{"class":196}," '",[123,398,146],{"class":350},[123,400,354],{"class":196},[123,402,403],{"class":141},");\n",[123,405,406,408,411,413,415,417,419,421,424,426,429,431,433],{"class":125,"line":178},[123,407,378],{"class":133},[123,409,410],{"class":145},"publicDir",[123,412,193],{"class":141},[123,414,386],{"class":137},[123,416,142],{"class":141},[123,418,366],{"class":145},[123,420,267],{"class":141},[123,422,423],{"class":145},"options",[123,425,267],{"class":141},[123,427,428],{"class":145},"output",[123,430,267],{"class":141},[123,432,410],{"class":145},[123,434,403],{"class":141},[123,436,437,439,442,444,447,449,451,453,455,458,460],{"class":125,"line":184},[123,438,378],{"class":133},[123,440,441],{"class":145},"rawContentDir",[123,443,193],{"class":141},[123,445,446],{"class":137}," join",[123,448,142],{"class":141},[123,450,410],{"class":145},[123,452,277],{"class":141},[123,454,396],{"class":196},[123,456,457],{"class":350},"_raw",[123,459,354],{"class":196},[123,461,403],{"class":141},[123,463,464],{"class":125,"line":258},[123,465,467],{"emptyLinePlaceholder":466},true,"\n",[123,469,470,473,475,478,480,483,485,487,489,492,494,496,498],{"class":125,"line":292},[123,471,472],{"class":133},"    async",[123,474,134],{"class":133},[123,476,477],{"class":137}," copyMarkdownFiles",[123,479,142],{"class":141},[123,481,482],{"class":145},"dir",[123,484,149],{"class":141},[123,486,153],{"class":152},[123,488,277],{"class":141},[123,490,491],{"class":145}," relativePath",[123,493,193],{"class":141},[123,495,280],{"class":196},[123,497,247],{"class":141},[123,499,162],{"class":141},[123,501,503,506,509,511,514,517,519,521,523,526,530,532,535],{"class":125,"line":502},8,[123,504,505],{"class":133},"      const ",[123,507,508],{"class":145},"entries",[123,510,193],{"class":141},[123,512,513],{"class":129}," await",[123,515,516],{"class":137}," readdir",[123,518,142],{"class":141},[123,520,482],{"class":145},[123,522,277],{"class":141},[123,524,525],{"class":141}," { ",[123,527,529],{"class":528},"sz8Xr","withFileTypes",[123,531,149],{"class":141},[123,533,534],{"class":129},"true",[123,536,537],{"class":141}," });\n",[123,539,541],{"class":125,"line":540},9,[123,542,467],{"emptyLinePlaceholder":466},[123,544,546,549,551,554,557,560,563,565],{"class":125,"line":545},10,[123,547,548],{"class":129},"      for",[123,550,363],{"class":141},[123,552,553],{"class":133},"const ",[123,555,556],{"class":145},"entry",[123,558,559],{"class":133}," of",[123,561,562],{"class":145}," entries",[123,564,247],{"class":141},[123,566,162],{"class":141},[123,568,570,573,575,577,579,582,585],{"class":125,"line":569},11,[123,571,572],{"class":129},"        if",[123,574,363],{"class":141},[123,576,556],{"class":145},[123,578,267],{"class":141},[123,580,581],{"class":137},"isDirectory",[123,583,584],{"class":141},"())",[123,586,162],{"class":141},[123,588,590,593,595,597,600,602,605],{"class":125,"line":589},12,[123,591,592],{"class":129},"          await",[123,594,477],{"class":137},[123,596,142],{"class":141},[123,598,599],{"class":145},"sourcePath",[123,601,277],{"class":141},[123,603,604],{"class":145}," currentRelativePath",[123,606,403],{"class":141},[123,608,610,613,616,619,621,623,627,630,633,635,638,640,643,645,647,649,652,655],{"class":125,"line":609},13,[123,611,612],{"class":141},"        }",[123,614,615],{"class":129}," else",[123,617,618],{"class":129}," if",[123,620,363],{"class":141},[123,622,252],{"class":196},[123,624,626],{"class":625},"svWSF","\\.",[123,628,629],{"class":203},"md",[123,631,632],{"class":129},"$",[123,634,252],{"class":196},[123,636,637],{"class":129},"i",[123,639,267],{"class":141},[123,641,642],{"class":137},"test",[123,644,142],{"class":141},[123,646,556],{"class":145},[123,648,267],{"class":141},[123,650,651],{"class":145},"name",[123,653,654],{"class":141},"))",[123,656,162],{"class":141},[123,658,660,663,665,667,669,672,674,676,678,680,683,685],{"class":125,"line":659},14,[123,661,662],{"class":133},"          const ",[123,664,146],{"class":145},[123,666,193],{"class":141},[123,668,513],{"class":129},[123,670,671],{"class":137}," readFile",[123,673,142],{"class":141},[123,675,599],{"class":145},[123,677,277],{"class":141},[123,679,396],{"class":196},[123,681,682],{"class":350},"utf-8",[123,684,354],{"class":196},[123,686,403],{"class":141},[123,688,690,692,695,697,699,701,703],{"class":125,"line":689},15,[123,691,662],{"class":133},[123,693,694],{"class":145},"cleanedContent",[123,696,193],{"class":141},[123,698,138],{"class":137},[123,700,142],{"class":141},[123,702,146],{"class":145},[123,704,403],{"class":141},[123,706,708,710,713,715,718,720,723,725,727,729,731],{"class":125,"line":707},16,[123,709,592],{"class":129},[123,711,712],{"class":137}," writeFile",[123,714,142],{"class":141},[123,716,717],{"class":145},"targetPath",[123,719,277],{"class":141},[123,721,722],{"class":145}," cleanedContent",[123,724,277],{"class":141},[123,726,396],{"class":196},[123,728,682],{"class":350},[123,730,354],{"class":196},[123,732,403],{"class":141},[123,734,736],{"class":125,"line":735},17,[123,737,738],{"class":141},"        }\n",[123,740,742],{"class":125,"line":741},18,[123,743,744],{"class":141},"      }\n",[123,746,748],{"class":125,"line":747},19,[123,749,750],{"class":141},"    }\n",[123,752,754],{"class":125,"line":753},20,[123,755,467],{"emptyLinePlaceholder":466},[123,757,759,762,764,766,768],{"class":125,"line":758},21,[123,760,761],{"class":129},"    await",[123,763,477],{"class":137},[123,765,142],{"class":141},[123,767,381],{"class":145},[123,769,403],{"class":141},[123,771,773],{"class":125,"line":772},22,[123,774,775],{"class":141},"  }\n",[123,777,779],{"class":125,"line":778},23,[123,780,295],{"class":141},[17,782,783],{},[299,784,785],{},"エラーハンドリング方針:",[303,787,788,794,800,806],{},[306,789,790,793],{},[299,791,792],{},"ディレクトリ読み取り失敗",": ビルド中断（致命的エラー）",[306,795,796,799],{},[299,797,798],{},"個別ファイル処理失敗",": ログして続行",[306,801,802,805],{},[299,803,804],{},"マークダウンコピー全体失敗",": ビルド中断（コピー機能が動作しなくなるため）",[306,807,808,811],{},[299,809,810],{},"画像コピー失敗",": ログして続行（画像がなくても記事は読める）",[111,813,815],{"id":814},"_3-マークダウン取得ヘルパーdocpagevue","3. マークダウン取得ヘルパー（DocPage.vue）",[24,817,819],{"className":117,"code":818,"language":119,"meta":33,"style":33},"const fetchMarkdownContent = async (path: string): Promise\u003Cstring | null> => {\n  // 試行するパスのリスト（優先順）\n  const pathsToTry = [\n    `/_raw${path}.md`,      // 静的ファイル（本番環境）\n    `/_raw${path}/index.md` // index.md形式\n  ];\n\n  // 静的ファイルを順番に試す\n  for (const mdPath of pathsToTry) {\n    try {\n      const response = await fetch(mdPath);\n      if (response.ok) {\n        return await response.text();\n      }\n    } catch {\n      // 次のパスを試す\n    }\n  }\n\n  // 開発環境用: APIにフォールバック\n  try {\n    const apiResponse = await $fetch\u003C{ content?: string }>(`/api/markdown${path}`);\n    if (apiResponse?.content) {\n      return apiResponse.content;\n    }\n  } catch (error) {\n    console.error('API fallback failed:', error);\n  }\n\n  return null;\n};\n",[31,820,821,865,870,882,909,929,934,938,943,964,971,991,1009,1025,1029,1039,1044,1048,1052,1056,1061,1068,1111,1129,1144,1149,1166,1192,1197,1202,1211],{"__ignoreMap":33},[123,822,823,825,828,830,833,835,838,840,842,844,847,850,852,855,858,861,863],{"class":125,"line":126},[123,824,553],{"class":133},[123,826,827],{"class":137},"fetchMarkdownContent",[123,829,193],{"class":141},[123,831,832],{"class":133}," async ",[123,834,142],{"class":141},[123,836,837],{"class":145},"path",[123,839,149],{"class":141},[123,841,153],{"class":152},[123,843,156],{"class":141},[123,845,846],{"class":152}," Promise",[123,848,849],{"class":141},"\u003C",[123,851,153],{"class":152},[123,853,854],{"class":141}," |",[123,856,857],{"class":133}," null",[123,859,860],{"class":141},">",[123,862,371],{"class":141},[123,864,162],{"class":141},[123,866,867],{"class":125,"line":165},[123,868,869],{"class":168},"  // 試行するパスのリスト（優先順）\n",[123,871,872,874,877,879],{"class":125,"line":172},[123,873,187],{"class":133},[123,875,876],{"class":145},"pathsToTry",[123,878,193],{"class":141},[123,880,881],{"class":141}," [\n",[123,883,884,887,890,893,895,898,901,904,906],{"class":125,"line":178},[123,885,886],{"class":196},"    `",[123,888,889],{"class":350},"/_raw",[123,891,892],{"class":129},"${",[123,894,837],{"class":350},[123,896,897],{"class":129},"}",[123,899,900],{"class":350},".md",[123,902,903],{"class":196},"`",[123,905,277],{"class":141},[123,907,908],{"class":168},"      // 静的ファイル（本番環境）\n",[123,910,911,913,915,917,919,921,924,926],{"class":125,"line":184},[123,912,886],{"class":196},[123,914,889],{"class":350},[123,916,892],{"class":129},[123,918,837],{"class":350},[123,920,897],{"class":129},[123,922,923],{"class":350},"/index.md",[123,925,903],{"class":196},[123,927,928],{"class":168}," // index.md形式\n",[123,930,931],{"class":125,"line":258},[123,932,933],{"class":141},"  ];\n",[123,935,936],{"class":125,"line":292},[123,937,467],{"emptyLinePlaceholder":466},[123,939,940],{"class":125,"line":502},[123,941,942],{"class":168},"  // 静的ファイルを順番に試す\n",[123,944,945,948,950,952,955,958,960,962],{"class":125,"line":540},[123,946,947],{"class":129},"  for",[123,949,363],{"class":141},[123,951,553],{"class":133},[123,953,954],{"class":145},"mdPath",[123,956,957],{"class":133}," of ",[123,959,876],{"class":145},[123,961,247],{"class":141},[123,963,162],{"class":141},[123,965,966,969],{"class":125,"line":545},[123,967,968],{"class":129},"    try",[123,970,162],{"class":141},[123,972,973,975,978,980,982,985,987,989],{"class":125,"line":569},[123,974,505],{"class":133},[123,976,977],{"class":145},"response",[123,979,193],{"class":141},[123,981,513],{"class":129},[123,983,984],{"class":137}," fetch",[123,986,142],{"class":141},[123,988,954],{"class":145},[123,990,403],{"class":141},[123,992,993,996,998,1000,1002,1005,1007],{"class":125,"line":589},[123,994,995],{"class":129},"      if",[123,997,363],{"class":141},[123,999,977],{"class":145},[123,1001,267],{"class":141},[123,1003,1004],{"class":145},"ok",[123,1006,247],{"class":141},[123,1008,162],{"class":141},[123,1010,1011,1014,1016,1019,1021,1023],{"class":125,"line":609},[123,1012,1013],{"class":129},"        return",[123,1015,513],{"class":129},[123,1017,1018],{"class":145}," response",[123,1020,267],{"class":141},[123,1022,29],{"class":137},[123,1024,289],{"class":141},[123,1026,1027],{"class":125,"line":659},[123,1028,744],{"class":141},[123,1030,1031,1034,1037],{"class":125,"line":689},[123,1032,1033],{"class":141},"    }",[123,1035,1036],{"class":129}," catch",[123,1038,162],{"class":141},[123,1040,1041],{"class":125,"line":707},[123,1042,1043],{"class":168},"      // 次のパスを試す\n",[123,1045,1046],{"class":125,"line":735},[123,1047,750],{"class":141},[123,1049,1050],{"class":125,"line":741},[123,1051,775],{"class":141},[123,1053,1054],{"class":125,"line":747},[123,1055,467],{"emptyLinePlaceholder":466},[123,1057,1058],{"class":125,"line":753},[123,1059,1060],{"class":168},"  // 開発環境用: APIにフォールバック\n",[123,1062,1063,1066],{"class":125,"line":758},[123,1064,1065],{"class":129},"  try",[123,1067,162],{"class":141},[123,1069,1070,1072,1075,1077,1079,1082,1085,1087,1089,1091,1093,1096,1098,1101,1103,1105,1107,1109],{"class":125,"line":772},[123,1071,378],{"class":133},[123,1073,1074],{"class":145},"apiResponse",[123,1076,193],{"class":141},[123,1078,513],{"class":129},[123,1080,1081],{"class":137}," $fetch",[123,1083,1084],{"class":141},"\u003C{",[123,1086,264],{"class":145},[123,1088,212],{"class":133},[123,1090,149],{"class":141},[123,1092,153],{"class":152},[123,1094,1095],{"class":141}," }>(",[123,1097,903],{"class":196},[123,1099,1100],{"class":350},"/api/markdown",[123,1102,892],{"class":129},[123,1104,837],{"class":350},[123,1106,897],{"class":129},[123,1108,903],{"class":196},[123,1110,403],{"class":141},[123,1112,1113,1116,1118,1120,1123,1125,1127],{"class":125,"line":778},[123,1114,1115],{"class":129},"    if",[123,1117,363],{"class":141},[123,1119,1074],{"class":145},[123,1121,1122],{"class":141},"?.",[123,1124,146],{"class":145},[123,1126,247],{"class":141},[123,1128,162],{"class":141},[123,1130,1132,1135,1138,1140,1142],{"class":125,"line":1131},24,[123,1133,1134],{"class":129},"      return",[123,1136,1137],{"class":145}," apiResponse",[123,1139,267],{"class":141},[123,1141,146],{"class":145},[123,1143,255],{"class":141},[123,1145,1147],{"class":125,"line":1146},25,[123,1148,750],{"class":141},[123,1150,1152,1155,1157,1159,1162,1164],{"class":125,"line":1151},26,[123,1153,1154],{"class":141},"  }",[123,1156,1036],{"class":129},[123,1158,363],{"class":141},[123,1160,1161],{"class":145},"error",[123,1163,247],{"class":141},[123,1165,162],{"class":141},[123,1167,1169,1172,1174,1176,1178,1180,1183,1185,1187,1190],{"class":125,"line":1168},27,[123,1170,1171],{"class":145},"    console",[123,1173,267],{"class":141},[123,1175,1161],{"class":137},[123,1177,142],{"class":141},[123,1179,354],{"class":196},[123,1181,1182],{"class":350},"API fallback failed:",[123,1184,354],{"class":196},[123,1186,277],{"class":141},[123,1188,1189],{"class":145}," error",[123,1191,403],{"class":141},[123,1193,1195],{"class":125,"line":1194},28,[123,1196,775],{"class":141},[123,1198,1200],{"class":125,"line":1199},29,[123,1201,467],{"emptyLinePlaceholder":466},[123,1203,1205,1207,1209],{"class":125,"line":1204},30,[123,1206,261],{"class":129},[123,1208,857],{"class":133},[123,1210,255],{"class":141},[123,1212,1214],{"class":125,"line":1213},31,[123,1215,1216],{"class":141},"};\n",[111,1218,1220],{"id":1219},"_4-クリップボードコピーdocpagevue","4. クリップボードコピー（DocPage.vue）",[24,1222,1224],{"className":117,"code":1223,"language":119,"meta":33,"style":33},"const copyToClipboard = async (text: string): Promise\u003Cboolean> => {\n  // まずClipboard APIを試す（HTTPS環境で動作）\n  if (navigator.clipboard?.writeText) {\n    try {\n      await navigator.clipboard.writeText(text);\n      return true;\n    } catch {\n      // フォールバックに進む\n    }\n  }\n\n  // フォールバック: textarea要素を使用（HTTP環境でも動作）\n  const textarea = document.createElement('textarea');\n  textarea.value = text;\n  textarea.style.position = 'fixed';\n  textarea.style.left = '-9999px';\n  document.body.appendChild(textarea);\n  textarea.focus();\n  textarea.select();\n\n  try {\n    const success = document.execCommand('copy');\n    document.body.removeChild(textarea);\n    return success;\n  } catch {\n    document.body.removeChild(textarea);\n    return false;\n  }\n};\n",[31,1225,1226,1260,1265,1289,1295,1317,1326,1334,1339,1343,1347,1351,1356,1383,1400,1425,1449,1470,1481,1492,1496,1502,1529,1549,1559,1567,1585,1594,1598],{"__ignoreMap":33},[123,1227,1228,1230,1233,1235,1237,1239,1241,1243,1245,1247,1249,1251,1254,1256,1258],{"class":125,"line":126},[123,1229,553],{"class":133},[123,1231,1232],{"class":137},"copyToClipboard",[123,1234,193],{"class":141},[123,1236,832],{"class":133},[123,1238,142],{"class":141},[123,1240,29],{"class":145},[123,1242,149],{"class":141},[123,1244,153],{"class":152},[123,1246,156],{"class":141},[123,1248,846],{"class":152},[123,1250,849],{"class":141},[123,1252,1253],{"class":152},"boolean",[123,1255,860],{"class":141},[123,1257,371],{"class":141},[123,1259,162],{"class":141},[123,1261,1262],{"class":125,"line":165},[123,1263,1264],{"class":168},"  // まずClipboard APIを試す（HTTPS環境で動作）\n",[123,1266,1267,1270,1272,1275,1277,1280,1282,1285,1287],{"class":125,"line":172},[123,1268,1269],{"class":129},"  if",[123,1271,363],{"class":141},[123,1273,1274],{"class":145},"navigator",[123,1276,267],{"class":141},[123,1278,1279],{"class":145},"clipboard",[123,1281,1122],{"class":141},[123,1283,1284],{"class":145},"writeText",[123,1286,247],{"class":141},[123,1288,162],{"class":141},[123,1290,1291,1293],{"class":125,"line":178},[123,1292,968],{"class":129},[123,1294,162],{"class":141},[123,1296,1297,1300,1303,1305,1307,1309,1311,1313,1315],{"class":125,"line":184},[123,1298,1299],{"class":129},"      await",[123,1301,1302],{"class":145}," navigator",[123,1304,267],{"class":141},[123,1306,1279],{"class":145},[123,1308,267],{"class":141},[123,1310,1284],{"class":137},[123,1312,142],{"class":141},[123,1314,29],{"class":145},[123,1316,403],{"class":141},[123,1318,1319,1321,1324],{"class":125,"line":258},[123,1320,1134],{"class":129},[123,1322,1323],{"class":129}," true",[123,1325,255],{"class":141},[123,1327,1328,1330,1332],{"class":125,"line":292},[123,1329,1033],{"class":141},[123,1331,1036],{"class":129},[123,1333,162],{"class":141},[123,1335,1336],{"class":125,"line":502},[123,1337,1338],{"class":168},"      // フォールバックに進む\n",[123,1340,1341],{"class":125,"line":540},[123,1342,750],{"class":141},[123,1344,1345],{"class":125,"line":545},[123,1346,775],{"class":141},[123,1348,1349],{"class":125,"line":569},[123,1350,467],{"emptyLinePlaceholder":466},[123,1352,1353],{"class":125,"line":589},[123,1354,1355],{"class":168},"  // フォールバック: textarea要素を使用（HTTP環境でも動作）\n",[123,1357,1358,1360,1363,1365,1368,1370,1373,1375,1377,1379,1381],{"class":125,"line":609},[123,1359,187],{"class":133},[123,1361,1362],{"class":145},"textarea",[123,1364,193],{"class":141},[123,1366,1367],{"class":145}," document",[123,1369,267],{"class":141},[123,1371,1372],{"class":137},"createElement",[123,1374,142],{"class":141},[123,1376,354],{"class":196},[123,1378,1362],{"class":350},[123,1380,354],{"class":196},[123,1382,403],{"class":141},[123,1384,1385,1388,1390,1393,1395,1398],{"class":125,"line":659},[123,1386,1387],{"class":145},"  textarea",[123,1389,267],{"class":141},[123,1391,1392],{"class":145},"value",[123,1394,193],{"class":141},[123,1396,1397],{"class":145}," text",[123,1399,255],{"class":141},[123,1401,1402,1404,1406,1409,1411,1414,1416,1418,1421,1423],{"class":125,"line":689},[123,1403,1387],{"class":145},[123,1405,267],{"class":141},[123,1407,1408],{"class":145},"style",[123,1410,267],{"class":141},[123,1412,1413],{"class":145},"position",[123,1415,193],{"class":141},[123,1417,396],{"class":196},[123,1419,1420],{"class":350},"fixed",[123,1422,354],{"class":196},[123,1424,255],{"class":141},[123,1426,1427,1429,1431,1433,1435,1438,1440,1442,1445,1447],{"class":125,"line":707},[123,1428,1387],{"class":145},[123,1430,267],{"class":141},[123,1432,1408],{"class":145},[123,1434,267],{"class":141},[123,1436,1437],{"class":145},"left",[123,1439,193],{"class":141},[123,1441,396],{"class":196},[123,1443,1444],{"class":350},"-9999px",[123,1446,354],{"class":196},[123,1448,255],{"class":141},[123,1450,1451,1454,1456,1459,1461,1464,1466,1468],{"class":125,"line":735},[123,1452,1453],{"class":145},"  document",[123,1455,267],{"class":141},[123,1457,1458],{"class":145},"body",[123,1460,267],{"class":141},[123,1462,1463],{"class":137},"appendChild",[123,1465,142],{"class":141},[123,1467,1362],{"class":145},[123,1469,403],{"class":141},[123,1471,1472,1474,1476,1479],{"class":125,"line":741},[123,1473,1387],{"class":145},[123,1475,267],{"class":141},[123,1477,1478],{"class":137},"focus",[123,1480,289],{"class":141},[123,1482,1483,1485,1487,1490],{"class":125,"line":747},[123,1484,1387],{"class":145},[123,1486,267],{"class":141},[123,1488,1489],{"class":137},"select",[123,1491,289],{"class":141},[123,1493,1494],{"class":125,"line":753},[123,1495,467],{"emptyLinePlaceholder":466},[123,1497,1498,1500],{"class":125,"line":758},[123,1499,1065],{"class":129},[123,1501,162],{"class":141},[123,1503,1504,1506,1509,1511,1513,1515,1518,1520,1522,1525,1527],{"class":125,"line":772},[123,1505,378],{"class":133},[123,1507,1508],{"class":145},"success",[123,1510,193],{"class":141},[123,1512,1367],{"class":145},[123,1514,267],{"class":141},[123,1516,1517],{"class":137},"execCommand",[123,1519,142],{"class":141},[123,1521,354],{"class":196},[123,1523,1524],{"class":350},"copy",[123,1526,354],{"class":196},[123,1528,403],{"class":141},[123,1530,1531,1534,1536,1538,1540,1543,1545,1547],{"class":125,"line":778},[123,1532,1533],{"class":145},"    document",[123,1535,267],{"class":141},[123,1537,1458],{"class":145},[123,1539,267],{"class":141},[123,1541,1542],{"class":137},"removeChild",[123,1544,142],{"class":141},[123,1546,1362],{"class":145},[123,1548,403],{"class":141},[123,1550,1551,1554,1557],{"class":125,"line":1131},[123,1552,1553],{"class":129},"    return",[123,1555,1556],{"class":145}," success",[123,1558,255],{"class":141},[123,1560,1561,1563,1565],{"class":125,"line":1146},[123,1562,1154],{"class":141},[123,1564,1036],{"class":129},[123,1566,162],{"class":141},[123,1568,1569,1571,1573,1575,1577,1579,1581,1583],{"class":125,"line":1151},[123,1570,1533],{"class":145},[123,1572,267],{"class":141},[123,1574,1458],{"class":145},[123,1576,267],{"class":141},[123,1578,1542],{"class":137},[123,1580,142],{"class":141},[123,1582,1362],{"class":145},[123,1584,403],{"class":141},[123,1586,1587,1589,1592],{"class":125,"line":1168},[123,1588,1553],{"class":129},[123,1590,1591],{"class":129}," false",[123,1593,255],{"class":141},[123,1595,1596],{"class":125,"line":1194},[123,1597,775],{"class":141},[123,1599,1600],{"class":125,"line":1199},[123,1601,1216],{"class":141},[13,1603,1604],{"id":1604},"ビルド後の構造",[24,1606,1609],{"className":1607,"code":1608,"language":29},[27],"dist/\n├── _raw/                    ← マークダウン（frontmatter除去済み）\n│   ├── 2025-12-31/\n│   │   ├── markdown-copy-feature.md\n│   │   └── ...\n│   └── ...\n├── 2025-12-31/              ← プリレンダリングされたHTML\n│   ├── markdown-copy-feature.html\n│   └── ...\n└── ...\n",[31,1610,1608],{"__ignoreMap":33},[13,1612,1613],{"id":1613},"動作確認",[303,1615,1616,1619,1622,1625],{},[306,1617,1618],{},"PC表示: ヘッダー右上に「共有」「MD」ボタンが表示される",[306,1620,1621],{},"MDボタンクリック: frontmatterを除いた本文がクリップボードにコピーされる",[306,1623,1624],{},"トースト通知: 「マークダウンをコピーしました」と表示",[306,1626,1627],{},"モバイル: ボタンは非表示（MobileFloatingButtonsを使用）",[1629,1630],"hr",{},[13,1632,1633],{"id":1633},"バグ修正履歴",[111,1635,1637],{"id":1636},"本番環境cloudflare-pagesで動作しない問題","本番環境（Cloudflare Pages）で動作しない問題",[17,1639,1640,1643],{},[299,1641,1642],{},"問題:"," ローカル開発環境では正常に動作するが本番環境では「コピーに失敗しました」と表示",[17,1645,1646,1649,1650,1652,1653,1656,1657],{},[299,1647,1648],{},"原因:"," ",[31,1651,83],{}," でNode.jsの ",[31,1654,1655],{},"fs/promises"," モジュールを使用。",[299,1658,1659,1660,1663],{},"Cloudflare WorkersはNode.jsの ",[31,1661,1662],{},"fs"," モジュールをサポートしていない",[17,1665,1666,1669],{},[299,1667,1668],{},"解決策:"," ビルド時に静的ファイルとしてコピーする方式に変更",[111,1671,1673],{"id":1672},"prレビュー対応","PRレビュー対応",[1675,1676,1677,1689,1695,1701,1707],"ol",{},[306,1678,1679,149,1682,1685,1686,1688],{},[299,1680,1681],{},"コード重複解消",[31,1683,1684],{},"removeFrontmatter","を",[31,1687,63],{},"に抽出",[306,1690,1691,1694],{},[299,1692,1693],{},"エラーハンドリング強化",": ビルドフックに適切なtry-catch追加",[306,1696,1697,1700],{},[299,1698,1699],{},"テスト追加",": 14のユニットテストを追加",[306,1702,1703,1706],{},[299,1704,1705],{},"型安全性向上",": APIレスポンスのnull/undefinedチェック",[306,1708,1709,1712],{},[299,1710,1711],{},"可読性向上",": ネストしたtry-catchをヘルパー関数に分離",[111,1714,1716],{"id":1715},"url構造変更後の404エラー2026-01-01","URL構造変更後の404エラー（2026-01-01）",[17,1718,1719,1721,1722,1724],{},[299,1720,1642],{}," frontmatterの",[31,1723,837],{},"フィールドでURL短縮後、MDボタンが404エラー",[17,1726,1727,1649,1729,1731,1732,1734],{},[299,1728,1648],{},[31,1730,457],{},"へのコピー処理が",[31,1733,837],{},"フィールドに追従していなかった",[17,1736,1737,1649,1739,1741,1742,1745,1746,1748],{},[299,1738,1668],{},[31,1740,73],{},"の",[31,1743,1744],{},"copyMarkdownFiles","関数を修正し、frontmatterの",[31,1747,837],{},"を読み取ってそのパスでコピーするように変更",[1408,1750,1751],{},"html pre.shiki code .sHkkW, html code.shiki .sHkkW{--shiki-default:#1E754F;--shiki-dark:#1E754F}html pre.shiki code .stQ0i, html code.shiki .stQ0i{--shiki-default:#AB5959;--shiki-dark:#AB5959}html pre.shiki code .senZ8, html code.shiki .senZ8{--shiki-default:#59873A;--shiki-dark:#59873A}html pre.shiki code .shFtX, html code.shiki .shFtX{--shiki-default:#999999;--shiki-dark:#999999}html pre.shiki code .s4oTP, html code.shiki .s4oTP{--shiki-default:#B07D48;--shiki-dark:#B07D48}html pre.shiki code .sSkh3, html code.shiki .sSkh3{--shiki-default:#2E8F82;--shiki-dark:#2E8F82}html pre.shiki code .sxvE3, html code.shiki .sxvE3{--shiki-default:#A0ADA0;--shiki-dark:#A0ADA0}html pre.shiki code .sMJiu, html code.shiki .sMJiu{--shiki-default:#B5695977;--shiki-dark:#B5695977}html pre.shiki code .sLkVR, html code.shiki .sLkVR{--shiki-default:#AB5E3F;--shiki-dark:#AB5E3F}html pre.shiki code .she3A, html code.shiki .she3A{--shiki-default:#5A6AA6;--shiki-dark:#5A6AA6}html pre.shiki code .sM54T, html code.shiki .sM54T{--shiki-default:#2F798A;--shiki-dark:#2F798A}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 .sdGka, html code.shiki .sdGka{--shiki-default:#B56959;--shiki-dark:#B56959}html pre.shiki code .sG7-3, html code.shiki .sG7-3{--shiki-default:#393A34;--shiki-dark:#393A34}html pre.shiki code .sz8Xr, html code.shiki .sz8Xr{--shiki-default:#998418;--shiki-dark:#998418}html pre.shiki code .svWSF, html code.shiki .svWSF{--shiki-default:#BDA437;--shiki-dark:#BDA437}",{"title":33,"searchDepth":165,"depth":165,"links":1753},[1754,1755,1756,1757,1763,1764,1765],{"id":15,"depth":165,"text":15},{"id":22,"depth":165,"text":22},{"id":36,"depth":165,"text":36},{"id":109,"depth":165,"text":109,"children":1758},[1759,1760,1761,1762],{"id":113,"depth":172,"text":114},{"id":326,"depth":172,"text":327},{"id":814,"depth":172,"text":815},{"id":1219,"depth":172,"text":1220},{"id":1604,"depth":165,"text":1604},{"id":1613,"depth":165,"text":1613},{"id":1633,"depth":165,"text":1633,"children":1766},[1767,1768,1769],{"id":1636,"depth":172,"text":1637},{"id":1672,"depth":172,"text":1673},{"id":1715,"depth":172,"text":1716},null,"PC表示で共有ボタンとマークダウンコピーボタンを追加。frontmatterを除外した本文のみをコピーできる機能",{},"/markdown-copy-feature",false,"2025-12-31T00:00:00.000Z",{"title":5,"description":1771},"2025-12/2025-12-31/markdown-copy-feature",[1779,1780,1781,1782,1783],"完了","Vue","Nuxt","機能追加","Cloudflare Pages","auCoLzbl2yTcnA-cWiNmzl48hBfr4QugsAM2N8slRoA",[],[1787,1791,1795,1798,1802],{"title":1788,"path":1789,"publishedAt":1790},"英語の図解Artifactを日本語化して単独Vueページで公開するまで - AI導入5ステップの移植記録","/ai-adoption-steps-page","2026-07-17T00:00:00.000Z",{"title":1792,"path":1793,"publishedAt":1794},"Herokuを止められるか、nuxt generate を4回回して確かめた — 旧Nuxt 2アプリ静的化の実測","/app-static-build-heroku-retirement","2026-07-27T00:00:00.000Z",{"title":1796,"path":1797,"publishedAt":1794},"移行サイトのSEO・AI-SEO改善を1日で回した — 画像altを480件、記事の要点を203本、OG画像を署名付きに","/wordpress-nuxt-ai-seo-phase-c",{"title":1799,"path":1800,"publishedAt":1801},"全18講に触って動くシミュレーターを入れた日 — PR12本のレビューから講座導線の作り直しまで","/code-course-interactive-rollout","2026-07-26T00:00:00.000Z",{"title":1803,"path":1804,"publishedAt":1801},"インタラクティブ教材を1枚に集約し、本文の線を減らした日","/interactive-content-consolidation","https://log.eurekapu.com/og/blog/markdown-copy-feature.png?v=2025-12-31T00%3A00%3A00.000Z&title=%E3%83%96%E3%83%AD%E3%82%B0%E8%A8%98%E4%BA%8B%E3%81%AE%E3%83%9E%E3%83%BC%E3%82%AF%E3%83%80%E3%82%A6%E3%83%B3%E3%82%B3%E3%83%94%E3%83%BC%E6%A9%9F%E8%83%BD%E3%82%92%E5%AE%9F%E8%A3%85&author=Kei%20Komatsu&sig=4cdf6e35e0470d59",1785654701887]