[{"data":1,"prerenderedAt":631},["ShallowReactive",2],{"post-\u002Fposts\u002Fship-a-static-blog-with-nuxt4":3,"surround-\u002Fposts\u002Fship-a-static-blog-with-nuxt4":626},{"id":4,"title":5,"body":6,"cover":611,"date":612,"description":613,"draft":614,"extension":615,"meta":616,"minutes":120,"navigation":123,"path":617,"seo":618,"stem":619,"tags":620,"updated":624,"__hash__":625},"posts\u002Fposts\u002Fship-a-static-blog-with-nuxt4.md","用 Nuxt 4 和 Content 把博客钉在静态文件上",{"type":7,"value":8,"toc":604},"minimark",[9,18,22,25,28,47,50,64,342,349,352,355,541,548,551,567,581,587,590,597,600],[10,11,12,13,17],"p",{},"这个站点没有后端服务，没有数据库，也没有管理后台。写作流程只有三步：新建一个 ",[14,15,16],"code",{},".md"," 文件，写完，提交。剩下的事情交给构建管道。",[19,20,21],"h2",{"id":21},"为什么坚持纯静态",[10,23,24],{},"动态博客系统的大部分能力我用不上。评论可以外挂，搜索可以在客户端做，统计交给日志。剩下的核心诉求只有一个：把文字变成网页。既然如此，服务器就是多余的运维负担。",[10,26,27],{},"静态文件带来的好处非常实在：",[29,30,31,35,38,41],"ul",{},[32,33,34],"li",{},"部署目标随便挑，对象存储、CDN、GitHub Pages 都行",[32,36,37],{},"没有可以被攻破的运行时，安全面趋近于零",[32,39,40],{},"首字节时间就是 CDN 的响应时间",[32,42,43,44],{},"整站可以塞进 Git，回滚等于 ",[14,45,46],{},"git revert",[19,48,49],{"id":49},"内容即文件",[10,51,52,55,56,59,60,63],{},[14,53,54],{},"@nuxt\u002Fcontent"," 把 ",[14,57,58],{},"content\u002F"," 目录变成一个类型安全的集合。集合的形状在 ",[14,61,62],{},"content.config.ts"," 里声明：",[65,66,71],"pre",{"className":67,"code":68,"language":69,"meta":70,"style":70},"language-ts shiki shiki-themes tokyo-night github-light","import { defineCollection, defineContentConfig, z } from '@nuxt\u002Fcontent'\n\nexport default defineContentConfig({\n  collections: {\n    posts: defineCollection({\n      type: 'page',\n      source: 'posts\u002F*.md',\n      schema: z.object({\n        date: z.string(),\n        tags: z.array(z.string()).default([]),\n        draft: z.boolean().default(false),\n      }),\n    }),\n  },\n})\n","ts","",[14,72,73,118,125,140,153,166,186,203,222,242,280,312,320,328,336],{"__ignoreMap":70},[74,75,78,82,86,90,94,97,99,102,105,108,112,115],"span",{"class":76,"line":77},"line",1,[74,79,81],{"class":80},"sljjc","import",[74,83,85],{"class":84},"sjXpA"," { ",[74,87,89],{"class":88},"sPjv5","defineCollection",[74,91,93],{"class":92},"szu1u",",",[74,95,96],{"class":88}," defineContentConfig",[74,98,93],{"class":92},[74,100,101],{"class":88}," z",[74,103,104],{"class":84}," }",[74,106,107],{"class":80}," from",[74,109,111],{"class":110},"sbW-k"," '",[74,113,54],{"class":114},"snP_1",[74,116,117],{"class":110},"'\n",[74,119,121],{"class":76,"line":120},2,[74,122,124],{"emptyLinePlaceholder":123},true,"\n",[74,126,128,131,134,137],{"class":76,"line":127},3,[74,129,130],{"class":80},"export",[74,132,133],{"class":80}," default",[74,135,96],{"class":136},"sxkyg",[74,138,139],{"class":84},"({\n",[74,141,143,147,150],{"class":76,"line":142},4,[74,144,146],{"class":145},"s9SBS","  collections",[74,148,149],{"class":92},":",[74,151,152],{"class":84}," {\n",[74,154,156,159,161,164],{"class":76,"line":155},5,[74,157,158],{"class":145},"    posts",[74,160,149],{"class":92},[74,162,163],{"class":136}," defineCollection",[74,165,139],{"class":84},[74,167,169,173,175,177,180,183],{"class":76,"line":168},6,[74,170,172],{"class":171},"swOC1","      type",[74,174,149],{"class":92},[74,176,111],{"class":110},[74,178,179],{"class":114},"page",[74,181,182],{"class":110},"'",[74,184,185],{"class":92},",\n",[74,187,189,192,194,196,199,201],{"class":76,"line":188},7,[74,190,191],{"class":171},"      source",[74,193,149],{"class":92},[74,195,111],{"class":110},[74,197,198],{"class":114},"posts\u002F*.md",[74,200,182],{"class":110},[74,202,185],{"class":92},[74,204,206,209,211,214,217,220],{"class":76,"line":205},8,[74,207,208],{"class":171},"      schema",[74,210,149],{"class":92},[74,212,101],{"class":213},"s4do5",[74,215,216],{"class":92},".",[74,218,219],{"class":136},"object",[74,221,139],{"class":84},[74,223,225,228,230,232,234,237,240],{"class":76,"line":224},9,[74,226,227],{"class":171},"        date",[74,229,149],{"class":92},[74,231,101],{"class":213},[74,233,216],{"class":92},[74,235,236],{"class":136},"string",[74,238,239],{"class":84},"()",[74,241,185],{"class":92},[74,243,245,248,250,252,254,257,260,263,265,267,270,272,275,278],{"class":76,"line":244},10,[74,246,247],{"class":171},"        tags",[74,249,149],{"class":92},[74,251,101],{"class":213},[74,253,216],{"class":92},[74,255,256],{"class":136},"array",[74,258,259],{"class":84},"(",[74,261,262],{"class":213},"z",[74,264,216],{"class":92},[74,266,236],{"class":136},[74,268,269],{"class":84},"())",[74,271,216],{"class":92},[74,273,274],{"class":136},"default",[74,276,277],{"class":84},"([])",[74,279,185],{"class":92},[74,281,283,286,288,290,292,295,297,299,301,303,307,310],{"class":76,"line":282},11,[74,284,285],{"class":171},"        draft",[74,287,149],{"class":92},[74,289,101],{"class":213},[74,291,216],{"class":92},[74,293,294],{"class":136},"boolean",[74,296,239],{"class":84},[74,298,216],{"class":92},[74,300,274],{"class":136},[74,302,259],{"class":84},[74,304,306],{"class":305},"sjYRd","false",[74,308,309],{"class":84},")",[74,311,185],{"class":92},[74,313,315,318],{"class":76,"line":314},12,[74,316,317],{"class":84},"      })",[74,319,185],{"class":92},[74,321,323,326],{"class":76,"line":322},13,[74,324,325],{"class":84},"    })",[74,327,185],{"class":92},[74,329,331,334],{"class":76,"line":330},14,[74,332,333],{"class":84},"  }",[74,335,185],{"class":92},[74,337,339],{"class":76,"line":338},15,[74,340,341],{"class":84},"})\n",[10,343,344,345,348],{},"frontmatter 写错字段，构建期就会报错，而不是等到页面渲染成 ",[14,346,347],{},"undefined"," 才发现。",[19,350,351],{"id":351},"页面里怎么查",[10,353,354],{},"查询 API 长得像一个极简的 ORM，在组件里直接用：",[65,356,360],{"className":357,"code":358,"language":359,"meta":70,"style":70},"language-vue shiki shiki-themes tokyo-night github-light","\u003Cscript setup lang=\"ts\">\nconst { data: posts } = await useAsyncData('posts', () =>\n  queryCollection('posts')\n    .where('draft', '=', false)\n    .order('date', 'DESC')\n    .all(),\n)\n\u003C\u002Fscript>\n","vue",[14,361,362,392,442,458,490,517,528,532],{"__ignoreMap":70},[74,363,364,368,372,376,379,382,385,387,389],{"class":76,"line":77},[74,365,367],{"class":366},"sUpvg","\u003C",[74,369,371],{"class":370},"seN_b","script",[74,373,375],{"class":374},"s71bh"," setup",[74,377,378],{"class":374}," lang",[74,380,381],{"class":92},"=",[74,383,384],{"class":110},"\"",[74,386,69],{"class":114},[74,388,384],{"class":110},[74,390,391],{"class":366},">\n",[74,393,394,398,401,405,407,411,413,417,421,424,426,428,431,433,435,438],{"class":76,"line":120},[74,395,397],{"class":396},"sG9mu","const",[74,399,400],{"class":92}," {",[74,402,404],{"class":403},"sAEpf"," data",[74,406,149],{"class":92},[74,408,410],{"class":409},"sV0ZF"," posts",[74,412,104],{"class":92},[74,414,416],{"class":415},"sNP9S"," =",[74,418,420],{"class":419},"skDMT"," await",[74,422,423],{"class":136}," useAsyncData",[74,425,259],{"class":84},[74,427,182],{"class":110},[74,429,430],{"class":114},"posts",[74,432,182],{"class":110},[74,434,93],{"class":92},[74,436,437],{"class":84}," ()",[74,439,441],{"class":440},"sBxw3"," =>\n",[74,443,444,447,449,451,453,455],{"class":76,"line":127},[74,445,446],{"class":136},"  queryCollection",[74,448,259],{"class":84},[74,450,182],{"class":110},[74,452,430],{"class":114},[74,454,182],{"class":110},[74,456,457],{"class":84},")\n",[74,459,460,463,466,468,470,473,475,477,479,481,483,485,488],{"class":76,"line":142},[74,461,462],{"class":92},"    .",[74,464,465],{"class":136},"where",[74,467,259],{"class":84},[74,469,182],{"class":110},[74,471,472],{"class":114},"draft",[74,474,182],{"class":110},[74,476,93],{"class":92},[74,478,111],{"class":110},[74,480,381],{"class":114},[74,482,182],{"class":110},[74,484,93],{"class":92},[74,486,487],{"class":305}," false",[74,489,457],{"class":84},[74,491,492,494,497,499,501,504,506,508,510,513,515],{"class":76,"line":155},[74,493,462],{"class":92},[74,495,496],{"class":136},"order",[74,498,259],{"class":84},[74,500,182],{"class":110},[74,502,503],{"class":114},"date",[74,505,182],{"class":110},[74,507,93],{"class":92},[74,509,111],{"class":110},[74,511,512],{"class":114},"DESC",[74,514,182],{"class":110},[74,516,457],{"class":84},[74,518,519,521,524,526],{"class":76,"line":168},[74,520,462],{"class":92},[74,522,523],{"class":136},"all",[74,525,239],{"class":84},[74,527,185],{"class":92},[74,529,530],{"class":76,"line":188},[74,531,457],{"class":84},[74,533,534,537,539],{"class":76,"line":205},[74,535,536],{"class":366},"\u003C\u002F",[74,538,371],{"class":370},[74,540,391],{"class":366},[10,542,543,544,547],{},"同一套 API 在 server route 里也能用，只是第一个参数换成 ",[14,545,546],{},"event","。RSS 和站点地图就是这么生成的。",[19,549,550],{"id":550},"一条命令产出整站",[65,552,556],{"className":553,"code":554,"language":555,"meta":70,"style":70},"language-bash shiki shiki-themes tokyo-night github-light","pnpm generate\n","bash",[14,557,558],{"__ignoreMap":70},[74,559,560,564],{"class":76,"line":77},[74,561,563],{"class":562},"sirr2","pnpm",[74,565,566],{"class":114}," generate\n",[10,568,569,572,573,576,577,580],{},[14,570,571],{},"nuxt generate"," 会启动一次性渲染，从 ",[14,574,575],{},"\u002F"," 开始爬取站内链接，把每个路由渲染成 HTML，连同 RSS、站点地图一起写进 ",[14,578,579],{},".output\u002Fpublic","。那个目录就是最终产物，扔到任何静态托管上都能跑。",[582,583,584],"blockquote",{},[10,585,586],{},"值得强调的是：预渲染的页面依然是完整的 Vue 应用。首屏是纯 HTML，水合之后站内跳转走客户端路由，无需再请求服务器。",[19,588,589],{"id":589},"写作体验",[10,591,592,593,596],{},"因为文章就是仓库里的文件，所有代码工具都顺便变成了写作工具：全文搜索用编辑器自带的，版本历史是 Git 的提交记录，写一半的文章加上 ",[14,594,595],{},"draft: true"," 就不会被发布。",[10,598,599],{},"这套组合不惊艳，但非常耐用。工具退到背景里，剩下的只有写字这件事本身。",[601,602,603],"style",{},"html pre.shiki code .sljjc, html code.shiki .sljjc{--shiki-default:#7DCFFF;--shiki-light:#D73A49}html pre.shiki code .sjXpA, html code.shiki .sjXpA{--shiki-default:#9ABDF5;--shiki-light:#24292E}html pre.shiki code .sPjv5, html code.shiki .sPjv5{--shiki-default:#0DB9D7;--shiki-light:#24292E}html pre.shiki code .szu1u, html code.shiki .szu1u{--shiki-default:#89DDFF;--shiki-light:#24292E}html pre.shiki code .sbW-k, html code.shiki .sbW-k{--shiki-default:#89DDFF;--shiki-light:#032F62}html pre.shiki code .snP_1, html code.shiki .snP_1{--shiki-default:#9ECE6A;--shiki-light:#032F62}html pre.shiki code .sxkyg, html code.shiki .sxkyg{--shiki-default:#7AA2F7;--shiki-light:#6F42C1}html pre.shiki code .s9SBS, html code.shiki .s9SBS{--shiki-default:#73DACA;--shiki-light:#24292E}html pre.shiki code .swOC1, html code.shiki .swOC1{--shiki-default:#41A6B5;--shiki-light:#24292E}html pre.shiki code .s4do5, html code.shiki .s4do5{--shiki-default:#C0CAF5;--shiki-light:#24292E}html pre.shiki code .sjYRd, html code.shiki .sjYRd{--shiki-default:#FF9E64;--shiki-light:#005CC5}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 .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html pre.shiki code .sUpvg, html code.shiki .sUpvg{--shiki-default:#BA3C97;--shiki-light:#24292E}html pre.shiki code .seN_b, html code.shiki .seN_b{--shiki-default:#F7768E;--shiki-light:#22863A}html pre.shiki code .s71bh, html code.shiki .s71bh{--shiki-default:#BB9AF7;--shiki-light:#6F42C1}html pre.shiki code .sG9mu, html code.shiki .sG9mu{--shiki-default:#9D7CD8;--shiki-default-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit}html pre.shiki code .sAEpf, html code.shiki .sAEpf{--shiki-default:#C0CAF5;--shiki-light:#E36209}html pre.shiki code .sV0ZF, html code.shiki .sV0ZF{--shiki-default:#BB9AF7;--shiki-light:#005CC5}html pre.shiki code .sNP9S, html code.shiki .sNP9S{--shiki-default:#89DDFF;--shiki-light:#D73A49}html pre.shiki code .skDMT, html code.shiki .skDMT{--shiki-default:#BB9AF7;--shiki-default-font-style:italic;--shiki-light:#D73A49;--shiki-light-font-style:inherit}html pre.shiki code .sBxw3, html code.shiki .sBxw3{--shiki-default:#BB9AF7;--shiki-light:#D73A49}html pre.shiki code .sirr2, html code.shiki .sirr2{--shiki-default:#C0CAF5;--shiki-light:#6F42C1}",{"title":70,"searchDepth":127,"depth":127,"links":605},[606,607,608,609,610],{"id":21,"depth":120,"text":21},{"id":49,"depth":120,"text":49},{"id":351,"depth":120,"text":351},{"id":550,"depth":120,"text":550},{"id":589,"depth":120,"text":589},"https:\u002F\u002Fpicsum.photos\u002Fseed\u002Fmoonstream-terminal\u002F1600\u002F1000","2026-08-05","不要数据库，不要管理后台。文章是仓库里的 Markdown 文件，构建命令跑完，整个站点变成一叠可以扔到任何地方的 HTML。这篇文章记录这套工作流的完整思路。",false,"md",{},"\u002Fposts\u002Fship-a-static-blog-with-nuxt4",{"title":5,"description":613},"posts\u002Fship-a-static-blog-with-nuxt4",[621,622,623],"nuxt","静态站点","工程化",null,"LcFWc0k-Rz31q9H0o2aJWJ_bf6pid-pGHoneA7gTdp4",[624,627],{"title":628,"path":629,"stem":630,"children":-1},"滚动驱动动画：让 scroll 监听器退休","\u002Fposts\u002Fcss-scroll-driven-animations","posts\u002Fcss-scroll-driven-animations",1786707084540]