[{"data":1,"prerenderedAt":577},["ShallowReactive",2],{"content:article:nuxt-static-blog":3,"content:article-surround:\u002Farticles\u002Fnuxt-static-blog":572},{"id":4,"title":5,"body":6,"category":555,"cover":556,"date":557,"description":558,"draft":559,"extension":560,"featured":561,"meta":562,"navigation":561,"path":563,"seo":564,"stem":565,"tags":566,"updated":570,"__hash__":571},"articles\u002Farticles\u002Fnuxt-static-blog.md","把博客做成纯静态：Nuxt 4 生成 + Nginx 托管",{"type":7,"value":8,"toc":544},"minimark",[9,13,18,26,88,91,94,104,279,282,303,307,312,329,365,379,383,399,410,414,453,459,462,473,512,522,526,540],[10,11,12],"p",{},"写博客的工具选型里有一个常见的误解：静态站 = 不需要服务端 = 简单。真正麻烦的地方在于「哪些东西必须发生在构建期」。这篇文章记录这个站点的几个决定。",[14,15,17],"h2",{"id":16},"为什么不是-spa","为什么不是 SPA",[10,19,20,21,25],{},"同样的 Nuxt 项目，",[22,23,24],"code",{},"ssr: false"," 打包出来只有 20 KB HTML 骨架，看起来更省。但博客的两个核心需求会直接落空：",[27,28,29,51],"table",{},[30,31,32],"thead",{},[33,34,35,39,45],"tr",{},[36,37,38],"th",{},"需求",[36,40,41,42,44],{},"SPA（",[22,43,24],{},"）",[36,46,47,48,44],{},"静态生成（",[22,49,50],{},"nuxt generate",[52,53,54,66,77],"tbody",{},[33,55,56,60,63],{},[57,58,59],"td",{},"文章被搜索引擎完整收录",[57,61,62],{},"取决于爬虫是否执行 JS",[57,64,65],{},"拿到的是完整正文",[33,67,68,71,74],{},[57,69,70],{},"分享链接的 OG 卡片",[57,72,73],{},"只有前端渲染后才有内容",[57,75,76],{},"HTML 里就带着 meta",[33,78,79,82,85],{},[57,80,81],{},"首屏",[57,83,84],{},"下载 JS → 执行 → 渲染",[57,86,87],{},"直接是可见内容",[10,89,90],{},"博客的生命周期以年计，读者从搜索与聊天窗口进来，这两条都在赌客户端会不会执行 JS。不值得。",[14,92,93],{"id":93},"内容查询必须留在构建期",[10,95,96,99,100,103],{},[22,97,98],{},"@nuxt\u002Fcontent"," 的查询在服务端执行。纯静态部署没有服务端，所以浏览器里重新执行查询就会请求一个不存在的接口。解决办法是把查询包进 ",[22,101,102],{},"useAsyncData","：",[105,106,111],"pre",{"className":107,"code":108,"language":109,"meta":110,"style":110},"language-ts shiki shiki-themes github-light github-dark","export function useArticles() {\n  return useAsyncData('content:articles', () =>\n    queryCollection('articles')\n      .where('draft', '=', false)\n      .order('date', 'DESC')\n      .select('title', 'description', 'date', 'category', 'tags', 'path')\n      .all(),\n  )\n}\n","ts","",[22,112,113,133,155,169,197,217,256,267,273],{"__ignoreMap":110},[114,115,118,122,125,129],"span",{"class":116,"line":117},"line",1,[114,119,121],{"class":120},"szBVR","export",[114,123,124],{"class":120}," function",[114,126,128],{"class":127},"sScJk"," useArticles",[114,130,132],{"class":131},"sVt8B","() {\n",[114,134,136,139,142,145,149,152],{"class":116,"line":135},2,[114,137,138],{"class":120},"  return",[114,140,141],{"class":127}," useAsyncData",[114,143,144],{"class":131},"(",[114,146,148],{"class":147},"sZZnC","'content:articles'",[114,150,151],{"class":131},", () ",[114,153,154],{"class":120},"=>\n",[114,156,158,161,163,166],{"class":116,"line":157},3,[114,159,160],{"class":127},"    queryCollection",[114,162,144],{"class":131},[114,164,165],{"class":147},"'articles'",[114,167,168],{"class":131},")\n",[114,170,172,175,178,180,183,186,189,191,195],{"class":116,"line":171},4,[114,173,174],{"class":131},"      .",[114,176,177],{"class":127},"where",[114,179,144],{"class":131},[114,181,182],{"class":147},"'draft'",[114,184,185],{"class":131},", ",[114,187,188],{"class":147},"'='",[114,190,185],{"class":131},[114,192,194],{"class":193},"sj4cs","false",[114,196,168],{"class":131},[114,198,200,202,205,207,210,212,215],{"class":116,"line":199},5,[114,201,174],{"class":131},[114,203,204],{"class":127},"order",[114,206,144],{"class":131},[114,208,209],{"class":147},"'date'",[114,211,185],{"class":131},[114,213,214],{"class":147},"'DESC'",[114,216,168],{"class":131},[114,218,220,222,225,227,230,232,235,237,239,241,244,246,249,251,254],{"class":116,"line":219},6,[114,221,174],{"class":131},[114,223,224],{"class":127},"select",[114,226,144],{"class":131},[114,228,229],{"class":147},"'title'",[114,231,185],{"class":131},[114,233,234],{"class":147},"'description'",[114,236,185],{"class":131},[114,238,209],{"class":147},[114,240,185],{"class":131},[114,242,243],{"class":147},"'category'",[114,245,185],{"class":131},[114,247,248],{"class":147},"'tags'",[114,250,185],{"class":131},[114,252,253],{"class":147},"'path'",[114,255,168],{"class":131},[114,257,259,261,264],{"class":116,"line":258},7,[114,260,174],{"class":131},[114,262,263],{"class":127},"all",[114,265,266],{"class":131},"(),\n",[114,268,270],{"class":116,"line":269},8,[114,271,272],{"class":131},"  )\n",[114,274,276],{"class":116,"line":275},9,[114,277,278],{"class":131},"}\n",[10,280,281],{},"两件事容易被忘掉：",[283,284,285,292],"ul",{},[286,287,288,291],"li",{},[22,289,290],{},"select()"," 不写，payload 里就会带上整篇正文的 AST，列表页凭空胖几十 KB；",[286,293,294,295,298,299,302],{},"详情页的 key 必须带 slug，并且用 ",[22,296,297],{},"definePageMeta({ key: route => route.fullPath })"," 强制重建组件，否则从 A 文章点到 B 文章，",[22,300,301],{},"setup"," 不会重新执行，页面上还是 A 的内容。",[14,304,306],{"id":305},"nginx-上要注意的三件事","Nginx 上要注意的三件事",[308,309,311],"h3",{"id":310},"_1-目录形式的静态文件","1. 目录形式的静态文件",[10,313,314,316,317,320,321,324,325,328],{},[22,315,50],{}," 把 ",[22,318,319],{},"\u002Farticles\u002Ffoo"," 输出成 ",[22,322,323],{},"articles\u002Ffoo\u002Findex.html","。",[22,326,327],{},"try_files"," 要按这个顺序找：",[105,330,334],{"className":331,"code":332,"language":333,"meta":110,"style":110},"language-nginx shiki shiki-themes github-light github-dark","location \u002F {\n  try_files $uri $uri\u002Findex.html $uri.html =404;\n}\n","nginx",[22,335,336,347,361],{"__ignoreMap":110},[114,337,338,341,344],{"class":116,"line":117},[114,339,340],{"class":120},"location",[114,342,343],{"class":127}," \u002F ",[114,345,346],{"class":131},"{\n",[114,348,349,352,355,358],{"class":116,"line":135},[114,350,351],{"class":120},"  try_files ",[114,353,354],{"class":131},"$uri $uri\u002Findex.html $uri.html ",[114,356,357],{"class":193},"=404",[114,359,360],{"class":131},";\n",[114,362,363],{"class":116,"line":157},[114,364,278],{"class":131},[10,366,367,368,371,372,374,375,378],{},"只写 ",[22,369,370],{},"$uri\u002F"," 的话，",[22,373,319],{}," 会先命中目录再落到 ",[22,376,377],{},"index"," 指令，看起来能用，但大小写敏感的目录列表配置一旦改动就会露馅。",[308,380,382],{"id":381},"_2-真正的-404-状态码","2. 真正的 404 状态码",[105,384,386],{"className":331,"code":385,"language":333,"meta":110,"style":110},"error_page 404 \u002F404\u002Findex.html;\n",[22,387,388],{"__ignoreMap":110},[114,389,390,393,396],{"class":116,"line":117},[114,391,392],{"class":120},"error_page ",[114,394,395],{"class":193},"404",[114,397,398],{"class":131}," \u002F404\u002Findex.html;\n",[10,400,401,402,405,406,409],{},"不要写成 ",[22,403,404],{},"error_page 404 = \u002F404\u002Findex.html;","。加了 ",[22,407,408],{},"="," 之后响应状态被改写成 200，搜索引擎会把不存在的链接当成可用页面收录，留下一堆软 404。",[308,411,413],{"id":412},"_3-哈希产物强缓存html-不缓存","3. 哈希产物强缓存，HTML 不缓存",[105,415,417],{"className":331,"code":416,"language":333,"meta":110,"style":110},"location \u002F_nuxt\u002F {\n  expires 1y;\n  add_header Cache-Control \"public, immutable\";\n}\n",[22,418,419,428,436,449],{"__ignoreMap":110},[114,420,421,423,426],{"class":116,"line":117},[114,422,340],{"class":120},[114,424,425],{"class":127}," \u002F_nuxt\u002F ",[114,427,346],{"class":131},[114,429,430,433],{"class":116,"line":135},[114,431,432],{"class":120},"  expires ",[114,434,435],{"class":131},"1y;\n",[114,437,438,441,444,447],{"class":116,"line":157},[114,439,440],{"class":120},"  add_header ",[114,442,443],{"class":131},"Cache-Control ",[114,445,446],{"class":147},"\"public, immutable\"",[114,448,360],{"class":131},[114,450,451],{"class":116,"line":171},[114,452,278],{"class":131},[10,454,455,458],{},[22,456,457],{},"\u002F_nuxt\u002F"," 下的文件名带内容哈希，可以放心 immutable；HTML 一旦缓存，读者点开链接看到的永远是旧构建。",[14,460,461],{"id":461},"检索是构建的最后一步",[10,463,464,465,468,469,472],{},"搜索用的是 Pagefind：它在生成之后扫描 ",[22,466,467],{},".output\u002Fpublic"," 下的 HTML，产出静态索引文件。于是 ",[22,470,471],{},"package.json"," 里的脚本顺序是固定的：",[105,474,478],{"className":475,"code":476,"language":477,"meta":110,"style":110},"language-json shiki shiki-themes github-light github-dark","{\n  \"scripts\": {\n    \"dist\": \"nuxt generate && pagefind --site .output\u002Fpublic\"\n  }\n}\n","json",[22,479,480,484,492,503,508],{"__ignoreMap":110},[114,481,482],{"class":116,"line":117},[114,483,346],{"class":131},[114,485,486,489],{"class":116,"line":135},[114,487,488],{"class":193},"  \"scripts\"",[114,490,491],{"class":131},": {\n",[114,493,494,497,500],{"class":116,"line":157},[114,495,496],{"class":193},"    \"dist\"",[114,498,499],{"class":131},": ",[114,501,502],{"class":147},"\"nuxt generate && pagefind --site .output\u002Fpublic\"\n",[114,504,505],{"class":116,"line":171},[114,506,507],{"class":131},"  }\n",[114,509,510],{"class":116,"line":199},[114,511,278],{"class":131},[10,513,514,515,517,518,521],{},"只跑 ",[22,516,50],{}," 也能得到一个能访问的站点，但搜索页会因为拿不到 ",[22,519,520],{},"\u002Fpagefind\u002Fpagefind.js"," 而报错。这类「构建步骤少一步、线上坏一个功能」的坑，最好在读者的浏览器里也能看见——所以搜索页在这种情况会显示一句明确的提示，而不是空白。",[14,523,525],{"id":524},"剩下的都交给-markdown","剩下的都交给 Markdown",[10,527,528,529,532,533,535,536,539],{},"写完这些之后，日常只需要两种动作：在仓库里改 ",[22,530,531],{},".md","，或者在网页编辑器里改同一批 ",[22,534,531],{},"。两者共用 ",[22,537,538],{},"content.config.ts"," 里的字段定义，保存下来仍然是同一个文件、同一套字段。静态站点的好处正在于此：内容的表示形式只有一种，工具可以换，数据不用迁。",[541,542,543],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":110,"searchDepth":157,"depth":157,"links":545},[546,547,548,553,554],{"id":16,"depth":135,"text":17},{"id":93,"depth":135,"text":93},{"id":305,"depth":135,"text":306,"children":549},[550,551,552],{"id":310,"depth":157,"text":311},{"id":381,"depth":157,"text":382},{"id":412,"depth":157,"text":413},{"id":461,"depth":135,"text":461},{"id":524,"depth":135,"text":525},"前端工程","\u002Fimages\u002Fcovers\u002Fnuxt-static-blog.jpg","2026-09-06","这个站点本身的实现笔记：为什么选静态生成而不是 SPA，以及 Nginx 上要注意的三件事。",false,"md",true,{},"\u002Farticles\u002Fnuxt-static-blog",{"title":5,"description":558},"articles\u002Fnuxt-static-blog",[567,568,569],"Nuxt","静态站点","Nginx",null,"olZZtemEpDrHQxEeH2Vqr6OnWJlaVawQScTSN3sS1O4",[570,573],{"title":574,"path":575,"date":576},"Spring 事务为什么没按预期回滚：沿着调用边界排查","\u002Farticles\u002Fspring-transaction-boundary","2026-02-27",1792063306798]