[{"data":1,"prerenderedAt":1909},["ShallowReactive",2],{"page-/post/nuxt/nuxt3-obsidian-build-your-blog":3,"surrounding-page":1900},{"id":4,"title":5,"author":6,"body":7,"date":1884,"description":1885,"extension":1886,"group":6,"lastmod":1887,"meta":1888,"navigation":588,"path":1890,"rawbody":1891,"seo":1892,"showTitle":6,"stem":1893,"tags":1894,"versions":1896,"__hash__":1899},"content/post/nuxt/nuxt3-obsidian-build-your-blog.md","基于 Nuxt3 + Obsidian 搭建个人博客",null,{"type":8,"value":9,"toc":1878},"minimark",[10,22,38,49,54,60,95,111,117,124,127,151,169,231,238,243,254,264,271,331,342,351,361,364,381,408,418,425,506,527,533,537,552,555,562,826,840,851,858,865,893,910,913,995,998,1005,1012,1465,1478,1484,1493,1501,1511,1520,1525,1528,1558,1563,1566,1575,1693,1708,1720,1723,1726,1729,1732,1735,1738,1750,1756,1759,1768,1781,1800,1811,1824,1830,1843,1850,1853,1856,1862,1865,1868,1871,1874],[11,12,13,17,18,21],"p",{},[14,15,16],"code",{},"Nuxt","是一个用Vue来编写的，可用来创建类型安全、高性能和生产级全栈 Web 应用程序和网站的全栈框架。后端是 ",[14,19,20],{},"Nitro","，一个可以被单独使用的Web服务端框架。",[11,23,24,25,29,30,33,34,37],{},"作为一个全栈框架，不仅具备了比使用Vue开发SPA客户端",[26,27,28],"strong",{},"更好的开发体验","，还能享受服务端渲染带来的",[26,31,32],{},"SEO优化","，同时Node服务可以实现帮你实现",[26,35,36],{},"更多的可能性","。",[11,39,40,41,44,45,48],{},"之所以基于Nuxt从零搭建，一是为了选全栈，二是发现了",[14,42,43],{},"nuxt/content","可以读取本地的",[14,46,47],{},"markdown","文件，三是选的模板达不到想要的效果，四是过程可以作为写文章的素材。",[50,51,53],"h2",{"id":52},"初始化nuxt项目","初始化Nuxt项目",[11,55,56,57,59],{},"使用",[14,58,43],{},"初始化项目，因为这个插件是博客的核心插件，所以初始化时直接装上就可以",[61,62,67],"pre",{"className":63,"code":64,"language":65,"meta":66,"style":66},"language-shell shiki shiki-themes github-light","npx nuxi@latest init content-app -t content\n","shell","",[14,68,69],{"__ignoreMap":66},[70,71,74,78,82,85,88,92],"span",{"class":72,"line":73},"line",1,[70,75,77],{"class":76},"s7eDp","npx",[70,79,81],{"class":80},"sYBdl"," nuxi@latest",[70,83,84],{"class":80}," init",[70,86,87],{"class":80}," content-app",[70,89,91],{"class":90},"sYu0t"," -t",[70,93,94],{"class":80}," content\n",[11,96,97,98,101,102,101,105,108,109],{},"选择",[14,99,100],{},"npm","、",[14,103,104],{},"pnpm",[14,106,107],{},"yarn","作为包管理器后，会生成项目目录，下载依赖，我选了",[14,110,100],{},[11,112,113],{},[114,115],"img",{"alt":66,"src":116},"https://img.zzao.club/1-img-20241106171162.png",[11,118,119,120,123],{},"此时直接运行 ",[14,121,122],{},"npm run dev"," 就可以启动项目了",[11,125,126],{},"启动后会有如下页面：",[11,128,129,132,135,136,139,140,143,144,147,148,150],{},[114,130],{"alt":66,"src":131},"https://img.zzao.club/1-img-20241106171191.png",[14,133,134],{},"app.vue","作为页面的入口文件，里面只有一个简单对的 ",[14,137,138],{},"NuxtPage"," ，作用类似于 ",[14,141,142],{},"Vue"," 中的 ",[14,145,146],{},"RouterView"," ，实际上它就是 ",[14,149,146],{}," 的包装。",[11,152,153,154,157,158,161,162],{},"同样的包装还有：",[14,155,156],{},"NuxtLink"," 、",[14,159,160],{},"NuxtImg"," 等等。 更多 ",[163,164,168],"a",{"href":165,"rel":166},"https://nuxt.com/docs/api/components/client-only",[167],"nofollow","Nuxt Component",[61,170,174],{"className":171,"code":172,"language":173,"meta":66,"style":66},"language-vue shiki shiki-themes github-light","\u003Ctemplate>\n  \u003Cdiv>\n    \u003CNuxtPage />\n  \u003C/div>\n\u003C/template>\n","vue",[14,175,176,189,200,211,221],{"__ignoreMap":66},[70,177,178,182,186],{"class":72,"line":73},[70,179,181],{"class":180},"sgsFI","\u003C",[70,183,185],{"class":184},"shJU0","template",[70,187,188],{"class":180},">\n",[70,190,192,195,198],{"class":72,"line":191},2,[70,193,194],{"class":180},"  \u003C",[70,196,197],{"class":184},"div",[70,199,188],{"class":180},[70,201,203,206,208],{"class":72,"line":202},3,[70,204,205],{"class":180},"    \u003C",[70,207,138],{"class":184},[70,209,210],{"class":180}," />\n",[70,212,214,217,219],{"class":72,"line":213},4,[70,215,216],{"class":180},"  \u003C/",[70,218,197],{"class":184},[70,220,188],{"class":180},[70,222,224,227,229],{"class":72,"line":223},5,[70,225,226],{"class":180},"\u003C/",[70,228,185],{"class":184},[70,230,188],{"class":180},[11,232,233,234,237],{},"它用来显示 ",[14,235,236],{},"pages/"," 下的页面",[11,239,240,242],{},[14,241,236],{}," 是Nuxt生成路由的一个约定目录，其内的文件会自动生成路由。",[11,244,245,246,249,250,253],{},"如 ",[14,247,248],{},"pages/a.vue"," 会生成 ",[14,251,252],{},"/a"," 路由",[11,255,256,257,260,261],{},"如果想接收url参数可以这样写： ",[14,258,259],{},"/pages/a/[id].vue"," 或 ",[14,262,263],{},"/pages/a-[group]/[id].vue",[11,265,266,267,270],{},"在文件内使用这种方式来获取参数，总之 ",[14,268,269],{},"[xxx]"," 是它的路由规则",[61,272,276],{"className":273,"code":274,"language":275,"meta":66,"style":66},"language-typescript shiki shiki-themes github-light","\u003Cscript setup lang=\"ts\">\nconst route = useRoute()\nconsole.log(route.params.id, route.params.group)\n\u003C/script>\n\n","typescript",[14,277,278,294,311,322],{"__ignoreMap":66},[70,279,280,283,286,289,292],{"class":72,"line":73},[70,281,181],{"class":282},"sD7c4",[70,284,285],{"class":180},"script setup lang",[70,287,288],{"class":282},"=",[70,290,291],{"class":80},"\"ts\"",[70,293,188],{"class":282},[70,295,296,299,302,305,308],{"class":72,"line":191},[70,297,298],{"class":282},"const",[70,300,301],{"class":90}," route",[70,303,304],{"class":282}," =",[70,306,307],{"class":76}," useRoute",[70,309,310],{"class":180},"()\n",[70,312,313,316,319],{"class":72,"line":202},[70,314,315],{"class":180},"console.",[70,317,318],{"class":76},"log",[70,320,321],{"class":180},"(route.params.id, route.params.group)\n",[70,323,324,326,329],{"class":72,"line":213},[70,325,226],{"class":282},[70,327,328],{"class":180},"script",[70,330,188],{"class":282},[11,332,333,334,337,338,341],{},"如果想匹配改路径下的所有路由，可以这样写：",[14,335,336],{},"/pages/a/[...slug].vue","，此时可以再去看一下\n",[14,339,340],{},"route.params.slug"," 的值是什么。",[11,343,344,345,350],{},"查看更多关于 ",[163,346,349],{"href":347,"rel":348},"https://nuxt.com/docs/guide/directory-structure/pages",[167],"Nuxt Pages"," 👈",[11,352,56,353,356,357,360],{},[14,354,355],{},"Pages","不仅仅是简化了",[14,358,359],{},"Router","的配置，也是为了服务端渲染，更好的SEO。做博客的话，当然希望别人搜索某些关键字可以直接搜到自己的文章页面。而不是全部内容都在一个单页面内，由浏览器渲染。",[11,362,363],{},"所以自带的两个页面是怎么渲染的就清楚了。",[11,365,366,367,370,371,373,374,377,378,37],{},"你可以看到上面我并没有引入",[14,368,369],{},"useRoute","，但也可以在",[14,372,173],{},"文件中直接使用，因为Nuxt提前做好了",[14,375,376],{},"自动import"," 的配置，它自动导入组件、可组合项、辅助函数和 ",[14,379,380],{},"Vue API",[11,382,383,384,391,392,399,400,407],{},"项目根目录下的  ",[163,385,388],{"href":386,"rel":387},"https://nuxt.com/docs/guide/directory-structure/components",[167],[14,389,390],{},"components/",", ",[163,393,396],{"href":394,"rel":395},"https://nuxt.com/docs/guide/directory-structure/composables",[167],[14,397,398],{},"composables/"," , ",[163,401,404],{"href":402,"rel":403},"https://nuxt.com/docs/guide/directory-structure/utils",[167],[14,405,406],{},"utils/"," 都可以直接使用，无需手动导入。",[11,409,410,411,414,415],{},"如果你想手动导入此类API，可以使用",[14,412,413],{},"#imports","， 如 ",[14,416,417],{},"import { ref, computed } from '#imports'",[11,419,420,421,424],{},"如果使用了第三方的包，也想支持自动导入，可以在 ",[14,422,423],{},"nuxt.config.ts"," 中配置。",[61,426,428],{"className":273,"code":427,"language":275,"meta":66,"style":66},"export default defineNuxtConfig({\n  imports: {\n    presets: [\n      {\n        from: 'vue-i18n',\n        imports: ['useI18n']\n      }\n    ]\n  }\n})\n\n",[14,429,430,444,449,454,459,470,482,488,494,500],{"__ignoreMap":66},[70,431,432,435,438,441],{"class":72,"line":73},[70,433,434],{"class":282},"export",[70,436,437],{"class":282}," default",[70,439,440],{"class":76}," defineNuxtConfig",[70,442,443],{"class":180},"({\n",[70,445,446],{"class":72,"line":191},[70,447,448],{"class":180},"  imports: {\n",[70,450,451],{"class":72,"line":202},[70,452,453],{"class":180},"    presets: [\n",[70,455,456],{"class":72,"line":213},[70,457,458],{"class":180},"      {\n",[70,460,461,464,467],{"class":72,"line":223},[70,462,463],{"class":180},"        from: ",[70,465,466],{"class":80},"'vue-i18n'",[70,468,469],{"class":180},",\n",[70,471,473,476,479],{"class":72,"line":472},6,[70,474,475],{"class":180},"        imports: [",[70,477,478],{"class":80},"'useI18n'",[70,480,481],{"class":180},"]\n",[70,483,485],{"class":72,"line":484},7,[70,486,487],{"class":180},"      }\n",[70,489,491],{"class":72,"line":490},8,[70,492,493],{"class":180},"    ]\n",[70,495,497],{"class":72,"line":496},9,[70,498,499],{"class":180},"  }\n",[70,501,503],{"class":72,"line":502},10,[70,504,505],{"class":180},"})\n",[11,507,508,509,511,512,515,516,519,520,523,524,526],{},"同时，",[14,510,423],{},"还可以配置 ",[14,513,514],{},"sourcemap"," ，全局css引入：",[14,517,518],{},"css"," ，",[14,521,522],{},"tailwindcss"," ，以及 ",[14,525,43],{}," 等等。",[11,528,529,530,37],{},"Nuxt的文档和Vue的类似，都非常全面，读过一遍后会对这个框架有比较清晰的了解，所以起步阶段还是",[26,531,532],{},"建议先读文档",[50,534,536],{"id":535},"nuxtcontent","NuxtContent",[11,538,539,540,543,544,547,548,551],{},"项目搭建好了，它默认加载了两个页面，也就是两个md文件，是",[14,541,542],{},"/content","目录下的 ",[14,545,546],{},"index.md"," 和 ",[14,549,550],{},"about.md"," 。",[11,553,554],{},"这不是重点，因为我也不会在这个项目目录下管理我的文章。",[11,556,557,561],{},[163,558,536],{"href":559,"rel":560},"https://content.nuxt.com/get-started/configuration#sources",[167]," 支持配置多种文件的来源，看一下官方的配置",[61,563,565],{"className":273,"code":564,"language":275,"meta":66,"style":66},"import { resolve } from \"node:path\";\n\nexport default defineNuxtConfig({\n  content: {\n    sources: {\n      // overwrite default source AKA `content` directory\n      content: {\n        driver: 'fs',\n        prefix: '/docs', // All contents inside this source will be prefixed with `/docs`\n        base: resolve(__dirname, 'content')\n      },\n      // Additional sources\n      fa: {\n        prefix: '/fa', // All contents inside this source will be prefixed with `/fa`\n        driver: 'fs',\n        // ...driverOptions\n        base: resolve(__dirname, 'content-fa') // Path for source directory\n      },\n      github: {\n        prefix: '/blog', // Prefix for routes used to query contents\n        driver: 'github', // Driver used to fetch contents (view unstorage documentation)\n        repo: \"\u003Cowner>/\u003Crepo>\",\n        branch: \"main\",\n        dir: \"content\", // Directory where contents are located. It could be a subdirectory of the repository.\n        // Imagine you have a blog inside your content folder. You can set this option to `content/blog` with the prefix option to `/blog` to avoid conflicts with local files.\n      },\n    }\n  }\n})\n\n",[14,566,567,584,590,600,605,610,616,621,631,645,662,668,674,680,693,702,708,726,731,737,750,763,774,785,799,805,810,816,821],{"__ignoreMap":66},[70,568,569,572,575,578,581],{"class":72,"line":73},[70,570,571],{"class":282},"import",[70,573,574],{"class":180}," { resolve } ",[70,576,577],{"class":282},"from",[70,579,580],{"class":80}," \"node:path\"",[70,582,583],{"class":180},";\n",[70,585,586],{"class":72,"line":191},[70,587,589],{"emptyLinePlaceholder":588},true,"\n",[70,591,592,594,596,598],{"class":72,"line":202},[70,593,434],{"class":282},[70,595,437],{"class":282},[70,597,440],{"class":76},[70,599,443],{"class":180},[70,601,602],{"class":72,"line":213},[70,603,604],{"class":180},"  content: {\n",[70,606,607],{"class":72,"line":223},[70,608,609],{"class":180},"    sources: {\n",[70,611,612],{"class":72,"line":472},[70,613,615],{"class":614},"sAwPA","      // overwrite default source AKA `content` directory\n",[70,617,618],{"class":72,"line":484},[70,619,620],{"class":180},"      content: {\n",[70,622,623,626,629],{"class":72,"line":490},[70,624,625],{"class":180},"        driver: ",[70,627,628],{"class":80},"'fs'",[70,630,469],{"class":180},[70,632,633,636,639,642],{"class":72,"line":496},[70,634,635],{"class":180},"        prefix: ",[70,637,638],{"class":80},"'/docs'",[70,640,641],{"class":180},", ",[70,643,644],{"class":614},"// All contents inside this source will be prefixed with `/docs`\n",[70,646,647,650,653,656,659],{"class":72,"line":502},[70,648,649],{"class":180},"        base: ",[70,651,652],{"class":76},"resolve",[70,654,655],{"class":180},"(__dirname, ",[70,657,658],{"class":80},"'content'",[70,660,661],{"class":180},")\n",[70,663,665],{"class":72,"line":664},11,[70,666,667],{"class":180},"      },\n",[70,669,671],{"class":72,"line":670},12,[70,672,673],{"class":614},"      // Additional sources\n",[70,675,677],{"class":72,"line":676},13,[70,678,679],{"class":180},"      fa: {\n",[70,681,683,685,688,690],{"class":72,"line":682},14,[70,684,635],{"class":180},[70,686,687],{"class":80},"'/fa'",[70,689,641],{"class":180},[70,691,692],{"class":614},"// All contents inside this source will be prefixed with `/fa`\n",[70,694,696,698,700],{"class":72,"line":695},15,[70,697,625],{"class":180},[70,699,628],{"class":80},[70,701,469],{"class":180},[70,703,705],{"class":72,"line":704},16,[70,706,707],{"class":614},"        // ...driverOptions\n",[70,709,711,713,715,717,720,723],{"class":72,"line":710},17,[70,712,649],{"class":180},[70,714,652],{"class":76},[70,716,655],{"class":180},[70,718,719],{"class":80},"'content-fa'",[70,721,722],{"class":180},") ",[70,724,725],{"class":614},"// Path for source directory\n",[70,727,729],{"class":72,"line":728},18,[70,730,667],{"class":180},[70,732,734],{"class":72,"line":733},19,[70,735,736],{"class":180},"      github: {\n",[70,738,740,742,745,747],{"class":72,"line":739},20,[70,741,635],{"class":180},[70,743,744],{"class":80},"'/blog'",[70,746,641],{"class":180},[70,748,749],{"class":614},"// Prefix for routes used to query contents\n",[70,751,753,755,758,760],{"class":72,"line":752},21,[70,754,625],{"class":180},[70,756,757],{"class":80},"'github'",[70,759,641],{"class":180},[70,761,762],{"class":614},"// Driver used to fetch contents (view unstorage documentation)\n",[70,764,766,769,772],{"class":72,"line":765},22,[70,767,768],{"class":180},"        repo: ",[70,770,771],{"class":80},"\"\u003Cowner>/\u003Crepo>\"",[70,773,469],{"class":180},[70,775,777,780,783],{"class":72,"line":776},23,[70,778,779],{"class":180},"        branch: ",[70,781,782],{"class":80},"\"main\"",[70,784,469],{"class":180},[70,786,788,791,794,796],{"class":72,"line":787},24,[70,789,790],{"class":180},"        dir: ",[70,792,793],{"class":80},"\"content\"",[70,795,641],{"class":180},[70,797,798],{"class":614},"// Directory where contents are located. It could be a subdirectory of the repository.\n",[70,800,802],{"class":72,"line":801},25,[70,803,804],{"class":614},"        // Imagine you have a blog inside your content folder. You can set this option to `content/blog` with the prefix option to `/blog` to avoid conflicts with local files.\n",[70,806,808],{"class":72,"line":807},26,[70,809,667],{"class":180},[70,811,813],{"class":72,"line":812},27,[70,814,815],{"class":180},"    }\n",[70,817,819],{"class":72,"line":818},28,[70,820,499],{"class":180},[70,822,824],{"class":72,"line":823},29,[70,825,505],{"class":180},[11,827,828,829,832,833,832,836,839],{},"可以看到不仅能用",[14,830,831],{},"content","目录，还能用",[14,834,835],{},"content-fa",[14,837,838],{},"github","拉取。",[11,841,842,843,846,847,850],{},"而目录下",[14,844,845],{},"base","配置传入的是一个文件夹路径，所以我们这里",[26,848,849],{},"直接写上自己经常用来写文章的一个目录绝对路径","就可以了。",[11,852,853,854,857],{},"在目录下所有文章的",[26,855,856],{},"改动会被监听","，也就是在写文章时有什么改动会实时更新在本地服务的页面上，非常方便。",[11,859,860,861,864],{},"我建议在一个比较高级别的目录下分出几个单独的文件夹，往博客上发的全都放在一个比如",[14,862,863],{},"blog","的目录下，其他可能不是文章的，就还是该咋写咋写。",[11,866,867,869,870,872,873,547,876,879,880,883,884,887,888,890,891,37],{},[14,868,863],{},"下的文章，有时候也不一定都能当天写完，",[14,871,43],{},"支持使用 ",[14,874,875],{},".",[14,877,878],{},"-"," 作为文件的前缀时",[26,881,882],{},"忽略此文章","，不会被处理。",[14,885,886],{},"Obsidian","不支持使用",[14,889,875],{},"作为前缀，所以我用的",[14,892,878],{},[11,894,895,896,898,899,901,902,905,906,909],{},"在",[14,897,863],{},"下的文章可以随意划分文件夹，",[14,900,863],{},"上不受影响，因为博客上的文章其实还是要根据",[14,903,904],{},"tag","或",[14,907,908],{},"category","用代码逻辑划分，和本地的文件夹没什么关系。",[11,911,912],{},"所以此时我的配置就是这样的：",[61,914,916],{"className":273,"code":915,"language":275,"meta":66,"style":66},"content: {\n    sources: {\n        obsidian: {\n        prefix: '/obsidian', // All contents inside this source will be prefixed with `/fa`\n        driver: 'fs',\n        // ...driverOptions\n        base: `/xxx/xxx/notion/blog` // Path for source directory\n      },\n    }\n}\n",[14,917,918,925,932,939,954,965,969,982,986,990],{"__ignoreMap":66},[70,919,920,922],{"class":72,"line":73},[70,921,831],{"class":76},[70,923,924],{"class":180},": {\n",[70,926,927,930],{"class":72,"line":191},[70,928,929],{"class":76},"    sources",[70,931,924],{"class":180},[70,933,934,937],{"class":72,"line":202},[70,935,936],{"class":76},"        obsidian",[70,938,924],{"class":180},[70,940,941,944,947,950,952],{"class":72,"line":213},[70,942,943],{"class":76},"        prefix",[70,945,946],{"class":180},": ",[70,948,949],{"class":80},"'/obsidian'",[70,951,641],{"class":180},[70,953,692],{"class":614},[70,955,956,959,961,963],{"class":72,"line":223},[70,957,958],{"class":76},"        driver",[70,960,946],{"class":180},[70,962,628],{"class":80},[70,964,469],{"class":180},[70,966,967],{"class":72,"line":472},[70,968,707],{"class":614},[70,970,971,974,976,979],{"class":72,"line":484},[70,972,973],{"class":76},"        base",[70,975,946],{"class":180},[70,977,978],{"class":80},"`/xxx/xxx/notion/blog`",[70,980,981],{"class":614}," // Path for source directory\n",[70,983,984],{"class":72,"line":490},[70,985,667],{"class":180},[70,987,988],{"class":72,"line":496},[70,989,815],{"class":180},[70,991,992],{"class":72,"line":502},[70,993,994],{"class":180},"}\n",[11,996,997],{},"要想正常的使用本地文件，还需要做一些改动。",[11,999,1000,1001,1004],{},"之前在Obsidian写东西，文件都是中文名，而content在处理中文名时会自动忽略，",[14,1002,1003],{},"我搜了下各种issue"," 其他语言也存在这种问题。好在content处理前后分别给了一个钩子函数，不然这博客直接夭折了",[11,1006,1007,1008,1011],{},"在这里写一个处理函数 ",[14,1009,1010],{},"/server/plugins/content.ts"," （没有的目录要手动新建）",[61,1013,1015],{"className":273,"code":1014,"language":275,"meta":66,"style":66},"// @ts-ignore\nexport default defineNitroPlugin((nitroApp) => {\n  nitroApp.hooks.hook('content:file:beforeParse', (file: { body: string }) => {\n    // 匹配markdown文件内的元信息\n    const match = file.body.match(/---\\n([\\s\\S]+?)\\n---\\n([\\s\\S]*)/);\n    if (match) {\n      let frontMatter = match[1];\n      const mainContent = match[2];\n      // 如果不包含_path字段, 则使用title字段和一个前缀来生成_path\n      if (!frontMatter.includes('_path:')) {\n        // 提取 title 字段的值\n        const titleMatch = frontMatter.match(/title:\\s*(.+)/);\n        if (titleMatch && titleMatch.length > 1) {\n            const titleValue = titleMatch[1].trim();\n            const pathValue = `/post/${titleValue}`;\n            // 将 _path 插入到 front-matter 中\n            frontMatter = `_path: ${pathValue}\\n` + frontMatter;\n        } else {\n          return;\n        }\n    }\n    // 重新组合文件内容\n    const newContent = `---\\n${frontMatter}\\n---\\n${mainContent}`;\n    file.body = newContent;\n    }\n    // 如果页面内没有 _path 属性, 则自动添加为 /blog/ + 文件名\n    \n  });\n})\n",[14,1016,1017,1022,1046,1087,1092,1147,1155,1174,1191,1196,1221,1226,1262,1288,1312,1332,1337,1364,1374,1381,1386,1390,1395,1432,1442,1446,1451,1456,1461],{"__ignoreMap":66},[70,1018,1019],{"class":72,"line":73},[70,1020,1021],{"class":614},"// @ts-ignore\n",[70,1023,1024,1026,1028,1031,1034,1038,1040,1043],{"class":72,"line":191},[70,1025,434],{"class":282},[70,1027,437],{"class":282},[70,1029,1030],{"class":76}," defineNitroPlugin",[70,1032,1033],{"class":180},"((",[70,1035,1037],{"class":1036},"sqxcx","nitroApp",[70,1039,722],{"class":180},[70,1041,1042],{"class":282},"=>",[70,1044,1045],{"class":180}," {\n",[70,1047,1048,1051,1054,1057,1060,1063,1066,1069,1072,1075,1077,1080,1083,1085],{"class":72,"line":202},[70,1049,1050],{"class":180},"  nitroApp.hooks.",[70,1052,1053],{"class":76},"hook",[70,1055,1056],{"class":180},"(",[70,1058,1059],{"class":80},"'content:file:beforeParse'",[70,1061,1062],{"class":180},", (",[70,1064,1065],{"class":1036},"file",[70,1067,1068],{"class":282},":",[70,1070,1071],{"class":180}," { ",[70,1073,1074],{"class":1036},"body",[70,1076,1068],{"class":282},[70,1078,1079],{"class":90}," string",[70,1081,1082],{"class":180}," }) ",[70,1084,1042],{"class":282},[70,1086,1045],{"class":180},[70,1088,1089],{"class":72,"line":213},[70,1090,1091],{"class":614},"    // 匹配markdown文件内的元信息\n",[70,1093,1094,1097,1100,1102,1105,1108,1110,1113,1116,1118,1121,1124,1127,1129,1132,1134,1136,1138,1141,1144],{"class":72,"line":223},[70,1095,1096],{"class":282},"    const",[70,1098,1099],{"class":90}," match",[70,1101,304],{"class":282},[70,1103,1104],{"class":180}," file.body.",[70,1106,1107],{"class":76},"match",[70,1109,1056],{"class":180},[70,1111,1112],{"class":80},"/---",[70,1114,1115],{"class":90},"\\n",[70,1117,1056],{"class":80},[70,1119,1120],{"class":90},"[\\s\\S]",[70,1122,1123],{"class":282},"+?",[70,1125,1126],{"class":80},")",[70,1128,1115],{"class":90},[70,1130,1131],{"class":80},"---",[70,1133,1115],{"class":90},[70,1135,1056],{"class":80},[70,1137,1120],{"class":90},[70,1139,1140],{"class":282},"*",[70,1142,1143],{"class":80},")/",[70,1145,1146],{"class":180},");\n",[70,1148,1149,1152],{"class":72,"line":472},[70,1150,1151],{"class":282},"    if",[70,1153,1154],{"class":180}," (match) {\n",[70,1156,1157,1160,1163,1165,1168,1171],{"class":72,"line":484},[70,1158,1159],{"class":282},"      let",[70,1161,1162],{"class":180}," frontMatter ",[70,1164,288],{"class":282},[70,1166,1167],{"class":180}," match[",[70,1169,1170],{"class":90},"1",[70,1172,1173],{"class":180},"];\n",[70,1175,1176,1179,1182,1184,1186,1189],{"class":72,"line":490},[70,1177,1178],{"class":282},"      const",[70,1180,1181],{"class":90}," mainContent",[70,1183,304],{"class":282},[70,1185,1167],{"class":180},[70,1187,1188],{"class":90},"2",[70,1190,1173],{"class":180},[70,1192,1193],{"class":72,"line":496},[70,1194,1195],{"class":614},"      // 如果不包含_path字段, 则使用title字段和一个前缀来生成_path\n",[70,1197,1198,1201,1204,1207,1210,1213,1215,1218],{"class":72,"line":502},[70,1199,1200],{"class":282},"      if",[70,1202,1203],{"class":180}," (",[70,1205,1206],{"class":282},"!",[70,1208,1209],{"class":180},"frontMatter.",[70,1211,1212],{"class":76},"includes",[70,1214,1056],{"class":180},[70,1216,1217],{"class":80},"'_path:'",[70,1219,1220],{"class":180},")) {\n",[70,1222,1223],{"class":72,"line":664},[70,1224,1225],{"class":614},"        // 提取 title 字段的值\n",[70,1227,1228,1231,1234,1236,1239,1241,1243,1246,1249,1251,1253,1255,1258,1260],{"class":72,"line":670},[70,1229,1230],{"class":282},"        const",[70,1232,1233],{"class":90}," titleMatch",[70,1235,304],{"class":282},[70,1237,1238],{"class":180}," frontMatter.",[70,1240,1107],{"class":76},[70,1242,1056],{"class":180},[70,1244,1245],{"class":80},"/title:",[70,1247,1248],{"class":90},"\\s",[70,1250,1140],{"class":282},[70,1252,1056],{"class":80},[70,1254,875],{"class":90},[70,1256,1257],{"class":282},"+",[70,1259,1143],{"class":80},[70,1261,1146],{"class":180},[70,1263,1264,1267,1270,1273,1276,1279,1282,1285],{"class":72,"line":676},[70,1265,1266],{"class":282},"        if",[70,1268,1269],{"class":180}," (titleMatch ",[70,1271,1272],{"class":282},"&&",[70,1274,1275],{"class":180}," titleMatch.",[70,1277,1278],{"class":90},"length",[70,1280,1281],{"class":282}," >",[70,1283,1284],{"class":90}," 1",[70,1286,1287],{"class":180},") {\n",[70,1289,1290,1293,1296,1298,1301,1303,1306,1309],{"class":72,"line":682},[70,1291,1292],{"class":282},"            const",[70,1294,1295],{"class":90}," titleValue",[70,1297,304],{"class":282},[70,1299,1300],{"class":180}," titleMatch[",[70,1302,1170],{"class":90},[70,1304,1305],{"class":180},"].",[70,1307,1308],{"class":76},"trim",[70,1310,1311],{"class":180},"();\n",[70,1313,1314,1316,1319,1321,1324,1327,1330],{"class":72,"line":695},[70,1315,1292],{"class":282},[70,1317,1318],{"class":90}," pathValue",[70,1320,304],{"class":282},[70,1322,1323],{"class":80}," `/post/${",[70,1325,1326],{"class":180},"titleValue",[70,1328,1329],{"class":80},"}`",[70,1331,583],{"class":180},[70,1333,1334],{"class":72,"line":704},[70,1335,1336],{"class":614},"            // 将 _path 插入到 front-matter 中\n",[70,1338,1339,1342,1344,1347,1350,1353,1355,1358,1361],{"class":72,"line":710},[70,1340,1341],{"class":180},"            frontMatter ",[70,1343,288],{"class":282},[70,1345,1346],{"class":80}," `_path: ${",[70,1348,1349],{"class":180},"pathValue",[70,1351,1352],{"class":80},"}",[70,1354,1115],{"class":90},[70,1356,1357],{"class":80},"`",[70,1359,1360],{"class":282}," +",[70,1362,1363],{"class":180}," frontMatter;\n",[70,1365,1366,1369,1372],{"class":72,"line":728},[70,1367,1368],{"class":180},"        } ",[70,1370,1371],{"class":282},"else",[70,1373,1045],{"class":180},[70,1375,1376,1379],{"class":72,"line":733},[70,1377,1378],{"class":282},"          return",[70,1380,583],{"class":180},[70,1382,1383],{"class":72,"line":739},[70,1384,1385],{"class":180},"        }\n",[70,1387,1388],{"class":72,"line":752},[70,1389,815],{"class":180},[70,1391,1392],{"class":72,"line":765},[70,1393,1394],{"class":614},"    // 重新组合文件内容\n",[70,1396,1397,1399,1402,1404,1407,1409,1412,1415,1417,1419,1421,1423,1425,1428,1430],{"class":72,"line":776},[70,1398,1096],{"class":282},[70,1400,1401],{"class":90}," newContent",[70,1403,304],{"class":282},[70,1405,1406],{"class":80}," `---",[70,1408,1115],{"class":90},[70,1410,1411],{"class":80},"${",[70,1413,1414],{"class":180},"frontMatter",[70,1416,1352],{"class":80},[70,1418,1115],{"class":90},[70,1420,1131],{"class":80},[70,1422,1115],{"class":90},[70,1424,1411],{"class":80},[70,1426,1427],{"class":180},"mainContent",[70,1429,1329],{"class":80},[70,1431,583],{"class":180},[70,1433,1434,1437,1439],{"class":72,"line":787},[70,1435,1436],{"class":180},"    file.body ",[70,1438,288],{"class":282},[70,1440,1441],{"class":180}," newContent;\n",[70,1443,1444],{"class":72,"line":801},[70,1445,815],{"class":180},[70,1447,1448],{"class":72,"line":807},[70,1449,1450],{"class":614},"    // 如果页面内没有 _path 属性, 则自动添加为 /blog/ + 文件名\n",[70,1452,1453],{"class":72,"line":812},[70,1454,1455],{"class":180},"    \n",[70,1457,1458],{"class":72,"line":818},[70,1459,1460],{"class":180},"  });\n",[70,1462,1463],{"class":72,"line":823},[70,1464,505],{"class":180},[11,1466,895,1467,1469,1470,1473,1474,1477],{},[14,1468,831],{},"拿到本地文件后，会编译一遍，然后放在 ",[14,1471,1472],{},".nuxt"," 缓存，其处理后的文件内容，带有一个 ",[14,1475,1476],{},"_path"," 属性，这个属性就是页面上对应的文章的地址。",[11,1479,1480,1481,1483],{},"前面说中文地址会丢掉，也是这个",[14,1482,1476],{},"出了问题，因为它默认是自己根据文件名生成的。",[11,1485,1486,1487,1489,1490,1492],{},"所以这里处理逻辑就是，处理原始内容前，给原始内容加上一个",[14,1488,1476],{},"。当原始内容里带了",[14,1491,1476],{},"，它就会优先用设置好的，不自动生成。",[11,1494,1495],{},[26,1496,1497,1498,1500],{},"所以你如果愿意手动给自己的每个md文件都手动加上一个",[14,1499,1476],{},"的话，也可以不用这个钩子。",[11,1502,1503,1504,1507,1508,551],{},"以前还要注意一个问题：",[14,1505,1506],{},"const pathValue = /post"," 这行代码，相当于写死了每篇文章的前缀是 ",[14,1509,1510],{},"/post",[11,1512,1513,1514,1516,1517,551],{},"所以说 ",[14,1515,236],{}," 下必须要有这样的结构 ",[14,1518,1519],{},"/pages/post/[...slug].vue",[11,1521,1522],{},[26,1523,1524],{},"如果你想更改前缀，请手动同步更新这两处",[11,1526,1527],{},"这样，我们无需在项目中增加文章目录，就实现了从本地直接拉取文章文件。",[11,1529,1530,1531,1533,1534,1537,1538,1541,1542,101,1545,101,1548,1551,1552,101,1555,1557],{},"用",[14,1532,886],{},"的小伙伴，可以用",[14,1535,1536],{},"Linter","这个插件格式化",[14,1539,1540],{},"YAML属性"," ，如",[14,1543,1544],{},"date",[14,1546,1547],{},"lastmod",[14,1549,1550],{},"title","，这三个是自动生成而且都是必用的。",[14,1553,1554],{},"tags",[14,1556,908],{}," 会用作分类和筛选。",[11,1559,1560],{},[114,1561],{"alt":66,"src":1562},"https://img.zzao.club/1-img-20241106181119.png",[11,1564,1565],{},"那文章有了，在哪里点进去呢？",[11,1567,1568,1570,1571,1574],{},[14,1569,831],{}," 提供了 ",[14,1572,1573],{},"queryContent"," 来查询内容， 你可以这样来查询：",[61,1576,1578],{"className":273,"code":1577,"language":275,"meta":66,"style":66},"const contentQuery = queryContent('post')\n// 总文章数 去除了被忽略的文章\nconst count = await queryContent('post').count()\n\nconst contentQuery2 = queryContent('post')\n// 按时间倒序以及分页后的数据\nconst pages await contentQuery2.sort({ date: -1 }).skip(skip).limit(pageSize.value).find()\n\n",[14,1579,1580,1599,1604,1630,1634,1651,1656],{"__ignoreMap":66},[70,1581,1582,1584,1587,1589,1592,1594,1597],{"class":72,"line":73},[70,1583,298],{"class":282},[70,1585,1586],{"class":90}," contentQuery",[70,1588,304],{"class":282},[70,1590,1591],{"class":76}," queryContent",[70,1593,1056],{"class":180},[70,1595,1596],{"class":80},"'post'",[70,1598,661],{"class":180},[70,1600,1601],{"class":72,"line":191},[70,1602,1603],{"class":614},"// 总文章数 去除了被忽略的文章\n",[70,1605,1606,1608,1611,1613,1616,1618,1620,1622,1625,1628],{"class":72,"line":202},[70,1607,298],{"class":282},[70,1609,1610],{"class":90}," count",[70,1612,304],{"class":282},[70,1614,1615],{"class":282}," await",[70,1617,1591],{"class":76},[70,1619,1056],{"class":180},[70,1621,1596],{"class":80},[70,1623,1624],{"class":180},").",[70,1626,1627],{"class":76},"count",[70,1629,310],{"class":180},[70,1631,1632],{"class":72,"line":213},[70,1633,589],{"emptyLinePlaceholder":588},[70,1635,1636,1638,1641,1643,1645,1647,1649],{"class":72,"line":223},[70,1637,298],{"class":282},[70,1639,1640],{"class":90}," contentQuery2",[70,1642,304],{"class":282},[70,1644,1591],{"class":76},[70,1646,1056],{"class":180},[70,1648,1596],{"class":80},[70,1650,661],{"class":180},[70,1652,1653],{"class":72,"line":472},[70,1654,1655],{"class":614},"// 按时间倒序以及分页后的数据\n",[70,1657,1658,1660,1663,1666,1668,1671,1673,1676,1679,1682,1685,1688,1691],{"class":72,"line":484},[70,1659,298],{"class":282},[70,1661,1662],{"class":90}," pages",[70,1664,1665],{"class":180}," await contentQuery2.sort({ date",[70,1667,1068],{"class":282},[70,1669,1670],{"class":180}," -",[70,1672,1170],{"class":90},[70,1674,1675],{"class":180}," }).",[70,1677,1678],{"class":76},"skip",[70,1680,1681],{"class":180},"(skip).",[70,1683,1684],{"class":76},"limit",[70,1686,1687],{"class":180},"(pageSize.value).",[70,1689,1690],{"class":76},"find",[70,1692,310],{"class":180},[11,1694,1695,1697,1698,1697,1700,1703,1704,1707],{},[14,1696,1678],{}," ",[14,1699,1684],{},[14,1701,1702],{},"sort"," 用这几个就能完成大部分操作，同时还支持 ",[14,1705,1706],{},"where"," 过滤内容，实现更多的分类查询功能。",[11,1709,1710,1711,1713,1714,1719],{},"关于",[14,1712,1573],{},"的",[163,1715,1718],{"href":1716,"rel":1717},"https://content.nuxt.com/composables/query-content",[167],"更多API","👈",[11,1721,1722],{},"到这里，基于本地文件有了，也没破坏本地的写作流程，各种分类、搜索功能有了，作为一个最简博客已经五脏俱全。",[11,1724,1725],{},"后续我会继续分享我自己的博客建设过程。",[11,1727,1728],{},"最后一步就是发布到服务器",[50,1730,1731],{"id":1731},"打包和部署",[11,1733,1734],{},"如果此时你没有初始化git，可以先初始化一下。",[11,1736,1737],{},"然后我要先说一个Nuxt和其他Node服务的不同点：",[11,1739,1740,1746,1747,551],{},[26,1741,1742,1743],{},"它打包后的文件内已经包含了",[14,1744,1745],{},"node_modules"," ，也就是说打包后的output文件就是它的完全体，不需要再 ",[14,1748,1749],{},"npm install",[11,1751,1752,1753,1755],{},"而博客的文章，是本地某个目录下写的，用的别的软件如",[14,1754,886],{}," 来管理。",[11,1757,1758],{},"这样就有三个问题：",[11,1760,1761,1762,1764,1765,1767],{},"1️⃣：打包要在本地打。因为博客文件在本地，其实也是在",[14,1763,1472],{},"内，但我们也不会把",[14,1766,1472],{}," 传到git上去。所以只能在本地打完了再传上去。",[11,1769,1770,1771,1774,1775,1777,1778,37],{},"2️⃣：要修改",[14,1772,1773],{},".gitignore","。这里要去掉两个 一个是 ",[14,1776,1745],{}," 一个是 ",[14,1779,1780],{},"dist",[11,1782,1783,1784,1786,1787,1789,1790,1713,1793,1796,1797,37],{},"我前期因为只去了 ",[14,1785,1745],{}," 落下了 ",[14,1788,1780],{},"，还让我一度怀疑是Nuxt的重大bug。因为我在服务器配了",[14,1791,1792],{},"gitea",[14,1794,1795],{},"action","，git push上去时，",[26,1798,1799],{},"需要把output整个都扔上去",[11,1801,1802,1803,1806,1807,1810],{},"而扔上去之后因为某些",[14,1804,1805],{},"node_module","插件少了dist目录，会导致有些导出引用不到，我在排查pm2日志时懒得cat整个errlog，直接用的",[14,1808,1809],{},"pm2 logs Blog --lines 30","，恰好没发现dist丢了的问题。",[11,1812,1813,1814,547,1817,1820,1821,1823],{},"然后还去github不停的搜问题，还真被我搜出一些 ",[14,1815,1816],{},"vite",[14,1818,1819],{},"nuxt"," 的早期bug，误以为现在还存在。于是就用重新",[14,1822,1749],{},"的方式暂时解决了问题。",[11,1825,1826,1827,1829],{},"直到我近几天才发现报错路径上有个dist，然后猛然想起 ",[14,1828,1773],{},"里忽略里dist。我滴个老天爷。",[11,1831,1832,1833,1835,1836,1839,1840,37],{},"3️⃣：要配个软件来写",[14,1834,47],{},"。如果已经有了还好，但是可能有人用的",[14,1837,1838],{},"vscode","之类的东西来写md，我不确定方不方便管理 ",[14,1841,1842],{},"front matter",[11,1844,1845,1846,1849],{},"有了以上几点，基本就是本地写文章，写完跑一下",[14,1847,1848],{},"build","，然后push上去就完成部署了",[11,1851,1852],{},"是不是还挺丝滑的？比项目内管理MD文件要舒服很多吧",[50,1854,1855],{"id":1855},"结语",[11,1857,1858,1859,1861],{},"以上就是用",[14,1860,16],{},"搭建一个可以集成本地文件的博客的最简起手流程了",[11,1863,1864],{},"其中我也是踩了不少的坑，这里分享给大家！",[11,1866,1867],{},"后续关于个人博客的建设也会一直更新，欢迎关注～～",[11,1869,1870],{},"有任何问题也欢迎私信交流",[11,1872,1873],{},"👏👏",[1875,1876,1877],"style",{},"html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#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 pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .shJU0, html code.shiki .shJU0{--shiki-default:#22863A}html pre.shiki code .sD7c4, html code.shiki .sD7c4{--shiki-default:#D73A49}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .sqxcx, html code.shiki .sqxcx{--shiki-default:#E36209}",{"title":66,"searchDepth":191,"depth":191,"links":1879},[1880,1881,1882,1883],{"id":52,"depth":191,"text":53},{"id":535,"depth":191,"text":536},{"id":1731,"depth":191,"text":1731},{"id":1855,"depth":191,"text":1855},"2024-11-06T00:00:00.000Z","Nuxt是一个用Vue来编写的，可用来创建类型安全、高性能和生产级全栈 Web 应用程序和网站的全栈框架。后端是 Nitro，一个可以被单独使用的Web服务端框架。","md","2025-08-19T00:00:00.000Z",{"describtion":1889},"NuxtContent 非常适合把已有的md数据源搭建为个人博客，本文介绍如何使用Obsidian来写文章，Nuxt3来自动部署个人博客","/post/nuxt/nuxt3-obsidian-build-your-blog","---\ntitle: 基于 Nuxt3 + Obsidian 搭建个人博客\ndate: 2024-11-06\nlastmod: 2025-08-19\ntags: [\"博客\", \"Nuxt\"]\nversions: [\"nuxt@3.14.0\", \"nitro@2.10.2\"]\ndescribtion: NuxtContent 非常适合把已有的md数据源搭建为个人博客，本文介绍如何使用Obsidian来写文章，Nuxt3来自动部署个人博客\n---\n`Nuxt`是一个用Vue来编写的，可用来创建类型安全、高性能和生产级全栈 Web 应用程序和网站的全栈框架。后端是 `Nitro`，一个可以被单独使用的Web服务端框架。\n\n作为一个全栈框架，不仅具备了比使用Vue开发SPA客户端**更好的开发体验**，还能享受服务端渲染带来的**SEO优化**，同时Node服务可以实现帮你实现**更多的可能性**。\n\n之所以基于Nuxt从零搭建，一是为了选全栈，二是发现了`nuxt/content`可以读取本地的`markdown`文件，三是选的模板达不到想要的效果，四是过程可以作为写文章的素材。\n\n## 初始化Nuxt项目\n\n\n使用`nuxt/content`初始化项目，因为这个插件是博客的核心插件，所以初始化时直接装上就可以\n\n```shell\nnpx nuxi@latest init content-app -t content\n```\n\n选择`npm`、`pnpm`、`yarn`作为包管理器后，会生成项目目录，下载依赖，我选了`npm`\n\n![](https://img.zzao.club/1-img-20241106171162.png)\n\n\n\n此时直接运行 `npm run dev` 就可以启动项目了\n\n启动后会有如下页面：\n\n![](https://img.zzao.club/1-img-20241106171191.png)\n`app.vue`作为页面的入口文件，里面只有一个简单对的 `NuxtPage` ，作用类似于 `Vue` 中的 `RouterView` ，实际上它就是 `RouterView` 的包装。\n\n同样的包装还有：`NuxtLink` 、`NuxtImg` 等等。 更多 [Nuxt Component](https://nuxt.com/docs/api/components/client-only)\n\n```vue\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003CNuxtPage />\n  \u003C/div>\n\u003C/template>\n```\n\n它用来显示 `pages/` 下的页面\n\n`pages/` 是Nuxt生成路由的一个约定目录，其内的文件会自动生成路由。\n\n如 `pages/a.vue` 会生成 `/a` 路由\n\n如果想接收url参数可以这样写： `/pages/a/[id].vue` 或 `/pages/a-[group]/[id].vue`\n\n在文件内使用这种方式来获取参数，总之 `[xxx]` 是它的路由规则\n\n```typescript\n\u003Cscript setup lang=\"ts\">\nconst route = useRoute()\nconsole.log(route.params.id, route.params.group)\n\u003C/script>\n\n```\n\n如果想匹配改路径下的所有路由，可以这样写：`/pages/a/[...slug].vue`，此时可以再去看一下\n`route.params.slug` 的值是什么。\n\n查看更多关于 [Nuxt Pages](https://nuxt.com/docs/guide/directory-structure/pages) 👈\n\n使用`Pages`不仅仅是简化了`Router`的配置，也是为了服务端渲染，更好的SEO。做博客的话，当然希望别人搜索某些关键字可以直接搜到自己的文章页面。而不是全部内容都在一个单页面内，由浏览器渲染。\n\n所以自带的两个页面是怎么渲染的就清楚了。\n\n你可以看到上面我并没有引入`useRoute`，但也可以在`vue`文件中直接使用，因为Nuxt提前做好了`自动import` 的配置，它自动导入组件、可组合项、辅助函数和 `Vue API`。\n\n项目根目录下的  [`components/`](https://nuxt.com/docs/guide/directory-structure/components), [`composables/`](https://nuxt.com/docs/guide/directory-structure/composables) , [`utils/`](https://nuxt.com/docs/guide/directory-structure/utils) 都可以直接使用，无需手动导入。\n\n如果你想手动导入此类API，可以使用`#imports`， 如 `import { ref, computed } from '#imports'`\n\n如果使用了第三方的包，也想支持自动导入，可以在 `nuxt.config.ts` 中配置。\n\n```typescript\nexport default defineNuxtConfig({\n  imports: {\n    presets: [\n      {\n        from: 'vue-i18n',\n        imports: ['useI18n']\n      }\n    ]\n  }\n})\n\n```\n\n同时，`nuxt.config.ts`还可以配置 `sourcemap` ，全局css引入：`css` ，`tailwindcss` ，以及 `nuxt/content` 等等。\n\nNuxt的文档和Vue的类似，都非常全面，读过一遍后会对这个框架有比较清晰的了解，所以起步阶段还是**建议先读文档**。\n## NuxtContent\n\n项目搭建好了，它默认加载了两个页面，也就是两个md文件，是`/content`目录下的 `index.md` 和 `about.md` 。\n\n这不是重点，因为我也不会在这个项目目录下管理我的文章。\n\n[NuxtContent](https://content.nuxt.com/get-started/configuration#sources) 支持配置多种文件的来源，看一下官方的配置\n\n```typescript\nimport { resolve } from \"node:path\";\n\nexport default defineNuxtConfig({\n  content: {\n    sources: {\n      // overwrite default source AKA `content` directory\n      content: {\n        driver: 'fs',\n        prefix: '/docs', // All contents inside this source will be prefixed with `/docs`\n        base: resolve(__dirname, 'content')\n      },\n      // Additional sources\n      fa: {\n        prefix: '/fa', // All contents inside this source will be prefixed with `/fa`\n        driver: 'fs',\n        // ...driverOptions\n        base: resolve(__dirname, 'content-fa') // Path for source directory\n      },\n      github: {\n        prefix: '/blog', // Prefix for routes used to query contents\n        driver: 'github', // Driver used to fetch contents (view unstorage documentation)\n        repo: \"\u003Cowner>/\u003Crepo>\",\n        branch: \"main\",\n        dir: \"content\", // Directory where contents are located. It could be a subdirectory of the repository.\n        // Imagine you have a blog inside your content folder. You can set this option to `content/blog` with the prefix option to `/blog` to avoid conflicts with local files.\n      },\n    }\n  }\n})\n\n```\n\n可以看到不仅能用`content`目录，还能用`content-fa`目录，还能用`github`拉取。\n\n而目录下`base`配置传入的是一个文件夹路径，所以我们这里**直接写上自己经常用来写文章的一个目录绝对路径**就可以了。\n\n在目录下所有文章的**改动会被监听**，也就是在写文章时有什么改动会实时更新在本地服务的页面上，非常方便。\n\n我建议在一个比较高级别的目录下分出几个单独的文件夹，往博客上发的全都放在一个比如`blog`的目录下，其他可能不是文章的，就还是该咋写咋写。\n\n`blog`下的文章，有时候也不一定都能当天写完，`nuxt/content`支持使用 `.` 和 `-` 作为文件的前缀时**忽略此文章**，不会被处理。`Obsidian`不支持使用`.`作为前缀，所以我用的`-`。\n\n在`blog`下的文章可以随意划分文件夹，`blog`上不受影响，因为博客上的文章其实还是要根据`tag`或`category`用代码逻辑划分，和本地的文件夹没什么关系。\n\n所以此时我的配置就是这样的：\n\n```typescript\ncontent: {\n\tsources: {\n\t\tobsidian: {\n        prefix: '/obsidian', // All contents inside this source will be prefixed with `/fa`\n        driver: 'fs',\n        // ...driverOptions\n        base: `/xxx/xxx/notion/blog` // Path for source directory\n      },\n\t}\n}\n```\n\n要想正常的使用本地文件，还需要做一些改动。\n\n之前在Obsidian写东西，文件都是中文名，而content在处理中文名时会自动忽略，`我搜了下各种issue` 其他语言也存在这种问题。好在content处理前后分别给了一个钩子函数，不然这博客直接夭折了\n\n在这里写一个处理函数 `/server/plugins/content.ts` （没有的目录要手动新建）\n\n```typescript\n// @ts-ignore\nexport default defineNitroPlugin((nitroApp) => {\n  nitroApp.hooks.hook('content:file:beforeParse', (file: { body: string }) => {\n    // 匹配markdown文件内的元信息\n    const match = file.body.match(/---\\n([\\s\\S]+?)\\n---\\n([\\s\\S]*)/);\n    if (match) {\n      let frontMatter = match[1];\n      const mainContent = match[2];\n      // 如果不包含_path字段, 则使用title字段和一个前缀来生成_path\n      if (!frontMatter.includes('_path:')) {\n        // 提取 title 字段的值\n        const titleMatch = frontMatter.match(/title:\\s*(.+)/);\n        if (titleMatch && titleMatch.length > 1) {\n            const titleValue = titleMatch[1].trim();\n            const pathValue = `/post/${titleValue}`;\n            // 将 _path 插入到 front-matter 中\n            frontMatter = `_path: ${pathValue}\\n` + frontMatter;\n        } else {\n          return;\n        }\n    }\n    // 重新组合文件内容\n    const newContent = `---\\n${frontMatter}\\n---\\n${mainContent}`;\n    file.body = newContent;\n    }\n    // 如果页面内没有 _path 属性, 则自动添加为 /blog/ + 文件名\n    \n  });\n})\n```\n\n在`content`拿到本地文件后，会编译一遍，然后放在 `.nuxt` 缓存，其处理后的文件内容，带有一个 `_path` 属性，这个属性就是页面上对应的文章的地址。\n\n前面说中文地址会丢掉，也是这个`_path`出了问题，因为它默认是自己根据文件名生成的。\n\n所以这里处理逻辑就是，处理原始内容前，给原始内容加上一个`_path`。当原始内容里带了`_path`，它就会优先用设置好的，不自动生成。\n\n**所以你如果愿意手动给自己的每个md文件都手动加上一个`_path`的话，也可以不用这个钩子。**\n\n以前还要注意一个问题：`const pathValue = /post` 这行代码，相当于写死了每篇文章的前缀是 `/post` 。\n\n所以说 `pages/` 下必须要有这样的结构 `/pages/post/[...slug].vue` 。\n\n**如果你想更改前缀，请手动同步更新这两处**\n\n这样，我们无需在项目中增加文章目录，就实现了从本地直接拉取文章文件。\n\n用`Obsidian`的小伙伴，可以用`Linter`这个插件格式化`YAML属性` ，如`date`、`lastmod`、`title`，这三个是自动生成而且都是必用的。`tags`、`category` 会用作分类和筛选。\n\n![](https://img.zzao.club/1-img-20241106181119.png)\n\n那文章有了，在哪里点进去呢？\n\n`content` 提供了 `queryContent` 来查询内容， 你可以这样来查询：\n\n```typescript\nconst contentQuery = queryContent('post')\n// 总文章数 去除了被忽略的文章\nconst count = await queryContent('post').count()\n\nconst contentQuery2 = queryContent('post')\n// 按时间倒序以及分页后的数据\nconst pages await contentQuery2.sort({ date: -1 }).skip(skip).limit(pageSize.value).find()\n\n```\n\n`skip` `limit` `sort` 用这几个就能完成大部分操作，同时还支持 `where` 过滤内容，实现更多的分类查询功能。\n\n关于`queryContent`的[更多API](https://content.nuxt.com/composables/query-content)👈\n\n到这里，基于本地文件有了，也没破坏本地的写作流程，各种分类、搜索功能有了，作为一个最简博客已经五脏俱全。\n\n后续我会继续分享我自己的博客建设过程。\n\n最后一步就是发布到服务器\n## 打包和部署\n\n如果此时你没有初始化git，可以先初始化一下。\n\n然后我要先说一个Nuxt和其他Node服务的不同点：\n\n**它打包后的文件内已经包含了`node_modules`** ，也就是说打包后的output文件就是它的完全体，不需要再 `npm install` 。\n\n而博客的文章，是本地某个目录下写的，用的别的软件如`Obsidian` 来管理。\n\n这样就有三个问题：\n\n1️⃣：打包要在本地打。因为博客文件在本地，其实也是在`.nuxt`内，但我们也不会把`.nuxt` 传到git上去。所以只能在本地打完了再传上去。\n\n2️⃣：要修改`.gitignore`。这里要去掉两个 一个是 `node_modules` 一个是 `dist`。\n\n我前期因为只去了 `node_modules` 落下了 `dist`，还让我一度怀疑是Nuxt的重大bug。因为我在服务器配了`gitea`的`action`，git push上去时，**需要把output整个都扔上去**。\n\n而扔上去之后因为某些`node_module`插件少了dist目录，会导致有些导出引用不到，我在排查pm2日志时懒得cat整个errlog，直接用的`pm2 logs Blog --lines 30`，恰好没发现dist丢了的问题。\n\n然后还去github不停的搜问题，还真被我搜出一些 `vite` 和 `nuxt` 的早期bug，误以为现在还存在。于是就用重新`npm install`的方式暂时解决了问题。\n\n直到我近几天才发现报错路径上有个dist，然后猛然想起 `.gitignore`里忽略里dist。我滴个老天爷。\n\n3️⃣：要配个软件来写`markdown`。如果已经有了还好，但是可能有人用的`vscode`之类的东西来写md，我不确定方不方便管理 `front matter`。\n\n有了以上几点，基本就是本地写文章，写完跑一下`build`，然后push上去就完成部署了\n\n是不是还挺丝滑的？比项目内管理MD文件要舒服很多吧\n\n## 结语\n\n以上就是用`Nuxt`搭建一个可以集成本地文件的博客的最简起手流程了\n\n其中我也是踩了不少的坑，这里分享给大家！\n\n后续关于个人博客的建设也会一直更新，欢迎关注～～\n\n有任何问题也欢迎私信交流\n\n👏👏\n\n",{"title":5,"description":1885},"post/nuxt/nuxt3-obsidian-build-your-blog",[1895,16],"博客",[1897,1898],"nuxt@3.14.0","nitro@2.10.2","UghKTcbth2LJrhuEy0aN2bq5YIn5KdfOLWGbnTL5-Mg",[1901,1905],{"title":1902,"path":1903,"stem":1904},"OpenClaw 安装入门（Windows）","/post/zzao/openclaw/openclaw-install-windows","post/zzao/openclaw/openclaw-install-windows",{"title":1906,"path":1907,"stem":1908},"假设你是AI，你的Skill应该是什么样的","/post/zzao/ai-skill-structure","post/zzao/ai-skill-structure",1779005086675]