[{"data":1,"prerenderedAt":689},["ShallowReactive",2],{"page-\u002Fcomponents\u002Fnuxt-content博客的一些个人思考":3},{"id":4,"title":5,"body":6,"description":677,"extension":678,"meta":679,"navigation":91,"path":685,"seo":686,"stem":687,"__hash__":688},"content\u002Fcomponents\u002Fnuxt-content博客的一些个人思考.md","Nuxt Content博客的一些个人思考",{"type":7,"value":8,"toc":671},"minimark",[9,47,50,433,440,445,448,456,459,462,465,473,476,542,545,548,551,554,564,584,587,593,596,600,603,609,616,622,625,632,642,645,651,654,661,664,667],[10,11,12,13,17,18,21,22,21,25,28,29,32,33,40,41,46],"p",{},"nuxt-content是一个以内容驱动的nuxt模块，本身是很适合制作我们的博客网站的，Nuxt Content 会读取你项目中的 ",[14,15,16],"code",{},"content\u002F"," 目录，解析 ",[14,19,20],{},".md","、",[14,23,24],{},".yml",[14,26,27],{},".csv"," 或 ",[14,30,31],{},".json"," 文件，并为你的应用程序创建一个强大的 ",[34,35,39],"a",{"href":36,"rel":37},"https:\u002F\u002Fnuxtjs.org.cn\u002Fmodules\u002Fcontent#",[38],"nofollow","数据","层。此外，还支持通过 ",[34,42,45],{"href":43,"rel":44},"https:\u002F\u002Fcontent.nuxtjs.org.cn\u002Fdocs\u002Ffiles\u002Fmarkdown",[38],"MDC 语法","在 Markdown 中使用 Vue 组件，这个在模板里有提到过一个counter计数器实现。",[10,48,49],{},"本次开发所使用的nuxt.config如下：",[51,52,57],"pre",{"className":53,"code":54,"language":55,"meta":56,"style":56},"language-ts shiki shiki-themes github-light github-dark","\u002F\u002F https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fapi\u002Fconfiguration\u002Fnuxt-config\nimport tailwindcss from '@tailwindcss\u002Fvite'\n\nexport default defineNuxtConfig({\n  css: ['~\u002Fassets\u002Fcss\u002Fmain.css', '~\u002Fassets\u002Fcss\u002Fmain.scss'],\n  modules: ['@nuxt\u002Fcontent', '@pinia\u002Fnuxt', '@vueuse\u002Fnuxt', '@nuxt\u002Feslint'],\n  app: {\n    head: {\n      script: [\n        {\n          innerHTML:\n            \"(function(){try{var s=localStorage.getItem('blog-theme');var d=s?s==='dark':window.matchMedia('(prefers-color-scheme: dark)').matches;if(d)document.documentElement.classList.add('dark')}catch(e){}})()\",\n          tagPosition: 'head' as const\n        }\n      ]\n    }\n  },\n  vite: {\n    plugins: [tailwindcss()]\n  },\n  content: {\n    build: {\n      pathMeta: {\n        \u002F\u002F slugify 默认把中文整段删掉（\\w 不含 CJK），补进白名单。\n        slugifyOptions: {\n          lower: true,\n          remove: \u002F[^\\w\\s$*_+~.()'\"!\\-:@\\u4e00-\\u9fff]+\u002Fg\n        }\n      }\n    }\n  },\n  nitro: {\n    preset: 'static',\n    prerender: {\n      crawlLinks: true,\n      routes: ['\u002F'],\n      failOnError: false\n    }\n  },\n  devtools: { enabled: true },\n  compatibilityDate: '2024-04-03'\n})\n","ts","",[14,58,59,68,86,93,109,127,153,159,165,171,177,183,192,207,213,219,225,231,237,249,254,260,266,272,278,284,296,323,328,334,339,344,350,361,367,377,388,397,402,407,418,427],{"__ignoreMap":56},[60,61,64],"span",{"class":62,"line":63},"line",1,[60,65,67],{"class":66},"sJ8bj","\u002F\u002F https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fapi\u002Fconfiguration\u002Fnuxt-config\n",[60,69,71,75,79,82],{"class":62,"line":70},2,[60,72,74],{"class":73},"szBVR","import",[60,76,78],{"class":77},"sVt8B"," tailwindcss ",[60,80,81],{"class":73},"from",[60,83,85],{"class":84},"sZZnC"," '@tailwindcss\u002Fvite'\n",[60,87,89],{"class":62,"line":88},3,[60,90,92],{"emptyLinePlaceholder":91},true,"\n",[60,94,96,99,102,106],{"class":62,"line":95},4,[60,97,98],{"class":73},"export",[60,100,101],{"class":73}," default",[60,103,105],{"class":104},"sScJk"," defineNuxtConfig",[60,107,108],{"class":77},"({\n",[60,110,112,115,118,121,124],{"class":62,"line":111},5,[60,113,114],{"class":77},"  css: [",[60,116,117],{"class":84},"'~\u002Fassets\u002Fcss\u002Fmain.css'",[60,119,120],{"class":77},", ",[60,122,123],{"class":84},"'~\u002Fassets\u002Fcss\u002Fmain.scss'",[60,125,126],{"class":77},"],\n",[60,128,130,133,136,138,141,143,146,148,151],{"class":62,"line":129},6,[60,131,132],{"class":77},"  modules: [",[60,134,135],{"class":84},"'@nuxt\u002Fcontent'",[60,137,120],{"class":77},[60,139,140],{"class":84},"'@pinia\u002Fnuxt'",[60,142,120],{"class":77},[60,144,145],{"class":84},"'@vueuse\u002Fnuxt'",[60,147,120],{"class":77},[60,149,150],{"class":84},"'@nuxt\u002Feslint'",[60,152,126],{"class":77},[60,154,156],{"class":62,"line":155},7,[60,157,158],{"class":77},"  app: {\n",[60,160,162],{"class":62,"line":161},8,[60,163,164],{"class":77},"    head: {\n",[60,166,168],{"class":62,"line":167},9,[60,169,170],{"class":77},"      script: [\n",[60,172,174],{"class":62,"line":173},10,[60,175,176],{"class":77},"        {\n",[60,178,180],{"class":62,"line":179},11,[60,181,182],{"class":77},"          innerHTML:\n",[60,184,186,189],{"class":62,"line":185},12,[60,187,188],{"class":84},"            \"(function(){try{var s=localStorage.getItem('blog-theme');var d=s?s==='dark':window.matchMedia('(prefers-color-scheme: dark)').matches;if(d)document.documentElement.classList.add('dark')}catch(e){}})()\"",[60,190,191],{"class":77},",\n",[60,193,195,198,201,204],{"class":62,"line":194},13,[60,196,197],{"class":77},"          tagPosition: ",[60,199,200],{"class":84},"'head'",[60,202,203],{"class":73}," as",[60,205,206],{"class":73}," const\n",[60,208,210],{"class":62,"line":209},14,[60,211,212],{"class":77},"        }\n",[60,214,216],{"class":62,"line":215},15,[60,217,218],{"class":77},"      ]\n",[60,220,222],{"class":62,"line":221},16,[60,223,224],{"class":77},"    }\n",[60,226,228],{"class":62,"line":227},17,[60,229,230],{"class":77},"  },\n",[60,232,234],{"class":62,"line":233},18,[60,235,236],{"class":77},"  vite: {\n",[60,238,240,243,246],{"class":62,"line":239},19,[60,241,242],{"class":77},"    plugins: [",[60,244,245],{"class":104},"tailwindcss",[60,247,248],{"class":77},"()]\n",[60,250,252],{"class":62,"line":251},20,[60,253,230],{"class":77},[60,255,257],{"class":62,"line":256},21,[60,258,259],{"class":77},"  content: {\n",[60,261,263],{"class":62,"line":262},22,[60,264,265],{"class":77},"    build: {\n",[60,267,269],{"class":62,"line":268},23,[60,270,271],{"class":77},"      pathMeta: {\n",[60,273,275],{"class":62,"line":274},24,[60,276,277],{"class":66},"        \u002F\u002F slugify 默认把中文整段删掉（\\w 不含 CJK），补进白名单。\n",[60,279,281],{"class":62,"line":280},25,[60,282,283],{"class":77},"        slugifyOptions: {\n",[60,285,287,290,294],{"class":62,"line":286},26,[60,288,289],{"class":77},"          lower: ",[60,291,293],{"class":292},"sj4cs","true",[60,295,191],{"class":77},[60,297,299,302,305,308,311,314,317,320],{"class":62,"line":298},27,[60,300,301],{"class":77},"          remove:",[60,303,304],{"class":84}," \u002F",[60,306,307],{"class":292},"[",[60,309,310],{"class":73},"^",[60,312,313],{"class":292},"\\w\\s$*_+~.()'\"!\\-:@\\u4e00-\\u9fff]",[60,315,316],{"class":73},"+",[60,318,319],{"class":84},"\u002F",[60,321,322],{"class":73},"g\n",[60,324,326],{"class":62,"line":325},28,[60,327,212],{"class":77},[60,329,331],{"class":62,"line":330},29,[60,332,333],{"class":77},"      }\n",[60,335,337],{"class":62,"line":336},30,[60,338,224],{"class":77},[60,340,342],{"class":62,"line":341},31,[60,343,230],{"class":77},[60,345,347],{"class":62,"line":346},32,[60,348,349],{"class":77},"  nitro: {\n",[60,351,353,356,359],{"class":62,"line":352},33,[60,354,355],{"class":77},"    preset: ",[60,357,358],{"class":84},"'static'",[60,360,191],{"class":77},[60,362,364],{"class":62,"line":363},34,[60,365,366],{"class":77},"    prerender: {\n",[60,368,370,373,375],{"class":62,"line":369},35,[60,371,372],{"class":77},"      crawlLinks: ",[60,374,293],{"class":292},[60,376,191],{"class":77},[60,378,380,383,386],{"class":62,"line":379},36,[60,381,382],{"class":77},"      routes: [",[60,384,385],{"class":84},"'\u002F'",[60,387,126],{"class":77},[60,389,391,394],{"class":62,"line":390},37,[60,392,393],{"class":77},"      failOnError: ",[60,395,396],{"class":292},"false\n",[60,398,400],{"class":62,"line":399},38,[60,401,224],{"class":77},[60,403,405],{"class":62,"line":404},39,[60,406,230],{"class":77},[60,408,410,413,415],{"class":62,"line":409},40,[60,411,412],{"class":77},"  devtools: { enabled: ",[60,414,293],{"class":292},[60,416,417],{"class":77}," },\n",[60,419,421,424],{"class":62,"line":420},41,[60,422,423],{"class":77},"  compatibilityDate: ",[60,425,426],{"class":84},"'2024-04-03'\n",[60,428,430],{"class":62,"line":429},42,[60,431,432],{"class":77},"})\n",[10,434,435,436,439],{},"本次项目默认设置的toc配置深度为2，搜索深度也为2。因此只会找到 ",[14,437,438],{},"## #"," 这两个分节。我们默认打开了contentHeading: true，让nuxt content根据文章开头的H1和后续内容来自动生成title和description，这个可以为我们省去很多时间去写解析器。nuxt默认使用shiki来提供代码高亮，这个对于代码博客很有作用，我们还可以指定加载哪些语言。",[441,442,444],"h2",{"id":443},"slugfy","Slugfy",[10,446,447],{},"nuxt content的路径构建过程中使用了slugfy逻辑，我们配置的时候就能够发现相关配置。它可以把md文件路径通过content build环节 进行 path\u002Fslug 处理之后存入数据库，这样我们使用queryCollection就可以提出了。",[10,449,450,451,455],{},"Content 在构建时需要把",[452,453,454],"strong",{},"文件路径转换成规范化的 content path","。",[441,457,458],{"id":458},"构建相关思考",[10,460,461],{},"原先框架本身是偏向SSR，用户访问的时候会通过Nitro服务器进行预先的SSR行为，queryCollection行为会在nitro里执行，然后获取Content了之后再通过vue ssr返回html给用户，这个是本来的路径。用户每访问一次页面，都可能出发服务器端的渲染过程，这个cpu、内存资源消耗还是挺大的。",[10,463,464],{},"我们的服务器资源没那么强，所以最好选择SSG好一些。",[51,466,471],{"className":467,"code":469,"language":470},[468],"language-text","content\u002F\n   │\n   ▼\nContent Build\n   │\n   ├── Markdown 解析\n   ├── frontmatter\n   ├── path\n   ├── slug\u002Fpath 规范化\n   └── metadata\n   │\n   ▼\nContent 数据库\n","text",[14,472,469],{"__ignoreMap":56},[10,474,475],{},"差不多是这样",[51,477,479],{"className":53,"code":478,"language":55,"meta":56,"style":56},"nitro: {\n  preset: 'static',\n  prerender: {\n    crawlLinks: true,\n    routes: ['\u002F']\n  }\n}\n",[14,480,481,489,501,508,519,532,537],{"__ignoreMap":56},[60,482,483,486],{"class":62,"line":63},[60,484,485],{"class":104},"nitro",[60,487,488],{"class":77},": {\n",[60,490,491,494,497,499],{"class":62,"line":70},[60,492,493],{"class":104},"  preset",[60,495,496],{"class":77},": ",[60,498,358],{"class":84},[60,500,191],{"class":77},[60,502,503,506],{"class":62,"line":88},[60,504,505],{"class":104},"  prerender",[60,507,488],{"class":77},[60,509,510,513,515,517],{"class":62,"line":95},[60,511,512],{"class":104},"    crawlLinks",[60,514,496],{"class":77},[60,516,293],{"class":292},[60,518,191],{"class":77},[60,520,521,524,527,529],{"class":62,"line":111},[60,522,523],{"class":104},"    routes",[60,525,526],{"class":77},": [",[60,528,385],{"class":84},[60,530,531],{"class":77},"]\n",[60,533,534],{"class":62,"line":129},[60,535,536],{"class":77},"  }\n",[60,538,539],{"class":62,"line":155},[60,540,541],{"class":77},"}\n",[10,543,544],{},"这样配合pnpm generate，实际上构建服务器就会通过nuxt build使用content模块进行queryCollection先生成文档的index.html。构建阶段我们会建立应用和content数据并开始预渲染。crawler预渲染爬虫就会开始工作。",[10,546,547],{},"假设content下有一些md文档，经过了content的build以后就形成了对应的content documents，以及他们的Path。但是content db 里有这个文档不代表预渲染会渲染这个url。虽然slugfy已经通过文件路径生成了content path 和 metadata，但是没有预渲染环节。",[10,549,550],{},"当crawler渲染完了首页之后就会从html链接中发现文档链接，比如说\u002Fvue\u002Freactive，这样就算是发现并进行了预渲染，这个也是很坑的地方之一，因为如果你写了分页查询的话，那么crawler就会找不到，也就不会预渲染和生成html，会导致导航失败。客户端可能是导航成功了，因为已经通过了slugfy得到了content document的path，结果静态部署目录里可能没有这个index.html，最后导致静态服务器找不到资源。",[441,552,553],{"id":553},"浏览器端的sqlite",[10,555,556,557,560,561],{},"这个是个很奇怪的一点，为什么content模块不去使用indexeddb，而却去使用wasm和嵌入式sqlite数据库，原先nuxt content v3的设计是客户端导航时，让queryCollection再浏览器本地执行sql查询，官方文档明确说，首次发生客户端content查询时，会从服务器下载构建好的database dump，并在浏览器里初始化本地化sqlite，之后查询在浏览器本地执行。在 Node 环境下，Nuxt Content 默认 SQLite adapter 可以使用 ",[14,558,559],{},"better-sqlite3","；Node 22.5+ 也可以使用原生 ",[14,562,563],{},"node:sqlite",[10,565,566,567,570,571,21,574,21,577,21,580,583],{},"但是浏览器没有node，因此使用sqlite compiled to wasm这样在浏览器里使用sqlite。Nuxt Content 官方 API 本身就明确把 ",[14,568,569],{},"queryCollection()"," 描述成 SQL-backed query builder，并支持 ",[14,572,573],{},"where",[14,575,576],{},"order",[14,578,579],{},"limit",[14,581,582],{},"skip"," 等 SQL 查询语义。",[10,585,586],{},"第一次查询会出发获取wasm的sqlite二进制程序本体的请求，在获取到dump文件之后：",[51,588,591],{"className":589,"code":590,"language":470},[468],"\u002F__nuxt_content\u002F{collection}\u002Fsql_dump.txt\n",[14,592,590],{"__ignoreMap":56},[10,594,595],{},"会查询得到content来恢复页面，这个过程就不涉及payload了。",[441,597,599],{"id":598},"_-payoadjson","_ payoad.json",[10,601,602],{},"一个_payload.json输出实际上可能是",[51,604,607],{"className":605,"code":606,"language":470},[468],"\u002Fcpp\u002Fboost\u002F编译python可调用的pyd-c模块\u002F\n│\n├── index.html\n└── _payload.json\n",[14,608,606],{"__ignoreMap":56},[10,610,611,612,615],{},"index.html负责html，",[14,613,614],{},"_padload.js","  负责nuxt页面运行时需要恢复的payload\u002Fstate。比如说这个payload里面已经有：",[51,617,620],{"className":618,"code":619,"language":470},[468],"page-\u002Fcpp\u002Fboost\u002F编译python可调用的pyd-c模块\n       ↓\nContent Document\n       ↓\ntitle\nbody\ndescription\nnavigation\npath\nseo\n...\n",[14,621,619],{"__ignoreMap":56},[10,623,624],{},"本身就包含了文章数据，构建阶段的queryCollection查询出来的结果就会被nuxt序列化进入该页面的payload，而不是浏览器通过查询再sqlite里查询。用户访问的时候GET请求去静态服务器里找_payload.json。拿到已经序列化的page来恢复数据。",[10,626,627,628,631],{},"之前提到的sqlite wasm机制不一样，是另一套客户端content查询机制，主要解决客户端需要自己执行content查询时候，如何再浏览器里继续查询content数据。如果我去点击一个没有点击过的文档，抓到的就不是payload.json了，而是wasm，官方文档也明确说了：在 SSG\u002F静态部署下，Nuxt Content 会把数据库带到浏览器，由 WASM SQLite 支持",[452,629,630],{},"客户端导航或客户端 action 中发生的 Content 查询","。第一次发生这类客户端 Content 查询时，会下载数据库 dump 并初始化浏览器本地 SQLite。",[10,633,634,635,637,638,641],{},"Nuxt Content 的 ",[14,636,569],{}," 在浏览器端确实会直接走 SQLite WASM；而且一旦这条查询代码真正被执行，",[14,639,640],{},"_payload.json"," 不会自动阻止它。那为什么 Nuxt Content 默认还把这个能力放进来？",[10,643,644],{},"因为 Nuxt Content v3 的设计目标不是单纯：",[646,647,648],"blockquote",{},[10,649,650],{},"“Markdown → SSG HTML”",[10,652,653],{},"而是：",[646,655,656],{},[10,657,658],{},[452,659,660],{},"统一的 SQL Content 数据层，同时支持 server \u002F serverless \u002F static \u002F client-side navigation。",[10,662,663],{},"官方明确把 v3 定位成 SQL-backed storage，并且声称同一套系统支持开发、静态生成、server、serverless、edge 等部署模式，因此我们再推论一下，数据层甚至是可以做到云服务上的，例如Cloudfalre D1，本质上就是把 Content 的数据层放到了 Cloudflare 的数据库服务上。",[10,665,666],{},"至于SSR下的具体表现，我确实还没有尝试过，因此先不写了。",[668,669,670],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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":56,"searchDepth":70,"depth":70,"links":672},[673,674,675,676],{"id":443,"depth":70,"text":444},{"id":458,"depth":70,"text":458},{"id":553,"depth":70,"text":553},{"id":598,"depth":70,"text":599},"nuxt-content是一个以内容驱动的nuxt模块，本身是很适合制作我们的博客网站的，Nuxt Content 会读取你项目中的 content\u002F 目录，解析 .md、.yml、.csv 或 .json 文件，并为你的应用程序创建一个强大的 数据层。此外，还支持通过 MDC 语法在 Markdown 中使用 Vue 组件，这个在模板里有提到过一个counter计数器实现。","md",{"date":680,"tags":681},"2026-10-05",[682,683,684],"nuxt","vue","typescript","\u002Fcomponents\u002Fnuxt-content博客的一些个人思考",{"description":677},"components\u002Fnuxt-content博客的一些个人思考","n_NgrQscZMsi8NYfCaAhNvXJIZG-xPkEu3503FuPGaE",1791207386952]