[{"data":1,"prerenderedAt":2753},["ShallowReactive",2],{"post-\u002Fblog\u002F2026\u002Fpublishing-on-github-pages-with-nuxt":3},{"id":4,"title":5,"body":6,"categories":2739,"date":2743,"description":2744,"extension":2745,"image":2746,"meta":2747,"navigation":249,"path":2748,"seo":2749,"stem":2750,"tags":2751,"__hash__":2752},"blog\u002Fblog\u002F2026\u002Fpublishing-on-github-pages-with-nuxt.md","Publishing a Static Blog on GitHub Pages with Nuxt",{"type":7,"value":8,"toc":2710},"minimark",[9,13,16,19,24,124,127,131,139,217,221,275,286,334,338,345,353,356,416,425,429,436,699,703,710,727,730,734,739,1122,1125,1152,1156,1166,1174,1180,1200,1210,1214,1217,1231,1234,1267,1272,1298,1314,1318,1321,1393,1396,1411,1415,1421,1428,1432,1515,1524,1527,1531,1534,1539,1558,1562,1593,1706,1717,1721,1732,1779,1783,1786,1832,1885,1899,1915,2101,2105,2194,2207,2209,2213,2216,2226,2287,2291,2360,2367,2421,2429,2441,2451,2453,2457,2467,2473,2477,2536,2540,2575,2579,2693,2706],[10,11,12],"p",{},"Every mole eventually digs its way to the surface. You have written something worth sharing – now you need a place to put it that costs nothing, needs no server to babysit, and deploys automatically every time you push.",[10,14,15],{},"GitHub Pages is that place. Pair it with Nuxt's static site generator and a GitHub Actions workflow and you get a fully automated pipeline: write Markdown, push, done.",[10,17,18],{},"This is exactly how this blog works. Let me walk you through it.",[20,21,23],"h2",{"id":22},"the-stack","The Stack",[25,26,27,43],"table",{},[28,29,30],"thead",{},[31,32,33,37,40],"tr",{},[34,35,36],"th",{},"Layer",[34,38,39],{},"Choice",[34,41,42],{},"Why",[44,45,46,64,79,94,109],"tbody",{},[31,47,48,52,61],{},[49,50,51],"td",{},"Framework",[49,53,54],{},[55,56,60],"a",{"href":57,"rel":58},"https:\u002F\u002Fnuxt.com\u002F",[59],"nofollow","Nuxt 4",[49,62,63],{},"SSG + Vue components + content module",[31,65,66,69,76],{},[49,67,68],{},"Content",[49,70,71],{},[55,72,75],{"href":73,"rel":74},"https:\u002F\u002Fcontent.nuxt.com\u002F",[59],"@nuxt\u002Fcontent",[49,77,78],{},"Markdown files as typed collections",[31,80,81,84,91],{},[49,82,83],{},"Styling",[49,85,86],{},[55,87,90],{"href":88,"rel":89},"https:\u002F\u002Ftailwindcss.com\u002F",[59],"Tailwind CSS",[49,92,93],{},"Utility-first, no runtime CSS",[31,95,96,99,106],{},[49,97,98],{},"Hosting",[49,100,101],{},[55,102,105],{"href":103,"rel":104},"https:\u002F\u002Fpages.github.com\u002F",[59],"GitHub Pages",[49,107,108],{},"Free, fast, zero ops",[31,110,111,114,121],{},[49,112,113],{},"CI\u002FCD",[49,115,116],{},[55,117,120],{"href":118,"rel":119},"https:\u002F\u002Fdocs.github.com\u002Fen\u002Factions",[59],"GitHub Actions",[49,122,123],{},"Build & deploy on every push",[10,125,126],{},"Everything runs at build time. No Node.js process, no database, no server to patch.",[20,128,130],{"id":129},"_1-create-the-repository","1. Create the Repository",[10,132,133,134,138],{},"GitHub Pages requires the repository to be named ",[135,136,137],"code",{},"\u003Cyour-username>.github.io"," for a user\u002Forganisation site. Create it on GitHub – public is fine, private works too but requires a paid plan for Pages.",[140,141,146],"pre",{"className":142,"code":143,"language":144,"meta":145,"style":145},"language-bash shiki shiki-themes github-light github-dark","# Clone it locally\ngit clone https:\u002F\u002Fgithub.com\u002F\u003Cyour-username>\u002F\u003Cyour-username>.github.io\ncd \u003Cyour-username>.github.io\n","bash","",[135,147,148,157,199],{"__ignoreMap":145},[149,150,153],"span",{"class":151,"line":152},"line",1,[149,154,156],{"class":155},"sJ8bj","# Clone it locally\n",[149,158,160,164,168,171,175,178,182,185,188,190,192,194,196],{"class":151,"line":159},2,[149,161,163],{"class":162},"sScJk","git",[149,165,167],{"class":166},"sZZnC"," clone",[149,169,170],{"class":166}," https:\u002F\u002Fgithub.com\u002F",[149,172,174],{"class":173},"szBVR","\u003C",[149,176,177],{"class":166},"your-usernam",[149,179,181],{"class":180},"sVt8B","e",[149,183,184],{"class":173},">",[149,186,187],{"class":166},"\u002F",[149,189,174],{"class":173},[149,191,177],{"class":166},[149,193,181],{"class":180},[149,195,184],{"class":173},[149,197,198],{"class":166},".github.io\n",[149,200,202,206,209,211,213,215],{"class":151,"line":201},3,[149,203,205],{"class":204},"sj4cs","cd",[149,207,208],{"class":173}," \u003C",[149,210,177],{"class":166},[149,212,181],{"class":180},[149,214,184],{"class":173},[149,216,198],{"class":166},[20,218,220],{"id":219},"_2-bootstrap-a-nuxt-project","2. Bootstrap a Nuxt Project",[140,222,224],{"className":142,"code":223,"language":144,"meta":145,"style":145},"# Initialise a new Nuxt app inside the cloned folder\nnpx nuxi@latest init .\n\n# Add the content module and Tailwind\nnpm install @nuxt\u002Fcontent @nuxtjs\u002Ftailwindcss @tailwindcss\u002Ftypography\n",[135,225,226,231,245,251,257],{"__ignoreMap":145},[149,227,228],{"class":151,"line":152},[149,229,230],{"class":155},"# Initialise a new Nuxt app inside the cloned folder\n",[149,232,233,236,239,242],{"class":151,"line":159},[149,234,235],{"class":162},"npx",[149,237,238],{"class":166}," nuxi@latest",[149,240,241],{"class":166}," init",[149,243,244],{"class":166}," .\n",[149,246,247],{"class":151,"line":201},[149,248,250],{"emptyLinePlaceholder":249},true,"\n",[149,252,254],{"class":151,"line":253},4,[149,255,256],{"class":155},"# Add the content module and Tailwind\n",[149,258,260,263,266,269,272],{"class":151,"line":259},5,[149,261,262],{"class":162},"npm",[149,264,265],{"class":166}," install",[149,267,268],{"class":166}," @nuxt\u002Fcontent",[149,270,271],{"class":166}," @nuxtjs\u002Ftailwindcss",[149,273,274],{"class":166}," @tailwindcss\u002Ftypography\n",[10,276,277,278,281,282,285],{},"Tell Nuxt to generate static HTML instead of running a server. In ",[135,279,280],{},"nuxt.config.ts"," nothing special is needed – ",[135,283,284],{},"nuxi generate"," defaults to full static output.",[140,287,291],{"className":288,"code":289,"language":290,"meta":145,"style":145},"language-ts shiki shiki-themes github-light github-dark","\u002F\u002F nuxt.config.ts\nexport default defineNuxtConfig({\n  modules: ['@nuxt\u002Fcontent', '@nuxtjs\u002Ftailwindcss'],\n})\n","ts",[135,292,293,298,312,329],{"__ignoreMap":145},[149,294,295],{"class":151,"line":152},[149,296,297],{"class":155},"\u002F\u002F nuxt.config.ts\n",[149,299,300,303,306,309],{"class":151,"line":159},[149,301,302],{"class":173},"export",[149,304,305],{"class":173}," default",[149,307,308],{"class":162}," defineNuxtConfig",[149,310,311],{"class":180},"({\n",[149,313,314,317,320,323,326],{"class":151,"line":201},[149,315,316],{"class":180},"  modules: [",[149,318,319],{"class":166},"'@nuxt\u002Fcontent'",[149,321,322],{"class":180},", ",[149,324,325],{"class":166},"'@nuxtjs\u002Ftailwindcss'",[149,327,328],{"class":180},"],\n",[149,330,331],{"class":151,"line":253},[149,332,333],{"class":180},"})\n",[20,335,337],{"id":336},"_3-write-content-as-markdown","3. Write Content as Markdown",[10,339,340,341,344],{},"Create a ",[135,342,343],{},"content\u002Fblog\u002F"," folder and drop Markdown files in:",[140,346,351],{"className":347,"code":349,"language":350,"meta":145},[348],"language-text","content\u002F\n  blog\u002F\n    2026\u002F\n      my-first-post.md\n","text",[135,352,349],{"__ignoreMap":145},[10,354,355],{},"A typical post starts with YAML frontmatter:",[140,357,361],{"className":358,"code":359,"language":360,"meta":145,"style":145},"language-markdown shiki shiki-themes github-light github-dark","---\ntitle: \"My First Post\"\ndate: '2026-05-08'\ndescription: 'A short teaser shown in the post list.'\nimage: \u002Fimages\u002Fblog\u002F2026\u002F05\u002Fcover.jpeg\ncategories:\n  - Dev\n---\n\nHello world! This is my first post.\n","markdown",[135,362,363,368,373,378,383,388,394,400,405,410],{"__ignoreMap":145},[149,364,365],{"class":151,"line":152},[149,366,367],{},"---\n",[149,369,370],{"class":151,"line":159},[149,371,372],{},"title: \"My First Post\"\n",[149,374,375],{"class":151,"line":201},[149,376,377],{},"date: '2026-05-08'\n",[149,379,380],{"class":151,"line":253},[149,381,382],{},"description: 'A short teaser shown in the post list.'\n",[149,384,385],{"class":151,"line":259},[149,386,387],{},"image: \u002Fimages\u002Fblog\u002F2026\u002F05\u002Fcover.jpeg\n",[149,389,391],{"class":151,"line":390},6,[149,392,393],{},"categories:\n",[149,395,397],{"class":151,"line":396},7,[149,398,399],{},"  - Dev\n",[149,401,403],{"class":151,"line":402},8,[149,404,367],{},[149,406,408],{"class":151,"line":407},9,[149,409,250],{"emptyLinePlaceholder":249},[149,411,413],{"class":151,"line":412},10,[149,414,415],{},"Hello world! This is my first post.\n",[10,417,418,420,421,424],{},[135,419,75],{}," picks up the files automatically, parses the frontmatter, and makes everything available via ",[135,422,423],{},"queryCollection()",".",[20,426,428],{"id":427},"_4-render-posts-with-a-dynamic-route","4. Render Posts with a Dynamic Route",[10,430,431,432,435],{},"Create ",[135,433,434],{},"pages\u002Fblog\u002F[...slug].vue",":",[140,437,441],{"className":438,"code":439,"language":440,"meta":145,"style":145},"language-vue shiki shiki-themes github-light github-dark","\u003Cscript setup lang=\"ts\">\nconst route = useRoute()\nconst { data: post } = await useAsyncData(`post-${route.path}`, () =>\n  queryCollection('blog').path(route.path).first()\n)\nif (!post.value) {\n  throw createError({ statusCode: 404, statusMessage: 'Post not found' })\n}\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Carticle v-if=\"post\">\n    \u003Ch1>{{ post.title }}\u003C\u002Fh1>\n    \u003CContentRenderer :value=\"post\" \u002F>\n  \u003C\u002Farticle>\n\u003C\u002Ftemplate>\n","vue",[135,442,443,466,483,534,557,562,576,599,604,613,617,627,646,662,680,690],{"__ignoreMap":145},[149,444,445,447,451,454,457,460,463],{"class":151,"line":152},[149,446,174],{"class":180},[149,448,450],{"class":449},"s9eBZ","script",[149,452,453],{"class":162}," setup",[149,455,456],{"class":162}," lang",[149,458,459],{"class":180},"=",[149,461,462],{"class":166},"\"ts\"",[149,464,465],{"class":180},">\n",[149,467,468,471,474,477,480],{"class":151,"line":159},[149,469,470],{"class":173},"const",[149,472,473],{"class":204}," route",[149,475,476],{"class":173}," =",[149,478,479],{"class":162}," useRoute",[149,481,482],{"class":180},"()\n",[149,484,485,487,490,494,497,500,503,505,508,511,514,517,520,522,525,528,531],{"class":151,"line":201},[149,486,470],{"class":173},[149,488,489],{"class":180}," { ",[149,491,493],{"class":492},"s4XuR","data",[149,495,496],{"class":180},": ",[149,498,499],{"class":204},"post",[149,501,502],{"class":180}," } ",[149,504,459],{"class":173},[149,506,507],{"class":173}," await",[149,509,510],{"class":162}," useAsyncData",[149,512,513],{"class":180},"(",[149,515,516],{"class":166},"`post-${",[149,518,519],{"class":180},"route",[149,521,424],{"class":166},[149,523,524],{"class":180},"path",[149,526,527],{"class":166},"}`",[149,529,530],{"class":180},", () ",[149,532,533],{"class":173},"=>\n",[149,535,536,539,541,544,547,549,552,555],{"class":151,"line":253},[149,537,538],{"class":162},"  queryCollection",[149,540,513],{"class":180},[149,542,543],{"class":166},"'blog'",[149,545,546],{"class":180},").",[149,548,524],{"class":162},[149,550,551],{"class":180},"(route.path).",[149,553,554],{"class":162},"first",[149,556,482],{"class":180},[149,558,559],{"class":151,"line":259},[149,560,561],{"class":180},")\n",[149,563,564,567,570,573],{"class":151,"line":390},[149,565,566],{"class":173},"if",[149,568,569],{"class":180}," (",[149,571,572],{"class":173},"!",[149,574,575],{"class":180},"post.value) {\n",[149,577,578,581,584,587,590,593,596],{"class":151,"line":396},[149,579,580],{"class":173},"  throw",[149,582,583],{"class":162}," createError",[149,585,586],{"class":180},"({ statusCode: ",[149,588,589],{"class":204},"404",[149,591,592],{"class":180},", statusMessage: ",[149,594,595],{"class":166},"'Post not found'",[149,597,598],{"class":180}," })\n",[149,600,601],{"class":151,"line":402},[149,602,603],{"class":180},"}\n",[149,605,606,609,611],{"class":151,"line":407},[149,607,608],{"class":180},"\u003C\u002F",[149,610,450],{"class":449},[149,612,465],{"class":180},[149,614,615],{"class":151,"line":412},[149,616,250],{"emptyLinePlaceholder":249},[149,618,620,622,625],{"class":151,"line":619},11,[149,621,174],{"class":180},[149,623,624],{"class":449},"template",[149,626,465],{"class":180},[149,628,630,633,636,639,641,644],{"class":151,"line":629},12,[149,631,632],{"class":180},"  \u003C",[149,634,635],{"class":449},"article",[149,637,638],{"class":162}," v-if",[149,640,459],{"class":180},[149,642,643],{"class":166},"\"post\"",[149,645,465],{"class":180},[149,647,649,652,655,658,660],{"class":151,"line":648},13,[149,650,651],{"class":180},"    \u003C",[149,653,654],{"class":449},"h1",[149,656,657],{"class":180},">{{ post.title }}\u003C\u002F",[149,659,654],{"class":449},[149,661,465],{"class":180},[149,663,665,667,670,673,675,677],{"class":151,"line":664},14,[149,666,651],{"class":180},[149,668,669],{"class":449},"ContentRenderer",[149,671,672],{"class":162}," :value",[149,674,459],{"class":180},[149,676,643],{"class":166},[149,678,679],{"class":180}," \u002F>\n",[149,681,683,686,688],{"class":151,"line":682},15,[149,684,685],{"class":180},"  \u003C\u002F",[149,687,635],{"class":449},[149,689,465],{"class":180},[149,691,693,695,697],{"class":151,"line":692},16,[149,694,608],{"class":180},[149,696,624],{"class":449},[149,698,465],{"class":180},[20,700,702],{"id":701},"_5-configure-github-pages","5. Configure GitHub Pages",[10,704,705,706,435],{},"In your repository go to ",[707,708,709],"strong",{},"Settings → Pages",[711,712,713,724],"ol",{},[714,715,716,719,720,723],"li",{},[707,717,718],{},"Source",": choose ",[721,722,120],"em",{}," (not the legacy \"Deploy from a branch\" option).",[714,725,726],{},"Leave everything else at defaults for now.",[10,728,729],{},"That single change tells GitHub to trust the artifact uploaded by your workflow instead of looking for files on a branch.",[20,731,733],{"id":732},"_6-the-github-actions-workflow","6. The GitHub Actions Workflow",[10,735,431,736,435],{},[135,737,738],{},".github\u002Fworkflows\u002Fpublish.yml",[140,740,744],{"className":741,"code":742,"language":743,"meta":145,"style":145},"language-yaml shiki shiki-themes github-light github-dark","on:\n  push:\n    branches: [main]\n\nname: Build & Deploy to GitHub Pages\n\npermissions:\n  contents: read\n  pages: write\n  id-token: write\n\nconcurrency:\n  group: \"pages-${{ github.ref }}\"\n  cancel-in-progress: true\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v4\n\n      - uses: actions\u002Fsetup-node@v4\n        with:\n          node-version: 22\n          cache: npm\n\n      - run: npm ci\n\n      - run: npm run generate\n\n      - uses: actions\u002Fupload-pages-artifact@v3\n        with:\n          path: \".output\u002Fpublic\"\n\n  deploy:\n    needs: build\n    runs-on: ubuntu-latest\n    environment:\n      name: github-pages\n      url: ${{ steps.deployment.outputs.page_url }}\n    steps:\n      - uses: actions\u002Fdeploy-pages@v4\n        id: deployment\n","yaml",[135,745,746,754,761,775,779,789,793,800,810,820,829,833,840,850,860,864,871,879,890,898,912,917,929,937,948,959,964,977,982,994,999,1011,1018,1029,1034,1042,1053,1062,1070,1081,1092,1099,1111],{"__ignoreMap":145},[149,747,748,751],{"class":151,"line":152},[149,749,750],{"class":204},"on",[149,752,753],{"class":180},":\n",[149,755,756,759],{"class":151,"line":159},[149,757,758],{"class":449},"  push",[149,760,753],{"class":180},[149,762,763,766,769,772],{"class":151,"line":201},[149,764,765],{"class":449},"    branches",[149,767,768],{"class":180},": [",[149,770,771],{"class":166},"main",[149,773,774],{"class":180},"]\n",[149,776,777],{"class":151,"line":253},[149,778,250],{"emptyLinePlaceholder":249},[149,780,781,784,786],{"class":151,"line":259},[149,782,783],{"class":449},"name",[149,785,496],{"class":180},[149,787,788],{"class":166},"Build & Deploy to GitHub Pages\n",[149,790,791],{"class":151,"line":390},[149,792,250],{"emptyLinePlaceholder":249},[149,794,795,798],{"class":151,"line":396},[149,796,797],{"class":449},"permissions",[149,799,753],{"class":180},[149,801,802,805,807],{"class":151,"line":402},[149,803,804],{"class":449},"  contents",[149,806,496],{"class":180},[149,808,809],{"class":166},"read\n",[149,811,812,815,817],{"class":151,"line":407},[149,813,814],{"class":449},"  pages",[149,816,496],{"class":180},[149,818,819],{"class":166},"write\n",[149,821,822,825,827],{"class":151,"line":412},[149,823,824],{"class":449},"  id-token",[149,826,496],{"class":180},[149,828,819],{"class":166},[149,830,831],{"class":151,"line":619},[149,832,250],{"emptyLinePlaceholder":249},[149,834,835,838],{"class":151,"line":629},[149,836,837],{"class":449},"concurrency",[149,839,753],{"class":180},[149,841,842,845,847],{"class":151,"line":648},[149,843,844],{"class":449},"  group",[149,846,496],{"class":180},[149,848,849],{"class":166},"\"pages-${{ github.ref }}\"\n",[149,851,852,855,857],{"class":151,"line":664},[149,853,854],{"class":449},"  cancel-in-progress",[149,856,496],{"class":180},[149,858,859],{"class":204},"true\n",[149,861,862],{"class":151,"line":682},[149,863,250],{"emptyLinePlaceholder":249},[149,865,866,869],{"class":151,"line":692},[149,867,868],{"class":449},"jobs",[149,870,753],{"class":180},[149,872,874,877],{"class":151,"line":873},17,[149,875,876],{"class":449},"  build",[149,878,753],{"class":180},[149,880,882,885,887],{"class":151,"line":881},18,[149,883,884],{"class":449},"    runs-on",[149,886,496],{"class":180},[149,888,889],{"class":166},"ubuntu-latest\n",[149,891,893,896],{"class":151,"line":892},19,[149,894,895],{"class":449},"    steps",[149,897,753],{"class":180},[149,899,901,904,907,909],{"class":151,"line":900},20,[149,902,903],{"class":180},"      - ",[149,905,906],{"class":449},"uses",[149,908,496],{"class":180},[149,910,911],{"class":166},"actions\u002Fcheckout@v4\n",[149,913,915],{"class":151,"line":914},21,[149,916,250],{"emptyLinePlaceholder":249},[149,918,920,922,924,926],{"class":151,"line":919},22,[149,921,903],{"class":180},[149,923,906],{"class":449},[149,925,496],{"class":180},[149,927,928],{"class":166},"actions\u002Fsetup-node@v4\n",[149,930,932,935],{"class":151,"line":931},23,[149,933,934],{"class":449},"        with",[149,936,753],{"class":180},[149,938,940,943,945],{"class":151,"line":939},24,[149,941,942],{"class":449},"          node-version",[149,944,496],{"class":180},[149,946,947],{"class":204},"22\n",[149,949,951,954,956],{"class":151,"line":950},25,[149,952,953],{"class":449},"          cache",[149,955,496],{"class":180},[149,957,958],{"class":166},"npm\n",[149,960,962],{"class":151,"line":961},26,[149,963,250],{"emptyLinePlaceholder":249},[149,965,967,969,972,974],{"class":151,"line":966},27,[149,968,903],{"class":180},[149,970,971],{"class":449},"run",[149,973,496],{"class":180},[149,975,976],{"class":166},"npm ci\n",[149,978,980],{"class":151,"line":979},28,[149,981,250],{"emptyLinePlaceholder":249},[149,983,985,987,989,991],{"class":151,"line":984},29,[149,986,903],{"class":180},[149,988,971],{"class":449},[149,990,496],{"class":180},[149,992,993],{"class":166},"npm run generate\n",[149,995,997],{"class":151,"line":996},30,[149,998,250],{"emptyLinePlaceholder":249},[149,1000,1002,1004,1006,1008],{"class":151,"line":1001},31,[149,1003,903],{"class":180},[149,1005,906],{"class":449},[149,1007,496],{"class":180},[149,1009,1010],{"class":166},"actions\u002Fupload-pages-artifact@v3\n",[149,1012,1014,1016],{"class":151,"line":1013},32,[149,1015,934],{"class":449},[149,1017,753],{"class":180},[149,1019,1021,1024,1026],{"class":151,"line":1020},33,[149,1022,1023],{"class":449},"          path",[149,1025,496],{"class":180},[149,1027,1028],{"class":166},"\".output\u002Fpublic\"\n",[149,1030,1032],{"class":151,"line":1031},34,[149,1033,250],{"emptyLinePlaceholder":249},[149,1035,1037,1040],{"class":151,"line":1036},35,[149,1038,1039],{"class":449},"  deploy",[149,1041,753],{"class":180},[149,1043,1045,1048,1050],{"class":151,"line":1044},36,[149,1046,1047],{"class":449},"    needs",[149,1049,496],{"class":180},[149,1051,1052],{"class":166},"build\n",[149,1054,1056,1058,1060],{"class":151,"line":1055},37,[149,1057,884],{"class":449},[149,1059,496],{"class":180},[149,1061,889],{"class":166},[149,1063,1065,1068],{"class":151,"line":1064},38,[149,1066,1067],{"class":449},"    environment",[149,1069,753],{"class":180},[149,1071,1073,1076,1078],{"class":151,"line":1072},39,[149,1074,1075],{"class":449},"      name",[149,1077,496],{"class":180},[149,1079,1080],{"class":166},"github-pages\n",[149,1082,1084,1087,1089],{"class":151,"line":1083},40,[149,1085,1086],{"class":449},"      url",[149,1088,496],{"class":180},[149,1090,1091],{"class":166},"${{ steps.deployment.outputs.page_url }}\n",[149,1093,1095,1097],{"class":151,"line":1094},41,[149,1096,895],{"class":449},[149,1098,753],{"class":180},[149,1100,1102,1104,1106,1108],{"class":151,"line":1101},42,[149,1103,903],{"class":180},[149,1105,906],{"class":449},[149,1107,496],{"class":180},[149,1109,1110],{"class":166},"actions\u002Fdeploy-pages@v4\n",[149,1112,1114,1117,1119],{"class":151,"line":1113},43,[149,1115,1116],{"class":449},"        id",[149,1118,496],{"class":180},[149,1120,1121],{"class":166},"deployment\n",[10,1123,1124],{},"Key points:",[1126,1127,1128,1140,1146],"ul",{},[714,1129,1130,1133,1134,1136,1137,424],{},[135,1131,1132],{},"npm run generate"," calls ",[135,1135,284],{}," and writes the fully static site into ",[135,1138,1139],{},".output\u002Fpublic\u002F",[714,1141,1142,1145],{},[135,1143,1144],{},"upload-pages-artifact"," packages that folder and makes it available to the deploy job.",[714,1147,1148,1151],{},[135,1149,1150],{},"deploy-pages"," pushes it to GitHub's CDN. The whole pipeline usually finishes in under two minutes.",[20,1153,1155],{"id":1154},"_7-custom-domain-optional","7. Custom Domain (Optional)",[10,1157,1158,1159,1162,1163,435],{},"If you want ",[135,1160,1161],{},"blog.yourdomain.com"," instead of ",[135,1164,1165],{},"\u003Cusername>.github.io",[711,1167,1168],{},[714,1169,340,1170,1173],{},[135,1171,1172],{},"public\u002FCNAME"," file containing only your domain:",[140,1175,1178],{"className":1176,"code":1177,"language":350,"meta":145},[348],"blog.yourdomain.com\n",[135,1179,1177],{"__ignoreMap":145},[711,1181,1182,1190],{"start":159},[714,1183,1184,1185,1187,1188,424],{},"Add a CNAME DNS record at your registrar pointing ",[135,1186,1161],{}," to ",[135,1189,1165],{},[714,1191,1192,1193,1196,1197,424],{},"In ",[707,1194,1195],{},"Settings → Pages → Custom domain"," enter the same domain and tick ",[707,1198,1199],{},"Enforce HTTPS",[10,1201,1202,1203,1206,1207,1209],{},"The ",[135,1204,1205],{},"CNAME"," file is copied verbatim into ",[135,1208,1139],{}," during the build, so GitHub Pages always knows which domain to serve.",[20,1211,1213],{"id":1212},"_8-secrets-and-environment-variables","8. Secrets and Environment Variables",[10,1215,1216],{},"Some values – analytics IDs, ad publisher IDs – should not be hard-coded. Store them as GitHub repository variables or secrets:",[1126,1218,1219,1225],{},[714,1220,1221,1224],{},[707,1222,1223],{},"Settings → Secrets and variables → Actions → Variables"," for non-sensitive IDs",[714,1226,1227,1230],{},[707,1228,1229],{},"Settings → Secrets and variables → Actions → Secrets"," for tokens and keys",[10,1232,1233],{},"Reference them in the workflow:",[140,1235,1237],{"className":741,"code":1236,"language":743,"meta":145,"style":145},"- run: npm run generate\n  env:\n    GOOGLE_ANALYTICS_MEAS_ID: ${{ vars.GOOGLE_ANALYTICS_MEAS_ID }}\n",[135,1238,1239,1250,1257],{"__ignoreMap":145},[149,1240,1241,1244,1246,1248],{"class":151,"line":152},[149,1242,1243],{"class":180},"- ",[149,1245,971],{"class":449},[149,1247,496],{"class":180},[149,1249,993],{"class":166},[149,1251,1252,1255],{"class":151,"line":159},[149,1253,1254],{"class":449},"  env",[149,1256,753],{"class":180},[149,1258,1259,1262,1264],{"class":151,"line":201},[149,1260,1261],{"class":449},"    GOOGLE_ANALYTICS_MEAS_ID",[149,1263,496],{"class":180},[149,1265,1266],{"class":166},"${{ vars.GOOGLE_ANALYTICS_MEAS_ID }}\n",[10,1268,1269,1270,435],{},"And read them in ",[135,1271,280],{},[140,1273,1275],{"className":288,"code":1274,"language":290,"meta":145,"style":145},"const gtagId = process.env.GOOGLE_ANALYTICS_MEAS_ID || 'G-XXXXXXXXXX'\n",[135,1276,1277],{"__ignoreMap":145},[149,1278,1279,1281,1284,1286,1289,1292,1295],{"class":151,"line":152},[149,1280,470],{"class":173},[149,1282,1283],{"class":204}," gtagId",[149,1285,476],{"class":173},[149,1287,1288],{"class":180}," process.env.",[149,1290,1291],{"class":204},"GOOGLE_ANALYTICS_MEAS_ID",[149,1293,1294],{"class":173}," ||",[149,1296,1297],{"class":166}," 'G-XXXXXXXXXX'\n",[1299,1300,1301],"blockquote",{},[10,1302,1303,1306,1307,1309,1310,1313],{},[707,1304,1305],{},"A word on static sites and \"secrets\"",": Because ",[135,1308,284],{}," runs at build time, any environment variable you inject ends up baked into the generated HTML and JavaScript. A visitor who inspects the page source ",[721,1311,1312],{},"can"," technically read your Google Analytics measurement ID or AdSense publisher ID. That sounds alarming, but in practice it is not a problem – these identifiers are designed to be public. They appear in the HTML of virtually every site that uses them, and there is nothing meaningful an attacker can do with them that they could not already do by just visiting your site. The rule of thumb: only inject values that are safe to be public. Real secrets – API keys with write access, database passwords, tokens that can charge money – must never be used in a static build. Keep those server-side only.",[20,1315,1317],{"id":1316},"_9-preview-pull-requests","9. Preview Pull Requests",[10,1319,1320],{},"Extend the workflow to upload a preview artefact on pull requests – useful for reviewing content changes before they go live:",[140,1322,1324],{"className":741,"code":1323,"language":743,"meta":145,"style":145},"- name: Upload PR preview\n  if: github.event_name == 'pull_request'\n  uses: actions\u002Fupload-artifact@v4\n  with:\n    name: \"pr-preview-${{ github.event.pull_request.number }}\"\n    path: \".output\u002Fpublic\"\n    retention-days: 7\n",[135,1325,1326,1337,1347,1357,1364,1374,1383],{"__ignoreMap":145},[149,1327,1328,1330,1332,1334],{"class":151,"line":152},[149,1329,1243],{"class":180},[149,1331,783],{"class":449},[149,1333,496],{"class":180},[149,1335,1336],{"class":166},"Upload PR preview\n",[149,1338,1339,1342,1344],{"class":151,"line":159},[149,1340,1341],{"class":449},"  if",[149,1343,496],{"class":180},[149,1345,1346],{"class":166},"github.event_name == 'pull_request'\n",[149,1348,1349,1352,1354],{"class":151,"line":201},[149,1350,1351],{"class":449},"  uses",[149,1353,496],{"class":180},[149,1355,1356],{"class":166},"actions\u002Fupload-artifact@v4\n",[149,1358,1359,1362],{"class":151,"line":253},[149,1360,1361],{"class":449},"  with",[149,1363,753],{"class":180},[149,1365,1366,1369,1371],{"class":151,"line":259},[149,1367,1368],{"class":449},"    name",[149,1370,496],{"class":180},[149,1372,1373],{"class":166},"\"pr-preview-${{ github.event.pull_request.number }}\"\n",[149,1375,1376,1379,1381],{"class":151,"line":390},[149,1377,1378],{"class":449},"    path",[149,1380,496],{"class":180},[149,1382,1028],{"class":166},[149,1384,1385,1388,1390],{"class":151,"line":396},[149,1386,1387],{"class":449},"    retention-days",[149,1389,496],{"class":180},[149,1391,1392],{"class":204},"7\n",[10,1394,1395],{},"Download the artefact from the Actions run summary and open it locally with any static file server:",[140,1397,1399],{"className":142,"code":1398,"language":144,"meta":145,"style":145},"npx serve pr-preview-42\u002F\n",[135,1400,1401],{"__ignoreMap":145},[149,1402,1403,1405,1408],{"class":151,"line":152},[149,1404,235],{"class":162},[149,1406,1407],{"class":166}," serve",[149,1409,1410],{"class":166}," pr-preview-42\u002F\n",[20,1412,1414],{"id":1413},"the-full-picture","The Full Picture",[140,1416,1419],{"className":1417,"code":1418,"language":350,"meta":145},[348],"you write Markdown\n  → git push\n    → GitHub Actions: npm ci + npm run generate\n      → .output\u002Fpublic\u002F uploaded as Pages artifact\n        → deploy-pages pushes to GitHub CDN\n          → https:\u002F\u002F\u003Cusername>.github.io is live\n",[135,1420,1418],{"__ignoreMap":145},[10,1422,1423,1424,1427],{},"No server. No containers in production. No ",[135,1425,1426],{},"ssh"," into anything at 2 a.m. because something crashed. The tunnel runs itself.",[20,1429,1431],{"id":1430},"summary","Summary",[25,1433,1434,1444],{},[28,1435,1436],{},[31,1437,1438,1441],{},[34,1439,1440],{},"Step",[34,1442,1443],{},"What to do",[44,1445,1446,1456,1466,1477,1491,1501],{},[31,1447,1448,1451],{},[49,1449,1450],{},"Repository",[49,1452,1453,1454],{},"Name it ",[135,1455,1165],{},[31,1457,1458,1461],{},[49,1459,1460],{},"Pages source",[49,1462,1463,1464],{},"Settings → Pages → Source: ",[707,1465,120],{},[31,1467,1468,1471],{},[49,1469,1470],{},"Nuxt config",[49,1472,1473,1476],{},[135,1474,1475],{},"modules: ['@nuxt\u002Fcontent']",", no extra setup",[31,1478,1479,1482],{},[49,1480,1481],{},"Workflow",[49,1483,1484,1486,1487,1486,1489],{},[135,1485,1132],{}," + ",[135,1488,1144],{},[135,1490,1150],{},[31,1492,1493,1496],{},[49,1494,1495],{},"Custom domain",[49,1497,1498,1500],{},[135,1499,1172],{}," + DNS CNAME record",[31,1502,1503,1506],{},[49,1504,1505],{},"Secrets",[49,1507,1508,1509,1512,1513],{},"GitHub Variables\u002FSecrets + ",[135,1510,1511],{},"process.env"," in ",[135,1514,280],{},[10,1516,1517,1518,1523],{},"The source code of this blog is ",[55,1519,1522],{"href":1520,"rel":1521},"https:\u002F\u002Fgithub.com\u002Fthe78mole-blog\u002Fthe78mole-blog.github.io",[59],"on GitHub"," – feel free to use it as a starting point.",[1525,1526],"hr",{},[20,1528,1530],{"id":1529},"side-tunnel-migrating-from-wordpress","Side Tunnel: Migrating from WordPress",[10,1532,1533],{},"Already running a WordPress blog and want to bring it along? This mole has been there. Here is the approach used for this very site.",[1535,1536,1538],"h3",{"id":1537},"step-1-export-from-wordpress","Step 1 – Export from WordPress",[10,1540,1541,1542,1545,1546,1549,1550,1553,1554,1557],{},"In your WordPress admin go to ",[707,1543,1544],{},"Tools → Export"," and choose ",[721,1547,1548],{},"All content",". Download the WXR (WordPress eXtended RSS) XML file. Also grab a full media backup – the easiest way is a database + files backup plugin such as ",[721,1551,1552],{},"UpdraftPlus"," or the built-in export from your hosting panel. An alternative is to use an FTP client to download the ",[135,1555,1556],{},"wp-content"," folder and let copilot find the assets, using some 'copilot-handcrafted' scripts.",[1535,1559,1561],{"id":1560},"step-2-convert-posts-to-markdown","Step 2 – Convert Posts to Markdown",[10,1563,1564,1565,1568,1569,1572,1573,1576,1577,1580,1581,1584,1585,1588,1589,1592],{},"The WXR file contains all posts as HTML inside ",[135,1566,1567],{},"\u003Ccontent:encoded>"," tags. A small Python script (using ",[135,1570,1571],{},"lxml"," and ",[135,1574,1575],{},"html2text"," or ",[135,1578,1579],{},"markdownify",") can walk every ",[135,1582,1583],{},"\u003Citem>"," with ",[135,1586,1587],{},"\u003Cwp:post_type>post\u003C\u002Fwp:post_type>"," and write one ",[135,1590,1591],{},".md"," file per post:",[140,1594,1598],{"className":1595,"code":1596,"language":1597,"meta":145,"style":145},"language-python shiki shiki-themes github-light github-dark","# Minimal sketch – production version lives in scripts\u002Fmigration.py\nfrom lxml import etree\nimport html2text\n\nWP = 'http:\u002F\u002Fwordpress.org\u002Fexport\u002F1.2\u002F'\ntree = etree.parse('molesblog.WordPress.xml')\n\nconverter = html2text.HTML2Text()\nconverter.ignore_links = False\n\nfor item in tree.findall('.\u002F\u002Fitem'):\n    post_type = item.findtext(f'{{{WP}}}post_type')\n    if post_type != 'post':\n        continue\n    slug  = item.findtext(f'{{{WP}}}post_name')\n    title = item.findtext('title')\n    date  = item.findtext(f'{{{WP}}}post_date')[:10]\n    html  = item.findtext('{http:\u002F\u002Fpurl.org\u002Frss\u002F1.0\u002Fmodules\u002Fcontent\u002F}encoded') or ''\n    md    = converter.handle(html)\n\n    path  = Path(f'content\u002Fblog\u002F{date[:4]}\u002F{slug}.md')\n    path.write_text(f'---\\ntitle: \"{title}\"\\ndate: \\'{date}\\'\\n---\\n\\n{md}')\n","python",[135,1599,1600,1605,1610,1615,1619,1624,1629,1633,1638,1643,1647,1652,1657,1662,1667,1672,1677,1682,1687,1692,1696,1701],{"__ignoreMap":145},[149,1601,1602],{"class":151,"line":152},[149,1603,1604],{},"# Minimal sketch – production version lives in scripts\u002Fmigration.py\n",[149,1606,1607],{"class":151,"line":159},[149,1608,1609],{},"from lxml import etree\n",[149,1611,1612],{"class":151,"line":201},[149,1613,1614],{},"import html2text\n",[149,1616,1617],{"class":151,"line":253},[149,1618,250],{"emptyLinePlaceholder":249},[149,1620,1621],{"class":151,"line":259},[149,1622,1623],{},"WP = 'http:\u002F\u002Fwordpress.org\u002Fexport\u002F1.2\u002F'\n",[149,1625,1626],{"class":151,"line":390},[149,1627,1628],{},"tree = etree.parse('molesblog.WordPress.xml')\n",[149,1630,1631],{"class":151,"line":396},[149,1632,250],{"emptyLinePlaceholder":249},[149,1634,1635],{"class":151,"line":402},[149,1636,1637],{},"converter = html2text.HTML2Text()\n",[149,1639,1640],{"class":151,"line":407},[149,1641,1642],{},"converter.ignore_links = False\n",[149,1644,1645],{"class":151,"line":412},[149,1646,250],{"emptyLinePlaceholder":249},[149,1648,1649],{"class":151,"line":619},[149,1650,1651],{},"for item in tree.findall('.\u002F\u002Fitem'):\n",[149,1653,1654],{"class":151,"line":629},[149,1655,1656],{},"    post_type = item.findtext(f'{{{WP}}}post_type')\n",[149,1658,1659],{"class":151,"line":648},[149,1660,1661],{},"    if post_type != 'post':\n",[149,1663,1664],{"class":151,"line":664},[149,1665,1666],{},"        continue\n",[149,1668,1669],{"class":151,"line":682},[149,1670,1671],{},"    slug  = item.findtext(f'{{{WP}}}post_name')\n",[149,1673,1674],{"class":151,"line":692},[149,1675,1676],{},"    title = item.findtext('title')\n",[149,1678,1679],{"class":151,"line":873},[149,1680,1681],{},"    date  = item.findtext(f'{{{WP}}}post_date')[:10]\n",[149,1683,1684],{"class":151,"line":881},[149,1685,1686],{},"    html  = item.findtext('{http:\u002F\u002Fpurl.org\u002Frss\u002F1.0\u002Fmodules\u002Fcontent\u002F}encoded') or ''\n",[149,1688,1689],{"class":151,"line":892},[149,1690,1691],{},"    md    = converter.handle(html)\n",[149,1693,1694],{"class":151,"line":900},[149,1695,250],{"emptyLinePlaceholder":249},[149,1697,1698],{"class":151,"line":914},[149,1699,1700],{},"    path  = Path(f'content\u002Fblog\u002F{date[:4]}\u002F{slug}.md')\n",[149,1702,1703],{"class":151,"line":919},[149,1704,1705],{},"    path.write_text(f'---\\ntitle: \"{title}\"\\ndate: \\'{date}\\'\\n---\\n\\n{md}')\n",[10,1707,1708,1709,1712,1713,1716],{},"Refine from there: extract categories, the featured image path, the excerpt for ",[135,1710,1711],{},"description",", and fix image URLs to point to your new ",[135,1714,1715],{},"\u002Fpublic\u002Fimages\u002F"," tree.",[1535,1718,1720],{"id":1719},"step-3-migrate-images","Step 3 – Migrate Images",[10,1722,1723,1724,1727,1728,1731],{},"WordPress stores uploads under ",[135,1725,1726],{},"wp-content\u002Fuploads\u002F\u003Cyear>\u002F\u003Cmonth>\u002F",". Copy the files to ",[135,1729,1730],{},"public\u002Fimages\u002Fblog\u002F\u003Cyear>\u002F\u003Cmonth>\u002F"," and do a find-and-replace in all Markdown files to rewrite the paths:",[140,1733,1735],{"className":142,"code":1734,"language":144,"meta":145,"style":145},"# Example: rewrite old WordPress upload URLs to new local paths\nfind content\u002F -name '*.md' -exec sed -i \\\n  's|https:\u002F\u002Fyourdomain.com\u002Fwp-content\u002Fuploads\u002F|\u002Fimages\u002Fblog\u002F|g' {} +\n",[135,1736,1737,1742,1768],{"__ignoreMap":145},[149,1738,1739],{"class":151,"line":152},[149,1740,1741],{"class":155},"# Example: rewrite old WordPress upload URLs to new local paths\n",[149,1743,1744,1747,1750,1753,1756,1759,1762,1765],{"class":151,"line":159},[149,1745,1746],{"class":162},"find",[149,1748,1749],{"class":166}," content\u002F",[149,1751,1752],{"class":204}," -name",[149,1754,1755],{"class":166}," '*.md'",[149,1757,1758],{"class":204}," -exec",[149,1760,1761],{"class":166}," sed",[149,1763,1764],{"class":204}," -i",[149,1766,1767],{"class":204}," \\\n",[149,1769,1770,1773,1776],{"class":151,"line":201},[149,1771,1772],{"class":166},"  's|https:\u002F\u002Fyourdomain.com\u002Fwp-content\u002Fuploads\u002F|\u002Fimages\u002Fblog\u002F|g'",[149,1774,1775],{"class":166}," {}",[149,1777,1778],{"class":166}," +\n",[1535,1780,1782],{"id":1781},"step-4-preserve-comments-with-giscus","Step 4 – Preserve Comments with Giscus",[10,1784,1785],{},"WordPress comments should not just vanish. The strategy used here is:",[711,1787,1788,1801,1814,1823],{},[714,1789,1790,1793,1794,1797,1798,546],{},[707,1791,1792],{},"Extract"," all ",[135,1795,1796],{},"\u003Cwp:comment>"," nodes from the WXR and PHP-serialised review metadata into a structured JSON file (",[135,1799,1800],{},"scripts\u002Fextract_comments.py",[714,1802,1803,1806,1807,1810,1811,546],{},[707,1804,1805],{},"Assign"," each comment thread to its corresponding blog post slug, building a ",[135,1808,1809],{},"discussions\u002F\u003Cslug>.json"," file with the full reply tree (",[135,1812,1813],{},"scripts\u002Fassign_comments.py",[714,1815,1816,1819,1820,546],{},[707,1817,1818],{},"Create"," GitHub Discussions via the GraphQL API – one Discussion per post, with historical comments as replies (",[135,1821,1822],{},"scripts\u002Fcreate_discussions.py",[714,1824,1825,1828,1829,424],{},[707,1826,1827],{},"Wire up Giscus"," in your Nuxt layout so the comment widget maps each page to the matching Discussion by ",[135,1830,1831],{},"pathname",[140,1833,1835],{"className":142,"code":1834,"language":144,"meta":145,"style":145},"# Run all three steps from the repo root (requires uv)\nuv run --project scripts scripts\u002Fextract_comments.py\nuv run --project scripts scripts\u002Fassign_comments.py\nuv run --project scripts scripts\u002Fcreate_discussions.py\n",[135,1836,1837,1842,1859,1872],{"__ignoreMap":145},[149,1838,1839],{"class":151,"line":152},[149,1840,1841],{"class":155},"# Run all three steps from the repo root (requires uv)\n",[149,1843,1844,1847,1850,1853,1856],{"class":151,"line":159},[149,1845,1846],{"class":162},"uv",[149,1848,1849],{"class":166}," run",[149,1851,1852],{"class":204}," --project",[149,1854,1855],{"class":166}," scripts",[149,1857,1858],{"class":166}," scripts\u002Fextract_comments.py\n",[149,1860,1861,1863,1865,1867,1869],{"class":151,"line":201},[149,1862,1846],{"class":162},[149,1864,1849],{"class":166},[149,1866,1852],{"class":204},[149,1868,1855],{"class":166},[149,1870,1871],{"class":166}," scripts\u002Fassign_comments.py\n",[149,1873,1874,1876,1878,1880,1882],{"class":151,"line":253},[149,1875,1846],{"class":162},[149,1877,1849],{"class":166},[149,1879,1852],{"class":204},[149,1881,1855],{"class":166},[149,1883,1884],{"class":166}," scripts\u002Fcreate_discussions.py\n",[10,1886,1202,1887,1890,1891,1894,1895,1898],{},[135,1888,1889],{},"create_discussions.py"," script is ",[707,1892,1893],{},"idempotent",": if a Discussion already exists for a post (tracked via ",[135,1896,1897],{},"discussion_id:"," in the Markdown frontmatter), it is skipped on re-runs.",[10,1900,1901,1902,1907,1908,1911,1912,1914],{},"On the Nuxt side, install ",[55,1903,1906],{"href":1904,"rel":1905},"https:\u002F\u002Fgiscus.app\u002F",[59],"giscus"," and add a ",[135,1909,1910],{},"\u003CGiscusComments \u002F>"," component to your post layout. The widget picks up the correct Discussion automatically because it matches on the page ",[135,1913,1831],{}," – exactly the same slug GitHub Actions deployed.",[140,1916,1918],{"className":438,"code":1917,"language":440,"meta":145,"style":145},"\u003C!-- components\u002FGiscusComments.vue (simplified) -->\n\u003Cscript setup lang=\"ts\">\nconst config = useRuntimeConfig()\n\u003C\u002Fscript>\n\u003Ctemplate>\n  \u003Cdiv class=\"giscus-wrapper\">\n    \u003Ccomponent\n      :is=\"'script'\"\n      src=\"https:\u002F\u002Fgiscus.app\u002Fclient.js\"\n      data-repo=\"the78mole-blog\u002Fthe78mole-blog.github.io\"\n      :data-repo-id=\"config.public.giscusRepoId\"\n      :data-category-id=\"config.public.giscusCategoryId\"\n      data-mapping=\"pathname\"\n      data-reactions-enabled=\"1\"\n      data-theme=\"preferred_color_scheme\"\n      async\n    \u002F>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n",[135,1919,1920,1925,1941,1955,1963,1971,1988,1995,2005,2015,2025,2035,2045,2055,2065,2075,2080,2085,2093],{"__ignoreMap":145},[149,1921,1922],{"class":151,"line":152},[149,1923,1924],{"class":155},"\u003C!-- components\u002FGiscusComments.vue (simplified) -->\n",[149,1926,1927,1929,1931,1933,1935,1937,1939],{"class":151,"line":159},[149,1928,174],{"class":180},[149,1930,450],{"class":449},[149,1932,453],{"class":162},[149,1934,456],{"class":162},[149,1936,459],{"class":180},[149,1938,462],{"class":166},[149,1940,465],{"class":180},[149,1942,1943,1945,1948,1950,1953],{"class":151,"line":201},[149,1944,470],{"class":173},[149,1946,1947],{"class":204}," config",[149,1949,476],{"class":173},[149,1951,1952],{"class":162}," useRuntimeConfig",[149,1954,482],{"class":180},[149,1956,1957,1959,1961],{"class":151,"line":253},[149,1958,608],{"class":180},[149,1960,450],{"class":449},[149,1962,465],{"class":180},[149,1964,1965,1967,1969],{"class":151,"line":259},[149,1966,174],{"class":180},[149,1968,624],{"class":449},[149,1970,465],{"class":180},[149,1972,1973,1975,1978,1981,1983,1986],{"class":151,"line":390},[149,1974,632],{"class":180},[149,1976,1977],{"class":449},"div",[149,1979,1980],{"class":162}," class",[149,1982,459],{"class":180},[149,1984,1985],{"class":166},"\"giscus-wrapper\"",[149,1987,465],{"class":180},[149,1989,1990,1992],{"class":151,"line":396},[149,1991,651],{"class":180},[149,1993,1994],{"class":449},"component\n",[149,1996,1997,2000,2002],{"class":151,"line":402},[149,1998,1999],{"class":162},"      :is",[149,2001,459],{"class":180},[149,2003,2004],{"class":166},"\"'script'\"\n",[149,2006,2007,2010,2012],{"class":151,"line":407},[149,2008,2009],{"class":162},"      src",[149,2011,459],{"class":180},[149,2013,2014],{"class":166},"\"https:\u002F\u002Fgiscus.app\u002Fclient.js\"\n",[149,2016,2017,2020,2022],{"class":151,"line":412},[149,2018,2019],{"class":162},"      data-repo",[149,2021,459],{"class":180},[149,2023,2024],{"class":166},"\"the78mole-blog\u002Fthe78mole-blog.github.io\"\n",[149,2026,2027,2030,2032],{"class":151,"line":619},[149,2028,2029],{"class":162},"      :data-repo-id",[149,2031,459],{"class":180},[149,2033,2034],{"class":166},"\"config.public.giscusRepoId\"\n",[149,2036,2037,2040,2042],{"class":151,"line":629},[149,2038,2039],{"class":162},"      :data-category-id",[149,2041,459],{"class":180},[149,2043,2044],{"class":166},"\"config.public.giscusCategoryId\"\n",[149,2046,2047,2050,2052],{"class":151,"line":648},[149,2048,2049],{"class":162},"      data-mapping",[149,2051,459],{"class":180},[149,2053,2054],{"class":166},"\"pathname\"\n",[149,2056,2057,2060,2062],{"class":151,"line":664},[149,2058,2059],{"class":162},"      data-reactions-enabled",[149,2061,459],{"class":180},[149,2063,2064],{"class":166},"\"1\"\n",[149,2066,2067,2070,2072],{"class":151,"line":682},[149,2068,2069],{"class":162},"      data-theme",[149,2071,459],{"class":180},[149,2073,2074],{"class":166},"\"preferred_color_scheme\"\n",[149,2076,2077],{"class":151,"line":692},[149,2078,2079],{"class":162},"      async\n",[149,2081,2082],{"class":151,"line":873},[149,2083,2084],{"class":180},"    \u002F>\n",[149,2086,2087,2089,2091],{"class":151,"line":881},[149,2088,685],{"class":180},[149,2090,1977],{"class":449},[149,2092,465],{"class":180},[149,2094,2095,2097,2099],{"class":151,"line":892},[149,2096,608],{"class":180},[149,2098,624],{"class":449},[149,2100,465],{"class":180},[1535,2102,2104],{"id":2103},"migration-summary","Migration Summary",[25,2106,2107,2117],{},[28,2108,2109],{},[31,2110,2111,2114],{},[34,2112,2113],{},"Task",[34,2115,2116],{},"Tool",[44,2118,2119,2127,2141,2155,2164,2173,2182],{},[31,2120,2121,2124],{},[49,2122,2123],{},"Export WordPress content",[49,2125,2126],{},"WordPress admin → Tools → Export",[31,2128,2129,2132],{},[49,2130,2131],{},"Convert HTML → Markdown",[49,2133,2134,1486,2136,2138,2139],{},[135,2135,1571],{},[135,2137,1575],{}," \u002F ",[135,2140,1579],{},[31,2142,2143,2146],{},[49,2144,2145],{},"Rewrite image paths",[49,2147,2148,2151,2152],{},[135,2149,2150],{},"sed"," + copy files to ",[135,2153,2154],{},"public\u002Fimages\u002F",[31,2156,2157,2160],{},[49,2158,2159],{},"Extract WordPress comments",[49,2161,2162],{},[135,2163,1800],{},[31,2165,2166,2169],{},[49,2167,2168],{},"Build Discussion JSON files",[49,2170,2171],{},[135,2172,1813],{},[31,2174,2175,2178],{},[49,2176,2177],{},"Create GitHub Discussions",[49,2179,2180],{},[135,2181,1822],{},[31,2183,2184,2187],{},[49,2185,2186],{},"Embed comment widget",[49,2188,2189,1486,2191,2193],{},[135,2190,1906],{},[135,2192,1910],{}," component",[10,2195,2196,2197,1512,2200,2202,2203,2206],{},"One final tip: keep the old WordPress URL structure alive as redirects. Nuxt's ",[135,2198,2199],{},"routeRules",[135,2201,280],{}," generates static ",[135,2204,2205],{},"meta-refresh"," pages for every old URL, so inbound links and search-engine rankings survive the move.",[1525,2208],{},[20,2210,2212],{"id":2211},"cross-tunnel-keeping-links-alive-with-the-link-checker","Cross Tunnel: Keeping Links Alive with the Link Checker",[10,2214,2215],{},"A blog that grows over years accumulates links that eventually rot. Images move, external services vanish, internal slugs get renamed. Rather than discovering this when a reader complains, there is a script for that.",[10,2217,2218,2221,2222,2225],{},[135,2219,2220],{},"scripts\u002Fcheck-links.py"," is a self-contained PEP 723 script (no virtualenv needed) that scans every Markdown file under ",[135,2223,2224],{},"content\u002F"," and validates three categories of links:",[25,2227,2228,2238],{},[28,2229,2230],{},[31,2231,2232,2235],{},[34,2233,2234],{},"Category",[34,2236,2237],{},"What is checked",[44,2239,2240,2255,2269],{},[31,2241,2242,2252],{},[49,2243,2244,2245,187,2248,2251],{},"External (",[135,2246,2247],{},"http",[135,2249,2250],{},"https",")",[49,2253,2254],{},"HTTP HEAD request, falls back to GET; measures response time",[31,2256,2257,2263],{},[49,2258,2259,2260],{},"Internal ",[135,2261,2262],{},"\u002Fimages\u002F…",[49,2264,2265,2266],{},"File existence under ",[135,2267,2268],{},"public\u002F",[31,2270,2271,2279],{},[49,2272,2259,2273,1572,2276],{},[135,2274,2275],{},"\u002Fblog\u002F…",[135,2277,2278],{},"\u002Fpages\u002F…",[49,2280,2281,2282,2284,2285],{},"Matching ",[135,2283,1591],{}," file under ",[135,2286,2224],{},[1535,2288,2290],{"id":2289},"running-it","Running it",[140,2292,2294],{"className":142,"code":2293,"language":144,"meta":145,"style":145},"# Internal links only – fast, no network required\nmake check-links-fast\n\n# Full check including external URLs (parallel, cached)\nmake check-links\n\n# Non-interactive mode for CI pipelines\nmake check-links-ci\n\n# Write a full report to a log file\nmake check-links-log LOG=\u002Ftmp\u002Flinks.log\n",[135,2295,2296,2301,2309,2313,2318,2325,2329,2334,2341,2345,2350],{"__ignoreMap":145},[149,2297,2298],{"class":151,"line":152},[149,2299,2300],{"class":155},"# Internal links only – fast, no network required\n",[149,2302,2303,2306],{"class":151,"line":159},[149,2304,2305],{"class":162},"make",[149,2307,2308],{"class":166}," check-links-fast\n",[149,2310,2311],{"class":151,"line":201},[149,2312,250],{"emptyLinePlaceholder":249},[149,2314,2315],{"class":151,"line":253},[149,2316,2317],{"class":155},"# Full check including external URLs (parallel, cached)\n",[149,2319,2320,2322],{"class":151,"line":259},[149,2321,2305],{"class":162},[149,2323,2324],{"class":166}," check-links\n",[149,2326,2327],{"class":151,"line":390},[149,2328,250],{"emptyLinePlaceholder":249},[149,2330,2331],{"class":151,"line":396},[149,2332,2333],{"class":155},"# Non-interactive mode for CI pipelines\n",[149,2335,2336,2338],{"class":151,"line":402},[149,2337,2305],{"class":162},[149,2339,2340],{"class":166}," check-links-ci\n",[149,2342,2343],{"class":151,"line":407},[149,2344,250],{"emptyLinePlaceholder":249},[149,2346,2347],{"class":151,"line":412},[149,2348,2349],{"class":155},"# Write a full report to a log file\n",[149,2351,2352,2354,2357],{"class":151,"line":619},[149,2353,2305],{"class":162},[149,2355,2356],{"class":166}," check-links-log",[149,2358,2359],{"class":166}," LOG=\u002Ftmp\u002Flinks.log\n",[10,2361,2362,2363,2366],{},"The script keeps a ",[135,2364,2365],{},".link_cache.json"," file in the repo root so external URLs are not hammered on every run:",[25,2368,2369,2379],{},[28,2370,2371],{},[31,2372,2373,2376],{},[34,2374,2375],{},"Cache status",[34,2377,2378],{},"Re-checked after",[44,2380,2381,2391,2401,2411],{},[31,2382,2383,2388],{},[49,2384,2385],{},[135,2386,2387],{},"passed",[49,2389,2390],{},"28 days",[31,2392,2393,2398],{},[49,2394,2395],{},[135,2396,2397],{},"manual",[49,2399,2400],{},"365 days (human-verified)",[31,2402,2403,2408],{},[49,2404,2405],{},[135,2406,2407],{},"captcha",[49,2409,2410],{},"365 days (CAPTCHA-protected page)",[31,2412,2413,2418],{},[49,2414,2415],{},[135,2416,2417],{},"failed",[49,2419,2420],{},"Every run",[10,2422,2423,2424,1576,2426,2428],{},"At the end of an interactive run the script opens a prompt where you can mark problematic links as ",[135,2425,2397],{},[135,2427,2407],{},", add them to an ignore file, or just note them for later. To reset the cache entirely:",[140,2430,2432],{"className":142,"code":2431,"language":144,"meta":145,"style":145},"make check-links-reset\n",[135,2433,2434],{"__ignoreMap":145},[149,2435,2436,2438],{"class":151,"line":152},[149,2437,2305],{"class":162},[149,2439,2440],{"class":166}," check-links-reset\n",[10,2442,1202,2443,2446,2447,2450],{},[135,2444,2445],{},"--ignore-file"," flag accepts a plain-text file with URL prefixes to skip (one per line, ",[135,2448,2449],{},"#"," comments allowed) – useful for localhost addresses or URLs that consistently trigger false positives.",[1525,2452],{},[20,2454,2456],{"id":2455},"the-moles-map-a-tour-of-the-makefile","The Mole's Map: A Tour of the Makefile",[10,2458,2459,2460,2463,2464,2466],{},"Every mole needs a map of its own tunnels. After a few weeks away from the repo it is easy to forget which command does what. The ",[135,2461,2462],{},"Makefile"," is that map – run ",[135,2465,2305],{}," with no arguments to see the full list:",[140,2468,2471],{"className":2469,"code":2470,"language":350,"meta":145},[348],"the78mole-blog – available targets\n\n  Nuxt\n    make install            Install npm dependencies\n    make dev                Start Nuxt dev server  (http:\u002F\u002Flocalhost:3000)\n    make build              Build for SSR\n    make generate           Static site generation (GitHub Pages)\n    make preview            Preview generated site\n\n  Link checking\n    make check-links        Full check: external + internal (interactive)\n    make check-links-fast   Internal links only, no HTTP requests\n    make check-links-ci     Non-interactive check (for CI pipelines)\n    make check-links-log    Full check + write log to $LOG\n    make check-links-reset  Clear the link cache (.link_cache.json)\n\n  Assets\n    make restore-assets     Dry-run: show missing WP assets to restore\n    make restore-assets-do  Actually copy missing WP assets to public\u002F\n",[135,2472,2470],{"__ignoreMap":145},[1535,2474,2476],{"id":2475},"the-day-to-day-workflow","The day-to-day workflow",[140,2478,2480],{"className":142,"code":2479,"language":144,"meta":145,"style":145},"make dev             # write content, hot-reload at localhost:3000\nmake check-links-fast  # verify internal links before pushing\ngit add -A && git commit -m \"new post: …\"\ngit push             # triggers publish.yml → live in ~90 seconds\n",[135,2481,2482,2492,2502,2526],{"__ignoreMap":145},[149,2483,2484,2486,2489],{"class":151,"line":152},[149,2485,2305],{"class":162},[149,2487,2488],{"class":166}," dev",[149,2490,2491],{"class":155},"             # write content, hot-reload at localhost:3000\n",[149,2493,2494,2496,2499],{"class":151,"line":159},[149,2495,2305],{"class":162},[149,2497,2498],{"class":166}," check-links-fast",[149,2500,2501],{"class":155},"  # verify internal links before pushing\n",[149,2503,2504,2506,2509,2512,2515,2517,2520,2523],{"class":151,"line":201},[149,2505,163],{"class":162},[149,2507,2508],{"class":166}," add",[149,2510,2511],{"class":204}," -A",[149,2513,2514],{"class":180}," && ",[149,2516,163],{"class":162},[149,2518,2519],{"class":166}," commit",[149,2521,2522],{"class":204}," -m",[149,2524,2525],{"class":166}," \"new post: …\"\n",[149,2527,2528,2530,2533],{"class":151,"line":253},[149,2529,163],{"class":162},[149,2531,2532],{"class":166}," push",[149,2534,2535],{"class":155},"             # triggers publish.yml → live in ~90 seconds\n",[1535,2537,2539],{"id":2538},"before-a-big-content-batch","Before a big content batch",[140,2541,2543],{"className":142,"code":2542,"language":144,"meta":145,"style":145},"make check-links     # full external check, interactive cache update\nmake restore-assets  # dry-run: see which WP images are still missing\nmake restore-assets-do  # actually copy them once you're happy\n",[135,2544,2545,2555,2565],{"__ignoreMap":145},[149,2546,2547,2549,2552],{"class":151,"line":152},[149,2548,2305],{"class":162},[149,2550,2551],{"class":166}," check-links",[149,2553,2554],{"class":155},"     # full external check, interactive cache update\n",[149,2556,2557,2559,2562],{"class":151,"line":159},[149,2558,2305],{"class":162},[149,2560,2561],{"class":166}," restore-assets",[149,2563,2564],{"class":155},"  # dry-run: see which WP images are still missing\n",[149,2566,2567,2569,2572],{"class":151,"line":201},[149,2568,2305],{"class":162},[149,2570,2571],{"class":166}," restore-assets-do",[149,2573,2574],{"class":155},"  # actually copy them once you're happy\n",[1535,2576,2578],{"id":2577},"targets-at-a-glance","Targets at a glance",[25,2580,2581,2591],{},[28,2582,2583],{},[31,2584,2585,2588],{},[34,2586,2587],{},"Target",[34,2589,2590],{},"Tunnel it digs",[44,2592,2593,2603,2615,2628,2638,2648,2658,2671,2681],{},[31,2594,2595,2600],{},[49,2596,2597],{},[135,2598,2599],{},"make dev",[49,2601,2602],{},"Nuxt dev server with hot-reload",[31,2604,2605,2610],{},[49,2606,2607],{},[135,2608,2609],{},"make generate",[49,2611,2612,2613],{},"Full static build into ",[135,2614,1139],{},[31,2616,2617,2622],{},[49,2618,2619],{},[135,2620,2621],{},"make preview",[49,2623,2624,2625,2627],{},"Serves ",[135,2626,1139],{}," locally",[31,2629,2630,2635],{},[49,2631,2632],{},[135,2633,2634],{},"make check-links-fast",[49,2636,2637],{},"Internal link sanity check (pre-push)",[31,2639,2640,2645],{},[49,2641,2642],{},[135,2643,2644],{},"make check-links",[49,2646,2647],{},"Full dead-link audit with cache",[31,2649,2650,2655],{},[49,2651,2652],{},[135,2653,2654],{},"make check-links-ci",[49,2656,2657],{},"Same, non-interactive (for GitHub Actions)",[31,2659,2660,2665],{},[49,2661,2662],{},[135,2663,2664],{},"make check-links-reset",[49,2666,2667,2668,2670],{},"Wipe ",[135,2669,2365],{}," and start fresh",[31,2672,2673,2678],{},[49,2674,2675],{},[135,2676,2677],{},"make restore-assets",[49,2679,2680],{},"Show missing WordPress media (dry-run)",[31,2682,2683,2688],{},[49,2684,2685],{},[135,2686,2687],{},"make restore-assets-do",[49,2689,2690,2691],{},"Copy missing WordPress media into ",[135,2692,2268],{},[10,2694,2695,2696,2698,2699,2701,2702,2705],{},"The map is always up to date – it is the ",[135,2697,2462],{}," itself. When you add a new script, add a ",[135,2700,2305],{}," target and a ",[135,2703,2704],{},"help"," line at the same time, and future-you will be grateful.",[2707,2708,2709],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}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 .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":145,"searchDepth":159,"depth":159,"links":2711},[2712,2713,2714,2715,2716,2717,2718,2719,2720,2721,2722,2723,2724,2731,2734],{"id":22,"depth":159,"text":23},{"id":129,"depth":159,"text":130},{"id":219,"depth":159,"text":220},{"id":336,"depth":159,"text":337},{"id":427,"depth":159,"text":428},{"id":701,"depth":159,"text":702},{"id":732,"depth":159,"text":733},{"id":1154,"depth":159,"text":1155},{"id":1212,"depth":159,"text":1213},{"id":1316,"depth":159,"text":1317},{"id":1413,"depth":159,"text":1414},{"id":1430,"depth":159,"text":1431},{"id":1529,"depth":159,"text":1530,"children":2725},[2726,2727,2728,2729,2730],{"id":1537,"depth":201,"text":1538},{"id":1560,"depth":201,"text":1561},{"id":1719,"depth":201,"text":1720},{"id":1781,"depth":201,"text":1782},{"id":2103,"depth":201,"text":2104},{"id":2211,"depth":159,"text":2212,"children":2732},[2733],{"id":2289,"depth":201,"text":2290},{"id":2455,"depth":159,"text":2456,"children":2735},[2736,2737,2738],{"id":2475,"depth":201,"text":2476},{"id":2538,"depth":201,"text":2539},{"id":2577,"depth":201,"text":2578},[2740,2741,2742],"Dev","Tools","Web","2026-05-08","How to turn a Nuxt content site into a fully automated, free static blog – from repository setup through custom domain to zero-click deployments.","md","\u002Fimages\u002Fblog\u002F2026\u002F05\u002Fgithub-pages-cover.png",{},"\u002Fblog\u002F2026\u002Fpublishing-on-github-pages-with-nuxt",{"title":5,"description":2744},"blog\u002F2026\u002Fpublishing-on-github-pages-with-nuxt",null,"vUj_9j9nWSUQlqSOfWpU0pMeBYMC3TkBi45DRw9VMcM",1778331518257]