[{"data":1,"prerenderedAt":4623},["ShallowReactive",2],{"\u002Fblog\u002F2026-05-31-nuxt-github-pages-obsidian":3},{"post":4,"newer":4575,"older":4576},{"id":5,"title":6,"body":7,"category":4562,"date":4563,"description":13,"extension":4564,"meta":4565,"navigation":251,"path":4566,"seo":4567,"stem":4568,"tags":4569,"__hash__":4574},"blog\u002Fblog\u002F2026-05-31-nuxt-github-pages-obsidian.md","Building a Blog with Nuxt, Nuxt SEO, GitHub Pages, and Obsidian",{"type":8,"value":9,"toc":4542},"minimark",[10,14,17,34,43,57,60,65,68,71,74,106,110,113,164,169,172,176,183,545,552,570,574,597,601,610,837,844,914,924,928,963,969,1262,1268,1773,1787,1791,1806,1964,1993,1999,2003,2014,2038,2250,2259,2320,2323,2327,2337,2340,2345,2360,2363,2366,2402,2417,2420,2424,2427,2442,2449,2452,2461,2591,2594,2669,2691,2695,2720,2754,2765,2786,2795,2802,2824,2828,2842,2848,3147,3165,3188,3192,3199,3212,3220,3243,3249,3262,3270,3274,3281,3287,3673,3679,3757,3768,3859,3869,3873,3876,3912,3921,4256,4263,4267,4270,4395,4398,4402,4405,4498,4501,4505,4508,4529,4532,4535,4538],[11,12,13],"p",{},"Over the years I have used various combinations to host and deploy a personal website.\nFrom good old Geocities to LAMP deploying via FTP to GitHub Pages and Jekyll.",[11,15,16],{},"At some point GitHub made it possible to use GitHub Actions and artifact upload to deploy websites. This opened the way to the current stack.",[11,18,19,20,27,28,33],{},"After working for a few years with ",[21,22,26],"a",{"href":23,"rel":24},"https:\u002F\u002Fnuxt.com",[25],"nofollow","Nuxt"," and ",[21,29,32],{"href":30,"rel":31},"https:\u002F\u002Fvuejs.org",[25],"Vue"," it seemed like a good idea to move the portfolio site to use technologies I am more familiar with instead of maintaining an outdated Jekyll website.",[11,35,36,37,42],{},"The benefits of such an approach are simple, you need no server, no database, no CMS.\nIt's Vue and CSS for the templates and Markdown files in a git repository as the content (posts) that get converted to static HTML by ",[21,38,41],{"href":39,"rel":40},"https:\u002F\u002Fcontent.nuxt.com",[25],"@nuxt\u002Fcontent",".",[11,44,45,46,51,52,42],{},"For the SEO you can use ",[21,47,50],{"href":48,"rel":49},"https:\u002F\u002Fnuxtseo.com",[25],"@nuxtjs\u002Fseo",". The content is written in plain Markdown and hosted for free on GitHub Pages, and the editing of the posts can be done entirely from ",[21,53,56],{"href":54,"rel":55},"https:\u002F\u002Fobsidian.md",[25],"Obsidian",[11,58,59],{},"This is in fact the exact stack that powers the site you are reading right now.\nHere is how the pieces fit together.",[61,62,64],"h2",{"id":63},"why-this-combination","Why this combination",[11,66,67],{},"There are a lot of \"build your own blog\" guides. Some reach for a hosted CMS or a heavyweight platform, others for a different kind of lightweight static solution. This is not the only, or the right, approach.",[11,69,70],{},"For me, I wanted something that was free to host, fast to load, owned entirely by me, and built with the tools I am comfortable with and use every day.",[11,72,73],{},"The four ingredients:",[75,76,77,84,90,96],"ul",{},[78,79,80,83],"li",{},[81,82,26],"strong",{}," - generates the whole site as static HTML, so it can live anywhere, including free static hosts. It also handles the portfolio and landing page.",[78,85,86,89],{},[81,87,88],{},"Nuxt SEO"," - handles the parts I never want to do by hand: sitemaps, robots, Open Graph images, and structured data, with almost no configuration.",[78,91,92,95],{},[81,93,94],{},"GitHub Pages"," - hosts the output for free and rebuilds on every push.",[78,97,98,100,101,105],{},[81,99,56],{}," - turns the ",[102,103,104],"code",{},"content\u002F"," folder into a comfortable place to write.",[61,107,109],{"id":108},"project-setup","Project setup",[11,111,112],{},"Start from a fresh Nuxt project and add the content and SEO modules:",[114,115,120],"pre",{"className":116,"code":117,"language":118,"meta":119,"style":119},"language-bash shiki shiki-themes catppuccin-frappe catppuccin-latte","pnpm create nuxt@latest my-blog\ncd my-blog\npnpm add @nuxt\u002Fcontent @nuxtjs\u002Fseo\n","bash","",[102,121,122,141,150],{"__ignoreMap":119},[123,124,127,131,135,138],"span",{"class":125,"line":126},"line",1,[123,128,130],{"class":129},"sd8PB","pnpm",[123,132,134],{"class":133},"sq21T"," create",[123,136,137],{"class":133}," nuxt@latest",[123,139,140],{"class":133}," my-blog\n",[123,142,144,148],{"class":125,"line":143},2,[123,145,147],{"class":146},"sf7uP","cd",[123,149,140],{"class":133},[123,151,153,155,158,161],{"class":125,"line":152},3,[123,154,130],{"class":129},[123,156,157],{"class":133}," add",[123,159,160],{"class":133}," @nuxt\u002Fcontent",[123,162,163],{"class":133}," @nuxtjs\u002Fseo\n",[11,165,166,168],{},[102,167,50],{}," is a meta-module, it pulls in sitemap, robots, OG image, schema.org, and link-checker modules together, so you only register one thing.",[11,170,171],{},"This takes care of the markdown to html part along with the blog's SEO after deploying it.",[61,173,175],{"id":174},"configuring-nuxt","Configuring Nuxt",[11,177,178,179,182],{},"The whole site is driven by ",[102,180,181],{},"nuxt.config.ts",". The important bits are the module list, your site\nidentity, and the static-generation settings:",[114,184,188],{"className":185,"code":186,"language":187,"meta":119,"style":119},"language-ts shiki shiki-themes catppuccin-frappe catppuccin-latte","export default defineNuxtConfig({\n  modules: [\n    '@nuxt\u002Fcontent',\n    '@nuxtjs\u002Fseo',\n  ],\n\n  \u002F\u002F Pre-render the homepage and crawl every link from there\n  nitro: {\n    prerender: {\n      crawlLinks: true,\n      routes: ['\u002F'],\n    },\n  },\n\n  \u002F\u002F Generate Open Graph images at build time — no runtime needed\n  ogImage: {\n    enabled: true,\n    zeroRuntime: true,\n  },\n\n  \u002F\u002F Structured data describing who the site is about\n  schemaOrg: {\n    identity: {\n      jobTitle: 'Some Occupation',\n      name: 'Your Name',\n      type: 'Person',\n    },\n  },\n\n  \u002F\u002F Used by every SEO module: canonical URLs, sitemap, OG tags\n  site: {\n    defaultLocale: 'en',\n    description: 'Short description used as the default meta description.',\n    name: 'Your Name',\n    url: 'https:\u002F\u002Fyour-domain.com',\n  },\n});\n","ts",[102,189,190,210,222,230,238,246,253,260,271,281,295,314,320,326,331,337,347,359,371,376,381,387,397,407,420,433,446,451,456,461,467,477,490,503,515,528,533],{"__ignoreMap":119},[123,191,192,196,199,202,206],{"class":125,"line":126},[123,193,195],{"class":194},"sfq9e","export",[123,197,198],{"class":194}," default",[123,200,201],{"class":129}," defineNuxtConfig",[123,203,205],{"class":204},"s5pMN","(",[123,207,209],{"class":208},"sRlM1","{\n",[123,211,212,215,219],{"class":125,"line":143},[123,213,214],{"class":204},"  modules",[123,216,218],{"class":217},"sW4Yu",":",[123,220,221],{"class":204}," [\n",[123,223,224,227],{"class":125,"line":152},[123,225,226],{"class":133},"    '@nuxt\u002Fcontent'",[123,228,229],{"class":208},",\n",[123,231,233,236],{"class":125,"line":232},4,[123,234,235],{"class":133},"    '@nuxtjs\u002Fseo'",[123,237,229],{"class":208},[123,239,241,244],{"class":125,"line":240},5,[123,242,243],{"class":204},"  ]",[123,245,229],{"class":208},[123,247,249],{"class":125,"line":248},6,[123,250,252],{"emptyLinePlaceholder":251},true,"\n",[123,254,256],{"class":125,"line":255},7,[123,257,259],{"class":258},"sRznC","  \u002F\u002F Pre-render the homepage and crawl every link from there\n",[123,261,263,266,268],{"class":125,"line":262},8,[123,264,265],{"class":204},"  nitro",[123,267,218],{"class":217},[123,269,270],{"class":208}," {\n",[123,272,274,277,279],{"class":125,"line":273},9,[123,275,276],{"class":204},"    prerender",[123,278,218],{"class":217},[123,280,270],{"class":208},[123,282,284,287,289,293],{"class":125,"line":283},10,[123,285,286],{"class":204},"      crawlLinks",[123,288,218],{"class":217},[123,290,292],{"class":291},"sId4k"," true",[123,294,229],{"class":208},[123,296,298,301,303,306,309,312],{"class":125,"line":297},11,[123,299,300],{"class":204},"      routes",[123,302,218],{"class":217},[123,304,305],{"class":204}," [",[123,307,308],{"class":133},"'\u002F'",[123,310,311],{"class":204},"]",[123,313,229],{"class":208},[123,315,317],{"class":125,"line":316},12,[123,318,319],{"class":208},"    },\n",[123,321,323],{"class":125,"line":322},13,[123,324,325],{"class":208},"  },\n",[123,327,329],{"class":125,"line":328},14,[123,330,252],{"emptyLinePlaceholder":251},[123,332,334],{"class":125,"line":333},15,[123,335,336],{"class":258},"  \u002F\u002F Generate Open Graph images at build time — no runtime needed\n",[123,338,340,343,345],{"class":125,"line":339},16,[123,341,342],{"class":204},"  ogImage",[123,344,218],{"class":217},[123,346,270],{"class":208},[123,348,350,353,355,357],{"class":125,"line":349},17,[123,351,352],{"class":204},"    enabled",[123,354,218],{"class":217},[123,356,292],{"class":291},[123,358,229],{"class":208},[123,360,362,365,367,369],{"class":125,"line":361},18,[123,363,364],{"class":204},"    zeroRuntime",[123,366,218],{"class":217},[123,368,292],{"class":291},[123,370,229],{"class":208},[123,372,374],{"class":125,"line":373},19,[123,375,325],{"class":208},[123,377,379],{"class":125,"line":378},20,[123,380,252],{"emptyLinePlaceholder":251},[123,382,384],{"class":125,"line":383},21,[123,385,386],{"class":258},"  \u002F\u002F Structured data describing who the site is about\n",[123,388,390,393,395],{"class":125,"line":389},22,[123,391,392],{"class":204},"  schemaOrg",[123,394,218],{"class":217},[123,396,270],{"class":208},[123,398,400,403,405],{"class":125,"line":399},23,[123,401,402],{"class":204},"    identity",[123,404,218],{"class":217},[123,406,270],{"class":208},[123,408,410,413,415,418],{"class":125,"line":409},24,[123,411,412],{"class":204},"      jobTitle",[123,414,218],{"class":217},[123,416,417],{"class":133}," 'Some Occupation'",[123,419,229],{"class":208},[123,421,423,426,428,431],{"class":125,"line":422},25,[123,424,425],{"class":204},"      name",[123,427,218],{"class":217},[123,429,430],{"class":133}," 'Your Name'",[123,432,229],{"class":208},[123,434,436,439,441,444],{"class":125,"line":435},26,[123,437,438],{"class":204},"      type",[123,440,218],{"class":217},[123,442,443],{"class":133}," 'Person'",[123,445,229],{"class":208},[123,447,449],{"class":125,"line":448},27,[123,450,319],{"class":208},[123,452,454],{"class":125,"line":453},28,[123,455,325],{"class":208},[123,457,459],{"class":125,"line":458},29,[123,460,252],{"emptyLinePlaceholder":251},[123,462,464],{"class":125,"line":463},30,[123,465,466],{"class":258},"  \u002F\u002F Used by every SEO module: canonical URLs, sitemap, OG tags\n",[123,468,470,473,475],{"class":125,"line":469},31,[123,471,472],{"class":204},"  site",[123,474,218],{"class":217},[123,476,270],{"class":208},[123,478,480,483,485,488],{"class":125,"line":479},32,[123,481,482],{"class":204},"    defaultLocale",[123,484,218],{"class":217},[123,486,487],{"class":133}," 'en'",[123,489,229],{"class":208},[123,491,493,496,498,501],{"class":125,"line":492},33,[123,494,495],{"class":204},"    description",[123,497,218],{"class":217},[123,499,500],{"class":133}," 'Short description used as the default meta description.'",[123,502,229],{"class":208},[123,504,506,509,511,513],{"class":125,"line":505},34,[123,507,508],{"class":204},"    name",[123,510,218],{"class":217},[123,512,430],{"class":133},[123,514,229],{"class":208},[123,516,518,521,523,526],{"class":125,"line":517},35,[123,519,520],{"class":204},"    url",[123,522,218],{"class":217},[123,524,525],{"class":133}," 'https:\u002F\u002Fyour-domain.com'",[123,527,229],{"class":208},[123,529,531],{"class":125,"line":530},36,[123,532,325],{"class":208},[123,534,536,539,542],{"class":125,"line":535},37,[123,537,538],{"class":208},"}",[123,540,541],{"class":204},")",[123,543,544],{"class":208},";\n",[11,546,547,548,551],{},"Get ",[102,549,550],{},"site.url"," right before anything else. Every canonical link, sitemap entry, and OG tag is built from it. Set it once and the modules do the rest.",[11,553,554,555,558,559,27,564,569],{},"The ",[102,556,557],{},"zeroRuntime"," OG image option renders each social-share image to a PNG at build time using ",[21,560,563],{"href":561,"rel":562},"https:\u002F\u002Fgithub.com\u002Fvercel\u002Fsatori",[25],"satori",[21,565,568],{"href":566,"rel":567},"https:\u002F\u002Fgithub.com\u002Fyisibl\u002Fresvg-js",[25],"resvg",", so there is no serverless function involved. That matters on GitHub Pages, where there is no server to run.",[61,571,573],{"id":572},"the-landing-page","The landing page",[11,575,576,577,580,581,586,587,592,593,596],{},"The blog is only one part of the site. The home page and portfolio are plain ",[21,578,26],{"href":23,"rel":579},[25]," pages styled with ",[21,582,585],{"href":583,"rel":584},"https:\u002F\u002Ftailwindcss.com",[25],"Tailwind"," (added through its ",[21,588,591],{"href":589,"rel":590},"https:\u002F\u002Ftailwindcss.com\u002Fdocs\u002Finstallation\u002Fframework-guides\u002Fnuxt",[25],"Nuxt setup guide","). There is nothing blog-specific about them; they are regular pages that link to ",[102,594,595],{},"\u002Fblog",", which also lets the prerenderer crawl to the listing. I am keeping the visual design out of scope here. Using Nuxt with tailwind you can do whatever you like.",[61,598,600],{"id":599},"writing-content-as-markdown","Writing content as Markdown",[11,602,603,605,606,609],{},[102,604,41],{}," (v3) uses typed collections. Define the shape of a blog post once in\n",[102,607,608],{},"content.config.ts"," and you get validation and type safety for free:",[114,611,613],{"className":185,"code":612,"language":187,"meta":119,"style":119},"import { defineCollection, defineContentConfig, z } from '@nuxt\u002Fcontent';\n\nexport default defineContentConfig({\n  collections: {\n    blog: defineCollection({\n      schema: z.object({\n        category: z.string(),\n        date: z.date(),\n        tags: z.array(z.string()),\n        title: z.string(),\n      }),\n      source: 'blog\u002F*.md',\n      type: 'page',\n    }),\n  },\n});\n",[102,614,615,647,651,663,672,685,704,723,741,767,784,793,805,816,825,829],{"__ignoreMap":119},[123,616,617,620,623,626,629,632,634,637,639,642,645],{"class":125,"line":126},[123,618,619],{"class":194},"import",[123,621,622],{"class":208}," {",[123,624,625],{"class":204}," defineCollection",[123,627,628],{"class":208},",",[123,630,631],{"class":204}," defineContentConfig",[123,633,628],{"class":208},[123,635,636],{"class":204}," z ",[123,638,538],{"class":208},[123,640,641],{"class":194}," from",[123,643,644],{"class":133}," '@nuxt\u002Fcontent'",[123,646,544],{"class":208},[123,648,649],{"class":125,"line":143},[123,650,252],{"emptyLinePlaceholder":251},[123,652,653,655,657,659,661],{"class":125,"line":152},[123,654,195],{"class":194},[123,656,198],{"class":194},[123,658,631],{"class":129},[123,660,205],{"class":204},[123,662,209],{"class":208},[123,664,665,668,670],{"class":125,"line":232},[123,666,667],{"class":204},"  collections",[123,669,218],{"class":217},[123,671,270],{"class":208},[123,673,674,677,679,681,683],{"class":125,"line":240},[123,675,676],{"class":204},"    blog",[123,678,218],{"class":217},[123,680,625],{"class":129},[123,682,205],{"class":204},[123,684,209],{"class":208},[123,686,687,690,692,695,697,700,702],{"class":125,"line":248},[123,688,689],{"class":204},"      schema",[123,691,218],{"class":217},[123,693,694],{"class":204}," z",[123,696,42],{"class":217},[123,698,699],{"class":129},"object",[123,701,205],{"class":204},[123,703,209],{"class":208},[123,705,706,709,711,713,715,718,721],{"class":125,"line":255},[123,707,708],{"class":204},"        category",[123,710,218],{"class":217},[123,712,694],{"class":204},[123,714,42],{"class":217},[123,716,717],{"class":129},"string",[123,719,720],{"class":204},"()",[123,722,229],{"class":208},[123,724,725,728,730,732,734,737,739],{"class":125,"line":262},[123,726,727],{"class":204},"        date",[123,729,218],{"class":217},[123,731,694],{"class":204},[123,733,42],{"class":217},[123,735,736],{"class":129},"date",[123,738,720],{"class":204},[123,740,229],{"class":208},[123,742,743,746,748,750,752,755,758,760,762,765],{"class":125,"line":273},[123,744,745],{"class":204},"        tags",[123,747,218],{"class":217},[123,749,694],{"class":204},[123,751,42],{"class":217},[123,753,754],{"class":129},"array",[123,756,757],{"class":204},"(z",[123,759,42],{"class":217},[123,761,717],{"class":129},[123,763,764],{"class":204},"())",[123,766,229],{"class":208},[123,768,769,772,774,776,778,780,782],{"class":125,"line":283},[123,770,771],{"class":204},"        title",[123,773,218],{"class":217},[123,775,694],{"class":204},[123,777,42],{"class":217},[123,779,717],{"class":129},[123,781,720],{"class":204},[123,783,229],{"class":208},[123,785,786,789,791],{"class":125,"line":297},[123,787,788],{"class":208},"      }",[123,790,541],{"class":204},[123,792,229],{"class":208},[123,794,795,798,800,803],{"class":125,"line":316},[123,796,797],{"class":204},"      source",[123,799,218],{"class":217},[123,801,802],{"class":133}," 'blog\u002F*.md'",[123,804,229],{"class":208},[123,806,807,809,811,814],{"class":125,"line":322},[123,808,438],{"class":204},[123,810,218],{"class":217},[123,812,813],{"class":133}," 'page'",[123,815,229],{"class":208},[123,817,818,821,823],{"class":125,"line":328},[123,819,820],{"class":208},"    }",[123,822,541],{"class":204},[123,824,229],{"class":208},[123,826,827],{"class":125,"line":333},[123,828,325],{"class":208},[123,830,831,833,835],{"class":125,"line":339},[123,832,538],{"class":208},[123,834,541],{"class":204},[123,836,544],{"class":208},[11,838,839,840,843],{},"Posts then live as Markdown files under ",[102,841,842],{},"content\u002Fblog\u002F",", each with frontmatter matching that schema:",[114,845,849],{"className":846,"code":847,"language":848,"meta":119,"style":119},"language-markdown shiki shiki-themes catppuccin-frappe catppuccin-latte","---\ntitle: 'My First Post'\ncategory: news\ntags: [ 'announcement' ]\ndate: 2026-05-31\n---\n\nThe body of the post goes here, in plain Markdown.\n","markdown",[102,850,851,856,867,877,892,901,905,909],{"__ignoreMap":119},[123,852,853],{"class":125,"line":126},[123,854,855],{"class":208},"---\n",[123,857,858,862,864],{"class":125,"line":143},[123,859,861],{"class":860},"sN9LH","title",[123,863,218],{"class":217},[123,865,866],{"class":133}," 'My First Post'\n",[123,868,869,872,874],{"class":125,"line":152},[123,870,871],{"class":860},"category",[123,873,218],{"class":217},[123,875,876],{"class":133}," news\n",[123,878,879,882,884,886,889],{"class":125,"line":232},[123,880,881],{"class":860},"tags",[123,883,218],{"class":217},[123,885,305],{"class":208},[123,887,888],{"class":133}," 'announcement'",[123,890,891],{"class":208}," ]\n",[123,893,894,896,898],{"class":125,"line":240},[123,895,736],{"class":860},[123,897,218],{"class":217},[123,899,900],{"class":204}," 2026-05-31\n",[123,902,903],{"class":125,"line":248},[123,904,855],{"class":208},[123,906,907],{"class":125,"line":255},[123,908,252],{"emptyLinePlaceholder":251},[123,910,911],{"class":125,"line":262},[123,912,913],{"class":204},"The body of the post goes here, in plain Markdown.\n",[11,915,916,917,920,921,42],{},"The filename becomes the URL slug, so for example ",[102,918,919],{},"content\u002Fblog\u002Fmy-first-post.md"," is served at ",[102,922,923],{},"\u002Fblog\u002Fmy-first-post",[61,925,927],{"id":926},"rendering-posts","Rendering posts",[11,929,930,931,934,935,940,941,944,945,947,948,944,951,954,955,958,959,962],{},"Both files go under the app's ",[102,932,933],{},"pages\u002F"," directory, and ",[21,936,939],{"href":937,"rel":938},"https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fguide\u002Fdirectory-structure\u002Fpages",[25],"Nuxt's file-based routing"," turns ",[102,942,943],{},"pages\u002Fblog\u002Findex.vue"," into ",[102,946,595],{}," and the catch-all ",[102,949,950],{},"pages\u002Fblog\u002F[...slug].vue",[102,952,953],{},"\u002Fblog\u002F\u003Cslug>"," on its own. Everything else, the ",[102,956,957],{},"app.vue"," entry, the dev server, and the build, comes from the ",[102,960,961],{},"create nuxt"," scaffold, so this is the only part you actually write.",[11,964,965,966,968],{},"Two pages do most of the work. The listing page (",[102,967,943],{},") queries every post, newest first, and renders a link for each:",[114,970,974],{"className":971,"code":972,"language":973,"meta":119,"style":119},"language-vue shiki shiki-themes catppuccin-frappe catppuccin-latte","\u003Cscript setup lang=\"ts\">\nconst { data: posts } = await useAsyncData('blog', () =>\n  queryCollection('blog').order('date', 'DESC').all());\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Ch1>Latest posts\u003C\u002Fh1>\n    \u003Cul>\n      \u003Cli\n        v-for=\"post in posts\"\n        :key=\"post.path\"\n      >\n        \u003CNuxtLink :to=\"post.path\">\n          {{ post.title }}\n        \u003C\u002FNuxtLink>\n        \u003Ctime :datetime=\"String(post.date)\">{{ post.date }}\u003C\u002Ftime>\n      \u003C\u002Fli>\n    \u003C\u002Ful>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n","vue",[102,975,976,1000,1039,1076,1085,1089,1098,1108,1128,1136,1144,1154,1164,1169,1187,1192,1201,1227,1236,1245,1254],{"__ignoreMap":119},[123,977,978,981,984,988,991,994,997],{"class":125,"line":126},[123,979,980],{"class":217},"\u003C",[123,982,983],{"class":860},"script",[123,985,987],{"class":986},"sUXCo"," setup",[123,989,990],{"class":986}," lang",[123,992,993],{"class":217},"=",[123,995,996],{"class":133},"\"ts\"",[123,998,999],{"class":217},">\n",[123,1001,1002,1005,1007,1010,1012,1015,1017,1020,1023,1026,1028,1031,1033,1036],{"class":125,"line":143},[123,1003,1004],{"class":194},"const",[123,1006,622],{"class":208},[123,1008,1009],{"class":204}," data",[123,1011,218],{"class":208},[123,1013,1014],{"class":204}," posts ",[123,1016,538],{"class":208},[123,1018,1019],{"class":217}," =",[123,1021,1022],{"class":194}," await",[123,1024,1025],{"class":129}," useAsyncData",[123,1027,205],{"class":204},[123,1029,1030],{"class":133},"'blog'",[123,1032,628],{"class":208},[123,1034,1035],{"class":208}," ()",[123,1037,1038],{"class":217}," =>\n",[123,1040,1041,1044,1046,1048,1050,1052,1055,1057,1060,1062,1065,1067,1069,1072,1074],{"class":125,"line":152},[123,1042,1043],{"class":129},"  queryCollection",[123,1045,205],{"class":204},[123,1047,1030],{"class":133},[123,1049,541],{"class":204},[123,1051,42],{"class":217},[123,1053,1054],{"class":129},"order",[123,1056,205],{"class":204},[123,1058,1059],{"class":133},"'date'",[123,1061,628],{"class":208},[123,1063,1064],{"class":133}," 'DESC'",[123,1066,541],{"class":204},[123,1068,42],{"class":217},[123,1070,1071],{"class":129},"all",[123,1073,764],{"class":204},[123,1075,544],{"class":208},[123,1077,1078,1081,1083],{"class":125,"line":232},[123,1079,1080],{"class":217},"\u003C\u002F",[123,1082,983],{"class":860},[123,1084,999],{"class":217},[123,1086,1087],{"class":125,"line":240},[123,1088,252],{"emptyLinePlaceholder":251},[123,1090,1091,1093,1096],{"class":125,"line":248},[123,1092,980],{"class":217},[123,1094,1095],{"class":860},"template",[123,1097,999],{"class":217},[123,1099,1100,1103,1106],{"class":125,"line":255},[123,1101,1102],{"class":217},"  \u003C",[123,1104,1105],{"class":860},"div",[123,1107,999],{"class":217},[123,1109,1110,1113,1116,1119,1122,1124,1126],{"class":125,"line":262},[123,1111,1112],{"class":217},"    \u003C",[123,1114,1115],{"class":860},"h1",[123,1117,1118],{"class":217},">",[123,1120,1121],{"class":204},"Latest posts",[123,1123,1080],{"class":217},[123,1125,1115],{"class":860},[123,1127,999],{"class":217},[123,1129,1130,1132,1134],{"class":125,"line":273},[123,1131,1112],{"class":217},[123,1133,75],{"class":860},[123,1135,999],{"class":217},[123,1137,1138,1141],{"class":125,"line":283},[123,1139,1140],{"class":217},"      \u003C",[123,1142,1143],{"class":860},"li\n",[123,1145,1146,1149,1151],{"class":125,"line":297},[123,1147,1148],{"class":986},"        v-for",[123,1150,993],{"class":217},[123,1152,1153],{"class":133},"\"post in posts\"\n",[123,1155,1156,1159,1161],{"class":125,"line":316},[123,1157,1158],{"class":986},"        :key",[123,1160,993],{"class":217},[123,1162,1163],{"class":133},"\"post.path\"\n",[123,1165,1166],{"class":125,"line":322},[123,1167,1168],{"class":217},"      >\n",[123,1170,1171,1174,1177,1180,1182,1185],{"class":125,"line":328},[123,1172,1173],{"class":217},"        \u003C",[123,1175,1176],{"class":860},"NuxtLink",[123,1178,1179],{"class":986}," :to",[123,1181,993],{"class":217},[123,1183,1184],{"class":133},"\"post.path\"",[123,1186,999],{"class":217},[123,1188,1189],{"class":125,"line":333},[123,1190,1191],{"class":204},"          {{ post.title }}\n",[123,1193,1194,1197,1199],{"class":125,"line":339},[123,1195,1196],{"class":217},"        \u003C\u002F",[123,1198,1176],{"class":860},[123,1200,999],{"class":217},[123,1202,1203,1205,1208,1211,1213,1216,1218,1221,1223,1225],{"class":125,"line":349},[123,1204,1173],{"class":217},[123,1206,1207],{"class":860},"time",[123,1209,1210],{"class":986}," :datetime",[123,1212,993],{"class":217},[123,1214,1215],{"class":133},"\"String(post.date)\"",[123,1217,1118],{"class":217},[123,1219,1220],{"class":204},"{{ post.date }}",[123,1222,1080],{"class":217},[123,1224,1207],{"class":860},[123,1226,999],{"class":217},[123,1228,1229,1232,1234],{"class":125,"line":361},[123,1230,1231],{"class":217},"      \u003C\u002F",[123,1233,78],{"class":860},[123,1235,999],{"class":217},[123,1237,1238,1241,1243],{"class":125,"line":373},[123,1239,1240],{"class":217},"    \u003C\u002F",[123,1242,75],{"class":860},[123,1244,999],{"class":217},[123,1246,1247,1250,1252],{"class":125,"line":378},[123,1248,1249],{"class":217},"  \u003C\u002F",[123,1251,1105],{"class":860},[123,1253,999],{"class":217},[123,1255,1256,1258,1260],{"class":125,"line":383},[123,1257,1080],{"class":217},[123,1259,1095],{"class":860},[123,1261,999],{"class":217},[11,1263,1264,1265,1267],{},"The individual post lives at a catch-all route (",[102,1266,950],{},"). It fetches the one post that matches the current path, renders its body, and wires up the SEO:",[114,1269,1271],{"className":971,"code":1270,"language":973,"meta":119,"style":119},"\u003Cscript setup lang=\"ts\">\nconst route = useRoute();\n\n\u002F\u002F Trim a trailing slash before matching. Static hosts often serve \u002Fblog\u002Ffoo\u002F\n\u002F\u002F while the stored path has none, so without this a valid post resolves to\n\u002F\u002F nothing and you get bounced to a 404.\nconst path = route.path.replace(\u002F\\\u002F+$\u002F, '') || '\u002F';\n\nconst { data: post } = await useAsyncData(path, () =>\n  queryCollection('blog').path(path).first());\n\n\u002F\u002F A genuinely missing post should 404, not render an empty page.\nif (!post.value) {\n  throw createError({ statusCode: 404, statusMessage: 'Post not found', fatal: true });\n}\n\nuseSeoMeta({\n  title: () => post.value?.title,\n  ogType: 'article',\n});\n\ndefineArticle({\n  headline: () => post.value?.title,\n  datePublished: () => post.value?.date,\n});\n\ndefineOgImage('Site', { title: post.value?.title });\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",[102,1272,1273,1289,1305,1309,1314,1319,1324,1377,1381,1411,1437,1441,1446,1467,1514,1519,1523,1532,1559,1571,1579,1583,1592,1615,1638,1646,1650,1686,1694,1698,1706,1723,1740,1757,1765],{"__ignoreMap":119},[123,1274,1275,1277,1279,1281,1283,1285,1287],{"class":125,"line":126},[123,1276,980],{"class":217},[123,1278,983],{"class":860},[123,1280,987],{"class":986},[123,1282,990],{"class":986},[123,1284,993],{"class":217},[123,1286,996],{"class":133},[123,1288,999],{"class":217},[123,1290,1291,1293,1296,1298,1301,1303],{"class":125,"line":143},[123,1292,1004],{"class":194},[123,1294,1295],{"class":204}," route ",[123,1297,993],{"class":217},[123,1299,1300],{"class":129}," useRoute",[123,1302,720],{"class":204},[123,1304,544],{"class":208},[123,1306,1307],{"class":125,"line":152},[123,1308,252],{"emptyLinePlaceholder":251},[123,1310,1311],{"class":125,"line":232},[123,1312,1313],{"class":258},"\u002F\u002F Trim a trailing slash before matching. Static hosts often serve \u002Fblog\u002Ffoo\u002F\n",[123,1315,1316],{"class":125,"line":240},[123,1317,1318],{"class":258},"\u002F\u002F while the stored path has none, so without this a valid post resolves to\n",[123,1320,1321],{"class":125,"line":248},[123,1322,1323],{"class":258},"\u002F\u002F nothing and you get bounced to a 404.\n",[123,1325,1326,1328,1331,1333,1336,1338,1341,1343,1346,1348,1352,1355,1358,1361,1363,1366,1369,1372,1375],{"class":125,"line":255},[123,1327,1004],{"class":194},[123,1329,1330],{"class":204}," path ",[123,1332,993],{"class":217},[123,1334,1335],{"class":204}," route",[123,1337,42],{"class":217},[123,1339,1340],{"class":204},"path",[123,1342,42],{"class":217},[123,1344,1345],{"class":129},"replace",[123,1347,205],{"class":204},[123,1349,1351],{"class":1350},"souGF","\u002F\\\u002F",[123,1353,1354],{"class":217},"+",[123,1356,1357],{"class":194},"$",[123,1359,1360],{"class":1350},"\u002F",[123,1362,628],{"class":208},[123,1364,1365],{"class":133}," ''",[123,1367,1368],{"class":204},") ",[123,1370,1371],{"class":217},"||",[123,1373,1374],{"class":133}," '\u002F'",[123,1376,544],{"class":208},[123,1378,1379],{"class":125,"line":262},[123,1380,252],{"emptyLinePlaceholder":251},[123,1382,1383,1385,1387,1389,1391,1394,1396,1398,1400,1402,1405,1407,1409],{"class":125,"line":273},[123,1384,1004],{"class":194},[123,1386,622],{"class":208},[123,1388,1009],{"class":204},[123,1390,218],{"class":208},[123,1392,1393],{"class":204}," post ",[123,1395,538],{"class":208},[123,1397,1019],{"class":217},[123,1399,1022],{"class":194},[123,1401,1025],{"class":129},[123,1403,1404],{"class":204},"(path",[123,1406,628],{"class":208},[123,1408,1035],{"class":208},[123,1410,1038],{"class":217},[123,1412,1413,1415,1417,1419,1421,1423,1425,1428,1430,1433,1435],{"class":125,"line":283},[123,1414,1043],{"class":129},[123,1416,205],{"class":204},[123,1418,1030],{"class":133},[123,1420,541],{"class":204},[123,1422,42],{"class":217},[123,1424,1340],{"class":129},[123,1426,1427],{"class":204},"(path)",[123,1429,42],{"class":217},[123,1431,1432],{"class":129},"first",[123,1434,764],{"class":204},[123,1436,544],{"class":208},[123,1438,1439],{"class":125,"line":297},[123,1440,252],{"emptyLinePlaceholder":251},[123,1442,1443],{"class":125,"line":316},[123,1444,1445],{"class":258},"\u002F\u002F A genuinely missing post should 404, not render an empty page.\n",[123,1447,1448,1451,1454,1457,1460,1462,1465],{"class":125,"line":322},[123,1449,1450],{"class":194},"if",[123,1452,1453],{"class":204}," (",[123,1455,1456],{"class":217},"!",[123,1458,1459],{"class":204},"post",[123,1461,42],{"class":217},[123,1463,1464],{"class":204},"value) ",[123,1466,209],{"class":208},[123,1468,1469,1472,1475,1477,1480,1483,1485,1488,1490,1493,1495,1498,1500,1503,1505,1507,1510,1512],{"class":125,"line":328},[123,1470,1471],{"class":194},"  throw",[123,1473,1474],{"class":129}," createError",[123,1476,205],{"class":204},[123,1478,1479],{"class":208},"{",[123,1481,1482],{"class":204}," statusCode",[123,1484,218],{"class":217},[123,1486,1487],{"class":291}," 404",[123,1489,628],{"class":208},[123,1491,1492],{"class":204}," statusMessage",[123,1494,218],{"class":217},[123,1496,1497],{"class":133}," 'Post not found'",[123,1499,628],{"class":208},[123,1501,1502],{"class":204}," fatal",[123,1504,218],{"class":217},[123,1506,292],{"class":291},[123,1508,1509],{"class":208}," }",[123,1511,541],{"class":204},[123,1513,544],{"class":208},[123,1515,1516],{"class":125,"line":333},[123,1517,1518],{"class":208},"}\n",[123,1520,1521],{"class":125,"line":339},[123,1522,252],{"emptyLinePlaceholder":251},[123,1524,1525,1528,1530],{"class":125,"line":349},[123,1526,1527],{"class":129},"useSeoMeta",[123,1529,205],{"class":204},[123,1531,209],{"class":208},[123,1533,1534,1537,1539,1541,1544,1547,1549,1552,1555,1557],{"class":125,"line":361},[123,1535,1536],{"class":129},"  title",[123,1538,218],{"class":217},[123,1540,1035],{"class":208},[123,1542,1543],{"class":217}," =>",[123,1545,1546],{"class":204}," post",[123,1548,42],{"class":217},[123,1550,1551],{"class":204},"value",[123,1553,1554],{"class":217},"?.",[123,1556,861],{"class":204},[123,1558,229],{"class":208},[123,1560,1561,1564,1566,1569],{"class":125,"line":373},[123,1562,1563],{"class":204},"  ogType",[123,1565,218],{"class":217},[123,1567,1568],{"class":133}," 'article'",[123,1570,229],{"class":208},[123,1572,1573,1575,1577],{"class":125,"line":378},[123,1574,538],{"class":208},[123,1576,541],{"class":204},[123,1578,544],{"class":208},[123,1580,1581],{"class":125,"line":383},[123,1582,252],{"emptyLinePlaceholder":251},[123,1584,1585,1588,1590],{"class":125,"line":389},[123,1586,1587],{"class":129},"defineArticle",[123,1589,205],{"class":204},[123,1591,209],{"class":208},[123,1593,1594,1597,1599,1601,1603,1605,1607,1609,1611,1613],{"class":125,"line":399},[123,1595,1596],{"class":129},"  headline",[123,1598,218],{"class":217},[123,1600,1035],{"class":208},[123,1602,1543],{"class":217},[123,1604,1546],{"class":204},[123,1606,42],{"class":217},[123,1608,1551],{"class":204},[123,1610,1554],{"class":217},[123,1612,861],{"class":204},[123,1614,229],{"class":208},[123,1616,1617,1620,1622,1624,1626,1628,1630,1632,1634,1636],{"class":125,"line":409},[123,1618,1619],{"class":129},"  datePublished",[123,1621,218],{"class":217},[123,1623,1035],{"class":208},[123,1625,1543],{"class":217},[123,1627,1546],{"class":204},[123,1629,42],{"class":217},[123,1631,1551],{"class":204},[123,1633,1554],{"class":217},[123,1635,736],{"class":204},[123,1637,229],{"class":208},[123,1639,1640,1642,1644],{"class":125,"line":422},[123,1641,538],{"class":208},[123,1643,541],{"class":204},[123,1645,544],{"class":208},[123,1647,1648],{"class":125,"line":435},[123,1649,252],{"emptyLinePlaceholder":251},[123,1651,1652,1655,1657,1660,1662,1664,1667,1669,1671,1673,1675,1677,1680,1682,1684],{"class":125,"line":448},[123,1653,1654],{"class":129},"defineOgImage",[123,1656,205],{"class":204},[123,1658,1659],{"class":133},"'Site'",[123,1661,628],{"class":208},[123,1663,622],{"class":208},[123,1665,1666],{"class":204}," title",[123,1668,218],{"class":217},[123,1670,1546],{"class":204},[123,1672,42],{"class":217},[123,1674,1551],{"class":204},[123,1676,1554],{"class":217},[123,1678,1679],{"class":204},"title ",[123,1681,538],{"class":208},[123,1683,541],{"class":204},[123,1685,544],{"class":208},[123,1687,1688,1690,1692],{"class":125,"line":453},[123,1689,1080],{"class":217},[123,1691,983],{"class":860},[123,1693,999],{"class":217},[123,1695,1696],{"class":125,"line":458},[123,1697,252],{"emptyLinePlaceholder":251},[123,1699,1700,1702,1704],{"class":125,"line":463},[123,1701,980],{"class":217},[123,1703,1095],{"class":860},[123,1705,999],{"class":217},[123,1707,1708,1710,1713,1716,1718,1721],{"class":125,"line":469},[123,1709,1102],{"class":217},[123,1711,1712],{"class":860},"article",[123,1714,1715],{"class":986}," v-if",[123,1717,993],{"class":217},[123,1719,1720],{"class":133},"\"post\"",[123,1722,999],{"class":217},[123,1724,1725,1727,1729,1731,1734,1736,1738],{"class":125,"line":479},[123,1726,1112],{"class":217},[123,1728,1115],{"class":860},[123,1730,1118],{"class":217},[123,1732,1733],{"class":204},"{{ post.title }}",[123,1735,1080],{"class":217},[123,1737,1115],{"class":860},[123,1739,999],{"class":217},[123,1741,1742,1744,1747,1750,1752,1754],{"class":125,"line":492},[123,1743,1112],{"class":217},[123,1745,1746],{"class":860},"ContentRenderer",[123,1748,1749],{"class":986}," :value",[123,1751,993],{"class":217},[123,1753,1720],{"class":133},[123,1755,1756],{"class":217}," \u002F>\n",[123,1758,1759,1761,1763],{"class":125,"line":505},[123,1760,1249],{"class":217},[123,1762,1712],{"class":860},[123,1764,999],{"class":217},[123,1766,1767,1769,1771],{"class":125,"line":517},[123,1768,1080],{"class":217},[123,1770,1095],{"class":860},[123,1772,999],{"class":217},[11,1774,1775,1778,1779,1781,1782,1781,1784,1786],{},[102,1776,1777],{},"\u003CContentRenderer>"," takes the parsed post and renders the Markdown body to HTML. The SEO helpers (",[102,1780,1527],{},", ",[102,1783,1587],{},[102,1785,1654],{},") come from Nuxt SEO and bake the per-post title, article schema, and social image in at prerender time.",[61,1788,1790],{"id":1789},"highlighting-code","Highlighting code",[11,1792,1793,1794,1799,1800,1802,1803,218],{},"Since this is a developer blog, the code blocks matter. Nuxt Content runs ",[21,1795,1798],{"href":1796,"rel":1797},"https:\u002F\u002Fshiki.style",[25],"Shiki"," under the hood, and you configure it in ",[102,1801,181],{}," under ",[102,1804,1805],{},"content.build.markdown.highlight",[114,1807,1809],{"className":185,"code":1808,"language":187,"meta":119,"style":119},"export default defineNuxtConfig({\n  content: {\n    build: {\n      markdown: {\n        highlight: {\n          preload: ['ts', 'vue', 'bash', 'yaml', 'json', 'diff'],\n          theme: {\n            dark: 'catppuccin-frappe',\n            default: 'catppuccin-latte',\n          },\n        },\n      },\n    },\n  },\n});\n",[102,1810,1811,1823,1832,1841,1850,1859,1900,1909,1921,1933,1938,1943,1948,1952,1956],{"__ignoreMap":119},[123,1812,1813,1815,1817,1819,1821],{"class":125,"line":126},[123,1814,195],{"class":194},[123,1816,198],{"class":194},[123,1818,201],{"class":129},[123,1820,205],{"class":204},[123,1822,209],{"class":208},[123,1824,1825,1828,1830],{"class":125,"line":143},[123,1826,1827],{"class":204},"  content",[123,1829,218],{"class":217},[123,1831,270],{"class":208},[123,1833,1834,1837,1839],{"class":125,"line":152},[123,1835,1836],{"class":204},"    build",[123,1838,218],{"class":217},[123,1840,270],{"class":208},[123,1842,1843,1846,1848],{"class":125,"line":232},[123,1844,1845],{"class":204},"      markdown",[123,1847,218],{"class":217},[123,1849,270],{"class":208},[123,1851,1852,1855,1857],{"class":125,"line":240},[123,1853,1854],{"class":204},"        highlight",[123,1856,218],{"class":217},[123,1858,270],{"class":208},[123,1860,1861,1864,1866,1868,1871,1873,1876,1878,1881,1883,1886,1888,1891,1893,1896,1898],{"class":125,"line":248},[123,1862,1863],{"class":204},"          preload",[123,1865,218],{"class":217},[123,1867,305],{"class":204},[123,1869,1870],{"class":133},"'ts'",[123,1872,628],{"class":208},[123,1874,1875],{"class":133}," 'vue'",[123,1877,628],{"class":208},[123,1879,1880],{"class":133}," 'bash'",[123,1882,628],{"class":208},[123,1884,1885],{"class":133}," 'yaml'",[123,1887,628],{"class":208},[123,1889,1890],{"class":133}," 'json'",[123,1892,628],{"class":208},[123,1894,1895],{"class":133}," 'diff'",[123,1897,311],{"class":204},[123,1899,229],{"class":208},[123,1901,1902,1905,1907],{"class":125,"line":255},[123,1903,1904],{"class":204},"          theme",[123,1906,218],{"class":217},[123,1908,270],{"class":208},[123,1910,1911,1914,1916,1919],{"class":125,"line":262},[123,1912,1913],{"class":204},"            dark",[123,1915,218],{"class":217},[123,1917,1918],{"class":133}," 'catppuccin-frappe'",[123,1920,229],{"class":208},[123,1922,1923,1926,1928,1931],{"class":125,"line":273},[123,1924,1925],{"class":204},"            default",[123,1927,218],{"class":217},[123,1929,1930],{"class":133}," 'catppuccin-latte'",[123,1932,229],{"class":208},[123,1934,1935],{"class":125,"line":283},[123,1936,1937],{"class":208},"          },\n",[123,1939,1940],{"class":125,"line":297},[123,1941,1942],{"class":208},"        },\n",[123,1944,1945],{"class":125,"line":316},[123,1946,1947],{"class":208},"      },\n",[123,1949,1950],{"class":125,"line":322},[123,1951,319],{"class":208},[123,1953,1954],{"class":125,"line":328},[123,1955,325],{"class":208},[123,1957,1958,1960,1962],{"class":125,"line":333},[123,1959,538],{"class":208},[123,1961,541],{"class":204},[123,1963,544],{"class":208},[11,1965,1966,1967,1970,1971,1974,1975,1978,1979,1981,1982,1985,1986,1989,1990,1992],{},"Two things are worth getting right here. The ",[102,1968,1969],{},"theme"," object uses ",[102,1972,1973],{},"default"," for light mode and ",[102,1976,1977],{},"dark"," for dark mode, and Nuxt Content wires the ",[102,1980,1977],{}," theme to the ",[102,1983,1984],{},".dark"," class on ",[102,1987,1988],{},"\u003Chtml>",", so highlighting follows the site's theme switch on its own. Make sure ",[102,1991,1973],{}," points at a theme that is actually light. If you give it a dark theme, the tokens come out light-on-light and you cannot read them (I learned that one the slow way).",[11,1994,554,1995,1998],{},[102,1996,1997],{},"preload"," list pins the language grammars that get bundled. List the languages you actually write in posts, otherwise a block in a language Shiki has not loaded can come out as plain text instead of highlighted.",[61,2000,2002],{"id":2001},"social-share-images","Social share images",[11,2004,2005,2006,2009,2010,2013],{},"Earlier, ",[102,2007,2008],{},"defineOgImage('Site', …)"," turned up without much explanation. ",[102,2011,2012],{},"Site"," is a component you write yourself, and it is the template for the preview image that shows up when a link is shared on social media.",[11,2015,2016,2017,2020,2021,2025,2026,2029,2030,2033,2034,2037],{},"With ",[102,2018,2019],{},"ogImage.zeroRuntime"," turned on, Nuxt SEO renders these at build time. It takes a Vue component, runs it through ",[21,2022,2024],{"href":561,"rel":2023},[25],"Satori"," to get an SVG, then rasterises that to a PNG with ",[21,2027,568],{"href":566,"rel":2028},[25],", so there is no server or headless browser involved. The component lives at ",[102,2031,2032],{},"app\u002Fcomponents\u002FOgImage\u002FSite.satori.vue"," (the ",[102,2035,2036],{},".satori"," suffix tells the module how to render it):",[114,2039,2041],{"className":971,"code":2040,"language":973,"meta":119,"style":119},"\u003Cscript setup lang=\"ts\">\nconst { title, description } = defineProps\u003C{\n  title?: string;\n  description?: string;\n}>();\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"flex h-full w-full flex-col justify-between bg-neutral-800 p-16\">\n    \u003Cspan class=\"text-3xl font-bold text-white\">Your Name\u003C\u002Fspan>\n    \u003Ch1 class=\"text-6xl font-light text-white\">\n      {{ title || 'A default title' }}\n    \u003C\u002Fh1>\n    \u003Cp class=\"text-2xl text-neutral-400\">\n      {{ description }}\n    \u003C\u002Fp>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n",[102,2042,2043,2059,2084,2097,2108,2118,2126,2130,2138,2154,2178,2193,2198,2206,2221,2226,2234,2242],{"__ignoreMap":119},[123,2044,2045,2047,2049,2051,2053,2055,2057],{"class":125,"line":126},[123,2046,980],{"class":217},[123,2048,983],{"class":860},[123,2050,987],{"class":986},[123,2052,990],{"class":986},[123,2054,993],{"class":217},[123,2056,996],{"class":133},[123,2058,999],{"class":217},[123,2060,2061,2063,2065,2067,2069,2072,2074,2076,2079,2082],{"class":125,"line":143},[123,2062,1004],{"class":194},[123,2064,622],{"class":208},[123,2066,1666],{"class":204},[123,2068,628],{"class":208},[123,2070,2071],{"class":204}," description ",[123,2073,538],{"class":208},[123,2075,1019],{"class":217},[123,2077,2078],{"class":129}," defineProps",[123,2080,980],{"class":2081},"sLkJv",[123,2083,209],{"class":208},[123,2085,2086,2089,2092,2095],{"class":125,"line":152},[123,2087,1536],{"class":2088},"sxUUc",[123,2090,2091],{"class":217},"?:",[123,2093,2094],{"class":194}," string",[123,2096,544],{"class":208},[123,2098,2099,2102,2104,2106],{"class":125,"line":232},[123,2100,2101],{"class":2088},"  description",[123,2103,2091],{"class":217},[123,2105,2094],{"class":194},[123,2107,544],{"class":208},[123,2109,2110,2112,2114,2116],{"class":125,"line":240},[123,2111,538],{"class":208},[123,2113,1118],{"class":2081},[123,2115,720],{"class":204},[123,2117,544],{"class":208},[123,2119,2120,2122,2124],{"class":125,"line":248},[123,2121,1080],{"class":217},[123,2123,983],{"class":860},[123,2125,999],{"class":217},[123,2127,2128],{"class":125,"line":255},[123,2129,252],{"emptyLinePlaceholder":251},[123,2131,2132,2134,2136],{"class":125,"line":262},[123,2133,980],{"class":217},[123,2135,1095],{"class":860},[123,2137,999],{"class":217},[123,2139,2140,2142,2144,2147,2149,2152],{"class":125,"line":273},[123,2141,1102],{"class":217},[123,2143,1105],{"class":860},[123,2145,2146],{"class":986}," class",[123,2148,993],{"class":217},[123,2150,2151],{"class":133},"\"flex h-full w-full flex-col justify-between bg-neutral-800 p-16\"",[123,2153,999],{"class":217},[123,2155,2156,2158,2160,2162,2164,2167,2169,2172,2174,2176],{"class":125,"line":283},[123,2157,1112],{"class":217},[123,2159,123],{"class":860},[123,2161,2146],{"class":986},[123,2163,993],{"class":217},[123,2165,2166],{"class":133},"\"text-3xl font-bold text-white\"",[123,2168,1118],{"class":217},[123,2170,2171],{"class":204},"Your Name",[123,2173,1080],{"class":217},[123,2175,123],{"class":860},[123,2177,999],{"class":217},[123,2179,2180,2182,2184,2186,2188,2191],{"class":125,"line":297},[123,2181,1112],{"class":217},[123,2183,1115],{"class":860},[123,2185,2146],{"class":986},[123,2187,993],{"class":217},[123,2189,2190],{"class":133},"\"text-6xl font-light text-white\"",[123,2192,999],{"class":217},[123,2194,2195],{"class":125,"line":316},[123,2196,2197],{"class":204},"      {{ title || 'A default title' }}\n",[123,2199,2200,2202,2204],{"class":125,"line":322},[123,2201,1240],{"class":217},[123,2203,1115],{"class":860},[123,2205,999],{"class":217},[123,2207,2208,2210,2212,2214,2216,2219],{"class":125,"line":328},[123,2209,1112],{"class":217},[123,2211,11],{"class":860},[123,2213,2146],{"class":986},[123,2215,993],{"class":217},[123,2217,2218],{"class":133},"\"text-2xl text-neutral-400\"",[123,2220,999],{"class":217},[123,2222,2223],{"class":125,"line":333},[123,2224,2225],{"class":204},"      {{ description }}\n",[123,2227,2228,2230,2232],{"class":125,"line":339},[123,2229,1240],{"class":217},[123,2231,11],{"class":860},[123,2233,999],{"class":217},[123,2235,2236,2238,2240],{"class":125,"line":349},[123,2237,1249],{"class":217},[123,2239,1105],{"class":860},[123,2241,999],{"class":217},[123,2243,2244,2246,2248],{"class":125,"line":361},[123,2245,1080],{"class":217},[123,2247,1095],{"class":860},[123,2249,999],{"class":217},[11,2251,2252,2253,27,2255,2258],{},"It is a normal component with ",[102,2254,861],{},[102,2256,2257],{},"description"," props. Any page then calls it with the values it wants:",[114,2260,2262],{"className":185,"code":2261,"language":187,"meta":119,"style":119},"defineOgImage('Site', {\n  description: post.value.description,\n  title: post.value.title,\n});\n",[102,2263,2264,2276,2294,2312],{"__ignoreMap":119},[123,2265,2266,2268,2270,2272,2274],{"class":125,"line":126},[123,2267,1654],{"class":129},[123,2269,205],{"class":204},[123,2271,1659],{"class":133},[123,2273,628],{"class":208},[123,2275,270],{"class":208},[123,2277,2278,2280,2282,2284,2286,2288,2290,2292],{"class":125,"line":143},[123,2279,2101],{"class":204},[123,2281,218],{"class":217},[123,2283,1546],{"class":204},[123,2285,42],{"class":217},[123,2287,1551],{"class":204},[123,2289,42],{"class":217},[123,2291,2257],{"class":204},[123,2293,229],{"class":208},[123,2295,2296,2298,2300,2302,2304,2306,2308,2310],{"class":125,"line":152},[123,2297,1536],{"class":204},[123,2299,218],{"class":217},[123,2301,1546],{"class":204},[123,2303,42],{"class":217},[123,2305,1551],{"class":204},[123,2307,42],{"class":217},[123,2309,861],{"class":204},[123,2311,229],{"class":208},[123,2313,2314,2316,2318],{"class":125,"line":232},[123,2315,538],{"class":208},[123,2317,541],{"class":204},[123,2319,544],{"class":208},[11,2321,2322],{},"Each post ends up with its own image, baked at prerender time with its title rendered in. The one constraint to keep in mind is that Satori only understands a subset of CSS, so stick to flexbox, plain text, and solid colors rather than the full Tailwind surface.",[61,2324,2326],{"id":2325},"editing-with-obsidian","Editing with Obsidian",[11,2328,2329,2330,2333,2334,2336],{},"This is the part I actually enjoy day to day. Obsidian is just a Markdown editor that operates on a folder of ",[102,2331,2332],{},".md"," files, which is what ",[102,2335,104],{}," already is.",[11,2338,2339],{},"To use Obsidian with your posts, do the following:",[11,2341,2342],{},[81,2343,2344],{},"Open the project as a vault:",[2346,2347,2348,2354],"ol",{},[78,2349,2350,2351,42],{},"In Obsidian, choose ",[81,2352,2353],{},"Open folder as vault",[78,2355,2356,2357,2359],{},"Point it at your repo root. (You can also open just ",[102,2358,104],{},", but the repo root is what lets the image workflow below work, so I open the whole project.)",[11,2361,2362],{},"That's it, now every post becomes a note. This means that you get live preview, a file tree, backlinks, tag search, and quick switching, all the things a plain text editor lacks, while writing in the same Markdown that Nuxt Content consumes. There is no export step, no need to copy-paste, which makes the flow extremely smooth.",[11,2364,2365],{},"A few extra tips that will smooth out the workflow:",[75,2367,2368,2382,2388],{},[78,2369,2370,2373,2374,1781,2376,2378,2379,2381],{},[81,2371,2372],{},"Frontmatter:"," Obsidian understands YAML frontmatter natively and shows it in its Properties panel, so ",[102,2375,861],{},[102,2377,881],{},", and ",[102,2380,736],{}," are editable as form fields instead of raw text.",[78,2383,2384,2387],{},[81,2385,2386],{},"Templates:"," You can use Obsidian's core Templates plugin to stamp out a new post with the correct frontmatter every time, no more forgetting a required field.",[78,2389,2390,2393,2394,2397,2398,2401],{},[81,2391,2392],{},"Links:"," Prefer standard Markdown links (",[102,2395,2396],{},"[text](\u002Fblog\u002Fother-post)",") over Obsidian's ",[102,2399,2400],{},"[[wikilinks]]"," for anything that needs to work on the published site, since Nuxt renders standard Markdown.",[11,2403,2404,2405,2408,2409,2412,2413,2416],{},"One housekeeping note: the first time you open the vault, Obsidian will write a hidden ",[102,2406,2407],{},".obsidian\u002F"," folder of editor settings into it. Add the ",[102,2410,2411],{},".obsidian"," directory to your ",[102,2414,2415],{},".gitignore"," so that per-machine config never lands in the repository.",[11,2418,2419],{},"When a draft is ready, you only need to commit and push, that's the only \"publish\" button you need.",[61,2421,2423],{"id":2422},"previewing-locally-and-managing-drafts","Previewing locally and managing drafts",[11,2425,2426],{},"Obsidian's reading view shows you the Markdown, but not your actual site, the theme, layout, OG image, or how a code block is highlighted. For that, you will need to run the dev server:",[114,2428,2430],{"className":116,"code":2429,"language":118,"meta":119,"style":119},"pnpm run dev\n",[102,2431,2432],{"__ignoreMap":119},[123,2433,2434,2436,2439],{"class":125,"line":126},[123,2435,130],{"class":129},[123,2437,2438],{"class":133}," run",[123,2440,2441],{"class":133}," dev\n",[11,2443,2444,2445,2448],{},"This serves the site at ",[102,2446,2447],{},"http:\u002F\u002Flocalhost:3000"," with hot reload, so saving a file in Obsidian will update the browser instantly. This is the fastest way to see a post exactly as it will publish, and it surfaces any possible schema validation errors (e.g. a missing or mistyped frontmatter field) before you ever push.",[11,2450,2451],{},"So how do you keep a half-finished post out of the build while you work on it?",[11,2453,2454,2455,2458,2459,218],{},"The simplest answer is to not commit it until it's done. If you don't mind incomplete content sitting in the repo, though, add an optional ",[102,2456,2457],{},"draft"," flag to the collection schema in ",[102,2460,608],{},[114,2462,2464],{"className":185,"code":2463,"language":187,"meta":119,"style":119},"const schema = z.object({\n  category: z.string(),\n  date: z.date(),\n  draft: z.boolean().optional(),\n  tags: z.array(z.string()),\n  title: z.string(),\n});\n",[102,2465,2466,2485,2502,2519,2544,2567,2583],{"__ignoreMap":119},[123,2467,2468,2470,2473,2475,2477,2479,2481,2483],{"class":125,"line":126},[123,2469,1004],{"class":194},[123,2471,2472],{"class":204}," schema ",[123,2474,993],{"class":217},[123,2476,694],{"class":204},[123,2478,42],{"class":217},[123,2480,699],{"class":129},[123,2482,205],{"class":204},[123,2484,209],{"class":208},[123,2486,2487,2490,2492,2494,2496,2498,2500],{"class":125,"line":143},[123,2488,2489],{"class":204},"  category",[123,2491,218],{"class":217},[123,2493,694],{"class":204},[123,2495,42],{"class":217},[123,2497,717],{"class":129},[123,2499,720],{"class":204},[123,2501,229],{"class":208},[123,2503,2504,2507,2509,2511,2513,2515,2517],{"class":125,"line":152},[123,2505,2506],{"class":204},"  date",[123,2508,218],{"class":217},[123,2510,694],{"class":204},[123,2512,42],{"class":217},[123,2514,736],{"class":129},[123,2516,720],{"class":204},[123,2518,229],{"class":208},[123,2520,2521,2524,2526,2528,2530,2533,2535,2537,2540,2542],{"class":125,"line":232},[123,2522,2523],{"class":204},"  draft",[123,2525,218],{"class":217},[123,2527,694],{"class":204},[123,2529,42],{"class":217},[123,2531,2532],{"class":129},"boolean",[123,2534,720],{"class":204},[123,2536,42],{"class":217},[123,2538,2539],{"class":129},"optional",[123,2541,720],{"class":204},[123,2543,229],{"class":208},[123,2545,2546,2549,2551,2553,2555,2557,2559,2561,2563,2565],{"class":125,"line":240},[123,2547,2548],{"class":204},"  tags",[123,2550,218],{"class":217},[123,2552,694],{"class":204},[123,2554,42],{"class":217},[123,2556,754],{"class":129},[123,2558,757],{"class":204},[123,2560,42],{"class":217},[123,2562,717],{"class":129},[123,2564,764],{"class":204},[123,2566,229],{"class":208},[123,2568,2569,2571,2573,2575,2577,2579,2581],{"class":125,"line":248},[123,2570,1536],{"class":204},[123,2572,218],{"class":217},[123,2574,694],{"class":204},[123,2576,42],{"class":217},[123,2578,717],{"class":129},[123,2580,720],{"class":204},[123,2582,229],{"class":208},[123,2584,2585,2587,2589],{"class":125,"line":255},[123,2586,538],{"class":208},[123,2588,541],{"class":204},[123,2590,544],{"class":208},[11,2592,2593],{},"Then you need to filter drafts out wherever you list posts, and crucially, out of the RSS feed too:",[114,2595,2597],{"className":185,"code":2596,"language":187,"meta":119,"style":119},"const posts = await queryCollection('blog')\n  .where('draft', '\u003C>', true)\n  .order('date', 'DESC')\n  .all();\n",[102,2598,2599,2619,2643,2659],{"__ignoreMap":119},[123,2600,2601,2603,2605,2607,2609,2612,2614,2616],{"class":125,"line":126},[123,2602,1004],{"class":194},[123,2604,1014],{"class":204},[123,2606,993],{"class":217},[123,2608,1022],{"class":194},[123,2610,2611],{"class":129}," queryCollection",[123,2613,205],{"class":204},[123,2615,1030],{"class":133},[123,2617,2618],{"class":204},")\n",[123,2620,2621,2624,2627,2629,2632,2634,2637,2639,2641],{"class":125,"line":143},[123,2622,2623],{"class":217},"  .",[123,2625,2626],{"class":129},"where",[123,2628,205],{"class":204},[123,2630,2631],{"class":133},"'draft'",[123,2633,628],{"class":208},[123,2635,2636],{"class":133}," '\u003C>'",[123,2638,628],{"class":208},[123,2640,292],{"class":291},[123,2642,2618],{"class":204},[123,2644,2645,2647,2649,2651,2653,2655,2657],{"class":125,"line":152},[123,2646,2623],{"class":217},[123,2648,1054],{"class":129},[123,2650,205],{"class":204},[123,2652,1059],{"class":133},[123,2654,628],{"class":208},[123,2656,1064],{"class":133},[123,2658,2618],{"class":204},[123,2660,2661,2663,2665,2667],{"class":125,"line":232},[123,2662,2623],{"class":217},[123,2664,1071],{"class":129},[123,2666,720],{"class":204},[123,2668,544],{"class":208},[11,2670,2671,2672,2674,2675,2678,2679,2682,2683,2686,2687,2690],{},"Because the condition only excludes posts where ",[102,2673,2457],{}," is explicitly ",[102,2676,2677],{},"true",", existing posts without the field are unaffected. While writing, set ",[102,2680,2681],{},"draft: true"," and the post will be visible on ",[102,2684,2685],{},"pnpm dev"," (where you want to preview it) but skipped by the generated site. Delete the flag, or set it to ",[102,2688,2689],{},"false",", when you are ready, then commit.",[61,2692,2694],{"id":2693},"handling-images","Handling images",[11,2696,2697,2698,2701,2702,2705,2706,2033,2709,2711,2712,2715,2716,2719],{},"Images need slightly more care than text, because Obsidian and the built site resolve paths a bit differently. Anything in Nuxt's ",[102,2699,2700],{},"public\u002F"," directory is served from the site root, so an image at ",[102,2703,2704],{},"public\u002Fimg\u002Fposts\u002Fpost.jpg"," is referenced in Markdown as ",[102,2707,2708],{},"\u002Fimg\u002Fposts\u002Fpost.jpg",[102,2710,2700],{}," prefix is dropped). I keep post images in ",[102,2713,2714],{},"public\u002Fimg\u002Fposts"," and point Obsidian's ",[81,2717,2718],{},"default attachment location"," there (Settings → Files and links → \"In the folder specified below\"), so pasted screenshots land where the site expects them.",[11,2721,2722,2723,2725,2726,2728,2729,2732,2733,2735,2736,2739,2740,2742,2743,2746,2747,2750,2751,2753],{},"There is one catch with that: Obsidian's attachment folder has to live inside the vault, and ",[102,2724,2700],{}," is at the repo root, not under ",[102,2727,104],{},". So open the vault at the ",[81,2730,2731],{},"repo root"," rather than at ",[102,2734,104],{},". The posts still sit in ",[102,2737,2738],{},"content\u002Fblog"," as a subfolder, and ",[102,2741,2714],{}," is now reachable as the attachment path. The setting is per-vault, stored in ",[102,2744,2745],{},".obsidian\u002Fapp.json"," as ",[102,2748,2749],{},"attachmentFolderPath","; since ",[102,2752,2407],{}," is git-ignored it stays local to your machine, so a fresh clone elsewhere will need it set again.",[11,2755,2756,2757,2760,2761,2764],{},"One mismatch to watch for: Obsidian's default paste syntax is its own embed format, ",[102,2758,2759],{},"![[post.jpg]]",", which Nuxt does ",[81,2762,2763],{},"not"," understand. You need to use a standard Markdown image instead:",[114,2766,2768],{"className":846,"code":2767,"language":848,"meta":119,"style":119},"![Description of the image](\u002Fimg\u002Fposts\u002Fpost.jpg)\n",[102,2769,2770],{"__ignoreMap":119},[123,2771,2772,2775,2778,2780,2782,2784],{"class":125,"line":126},[123,2773,2774],{"class":860},"![",[123,2776,2777],{"class":133},"Description of the image",[123,2779,311],{"class":860},[123,2781,205],{"class":208},[123,2783,2708],{"class":860},[123,2785,2618],{"class":208},[11,2787,2788,2789,2791,2792,2794],{},"Note that Obsidian writes the link as a vault path (",[102,2790,2704],{},"); drop the ",[102,2793,2700],{}," prefix so it matches the served URL.",[11,2796,2797,2798,2801],{},"You can switch Obsidian to use Markdown-style links globally under Settings → Files and links → ",[81,2799,2800],{},"Use [[Wikilinks]]"," (turn it off). With that flipped, pasted images produce the right syntax automatically.",[11,2803,2804,2805,2808,2809,2812,2813,2815,2816,2819,2820,2823],{},"For better performance, you can render images through ",[102,2806,2807],{},"@nuxt\u002Fimage"," rather than a plain ",[102,2810,2811],{},"\u003Cimg>",". Since ",[102,2814,41],{}," turns Markdown ",[102,2817,2818],{},"![]()"," into a configurable component, you map it to ",[102,2821,2822],{},"\u003CNuxtImg>"," once and carry on writing plain Markdown. You get automatic resizing, lazy loading, and modern formats (WebP\u002FAVIF) at build time, and nothing changes about how you write a post.",[61,2825,2827],{"id":2826},"deploying-to-github-pages","Deploying to GitHub Pages",[11,2829,2830,2831,2834,2835,2838,2839,42],{},"GitHub Pages can now build the site using GitHub Actions and then deploy the result. The key is that GitHub Pages serves static files, so you run ",[102,2832,2833],{},"nuxt generate"," (which produces ",[102,2836,2837],{},".output\u002Fpublic\u002F",") rather than using ",[102,2840,2841],{},"nuxt build",[11,2843,2844,2845,218],{},"Here is a minimal ",[102,2846,2847],{},".github\u002Fworkflows\u002Fdeploy.yml",[114,2849,2853],{"className":2850,"code":2851,"language":2852,"meta":119,"style":119},"language-yaml shiki shiki-themes catppuccin-frappe catppuccin-latte","name: Deploy\n\non:\n  push:\n    branches: [main]\n\npermissions:\n  contents: read\n  pages: write\n  id-token: write\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v4\n      - uses: pnpm\u002Faction-setup@v4\n      - uses: actions\u002Fsetup-node@v4\n        with:\n          node-version-file: .nvmrc\n          cache: pnpm\n      - run: pnpm install --frozen-lockfile\n      - run: pnpm run generate\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    steps:\n      - uses: actions\u002Fdeploy-pages@v4\n","yaml",[102,2854,2855,2865,2869,2877,2884,2899,2903,2910,2920,2930,2939,2943,2950,2957,2967,2974,2987,2998,3009,3016,3026,3036,3047,3058,3069,3075,3085,3089,3096,3106,3114,3121,3130,3136],{"__ignoreMap":119},[123,2856,2857,2860,2862],{"class":125,"line":126},[123,2858,2859],{"class":860},"name",[123,2861,218],{"class":217},[123,2863,2864],{"class":133}," Deploy\n",[123,2866,2867],{"class":125,"line":143},[123,2868,252],{"emptyLinePlaceholder":251},[123,2870,2871,2874],{"class":125,"line":152},[123,2872,2873],{"class":291},"on",[123,2875,2876],{"class":217},":\n",[123,2878,2879,2882],{"class":125,"line":232},[123,2880,2881],{"class":860},"  push",[123,2883,2876],{"class":217},[123,2885,2886,2889,2891,2893,2896],{"class":125,"line":240},[123,2887,2888],{"class":860},"    branches",[123,2890,218],{"class":217},[123,2892,305],{"class":208},[123,2894,2895],{"class":133},"main",[123,2897,2898],{"class":208},"]\n",[123,2900,2901],{"class":125,"line":248},[123,2902,252],{"emptyLinePlaceholder":251},[123,2904,2905,2908],{"class":125,"line":255},[123,2906,2907],{"class":860},"permissions",[123,2909,2876],{"class":217},[123,2911,2912,2915,2917],{"class":125,"line":262},[123,2913,2914],{"class":860},"  contents",[123,2916,218],{"class":217},[123,2918,2919],{"class":133}," read\n",[123,2921,2922,2925,2927],{"class":125,"line":273},[123,2923,2924],{"class":860},"  pages",[123,2926,218],{"class":217},[123,2928,2929],{"class":133}," write\n",[123,2931,2932,2935,2937],{"class":125,"line":283},[123,2933,2934],{"class":860},"  id-token",[123,2936,218],{"class":217},[123,2938,2929],{"class":133},[123,2940,2941],{"class":125,"line":297},[123,2942,252],{"emptyLinePlaceholder":251},[123,2944,2945,2948],{"class":125,"line":316},[123,2946,2947],{"class":860},"jobs",[123,2949,2876],{"class":217},[123,2951,2952,2955],{"class":125,"line":322},[123,2953,2954],{"class":860},"  build",[123,2956,2876],{"class":217},[123,2958,2959,2962,2964],{"class":125,"line":328},[123,2960,2961],{"class":860},"    runs-on",[123,2963,218],{"class":217},[123,2965,2966],{"class":133}," ubuntu-latest\n",[123,2968,2969,2972],{"class":125,"line":333},[123,2970,2971],{"class":860},"    steps",[123,2973,2876],{"class":217},[123,2975,2976,2979,2982,2984],{"class":125,"line":339},[123,2977,2978],{"class":208},"      -",[123,2980,2981],{"class":860}," uses",[123,2983,218],{"class":217},[123,2985,2986],{"class":133}," actions\u002Fcheckout@v4\n",[123,2988,2989,2991,2993,2995],{"class":125,"line":349},[123,2990,2978],{"class":208},[123,2992,2981],{"class":860},[123,2994,218],{"class":217},[123,2996,2997],{"class":133}," pnpm\u002Faction-setup@v4\n",[123,2999,3000,3002,3004,3006],{"class":125,"line":361},[123,3001,2978],{"class":208},[123,3003,2981],{"class":860},[123,3005,218],{"class":217},[123,3007,3008],{"class":133}," actions\u002Fsetup-node@v4\n",[123,3010,3011,3014],{"class":125,"line":373},[123,3012,3013],{"class":860},"        with",[123,3015,2876],{"class":217},[123,3017,3018,3021,3023],{"class":125,"line":378},[123,3019,3020],{"class":860},"          node-version-file",[123,3022,218],{"class":217},[123,3024,3025],{"class":133}," .nvmrc\n",[123,3027,3028,3031,3033],{"class":125,"line":383},[123,3029,3030],{"class":860},"          cache",[123,3032,218],{"class":217},[123,3034,3035],{"class":133}," pnpm\n",[123,3037,3038,3040,3042,3044],{"class":125,"line":389},[123,3039,2978],{"class":208},[123,3041,2438],{"class":860},[123,3043,218],{"class":217},[123,3045,3046],{"class":133}," pnpm install --frozen-lockfile\n",[123,3048,3049,3051,3053,3055],{"class":125,"line":399},[123,3050,2978],{"class":208},[123,3052,2438],{"class":860},[123,3054,218],{"class":217},[123,3056,3057],{"class":133}," pnpm run generate\n",[123,3059,3060,3062,3064,3066],{"class":125,"line":409},[123,3061,2978],{"class":208},[123,3063,2981],{"class":860},[123,3065,218],{"class":217},[123,3067,3068],{"class":133}," actions\u002Fupload-pages-artifact@v3\n",[123,3070,3071,3073],{"class":125,"line":422},[123,3072,3013],{"class":860},[123,3074,2876],{"class":217},[123,3076,3077,3080,3082],{"class":125,"line":435},[123,3078,3079],{"class":860},"          path",[123,3081,218],{"class":217},[123,3083,3084],{"class":133}," .output\u002Fpublic\n",[123,3086,3087],{"class":125,"line":448},[123,3088,252],{"emptyLinePlaceholder":251},[123,3090,3091,3094],{"class":125,"line":453},[123,3092,3093],{"class":860},"  deploy",[123,3095,2876],{"class":217},[123,3097,3098,3101,3103],{"class":125,"line":458},[123,3099,3100],{"class":860},"    needs",[123,3102,218],{"class":217},[123,3104,3105],{"class":133}," build\n",[123,3107,3108,3110,3112],{"class":125,"line":463},[123,3109,2961],{"class":860},[123,3111,218],{"class":217},[123,3113,2966],{"class":133},[123,3115,3116,3119],{"class":125,"line":469},[123,3117,3118],{"class":860},"    environment",[123,3120,2876],{"class":217},[123,3122,3123,3125,3127],{"class":125,"line":479},[123,3124,425],{"class":860},[123,3126,218],{"class":217},[123,3128,3129],{"class":133}," github-pages\n",[123,3131,3132,3134],{"class":125,"line":492},[123,3133,2971],{"class":860},[123,3135,2876],{"class":217},[123,3137,3138,3140,3142,3144],{"class":125,"line":505},[123,3139,2978],{"class":208},[123,3141,2981],{"class":860},[123,3143,218],{"class":217},[123,3145,3146],{"class":133}," actions\u002Fdeploy-pages@v4\n",[11,3148,3149,3150,3153,3154,3157,3158,3160,3161,3164],{},"In your repository settings, you need to set ",[81,3151,3152],{},"Pages → Build and deployment → Source"," to ",[81,3155,3156],{},"GitHub Actions",", and every push to ",[102,3159,2895],{}," will regenerate and redeploy the site. The OIDC token (",[102,3162,3163],{},"id-token: write",") means no secrets or personal access tokens are needed.",[11,3166,3167,3168,3170,3171,3173,3174,3176,3177,3179,3180,3183,3184,3187],{},"One thing to watch with static generation: ",[102,3169,2833],{}," discovers pages by crawling links out from ",[102,3172,1360],{},", so a post only gets built if something links to it. The ",[102,3175,595],{}," listing links to every post, so the chain holds. But a page that is reachable only by typing its URL will not be generated, and it will 404 in production even though it works fine in ",[102,3178,2685],{},". When that happens, add the path to the ",[102,3181,3182],{},"routes"," array explicitly, the same way ",[102,3185,3186],{},"\u002Ffeed.xml"," is in the RSS section.",[61,3189,3191],{"id":3190},"using-a-custom-domain","Using a custom domain",[11,3193,3194,3195,3198],{},"GitHub Pages serves your site at ",[102,3196,3197],{},"username.github.io"," by default, but pointing your own domain at it is free and takes two steps.",[11,3200,3201,3202,3205,3206,3208,3209,3211],{},"First, tell GitHub which domain to serve by adding a ",[102,3203,3204],{},"CNAME"," file to ",[102,3207,2700],{},". Anything in ",[102,3210,2700],{}," is copied verbatim into the build output, so the file survives every deploy:",[114,3213,3218],{"className":3214,"code":3216,"language":3217},[3215],"language-text","your-domain.com\n","text",[102,3219,3216],{"__ignoreMap":119},[11,3221,3222,3223,3226,3227,1360,3230,3233,3234,3237,3238,3240,3241,218],{},"Second, you need to configure DNS at your registrar. For an apex domain (",[102,3224,3225],{},"your-domain.com",") add ",[102,3228,3229],{},"A",[102,3231,3232],{},"AAAA"," records pointing at GitHub's IPs; for a subdomain (",[102,3235,3236],{},"www.your-domain.com",") add a ",[102,3239,3204],{}," record pointing at ",[102,3242,3197],{},[114,3244,3247],{"className":3245,"code":3246,"language":3217},[3215],"# Apex domain — A records\n@   A   185.199.108.153\n@   A   185.199.109.153\n@   A   185.199.110.153\n@   A   185.199.111.153\n\n# Subdomain — CNAME record\nwww CNAME username.github.io.\n",[102,3248,3246],{"__ignoreMap":119},[11,3250,3251,3252,3255,3256,3258,3259,3261],{},"Once DNS propagates, enable ",[81,3253,3254],{},"Enforce HTTPS"," in the repository's Pages settings. GitHub provisions a free certificate automatically. Finally, make sure ",[102,3257,550],{}," in ",[102,3260,181],{}," matches the custom domain, since every canonical link, sitemap entry, and OG tag is derived from it.",[11,3263,3264,3265,42],{},"You can find more details about configuring a custom domain with GitHub Pages ",[21,3266,3269],{"href":3267,"rel":3268},"https:\u002F\u002Fdocs.github.com\u002Fen\u002Fpages\u002Fconfiguring-a-custom-domain-for-your-github-pages-site\u002Fmanaging-a-custom-domain-for-your-github-pages-site",[25],"here",[61,3271,3273],{"id":3272},"adding-an-rss-feed","Adding an RSS feed",[11,3275,3276,3277,3280],{},"A blog should have a feed so readers can subscribe. Because the site is fully static, I use a Nitro route that is pre-rendered to a plain ",[102,3278,3279],{},"feed.xml"," at build time.",[11,3282,3283,3284,218],{},"To do so create ",[102,3285,3286],{},"server\u002Froutes\u002Ffeed.xml.ts",[114,3288,3290],{"className":185,"code":3289,"language":187,"meta":119,"style":119},"import { queryCollection } from '#content\u002Fserver';\n\nexport default defineEventHandler(async (event) => {\n  const posts = await queryCollection(event, 'blog').order('date', 'DESC').all();\n\n  const site = 'https:\u002F\u002Fyour-domain.com';\n  const items = posts\n    .map(post => `\n      \u003Citem>\n        \u003Ctitle>${post.title}\u003C\u002Ftitle>\n        \u003Clink>${site}${post.path}\u003C\u002Flink>\n        \u003Cguid>${site}${post.path}\u003C\u002Fguid>\n        \u003CpubDate>${new Date(post.date).toUTCString()}\u003C\u002FpubDate>\n      \u003C\u002Fitem>`)\n    .join('');\n\n  const feed = `\u003C?xml version=\"1.0\" encoding=\"UTF-8\"?>\n    \u003Crss version=\"2.0\">\n      \u003Cchannel>\n        \u003Ctitle>Your Name\u003C\u002Ftitle>\n        \u003Clink>${site}\u003C\u002Flink>\n        \u003Cdescription>Latest posts\u003C\u002Fdescription>\n        ${items}\n      \u003C\u002Fchannel>\n    \u003C\u002Frss>`;\n\n  setHeader(event, 'content-type', 'application\u002Fxml');\n  return feed;\n});\n",[102,3291,3292,3310,3314,3340,3385,3389,3402,3414,3431,3436,3455,3479,3501,3537,3544,3560,3564,3576,3581,3586,3591,3603,3608,3618,3623,3630,3634,3655,3665],{"__ignoreMap":119},[123,3293,3294,3296,3298,3301,3303,3305,3308],{"class":125,"line":126},[123,3295,619],{"class":194},[123,3297,622],{"class":208},[123,3299,3300],{"class":204}," queryCollection ",[123,3302,538],{"class":208},[123,3304,641],{"class":194},[123,3306,3307],{"class":133}," '#content\u002Fserver'",[123,3309,544],{"class":208},[123,3311,3312],{"class":125,"line":143},[123,3313,252],{"emptyLinePlaceholder":251},[123,3315,3316,3318,3320,3323,3325,3328,3330,3334,3336,3338],{"class":125,"line":152},[123,3317,195],{"class":194},[123,3319,198],{"class":194},[123,3321,3322],{"class":129}," defineEventHandler",[123,3324,205],{"class":204},[123,3326,3327],{"class":194},"async",[123,3329,1453],{"class":208},[123,3331,3333],{"class":3332},"s697J","event",[123,3335,541],{"class":208},[123,3337,1543],{"class":217},[123,3339,270],{"class":208},[123,3341,3342,3345,3347,3349,3351,3353,3356,3358,3361,3363,3365,3367,3369,3371,3373,3375,3377,3379,3381,3383],{"class":125,"line":232},[123,3343,3344],{"class":194},"  const",[123,3346,1014],{"class":204},[123,3348,993],{"class":217},[123,3350,1022],{"class":194},[123,3352,2611],{"class":129},[123,3354,3355],{"class":204},"(event",[123,3357,628],{"class":208},[123,3359,3360],{"class":133}," 'blog'",[123,3362,541],{"class":204},[123,3364,42],{"class":217},[123,3366,1054],{"class":129},[123,3368,205],{"class":204},[123,3370,1059],{"class":133},[123,3372,628],{"class":208},[123,3374,1064],{"class":133},[123,3376,541],{"class":204},[123,3378,42],{"class":217},[123,3380,1071],{"class":129},[123,3382,720],{"class":204},[123,3384,544],{"class":208},[123,3386,3387],{"class":125,"line":240},[123,3388,252],{"emptyLinePlaceholder":251},[123,3390,3391,3393,3396,3398,3400],{"class":125,"line":248},[123,3392,3344],{"class":194},[123,3394,3395],{"class":204}," site ",[123,3397,993],{"class":217},[123,3399,525],{"class":133},[123,3401,544],{"class":208},[123,3403,3404,3406,3409,3411],{"class":125,"line":255},[123,3405,3344],{"class":194},[123,3407,3408],{"class":204}," items ",[123,3410,993],{"class":217},[123,3412,3413],{"class":204}," posts\n",[123,3415,3416,3419,3422,3424,3426,3428],{"class":125,"line":262},[123,3417,3418],{"class":217},"    .",[123,3420,3421],{"class":129},"map",[123,3423,205],{"class":204},[123,3425,1459],{"class":3332},[123,3427,1543],{"class":217},[123,3429,3430],{"class":133}," `\n",[123,3432,3433],{"class":125,"line":273},[123,3434,3435],{"class":133},"      \u003Citem>\n",[123,3437,3438,3441,3444,3446,3448,3450,3452],{"class":125,"line":283},[123,3439,3440],{"class":133},"        \u003Ctitle>",[123,3442,3443],{"class":208},"${",[123,3445,1459],{"class":204},[123,3447,42],{"class":217},[123,3449,861],{"class":204},[123,3451,538],{"class":208},[123,3453,3454],{"class":133},"\u003C\u002Ftitle>\n",[123,3456,3457,3460,3462,3465,3468,3470,3472,3474,3476],{"class":125,"line":297},[123,3458,3459],{"class":133},"        \u003Clink>",[123,3461,3443],{"class":208},[123,3463,3464],{"class":204},"site",[123,3466,3467],{"class":208},"}${",[123,3469,1459],{"class":204},[123,3471,42],{"class":217},[123,3473,1340],{"class":204},[123,3475,538],{"class":208},[123,3477,3478],{"class":133},"\u003C\u002Flink>\n",[123,3480,3481,3484,3486,3488,3490,3492,3494,3496,3498],{"class":125,"line":316},[123,3482,3483],{"class":133},"        \u003Cguid>",[123,3485,3443],{"class":208},[123,3487,3464],{"class":204},[123,3489,3467],{"class":208},[123,3491,1459],{"class":204},[123,3493,42],{"class":217},[123,3495,1340],{"class":204},[123,3497,538],{"class":208},[123,3499,3500],{"class":133},"\u003C\u002Fguid>\n",[123,3502,3503,3506,3508,3512,3515,3517,3519,3521,3523,3525,3527,3530,3532,3534],{"class":125,"line":322},[123,3504,3505],{"class":133},"        \u003CpubDate>",[123,3507,3443],{"class":208},[123,3509,3511],{"class":3510},"sdCNH","new",[123,3513,3514],{"class":129}," Date",[123,3516,205],{"class":133},[123,3518,1459],{"class":204},[123,3520,42],{"class":217},[123,3522,736],{"class":204},[123,3524,541],{"class":133},[123,3526,42],{"class":217},[123,3528,3529],{"class":129},"toUTCString",[123,3531,720],{"class":133},[123,3533,538],{"class":208},[123,3535,3536],{"class":133},"\u003C\u002FpubDate>\n",[123,3538,3539,3542],{"class":125,"line":328},[123,3540,3541],{"class":133},"      \u003C\u002Fitem>`",[123,3543,2618],{"class":204},[123,3545,3546,3548,3551,3553,3556,3558],{"class":125,"line":333},[123,3547,3418],{"class":217},[123,3549,3550],{"class":129},"join",[123,3552,205],{"class":204},[123,3554,3555],{"class":133},"''",[123,3557,541],{"class":204},[123,3559,544],{"class":208},[123,3561,3562],{"class":125,"line":339},[123,3563,252],{"emptyLinePlaceholder":251},[123,3565,3566,3568,3571,3573],{"class":125,"line":349},[123,3567,3344],{"class":194},[123,3569,3570],{"class":204}," feed ",[123,3572,993],{"class":217},[123,3574,3575],{"class":133}," `\u003C?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",[123,3577,3578],{"class":125,"line":361},[123,3579,3580],{"class":133},"    \u003Crss version=\"2.0\">\n",[123,3582,3583],{"class":125,"line":373},[123,3584,3585],{"class":133},"      \u003Cchannel>\n",[123,3587,3588],{"class":125,"line":378},[123,3589,3590],{"class":133},"        \u003Ctitle>Your Name\u003C\u002Ftitle>\n",[123,3592,3593,3595,3597,3599,3601],{"class":125,"line":383},[123,3594,3459],{"class":133},[123,3596,3443],{"class":208},[123,3598,3464],{"class":204},[123,3600,538],{"class":208},[123,3602,3478],{"class":133},[123,3604,3605],{"class":125,"line":389},[123,3606,3607],{"class":133},"        \u003Cdescription>Latest posts\u003C\u002Fdescription>\n",[123,3609,3610,3613,3616],{"class":125,"line":399},[123,3611,3612],{"class":208},"        ${",[123,3614,3615],{"class":204},"items",[123,3617,1518],{"class":208},[123,3619,3620],{"class":125,"line":409},[123,3621,3622],{"class":133},"      \u003C\u002Fchannel>\n",[123,3624,3625,3628],{"class":125,"line":422},[123,3626,3627],{"class":133},"    \u003C\u002Frss>`",[123,3629,544],{"class":208},[123,3631,3632],{"class":125,"line":435},[123,3633,252],{"emptyLinePlaceholder":251},[123,3635,3636,3639,3641,3643,3646,3648,3651,3653],{"class":125,"line":448},[123,3637,3638],{"class":129},"  setHeader",[123,3640,3355],{"class":204},[123,3642,628],{"class":208},[123,3644,3645],{"class":133}," 'content-type'",[123,3647,628],{"class":208},[123,3649,3650],{"class":133}," 'application\u002Fxml'",[123,3652,541],{"class":204},[123,3654,544],{"class":208},[123,3656,3657,3660,3663],{"class":125,"line":453},[123,3658,3659],{"class":194},"  return",[123,3661,3662],{"class":204}," feed",[123,3664,544],{"class":208},[123,3666,3667,3669,3671],{"class":125,"line":458},[123,3668,538],{"class":208},[123,3670,541],{"class":204},[123,3672,544],{"class":208},[11,3674,3675,3676,3678],{},"Then add the route to your prerender list in ",[102,3677,181],{}," so it is generated as a static file:",[114,3680,3682],{"className":185,"code":3681,"language":187,"meta":119,"style":119},"export default defineNuxtConfig({\n  nitro: {\n    prerender: {\n      crawlLinks: true,\n      routes: ['\u002F', '\u002Ffeed.xml'],\n    },\n  },\n});\n",[102,3683,3684,3696,3704,3712,3722,3741,3745,3749],{"__ignoreMap":119},[123,3685,3686,3688,3690,3692,3694],{"class":125,"line":126},[123,3687,195],{"class":194},[123,3689,198],{"class":194},[123,3691,201],{"class":129},[123,3693,205],{"class":204},[123,3695,209],{"class":208},[123,3697,3698,3700,3702],{"class":125,"line":143},[123,3699,265],{"class":204},[123,3701,218],{"class":217},[123,3703,270],{"class":208},[123,3705,3706,3708,3710],{"class":125,"line":152},[123,3707,276],{"class":204},[123,3709,218],{"class":217},[123,3711,270],{"class":208},[123,3713,3714,3716,3718,3720],{"class":125,"line":232},[123,3715,286],{"class":204},[123,3717,218],{"class":217},[123,3719,292],{"class":291},[123,3721,229],{"class":208},[123,3723,3724,3726,3728,3730,3732,3734,3737,3739],{"class":125,"line":240},[123,3725,300],{"class":204},[123,3727,218],{"class":217},[123,3729,305],{"class":204},[123,3731,308],{"class":133},[123,3733,628],{"class":208},[123,3735,3736],{"class":133}," '\u002Ffeed.xml'",[123,3738,311],{"class":204},[123,3740,229],{"class":208},[123,3742,3743],{"class":125,"line":248},[123,3744,319],{"class":208},[123,3746,3747],{"class":125,"line":255},[123,3748,325],{"class":208},[123,3750,3751,3753,3755],{"class":125,"line":262},[123,3752,538],{"class":208},[123,3754,541],{"class":204},[123,3756,544],{"class":208},[11,3758,3759,3760,3763,3764,3767],{},"Now ",[102,3761,3762],{},"https:\u002F\u002Fyour-domain.com\u002Ffeed.xml"," is a real file in the deploy output. Advertise it in your site's ",[102,3765,3766],{},"\u003Chead>"," so feed readers and browsers can discover it automatically:",[114,3769,3771],{"className":185,"code":3770,"language":187,"meta":119,"style":119},"useHead({\n  link: [\n    {\n      href: '\u002Ffeed.xml',\n      rel: 'alternate',\n      title: 'Your Name',\n      type: 'application\u002Frss+xml',\n    },\n  ],\n});\n",[102,3772,3773,3782,3791,3796,3807,3819,3830,3841,3845,3851],{"__ignoreMap":119},[123,3774,3775,3778,3780],{"class":125,"line":126},[123,3776,3777],{"class":129},"useHead",[123,3779,205],{"class":204},[123,3781,209],{"class":208},[123,3783,3784,3787,3789],{"class":125,"line":143},[123,3785,3786],{"class":204},"  link",[123,3788,218],{"class":217},[123,3790,221],{"class":204},[123,3792,3793],{"class":125,"line":152},[123,3794,3795],{"class":208},"    {\n",[123,3797,3798,3801,3803,3805],{"class":125,"line":232},[123,3799,3800],{"class":204},"      href",[123,3802,218],{"class":217},[123,3804,3736],{"class":133},[123,3806,229],{"class":208},[123,3808,3809,3812,3814,3817],{"class":125,"line":240},[123,3810,3811],{"class":204},"      rel",[123,3813,218],{"class":217},[123,3815,3816],{"class":133}," 'alternate'",[123,3818,229],{"class":208},[123,3820,3821,3824,3826,3828],{"class":125,"line":248},[123,3822,3823],{"class":204},"      title",[123,3825,218],{"class":217},[123,3827,430],{"class":133},[123,3829,229],{"class":208},[123,3831,3832,3834,3836,3839],{"class":125,"line":255},[123,3833,438],{"class":204},[123,3835,218],{"class":217},[123,3837,3838],{"class":133}," 'application\u002Frss+xml'",[123,3840,229],{"class":208},[123,3842,3843],{"class":125,"line":262},[123,3844,319],{"class":208},[123,3846,3847,3849],{"class":125,"line":273},[123,3848,243],{"class":204},[123,3850,229],{"class":208},[123,3852,3853,3855,3857],{"class":125,"line":283},[123,3854,538],{"class":208},[123,3856,541],{"class":204},[123,3858,544],{"class":208},[11,3860,3861,3862,3864,3865,3868],{},"None of the modules already in this stack generate a feed. ",[102,3863,50],{}," handles sitemaps, robots, and structured data, but RSS is out of its scope. For thirty lines of XML I would rather not add another dependency, and hand-rolling the route gives me full control over what each ",[102,3866,3867],{},"\u003Citem>"," contains.",[61,3870,3872],{"id":3871},"adding-comments","Adding comments",[11,3874,3875],{},"A static site has no backend, so comments need a third-party service. You can also just skip them. If you do want them, there are a few options, and which one fits depends on how much you care about simplicity, privacy, and ownership:",[75,3877,3878,3892,3906],{},[78,3879,3880,3887,3888,3891],{},[81,3881,3882],{},[21,3883,3886],{"href":3884,"rel":3885},"https:\u002F\u002Fgiscus.app",[25],"Giscus"," stores comments as ",[81,3889,3890],{},"GitHub Discussions"," on your repo. Free, no\nads, no tracking, and commenters sign in with GitHub, which suits a developer audience. For a GitHub-hosted blog it is the obvious choice.",[78,3893,3894,3901,3902,3905],{},[81,3895,3896],{},[21,3897,3900],{"href":3898,"rel":3899},"https:\u002F\u002Futteranc.es",[25],"Utterances"," is the same idea but backed by GitHub ",[81,3903,3904],{},"Issues"," instead. It is lighter than\nGiscus, though Discussions is the better fit for conversations. Utterances is widely used and stable but only lightly maintained these days, so Giscus is effectively its actively-developed successor.",[78,3907,3908,3911],{},[81,3909,3910],{},"No comments at all"," is the option I have lived with for a long time. Static-site comment widgets tend to attract spam and also add third-party scripts; a lot of the time, pointing readers at a social thread (Bluesky, Mastodon) is enough.",[11,3913,3914,3915,3920],{},"I went with Giscus. After enabling Discussions on your repository and installing the ",[21,3916,3919],{"href":3917,"rel":3918},"https:\u002F\u002Fgithub.com\u002Fapps\u002Fgiscus",[25],"Giscus GitHub app",", you add a small client-only component on your post page:",[114,3922,3924],{"className":971,"code":3923,"language":973,"meta":119,"style":119},"\u003Cscript setup lang=\"ts\">\nconst container = useTemplateRef\u003CHTMLElement>('container');\n\nonMounted(() => {\n  const script = document.createElement('script');\n  script.src = 'https:\u002F\u002Fgiscus.app\u002Fclient.js';\n  script.async = true;\n  script.crossOrigin = 'anonymous';\n  Object.assign(script.dataset, {\n    repo: 'username\u002Frepo',\n    repoId: 'YOUR_REPO_ID',\n    category: 'Announcements',\n    categoryId: 'YOUR_CATEGORY_ID',\n    mapping: 'pathname',\n    theme: 'preferred_color_scheme',\n  });\n  container.value?.append(script);\n});\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003CClientOnly>\n    \u003Cdiv ref=\"container\" \u002F>\n  \u003C\u002FClientOnly>\n\u003C\u002Ftemplate>\n",[102,3925,3926,3942,3971,3975,3988,4014,4031,4046,4062,4084,4096,4108,4120,4132,4144,4156,4165,4184,4192,4200,4204,4212,4221,4240,4248],{"__ignoreMap":119},[123,3927,3928,3930,3932,3934,3936,3938,3940],{"class":125,"line":126},[123,3929,980],{"class":217},[123,3931,983],{"class":860},[123,3933,987],{"class":986},[123,3935,990],{"class":986},[123,3937,993],{"class":217},[123,3939,996],{"class":133},[123,3941,999],{"class":217},[123,3943,3944,3946,3949,3951,3954,3956,3960,3962,3964,3967,3969],{"class":125,"line":143},[123,3945,1004],{"class":194},[123,3947,3948],{"class":204}," container ",[123,3950,993],{"class":217},[123,3952,3953],{"class":129}," useTemplateRef",[123,3955,980],{"class":2081},[123,3957,3959],{"class":3958},"slII9","HTMLElement",[123,3961,1118],{"class":2081},[123,3963,205],{"class":204},[123,3965,3966],{"class":133},"'container'",[123,3968,541],{"class":204},[123,3970,544],{"class":208},[123,3972,3973],{"class":125,"line":152},[123,3974,252],{"emptyLinePlaceholder":251},[123,3976,3977,3980,3982,3984,3986],{"class":125,"line":232},[123,3978,3979],{"class":129},"onMounted",[123,3981,205],{"class":204},[123,3983,720],{"class":208},[123,3985,1543],{"class":217},[123,3987,270],{"class":208},[123,3989,3990,3992,3995,3997,4000,4002,4005,4007,4010,4012],{"class":125,"line":240},[123,3991,3344],{"class":194},[123,3993,3994],{"class":204}," script ",[123,3996,993],{"class":217},[123,3998,3999],{"class":204}," document",[123,4001,42],{"class":217},[123,4003,4004],{"class":129},"createElement",[123,4006,205],{"class":204},[123,4008,4009],{"class":133},"'script'",[123,4011,541],{"class":204},[123,4013,544],{"class":208},[123,4015,4016,4019,4021,4024,4026,4029],{"class":125,"line":248},[123,4017,4018],{"class":204},"  script",[123,4020,42],{"class":217},[123,4022,4023],{"class":204},"src ",[123,4025,993],{"class":217},[123,4027,4028],{"class":133}," 'https:\u002F\u002Fgiscus.app\u002Fclient.js'",[123,4030,544],{"class":208},[123,4032,4033,4035,4037,4040,4042,4044],{"class":125,"line":255},[123,4034,4018],{"class":204},[123,4036,42],{"class":217},[123,4038,4039],{"class":204},"async ",[123,4041,993],{"class":217},[123,4043,292],{"class":291},[123,4045,544],{"class":208},[123,4047,4048,4050,4052,4055,4057,4060],{"class":125,"line":262},[123,4049,4018],{"class":204},[123,4051,42],{"class":217},[123,4053,4054],{"class":204},"crossOrigin ",[123,4056,993],{"class":217},[123,4058,4059],{"class":133}," 'anonymous'",[123,4061,544],{"class":208},[123,4063,4064,4067,4069,4072,4075,4077,4080,4082],{"class":125,"line":273},[123,4065,4066],{"class":204},"  Object",[123,4068,42],{"class":217},[123,4070,4071],{"class":129},"assign",[123,4073,4074],{"class":204},"(script",[123,4076,42],{"class":217},[123,4078,4079],{"class":204},"dataset",[123,4081,628],{"class":208},[123,4083,270],{"class":208},[123,4085,4086,4089,4091,4094],{"class":125,"line":283},[123,4087,4088],{"class":204},"    repo",[123,4090,218],{"class":217},[123,4092,4093],{"class":133}," 'username\u002Frepo'",[123,4095,229],{"class":208},[123,4097,4098,4101,4103,4106],{"class":125,"line":297},[123,4099,4100],{"class":204},"    repoId",[123,4102,218],{"class":217},[123,4104,4105],{"class":133}," 'YOUR_REPO_ID'",[123,4107,229],{"class":208},[123,4109,4110,4113,4115,4118],{"class":125,"line":316},[123,4111,4112],{"class":204},"    category",[123,4114,218],{"class":217},[123,4116,4117],{"class":133}," 'Announcements'",[123,4119,229],{"class":208},[123,4121,4122,4125,4127,4130],{"class":125,"line":322},[123,4123,4124],{"class":204},"    categoryId",[123,4126,218],{"class":217},[123,4128,4129],{"class":133}," 'YOUR_CATEGORY_ID'",[123,4131,229],{"class":208},[123,4133,4134,4137,4139,4142],{"class":125,"line":328},[123,4135,4136],{"class":204},"    mapping",[123,4138,218],{"class":217},[123,4140,4141],{"class":133}," 'pathname'",[123,4143,229],{"class":208},[123,4145,4146,4149,4151,4154],{"class":125,"line":333},[123,4147,4148],{"class":204},"    theme",[123,4150,218],{"class":217},[123,4152,4153],{"class":133}," 'preferred_color_scheme'",[123,4155,229],{"class":208},[123,4157,4158,4161,4163],{"class":125,"line":339},[123,4159,4160],{"class":208},"  }",[123,4162,541],{"class":204},[123,4164,544],{"class":208},[123,4166,4167,4170,4172,4174,4176,4179,4182],{"class":125,"line":349},[123,4168,4169],{"class":204},"  container",[123,4171,42],{"class":217},[123,4173,1551],{"class":204},[123,4175,1554],{"class":217},[123,4177,4178],{"class":129},"append",[123,4180,4181],{"class":204},"(script)",[123,4183,544],{"class":208},[123,4185,4186,4188,4190],{"class":125,"line":361},[123,4187,538],{"class":208},[123,4189,541],{"class":204},[123,4191,544],{"class":208},[123,4193,4194,4196,4198],{"class":125,"line":373},[123,4195,1080],{"class":217},[123,4197,983],{"class":860},[123,4199,999],{"class":217},[123,4201,4202],{"class":125,"line":378},[123,4203,252],{"emptyLinePlaceholder":251},[123,4205,4206,4208,4210],{"class":125,"line":383},[123,4207,980],{"class":217},[123,4209,1095],{"class":860},[123,4211,999],{"class":217},[123,4213,4214,4216,4219],{"class":125,"line":389},[123,4215,1102],{"class":217},[123,4217,4218],{"class":860},"ClientOnly",[123,4220,999],{"class":217},[123,4222,4223,4225,4227,4230,4232,4235,4238],{"class":125,"line":399},[123,4224,1112],{"class":217},[123,4226,1105],{"class":860},[123,4228,4229],{"class":986}," ref",[123,4231,993],{"class":217},[123,4233,4234],{"class":133},"\"container\"",[123,4236,4237],{"class":204}," \u002F",[123,4239,999],{"class":217},[123,4241,4242,4244,4246],{"class":125,"line":409},[123,4243,1249],{"class":217},[123,4245,4218],{"class":860},[123,4247,999],{"class":217},[123,4249,4250,4252,4254],{"class":125,"line":422},[123,4251,1080],{"class":217},[123,4253,1095],{"class":860},[123,4255,999],{"class":217},[11,4257,4258,4259,4262],{},"Wrapping it in ",[102,4260,4261],{},"\u003CClientOnly>"," keeps the third-party script out of the pre-rendered HTML, so it does not slow down the initial page load or get in the way of crawlers.",[61,4264,4266],{"id":4265},"what-you-configure-on-github-itself","What you configure on GitHub itself",[11,4268,4269],{},"Most of the setup lives in code, but a handful of settings only exist in the GitHub web UI. Miss one and the deploy quietly does nothing, so here is the checklist:",[75,4271,4272,4295,4315,4337,4351,4373],{},[78,4273,4274,4277,4278,4280,4281,4284,4285,4288,4289,3258,4292,4294],{},[81,4275,4276],{},"Repository name."," A repo named ",[102,4279,3197],{}," is published at the apex\n",[102,4282,4283],{},"https:\u002F\u002Fusername.github.io","; any other name is served from a ",[102,4286,4287],{},"\u002Frepo-name\u002F"," subpath. If you use a subpath, set ",[102,4290,4291],{},"app.baseURL",[102,4293,181],{}," to match, or links and assets will 404.",[78,4296,4297,4300,4301,4304,4305,3153,4308,4310,4311,4314],{},[81,4298,4299],{},"Pages source."," Under ",[81,4302,4303],{},"Settings → Pages → Build and deployment",", set ",[81,4306,4307],{},"Source",[81,4309,3156],{}," (not the older \"Deploy from a branch\"). Without this, the ",[102,4312,4313],{},"deploy-pages"," step has nowhere to publish.",[78,4316,4317,4320,4321,4323,4324,1453,4327,1781,4330,4332,4333,4336],{},[81,4318,4319],{},"Workflow permissions."," The ",[102,4322,2907],{}," block in ",[102,4325,4326],{},"deploy.yml",[102,4328,4329],{},"pages: write",[102,4331,3163],{},") grants what the deploy needs per-run. If your organisation locks workflow permissions down, also check ",[81,4334,4335],{},"Settings → Actions → General → Workflow permissions"," so the Actions runner is allowed those scopes.",[78,4338,4339,4342,4343,4346,4347,4350],{},[81,4340,4341],{},"Environment."," The first deploy creates a ",[102,4344,4345],{},"github-pages"," environment automatically. If you add branch protection or required reviewers to it under ",[81,4348,4349],{},"Settings → Environments",", deploys will pause for approval, which is fine if intentional, surprising if not.",[78,4352,4353,4356,4357,4360,4361,4363,4364,4366,4367,4369,4370,4372],{},[81,4354,4355],{},"Custom domain."," Enter the domain under ",[81,4358,4359],{},"Settings → Pages → Custom domain"," (this writes the same ",[102,4362,3204],{}," value GitHub expects), then tick ",[81,4365,3254],{}," once the certificate is provisioned. Keep the ",[102,4368,3204],{}," file in ",[102,4371,2700],{}," too, so a redeploy never wipes the setting.",[78,4374,4375,4378,4379,4382,4383,4387,4388,27,4391,4394],{},[81,4376,4377],{},"Discussions (only for Giscus comments)."," Enable ",[81,4380,4381],{},"Settings → General → Features → Discussions",", then install the ",[21,4384,4386],{"href":3917,"rel":4385},[25],"Giscus app"," and grant it access to the repo. Giscus' own\nconfigurator reads back your ",[102,4389,4390],{},"repoId",[102,4392,4393],{},"categoryId"," from these once they exist.",[11,4396,4397],{},"A public repository is required for Pages on the free plan; private-repo Pages requires a paid plan.",[61,4399,4401],{"id":4400},"going-further","Going further",[11,4403,4404],{},"The setup above is enough to publish a blog. A few things I have either added or kept in mind for later, all of which stay within the static, no-backend approach:",[75,4406,4407,4430,4462,4472,4488],{},[78,4408,4409,4412,4413,229,4418,4423,4424,4429],{},[81,4410,4411],{},"Analytics"," - you can use a privacy-friendly, cookieless option like ",[21,4414,4417],{"href":4415,"rel":4416},"https:\u002F\u002Fplausible.io",[25],"Plausible",[21,4419,4422],{"href":4420,"rel":4421},"https:\u002F\u002Fumami.is",[25],"Umami",", or ",[21,4425,4428],{"href":4426,"rel":4427},"https:\u002F\u002Fwww.goatcounter.com",[25],"GoatCounter"," gives you visitor numbers without the weight (or the consent banner) of Google Analytics.",[78,4431,4432,4435,4436,4443,4444,4449,4450,4455,4456,4461],{},[81,4433,4434],{},"Client-side search"," - Nuxt Content's own ",[21,4437,4440],{"href":4438,"rel":4439},"https:\u002F\u002Fcontent.nuxt.com\u002Fdocs\u002Fadvanced\u002Ffulltext-search",[25],[102,4441,4442],{},"queryCollectionSearchSections"," breaks your posts into searchable sections at build time, which you pair with a small library like ",[21,4445,4448],{"href":4446,"rel":4447},"https:\u002F\u002Flucaong.github.io\u002Fminisearch\u002F",[25],"MiniSearch"," or ",[21,4451,4454],{"href":4452,"rel":4453},"https:\u002F\u002Fwww.fusejs.io",[25],"Fuse.js"," for the actual matching. For a larger site, ",[21,4457,4460],{"href":4458,"rel":4459},"https:\u002F\u002Fpagefind.app",[25],"Pagefind"," is a good alternative that indexes the generated HTML into a lazy-loaded index instead of loading everything into memory. Either way the search stays fully static, with no server.",[78,4463,4464,4467,4468,4471],{},[81,4465,4466],{},"A custom 404 page"," - you can add ",[102,4469,4470],{},"app\u002Ferror.vue"," so broken links land on your own page instead of GitHub's default one.",[78,4473,4474,4477,4478,4480,4481,4483,4484,4487],{},[81,4475,4476],{},"Favicon and web manifest"," - drop the icons in ",[102,4479,2700],{}," and wire them up with ",[102,4482,3777],{},", or let a module like ",[102,4485,4486],{},"@vite-pwa\u002Fnuxt"," generate them.",[78,4489,4490,4493,4494,4497],{},[81,4491,4492],{},"Redirects"," - when migrating an existing blog, map old URLs to new ones with Nitro ",[102,4495,4496],{},"routeRules"," so you keep your search ranking and don't break inbound links.",[11,4499,4500],{},"None of these are needed to launch. Ship the blog first and add them when you actually want them.",[61,4502,4504],{"id":4503},"the-full-loop","The full loop",[11,4506,4507],{},"So once it's all wired up, the day-to-day should look like this:",[2346,4509,4510,4515,4520,4526],{},[78,4511,4512,4513,42],{},"Open Obsidian, write a new post in ",[102,4514,842],{},[78,4516,4517,42],{},[102,4518,4519],{},"git commit && git push",[78,4521,4522,4523,4525],{},"GitHub Actions runs ",[102,4524,2833],{}," and deploys the static output.",[78,4527,4528],{},"Nuxt SEO has already baked in the sitemap, robots rules, OG image, and structured data.",[11,4530,4531],{},"This way you get a free, fast, fully owned blog where the only thing you actually need to touch is Markdown.",[11,4533,4534],{},"The boring part is done. Go write something you actually care about, that is the fun bit.",[11,4536,4537],{},"Regards, Kelsos.",[4539,4540,4541],"style",{},"html pre.shiki code .sd8PB, html code.shiki .sd8PB{--shiki-dark:#8CAAEE;--shiki-dark-font-style:italic;--shiki-default:#1E66F5;--shiki-default-font-style:italic}html pre.shiki code .sq21T, html code.shiki .sq21T{--shiki-dark:#A6D189;--shiki-default:#40A02B}html pre.shiki code .sf7uP, html code.shiki .sf7uP{--shiki-dark:#E78284;--shiki-dark-font-style:italic;--shiki-default:#D20F39;--shiki-default-font-style:italic}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 .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 pre.shiki code .sfq9e, html code.shiki .sfq9e{--shiki-dark:#CA9EE6;--shiki-default:#8839EF}html pre.shiki code .s5pMN, html code.shiki .s5pMN{--shiki-dark:#C6D0F5;--shiki-default:#4C4F69}html pre.shiki code .sRlM1, html code.shiki .sRlM1{--shiki-dark:#949CBB;--shiki-default:#7C7F93}html pre.shiki code .sW4Yu, html code.shiki .sW4Yu{--shiki-dark:#81C8BE;--shiki-default:#179299}html pre.shiki code .sRznC, html code.shiki .sRznC{--shiki-dark:#949CBB;--shiki-dark-font-style:italic;--shiki-default:#7C7F93;--shiki-default-font-style:italic}html pre.shiki code .sId4k, html code.shiki .sId4k{--shiki-dark:#EF9F76;--shiki-default:#FE640B}html pre.shiki code .sN9LH, html code.shiki .sN9LH{--shiki-dark:#8CAAEE;--shiki-default:#1E66F5}html pre.shiki code .sUXCo, html code.shiki .sUXCo{--shiki-dark:#E5C890;--shiki-default:#DF8E1D}html pre.shiki code .souGF, html code.shiki .souGF{--shiki-dark:#F4B8E4;--shiki-default:#EA76CB}html pre.shiki code .sLkJv, html code.shiki .sLkJv{--shiki-dark:#99D1DB;--shiki-default:#04A5E5}html pre.shiki code .sxUUc, html code.shiki .sxUUc{--shiki-dark:#C6D0F5;--shiki-dark-font-style:italic;--shiki-default:#4C4F69;--shiki-default-font-style:italic}html pre.shiki code .s697J, html code.shiki .s697J{--shiki-dark:#EA999C;--shiki-dark-font-style:italic;--shiki-default:#E64553;--shiki-default-font-style:italic}html pre.shiki code .sdCNH, html code.shiki .sdCNH{--shiki-dark:#CA9EE6;--shiki-dark-font-weight:bold;--shiki-default:#8839EF;--shiki-default-font-weight:bold}html pre.shiki code .slII9, html code.shiki .slII9{--shiki-dark:#E5C890;--shiki-dark-font-style:italic;--shiki-default:#DF8E1D;--shiki-default-font-style:italic}",{"title":119,"searchDepth":143,"depth":143,"links":4543},[4544,4545,4546,4547,4548,4549,4550,4551,4552,4553,4554,4555,4556,4557,4558,4559,4560,4561],{"id":63,"depth":143,"text":64},{"id":108,"depth":143,"text":109},{"id":174,"depth":143,"text":175},{"id":572,"depth":143,"text":573},{"id":599,"depth":143,"text":600},{"id":926,"depth":143,"text":927},{"id":1789,"depth":143,"text":1790},{"id":2001,"depth":143,"text":2002},{"id":2325,"depth":143,"text":2326},{"id":2422,"depth":143,"text":2423},{"id":2693,"depth":143,"text":2694},{"id":2826,"depth":143,"text":2827},{"id":3190,"depth":143,"text":3191},{"id":3272,"depth":143,"text":3273},{"id":3871,"depth":143,"text":3872},{"id":4265,"depth":143,"text":4266},{"id":4400,"depth":143,"text":4401},{"id":4503,"depth":143,"text":4504},"guide","2026-05-31","md",{},"\u002Fblog\u002F2026-05-31-nuxt-github-pages-obsidian",{"title":6,"description":13},"blog\u002F2026-05-31-nuxt-github-pages-obsidian",[4570,4571,4345,4572,4573],"nuxt","seo","obsidian","static-site","K9nzajqnYDUXA1_KZJpYttY66Kvc0ZDqhOoZbr83o98",null,{"id":4577,"title":4578,"body":4579,"category":4615,"date":4616,"description":4583,"extension":4564,"meta":4617,"navigation":251,"path":4618,"seo":4619,"stem":4620,"tags":4621,"__hash__":4622},"blog\u002Fblog\u002F2023-06-28-update.md","Site Update",{"type":8,"value":4580,"toc":4613},[4581,4584,4607,4610],[11,4582,4583],{},"The previous jekyll template was quite outdated, and it has not been properly updated for a long time,\nso I decided to modernize it a bit.",[11,4585,4586,4587,229,4591,4596,4597,4601,4602,42],{},"Since I am mostly working with Vue lately I decided to build the updated site using a combination of ",[21,4588,4590],{"href":23,"rel":4589},[25],"Nuxt 3",[21,4592,4595],{"href":4593,"rel":4594},"https:\u002F\u002Fui.nuxtlabs.com",[25],"NuxtLabs UI"," along with the ",[21,4598,41],{"href":4599,"rel":4600},"https:\u002F\u002Fcontent.nuxtjs.org",[25]," plugin,\nand ",[21,4603,4606],{"href":4604,"rel":4605},"https:\u002F\u002Ftailwindcss.com\u002F",[25],"tailwind",[11,4608,4609],{},"Most of the site should be as it was before, but I decided to remove disqus as a comment mechanism.",[11,4611,4612],{},"Regards Kelsos.",{"title":119,"searchDepth":143,"depth":143,"links":4614},[],"news","2023-06-28",{},"\u002Fblog\u002F2023-06-28-update",{"title":4578,"description":4583},"blog\u002F2023-06-28-update",[4615],"ADpz9YdxId1xGIlyl4eibrfcxplVwWQdMkee_iTfBrA",1780265985777]