[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"content-blog-upgrade":3,"$fga38i4590t8":23},{"id":4,"type":5,"slug":6,"title":7,"date":8,"category":9,"tags":10,"body_markdown":15,"permalink":16,"excerpt_src":16,"media_type":16,"media_title":16,"media_author":16,"media_url":16,"rating":16,"layout":16,"pv":17,"admin_only":17,"created_at":18,"updated_at":18,"deleted_at":16,"status":19,"html":20,"excerpt":21,"cover":22},132,"article","blog-upgrade","个人主页5.0升级全记录","2023-09-28 16:00","tech",[11,12,13,14],"博客","个人主页","升级","VitePress","\n我大概是从2007年起开始写博客的，过去这么多年中，也有更新非常频繁的时间，但总体来看最近几年的确是写得越来越少了。随着公众号、短视频的兴起，大家也不再关注博客，而我也一度不怎么更新，甚至想不起来要写一写文字。\n\n但是最近两年，我又突然发现，虽然写内容的平台到处都是，但只有博客是真正属于自己的，那种永远不会背叛你的安全感是其他平台无法比拟的。\n\n再回头看看自己的博客，发现已经很久没有更新过了，而前一版整理的[AMP版本](\u002Farticle\u002Fweb\u002F2017\u002Fmigrate-blog-to-amp.html)，现在回头看也算是掉坑里了。刚好看到勾股写的[博客站迁移至 VitePress 的备忘](https:\u002F\u002Fjiongks.name\u002Fblog\u002Fmigrate-my-blog-to-vitepress)，于是就打算自己也再整理一下，顺便做一个升级，拍个脑袋就叫“个人主页5.0”。\n\n以下为过程中的一些记录。\n\n\u003C!-- more -->\n\n![首页截图](\u002Fassets\u002Ftech\u002F2023\u002Fblog-upgrade\u002F01.png)\n\n## 换程序\n\n之前使用的是Hexo，是一个典型的“静态博客”，即所有内容都在构建时生成，托管时只有静态的HTML和对应的资源文件。这次打算换成[VitePress](https:\u002F\u002Fvitepress.dev\u002F)，也同样是“静态”的，因此核心概念和大致用法上比较类似。按照VitePress的文档，首先安装好依赖，然后将文章（`.md`文件）放到`docs`目录下即可。\n\n由于之前使用Hexo时就在路由中加了一级`\u002Farticle`，本次迁移过程中也需要建立一个对应的子目录，因此将文章目录由`source\u002F_posts`修改为`docs\u002Farticle`即可。\n\n与之对应的，构建脚本也需要做修改，以前用`hexo server`和`hexo generate`，现在换成`vitepress dev docs`和`vitepress build docs`。需要注意的是，VitePress是一个“极为先进”的项目，所以需要在`package.json`中加入`\"type\": \"module\"`，否则会报错。\n\n图片的引用路径同样需要修改，按照VitePress的文档，图片最好放在`docs`目录下的子目录，例如`docs\u002Fassets`，然后在文章中使用相对路径引用即可。但最终我还是将图片放到了`docs\u002Fpublic\u002Fassets`目录下，具体原因见后文。\n\nVitePress的主题是通过`docs\u002F.vitepress\u002Ftheme`目录下的文件来定义的，其中`index.js`是主要的入口文件。我们可以自定义一个新的主题来使用，但如果这样做的话默认主题的一些功能就会丢失，例如暗黑模式、顶部导航、404页面等，于是我选择了直接修改默认主题，即从代码仓库中将整个默认主题拷贝过来，然后针对个别地方进行修改。\n\n基本上经过简单的替换以后，文章详情页就可以正常访问了，但是还有一些页面需要处理，例如首页、文章列表页以及一些固定的页面（如[关于](\u002Fabout.html)）。\n\n## 导航&固定页面\n\n固定页面的实现是最简单的，只需要在`docs`目录下新建一个`.md`文件即可，例如`docs\u002Fabout.md`，就能通过`\u002Fabout.html`访问。\n\nVitePress中顶部导航是通过`docs\u002F.vitepress\u002Fconfig.js`中的`themeConfig.nav`配置来实现的：\n\n```js\nthemeConfig: {\n    logo: '\u002Flogo.png',\n    nav: [\n        { text: '首页', link: '\u002F' },\n        { text: '作品', link: '\u002Fworks.html'},\n        { text: '关于', link: '\u002Fabout.html' },\n    ],\n    ...\n}\n```\n\n## 首页\n\n首页和固定页面的概念是一样的，只需要在`docs`目录下新建一个`index.md`文件即可，就能通过`\u002F`访问。但是如果直接在这个文件中写文字内容的话，它的表现和普通文章是一样的，因此这里需要更多的定制化内容。\n\n到这里终于可以体会到VitePress的高明之处了：所有的`.md`文件都会被转换为`.vue`组件，然后通过Vue的工具链进行编译，因此我们可以在`.md`文件中使用Vue的语法，例如`\u003Cscript>`、`\u003Cstyle>`等，基本上和写一个`.vue`组件没有太大差异。\n\n也正是因为有这样的机制在，使得我们可以在首页中使用一些Vue生态中可用的开源项目，例如为了编写样式，我引入了TailwindCSS，引入方法和在Vue 3项目中几乎没有区别，值得注意的有几个点：\n\n第一，通常Vue项目中我们会在`main.js`或者`app.js`中引入一个公共CSS文件，而VitePress中需要在`docs\u002F.vitepress\u002Ftheme\u002Findex.js`中引入（例如`tailwind.css`），其内容如下：\n\n```css\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n```\n\n第二，`tailwind.config.js`中配置的`content`选项需要修改，因为VitePress中的`.md`文件也需要处理，因此需要在`content`中加入`.md`后续。除此之外，因为`.vitepress`目录以`.`开头，默认会被忽略，需要单独写出来。\n\n为了适配VitePress的暗黑模式，这里可以顺便设置`darkMode: class`，这样就可以使用TailwindCSS中的`dark:`前缀为暗黑模式匹配样式。\n\n```js\nexport default {\n    content: [\n        '.\u002Fdocs\u002F**\u002F*.{vue,ts,js,md}',\n        '.\u002Fdocs\u002F.vitepress\u002F**\u002F*.{vue,ts,js,md}',\n    ],\n    darkMode: 'class',\n}\n```\n\n具体到页面内容，就比较无聊了，使用Grid布局放了几个大块块，然后适当做了一点响应式布局的调整就完成了。\n\n## 文章分类页\n\n![文章分类页](\u002Fassets\u002Ftech\u002F2023\u002Fblog-upgrade\u002F02.png)\n\n有了固定页的经验，按道理分类页也很简单，只需要在`docs`目录下新建对应分类的`.md`文件即可，例如`docs\u002Farticle\u002Fweb.md`，就能通过`\u002Farticle\u002Fweb.html`访问。但这样做有两个问题：\n\n1. 分类是写死的，一旦有新增或者变化就需要手工调整\n2. 无法处理分页的情况\n\n此时就可以使用VitePress的“动态路由”功能，具体而言，是这么做：\n\n1. 建立一个带有占位符的`.md`文件，例如`docs\u002Farticle\u002F[category]-[page].md`，其中`[category]`和`[page]`都是占位符\n2. 建立一个同名的`.paths.js`文件，例如`docs\u002Farticle\u002F[category]-[page].paths.js`，它的作用是输出`[category]`和`[page]`占位符的所有可能值，例如`web-1` `web-2` `tech-1` `tech-2`等等\n\n这样，就可以通过`\u002Farticle\u002Fweb-1.html` `article\u002Fweb-2.html`等访问到对应的分类页了。为了额外提供一个“所有”分类，在处理`[category]`的值时加入一个`all`作为分类，这样就可以通过`\u002Farticle\u002Fall-1.html`访问到所有文章的第一页了。\n\n具体的写法大致如下：\n\n```javascript\n\u002F\u002F [category]-[page].paths.js\n\n\u002F\u002F Recursively get all files in a directory\nfunction getFiles(dir: string): MdFile[] {\n  const files = readdirSync(dir);\n  let fileList: MdFile[] = [];\n\n  files.forEach((file) => {\n    const filePath = join(dir, file);\n    const stats = statSync(filePath);\n\n    if (stats.isDirectory()) {\n      fileList = fileList.concat(getFiles(filePath));\n    } else if (filePath.endsWith('.md') && !filePath.endsWith('\u002Findex.md') && !\u002F\\.\\\u002Fdocs\\\u002F[^\u002F]+\\.md\u002F.test(filePath)) {\n      const fileContent = readFileSync(filePath, 'utf8');\n      const { data } = matter(fileContent);\n      const category = data.category || (Array.isArray(data.categories) && data.categories[0]);\n      fileList.push({ filePath, category });\n    }\n  });\n\n  return fileList;\n}\n\n\u002F\u002F Get all markdown files\nconst data = getFiles('.\u002Fdocs');\n\n\u002F\u002F Group files by category\nconst categories: { [key: string]: MdFile[] } = {};\ndata.forEach((file) => {\n  const category = file.category || 'all';\n  if (!categories[category]) {\n    categories[category] = [];\n  }\n  categories[category].push(file);\n\n  if (category !== 'all') {\n    if (!categories.all) {\n      categories.all = [];\n    }\n    categories.all.push(file);\n  }\n});\n\n\u002F\u002F Calculate page count for each category\nconst pageParams: PageParams[] = [];\nObject.entries(categories).forEach(([category, files]) => {\n  const pageCount = Math.ceil(files.length \u002F PAGE_SIZE);\n  for (let i = 1; i \u003C= pageCount; i++) {\n    pageParams.push({ params: { category, page: i } });\n  }\n});\n\nexport default {\n  paths() {\n    return pageParams;\n  },\n};\n```\n\n> 写本文的时候在想为什么这里没有使用createContentLoader来写，暂时没有想清楚，可能尝试过碰到了问题，记不太清了，回头再试一下。\n\n按照官方文档，在`[category]-[page].md`中，可以使用`createContentLoader`来获取文章数据，这一段逻辑被封装到一个单独的文件`posts.data.js`中：\n\n```javascript\nexport default createContentLoader('article\u002F**\u002F*.md', {\n    excerpt: '\u003C!-- more -->',\n    includeSrc: true,\n    transform(raw): Post[] {\n        return raw\n            .filter(({ url }) => url && !\u002F\\[.+\\]\u002F.test(url))\n            .map(({ src, url, frontmatter, excerpt }) => ({\n                title: frontmatter.title,\n                url,\n                date: formatDate(frontmatter.date),\n                category: frontmatter.category || frontmatter.categories?.[0] || 'all',\n                excerpt,\n                cover: getCover(src),\n            }))\n            .sort((a, b) => b.date.time - a.date.time)\n    }\n});\n```\n\n其中`getCover()`是一个从原文中解析封面图片的方法。\n\n这里有一个困扰了很久的点，就是图片路径的问题。按照官方文档，推荐将图片放在`docs\u002Fassets`中，在`.md`文件中通过相对路径进行引用，如果这样做的话，这个`getCover()`获取到的将是构建前的图片原始地址，而构建之后，图片地址会发生变化，使得封面无法显示。因此最终还是将图片放在了`docs\u002Fpublic\u002Fassets`中。\n\n渲染的时候根据根据`params`中的`category`和`page`来筛选文章，最后将结果渲染出来即可：\n\n```javascript\nimport { data as posts } from '..\u002F.vitepress\u002Ftheme\u002Fposts.data'\n\nconst { params } = useData()\n\nconst PAGE_SIZE = 12\nconst PAGE = +params.value.page\nconst CATEGORY = params.value.category\n\nconst categoryPosts = computed(() => {\n    return posts.filter(post => CATEGORY === 'all' || post.category === CATEGORY);\n});\n\nconst currentPosts = computed(() => {\n    const start = (PAGE - 1) * PAGE_SIZE\n    const end = start + PAGE_SIZE\n    return categoryPosts.value.slice(start, end)\n});\n```\n\n在界面上还要放一些分类选择、页面选择的组件，比较常规，不过多记录。\n\n## 影集页面\n\n![影集详情页](\u002Fassets\u002Ftech\u002F2023\u002Fblog-upgrade\u002F03.png)\n\n影集模块中的详情页主要用来展示照片，这里不想使用文章中把图片顺序铺排下来的方式，而是希望有一个自定义的布局，于是扩展了一个单独的`GalaryLayout`来展示。\n\n首先编写好布局组件，就是非常常规的Vue组件，使用`useData()`方法获取页面对应的数据，然后进行展示即可。为了方便，在`docs\u002F.vitepress\u002Fconfig.js`中的`transformPageData()`hook中加了一个解析页面图片的功能：\n\n```javascript\ntransformPageData(pageData, ctx) {\n    if (pageData.frontmatter.category === 'galary') {\n        \u002F\u002F @ts-ignore\n        pageData.images = getImages(readFileSync(join(process.cwd(), 'docs', pageData.relativePath), 'utf8'));\n    }\n\n    return pageData;\n},\n```\n\n> 这个页面还有待进一步优化，图片的查看方式、整体视觉风格都还不够好。\n\n## 敏感信息擦除\n\n因为有一些文章中有部分文字不适宜对外发布，但作为个人的文字记录，又不想删掉，于是之前就做了一个敏感信息的标记（`[*敏感文字*]`），在渲染的时候会擦除标记中的文字。这次迁移的时候发现VitePress不容易做这样的处理，因为它没有给出在渲染前进行原文修改的hook，只能修改渲染后的HTML代码。\n\n于是修改后的代码如下：\n\n```javascript\nexport function eraseSensitiveText(text?: string): string {\n    if (!text) return '';\n    return text.replace(\u002F\\[\u003Cem>.*?\u003C\\\u002Fem>\\]\u002Fg, '[***]');\n}\n```\n\n在`docs\u002F.vitepress\u002Fconfig.js`中使用`trnasformHtml()`hook来调用：\n\n```javascript\ntransformHtml(code, id, ctx) {\n    return eraseSensitiveText(code);\n},\n```\n\n> 写成`transformHtml: eraseSensitiveText`的形式也可以，但个人习惯保留回调函数的完整参数信息。\n\n## 评论\n\n写作的时候Copilot提示说“评论是博客不可缺少的功能”，想了下也有几分道理，我的确是这么想的。\n\n之前使用的评论服务是Disqus，考虑到它已经被墙，且开始提供越来越多与评论无关的功能，这次打算顺手迁移一下。\n\n因为站点本身是静态的，不具备数据库和后端逻辑，因此评论数据需要托管到一个另外的地方。这里大致有几种路线：\n\n1. 基于Github issue的评论系统\n2. 基于LeanCloud之类的BaaS的评论系统\n3. 基于自托管的评论系统\n\n在方案选择上纠结了很久。首先个人觉得使用issue作为评论数据托管的地方，多多少少有一些滥用的嫌疑，而且Github的API现在也有频率限制，因此放弃这个路线。然后是LeanCloud之类的数据托管，有一些很私人的考虑，不太想再使用国内的服务，而国外的类似产品又不多（也可能是没有去穷尽）。\n\n最后看到了[Artalk](https:\u002F\u002Fartalk.js.org\u002F)这个产品，属于自托管的评论系统。看了一下前端界面还是挺符合需求的，但它的后端是用Go写的。我希望这个服务能部署到Cloudflare workers中去，因为刚好它还有D1（SQLite）服务，这样就可以完美解决后端程序和数据库的问题，既不用花钱，也不用自己运维。\n\n于是就翻了一下Artalk的文档和源码，自己写了一个接口完全一致的后端，部署到了Cloudflare workers中。当然，因为我只有自己使用，有一部分特性用不着，所以只实现了最核心的评论相关的特性，其它部分都未做实现。\n\n> Cloudfalre有一篇官方教程，讲的就是如何为静态站点写一个评论功能，作为入门参考非常合适。\u003Chttps:\u002F\u002Fdevelopers.cloudflare.com\u002Fd1\u002Ftutorials\u002Fbuild-a-comments-api\u002F>\n\n前端部分的使用和文档基本一致，需要注意的是VitePress生成的是一个SPA应用，即点击站内链接时页面不会刷新，这将导致评论内容也不更新。因此需要手工调用`update()`方法传入新的配置并调用`reload()`重新拉取评论。\n\n```javascript\nconst router = useRouter()\n\nrouter.onAfterRouteChanged = async () => {\n    initArtalk();\n}\n\nlet Artalk, artalk;\n\nconst initArtalk = () => {\n\n    const conf = {\n        el: '#artalk-container',\n        pageKey: location.href.replace(\u002F\\?.*\u002F, '').replace(\u002F#.*\u002F, '').replace(\u002Fhttp:\\\u002F\\\u002Flocalhost:\\d+\u002F, 'https:\u002F\u002Fwww.toobug.net'),\n        pageTitle: document.title,\n        server: location.host.includes('localhost') ? 'http:\u002F\u002Flocalhost:8787' : 'https:\u002F\u002Fcomments.toobug.net',\n        site: 'TooBug',\n        nestMax: 2,\n        emoticons: false,\n    }\n\n    if (!artalk) {\n        artalk = Artalk.init(conf);\n    } else {\n        artalk.update(conf);\n        artalk.reload();\n    }\n\n};\n```\n\n在这个过程中还发现了一个Bug，Artalk在调用`update()`方法时会重复为按钮绑定事件，从而导致点击时出现重复评论的情况，目前给官方报了issue（\u003Chttps:\u002F\u002Fgithub.com\u002FArtalkJS\u002FArtalk\u002Fissues\u002F592>），期待解决。而我自己先加了一个初始化的标识放在`dataset`上，使用`pnpm patch`打了一个补丁作为临时解决方案：\n\n```javascript\nfunction submitBtn(editor) {\n  editor.refreshSendBtnText();\n  const $submitBtn = editor.getUI().$submitBtn;\n  const isInit = $submitBtn.dataset.init === \"1\";\n  if (!isInit) {\n    $submitBtn.addEventListener(\"click\", () => editor.submit());\n    $submitBtn.dataset.init = \"1\";\n  }\n}\n```\n\n在评论的通知部分，Artalk原版后端应该是邮件通知的，这部分我没去做实现，只是简单接了一个Telegram机器人，这样当别人添加新的评论的时候我就能收到机器人的推送通知。\n\n## 其他\n\n### RSS\n\nRSS基本上照抄Vue博客的代码，源码位于\u003Chttps:\u002F\u002Fgithub.com\u002Fvuejs\u002Fblog\u002Fblob\u002Fmain\u002F.vitepress\u002FgenFeed.ts>。唯一的改动是我没有输入全部文章，因此加了一个`.slice()`方法来限制输出的文章数量。\n\n### GA & favicon\n\nGA和favicon之类的都很简单，只需要在`\u003Chead>`中添加几行代码即可，对应到VitePress是在`docs\u002F.vitepress\u002Fconfig.js`中添加配置：\n\n```javascript\nhead: [\n    ['link', { rel: 'icon', href: '\u002Flogo.png' }],\n    ['script', { async: '', src: 'https:\u002F\u002Fwww.googletagmanager.com\u002Fgtag\u002Fjs?id=G-XXXXXXXX' }],\n    ['script', {}, `window.dataLayer = window.dataLayer || [];function gtag(){dataLayer.push(arguments);}gtag('js', new Date());gtag('config', 'G-XXXXXXXX');`],\n],\n```\n\n### 首页彩蛋\n\n![首页渐变背景](\u002Fassets\u002Ftech\u002F2023\u002Fblog-upgrade\u002F04.png)\n\n在正式上线之前，临时为首页的模块加了一点点效果：鼠标hover到某个模块上时，会产生一个渐变的背景，并且每一次hover看到的颜色都是随机的。利益于Tailwind的封装，整体代码比较简洁：\n\n```html\n\u003Csection class=\"hover:bg-gradient-to-br\" :class=\"getGradient()\" @mouseenter=\"refreshGradient\">\n...\n\u003C\u002Fsection>\n\n\u003Cscript setup>\nconst gradientList = {\n    light: [\n        ['from-amber-200', 'to-yellow-400'],\n        ['from-amber-500', 'to-pink-500'],\n        ['from-violet-200', 'to-pink-200'],\n        ['from-blue-200', 'to-cyan-200'],\n        ['from-teal-200', 'to-teal-500'],\n    ],\n    dark: [\n        ['from-purple-500', 'to-purple-900'],\n        ['from-blue-800', 'to-indigo-900'],\n        ['from-emerald-500', 'to-emerald-900'],\n        ['from-slate-500', 'to-slate-800'],\n        ['from-violet-600', 'to-indigo-800'],\n    ]\n};\n\nconst isDark = ref(false);\nconst checkDark = () => {\n    isDark.value = document.documentElement.classList.contains('dark');\n}\n\nconst refreshGradient = () => {\n    isDark.value = true;\n    isDark.value = false;\n    checkDark();\n}\n\nconst themeList = computed(() => {\n    const listName = isDark.value ? 'dark' : 'light';\n    return gradientList[listName];\n});\n\nconst getGradient = () => {\n    const index = Math.floor(Math.random() * themeList.value.length);\n    return themeList.value[index].join(' ')\n};\n\nonMounted(checkDark)\n\u003C\u002Fscript>\n```\n\n整体思路是当鼠标hover的时候重置`isDark`的值，然后就会依次更新`themeList`和`getGradient()`的值，从而实现更换渐变背景的效果。\n\n## 小结\n\n翻了一下提交记录，第一次安装`vitepress`依赖的时候是8月28日，不知不觉居然折腾了一个月了，还是花费了不少时间的。从最终结果上来看，基本上达到了我的预期，希望后续可以在这个站点上做更多的记录。\n\n这里也遗留了很多后续需要去处理的问题：\n\n- 整体视觉风格的优化，包括TailwindCSS和VitePress默认主题的颜色不一致的问题处理以及暗黑模式下一些细节的优化\n- 影集模块的功能和视觉优化\n- 首页动画的微调和优化\n- 评论系统的完善，包括邮件通知、回复等功能\n- 移动端一些样式优化和交互优化\n- 构建性能的优化（目前需要3分钟）\n\n希望以后有时间可以慢慢去完善。\n\n### 题外话\n\n通过这次迁移，对VitePress的认知也更深刻了。总体上来看，它的设计非常优雅，能够使用的特性并不太多，但是有足够的灵活性来满足各种各样的需求。而且因为它的最终产物是SPA，使用体验极好，相对Astro之类的框架来说，最终的使用体验是更好的。因此如果VitePress在后续的扩展和迭代中保持目前设计上的优异表现，我相信完全可以产生一个更大的生态圈。\n",null,0,"2026-08-28 04:37:17","published","\u003Cp>我大概是从2007年起开始写博客的，过去这么多年中，也有更新非常频繁的时间，但总体来看最近几年的确是写得越来越少了。随着公众号、短视频的兴起，大家也不再关注博客，而我也一度不怎么更新，甚至想不起来要写一写文字。\u003C\u002Fp>\n\u003Cp>但是最近两年，我又突然发现，虽然写内容的平台到处都是，但只有博客是真正属于自己的，那种永远不会背叛你的安全感是其他平台无法比拟的。\u003C\u002Fp>\n\u003Cp>再回头看看自己的博客，发现已经很久没有更新过了，而前一版整理的\u003Ca href=\"\u002Farticle\u002Fweb\u002F2017\u002Fmigrate-blog-to-amp.html\">AMP版本\u003C\u002Fa>，现在回头看也算是掉坑里了。刚好看到勾股写的\u003Ca href=\"https:\u002F\u002Fjiongks.name\u002Fblog\u002Fmigrate-my-blog-to-vitepress\">博客站迁移至 VitePress 的备忘\u003C\u002Fa>，于是就打算自己也再整理一下，顺便做一个升级，拍个脑袋就叫“个人主页5.0”。\u003C\u002Fp>\n\u003Cp>以下为过程中的一些记录。\u003C\u002Fp>\n\u003Cp>\u003Cimg src=\"https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Ftech\u002F2023\u002Fblog-upgrade\u002F01.png\" alt=\"首页截图\">\u003C\u002Fp>\n\u003Ch2>换程序\u003C\u002Fh2>\n\u003Cp>之前使用的是Hexo，是一个典型的“静态博客”，即所有内容都在构建时生成，托管时只有静态的HTML和对应的资源文件。这次打算换成\u003Ca href=\"https:\u002F\u002Fvitepress.dev\u002F\">VitePress\u003C\u002Fa>，也同样是“静态”的，因此核心概念和大致用法上比较类似。按照VitePress的文档，首先安装好依赖，然后将文章（\u003Ccode>.md\u003C\u002Fcode>文件）放到\u003Ccode>docs\u003C\u002Fcode>目录下即可。\u003C\u002Fp>\n\u003Cp>由于之前使用Hexo时就在路由中加了一级\u003Ccode>\u002Farticle\u003C\u002Fcode>，本次迁移过程中也需要建立一个对应的子目录，因此将文章目录由\u003Ccode>source\u002F_posts\u003C\u002Fcode>修改为\u003Ccode>docs\u002Farticle\u003C\u002Fcode>即可。\u003C\u002Fp>\n\u003Cp>与之对应的，构建脚本也需要做修改，以前用\u003Ccode>hexo server\u003C\u002Fcode>和\u003Ccode>hexo generate\u003C\u002Fcode>，现在换成\u003Ccode>vitepress dev docs\u003C\u002Fcode>和\u003Ccode>vitepress build docs\u003C\u002Fcode>。需要注意的是，VitePress是一个“极为先进”的项目，所以需要在\u003Ccode>package.json\u003C\u002Fcode>中加入\u003Ccode>&quot;type&quot;: &quot;module&quot;\u003C\u002Fcode>，否则会报错。\u003C\u002Fp>\n\u003Cp>图片的引用路径同样需要修改，按照VitePress的文档，图片最好放在\u003Ccode>docs\u003C\u002Fcode>目录下的子目录，例如\u003Ccode>docs\u002Fassets\u003C\u002Fcode>，然后在文章中使用相对路径引用即可。但最终我还是将图片放到了\u003Ccode>docs\u002Fpublic\u002Fassets\u003C\u002Fcode>目录下，具体原因见后文。\u003C\u002Fp>\n\u003Cp>VitePress的主题是通过\u003Ccode>docs\u002F.vitepress\u002Ftheme\u003C\u002Fcode>目录下的文件来定义的，其中\u003Ccode>index.js\u003C\u002Fcode>是主要的入口文件。我们可以自定义一个新的主题来使用，但如果这样做的话默认主题的一些功能就会丢失，例如暗黑模式、顶部导航、404页面等，于是我选择了直接修改默认主题，即从代码仓库中将整个默认主题拷贝过来，然后针对个别地方进行修改。\u003C\u002Fp>\n\u003Cp>基本上经过简单的替换以后，文章详情页就可以正常访问了，但是还有一些页面需要处理，例如首页、文章列表页以及一些固定的页面（如\u003Ca href=\"\u002Fabout.html\">关于\u003C\u002Fa>）。\u003C\u002Fp>\n\u003Ch2>导航&amp;固定页面\u003C\u002Fh2>\n\u003Cp>固定页面的实现是最简单的，只需要在\u003Ccode>docs\u003C\u002Fcode>目录下新建一个\u003Ccode>.md\u003C\u002Fcode>文件即可，例如\u003Ccode>docs\u002Fabout.md\u003C\u002Fcode>，就能通过\u003Ccode>\u002Fabout.html\u003C\u002Fcode>访问。\u003C\u002Fp>\n\u003Cp>VitePress中顶部导航是通过\u003Ccode>docs\u002F.vitepress\u002Fconfig.js\u003C\u002Fcode>中的\u003Ccode>themeConfig.nav\u003C\u002Fcode>配置来实现的：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>themeConfig: {\n    logo: '\u002Flogo.png',\n    nav: [\n        { text: '首页', link: '\u002F' },\n        { text: '作品', link: '\u002Fworks.html'},\n        { text: '关于', link: '\u002Fabout.html' },\n    ],\n    ...\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2>首页\u003C\u002Fh2>\n\u003Cp>首页和固定页面的概念是一样的，只需要在\u003Ccode>docs\u003C\u002Fcode>目录下新建一个\u003Ccode>index.md\u003C\u002Fcode>文件即可，就能通过\u003Ccode>\u002F\u003C\u002Fcode>访问。但是如果直接在这个文件中写文字内容的话，它的表现和普通文章是一样的，因此这里需要更多的定制化内容。\u003C\u002Fp>\n\u003Cp>到这里终于可以体会到VitePress的高明之处了：所有的\u003Ccode>.md\u003C\u002Fcode>文件都会被转换为\u003Ccode>.vue\u003C\u002Fcode>组件，然后通过Vue的工具链进行编译，因此我们可以在\u003Ccode>.md\u003C\u002Fcode>文件中使用Vue的语法，例如\u003Ccode>&lt;script&gt;\u003C\u002Fcode>、\u003Ccode>&lt;style&gt;\u003C\u002Fcode>等，基本上和写一个\u003Ccode>.vue\u003C\u002Fcode>组件没有太大差异。\u003C\u002Fp>\n\u003Cp>也正是因为有这样的机制在，使得我们可以在首页中使用一些Vue生态中可用的开源项目，例如为了编写样式，我引入了TailwindCSS，引入方法和在Vue 3项目中几乎没有区别，值得注意的有几个点：\u003C\u002Fp>\n\u003Cp>第一，通常Vue项目中我们会在\u003Ccode>main.js\u003C\u002Fcode>或者\u003Ccode>app.js\u003C\u002Fcode>中引入一个公共CSS文件，而VitePress中需要在\u003Ccode>docs\u002F.vitepress\u002Ftheme\u002Findex.js\u003C\u002Fcode>中引入（例如\u003Ccode>tailwind.css\u003C\u002Fcode>），其内容如下：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>第二，\u003Ccode>tailwind.config.js\u003C\u002Fcode>中配置的\u003Ccode>content\u003C\u002Fcode>选项需要修改，因为VitePress中的\u003Ccode>.md\u003C\u002Fcode>文件也需要处理，因此需要在\u003Ccode>content\u003C\u002Fcode>中加入\u003Ccode>.md\u003C\u002Fcode>后续。除此之外，因为\u003Ccode>.vitepress\u003C\u002Fcode>目录以\u003Ccode>.\u003C\u002Fcode>开头，默认会被忽略，需要单独写出来。\u003C\u002Fp>\n\u003Cp>为了适配VitePress的暗黑模式，这里可以顺便设置\u003Ccode>darkMode: class\u003C\u002Fcode>，这样就可以使用TailwindCSS中的\u003Ccode>dark:\u003C\u002Fcode>前缀为暗黑模式匹配样式。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>export default {\n    content: [\n        '.\u002Fdocs\u002F**\u002F*.{vue,ts,js,md}',\n        '.\u002Fdocs\u002F.vitepress\u002F**\u002F*.{vue,ts,js,md}',\n    ],\n    darkMode: 'class',\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>具体到页面内容，就比较无聊了，使用Grid布局放了几个大块块，然后适当做了一点响应式布局的调整就完成了。\u003C\u002Fp>\n\u003Ch2>文章分类页\u003C\u002Fh2>\n\u003Cp>\u003Cimg src=\"https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Ftech\u002F2023\u002Fblog-upgrade\u002F02.png\" alt=\"文章分类页\">\u003C\u002Fp>\n\u003Cp>有了固定页的经验，按道理分类页也很简单，只需要在\u003Ccode>docs\u003C\u002Fcode>目录下新建对应分类的\u003Ccode>.md\u003C\u002Fcode>文件即可，例如\u003Ccode>docs\u002Farticle\u002Fweb.md\u003C\u002Fcode>，就能通过\u003Ccode>\u002Farticle\u002Fweb.html\u003C\u002Fcode>访问。但这样做有两个问题：\u003C\u002Fp>\n\u003Col>\n\u003Cli>分类是写死的，一旦有新增或者变化就需要手工调整\u003C\u002Fli>\n\u003Cli>无法处理分页的情况\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>此时就可以使用VitePress的“动态路由”功能，具体而言，是这么做：\u003C\u002Fp>\n\u003Col>\n\u003Cli>建立一个带有占位符的\u003Ccode>.md\u003C\u002Fcode>文件，例如\u003Ccode>docs\u002Farticle\u002F[category]-[page].md\u003C\u002Fcode>，其中\u003Ccode>[category]\u003C\u002Fcode>和\u003Ccode>[page]\u003C\u002Fcode>都是占位符\u003C\u002Fli>\n\u003Cli>建立一个同名的\u003Ccode>.paths.js\u003C\u002Fcode>文件，例如\u003Ccode>docs\u002Farticle\u002F[category]-[page].paths.js\u003C\u002Fcode>，它的作用是输出\u003Ccode>[category]\u003C\u002Fcode>和\u003Ccode>[page]\u003C\u002Fcode>占位符的所有可能值，例如\u003Ccode>web-1\u003C\u002Fcode> \u003Ccode>web-2\u003C\u002Fcode> \u003Ccode>tech-1\u003C\u002Fcode> \u003Ccode>tech-2\u003C\u002Fcode>等等\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>这样，就可以通过\u003Ccode>\u002Farticle\u002Fweb-1.html\u003C\u002Fcode> \u003Ccode>article\u002Fweb-2.html\u003C\u002Fcode>等访问到对应的分类页了。为了额外提供一个“所有”分类，在处理\u003Ccode>[category]\u003C\u002Fcode>的值时加入一个\u003Ccode>all\u003C\u002Fcode>作为分类，这样就可以通过\u003Ccode>\u002Farticle\u002Fall-1.html\u003C\u002Fcode>访问到所有文章的第一页了。\u003C\u002Fp>\n\u003Cp>具体的写法大致如下：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>\u002F\u002F [category]-[page].paths.js\n\n\u002F\u002F Recursively get all files in a directory\nfunction getFiles(dir: string): MdFile[] {\n  const files = readdirSync(dir);\n  let fileList: MdFile[] = [];\n\n  files.forEach((file) =&gt; {\n    const filePath = join(dir, file);\n    const stats = statSync(filePath);\n\n    if (stats.isDirectory()) {\n      fileList = fileList.concat(getFiles(filePath));\n    } else if (filePath.endsWith('.md') &amp;&amp; !filePath.endsWith('\u002Findex.md') &amp;&amp; !\u002F\\.\\\u002Fdocs\\\u002F[^\u002F]+\\.md\u002F.test(filePath)) {\n      const fileContent = readFileSync(filePath, 'utf8');\n      const { data } = matter(fileContent);\n      const category = data.category || (Array.isArray(data.categories) &amp;&amp; data.categories[0]);\n      fileList.push({ filePath, category });\n    }\n  });\n\n  return fileList;\n}\n\n\u002F\u002F Get all markdown files\nconst data = getFiles('.\u002Fdocs');\n\n\u002F\u002F Group files by category\nconst categories: { [key: string]: MdFile[] } = {};\ndata.forEach((file) =&gt; {\n  const category = file.category || 'all';\n  if (!categories[category]) {\n    categories[category] = [];\n  }\n  categories[category].push(file);\n\n  if (category !== 'all') {\n    if (!categories.all) {\n      categories.all = [];\n    }\n    categories.all.push(file);\n  }\n});\n\n\u002F\u002F Calculate page count for each category\nconst pageParams: PageParams[] = [];\nObject.entries(categories).forEach(([category, files]) =&gt; {\n  const pageCount = Math.ceil(files.length \u002F PAGE_SIZE);\n  for (let i = 1; i &lt;= pageCount; i++) {\n    pageParams.push({ params: { category, page: i } });\n  }\n});\n\nexport default {\n  paths() {\n    return pageParams;\n  },\n};\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cblockquote>\n\u003Cp>写本文的时候在想为什么这里没有使用createContentLoader来写，暂时没有想清楚，可能尝试过碰到了问题，记不太清了，回头再试一下。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>按照官方文档，在\u003Ccode>[category]-[page].md\u003C\u002Fcode>中，可以使用\u003Ccode>createContentLoader\u003C\u002Fcode>来获取文章数据，这一段逻辑被封装到一个单独的文件\u003Ccode>posts.data.js\u003C\u002Fcode>中：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>export default createContentLoader('article\u002F**\u002F*.md', {\n    excerpt: '&lt;!-- more --&gt;',\n    includeSrc: true,\n    transform(raw): Post[] {\n        return raw\n            .filter(({ url }) =&gt; url &amp;&amp; !\u002F\\[.+\\]\u002F.test(url))\n            .map(({ src, url, frontmatter, excerpt }) =&gt; ({\n                title: frontmatter.title,\n                url,\n                date: formatDate(frontmatter.date),\n                category: frontmatter.category || frontmatter.categories?.[0] || 'all',\n                excerpt,\n                cover: getCover(src),\n            }))\n            .sort((a, b) =&gt; b.date.time - a.date.time)\n    }\n});\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>其中\u003Ccode>getCover()\u003C\u002Fcode>是一个从原文中解析封面图片的方法。\u003C\u002Fp>\n\u003Cp>这里有一个困扰了很久的点，就是图片路径的问题。按照官方文档，推荐将图片放在\u003Ccode>docs\u002Fassets\u003C\u002Fcode>中，在\u003Ccode>.md\u003C\u002Fcode>文件中通过相对路径进行引用，如果这样做的话，这个\u003Ccode>getCover()\u003C\u002Fcode>获取到的将是构建前的图片原始地址，而构建之后，图片地址会发生变化，使得封面无法显示。因此最终还是将图片放在了\u003Ccode>docs\u002Fpublic\u002Fassets\u003C\u002Fcode>中。\u003C\u002Fp>\n\u003Cp>渲染的时候根据根据\u003Ccode>params\u003C\u002Fcode>中的\u003Ccode>category\u003C\u002Fcode>和\u003Ccode>page\u003C\u002Fcode>来筛选文章，最后将结果渲染出来即可：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>import { data as posts } from '..\u002F.vitepress\u002Ftheme\u002Fposts.data'\n\nconst { params } = useData()\n\nconst PAGE_SIZE = 12\nconst PAGE = +params.value.page\nconst CATEGORY = params.value.category\n\nconst categoryPosts = computed(() =&gt; {\n    return posts.filter(post =&gt; CATEGORY === 'all' || post.category === CATEGORY);\n});\n\nconst currentPosts = computed(() =&gt; {\n    const start = (PAGE - 1) * PAGE_SIZE\n    const end = start + PAGE_SIZE\n    return categoryPosts.value.slice(start, end)\n});\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>在界面上还要放一些分类选择、页面选择的组件，比较常规，不过多记录。\u003C\u002Fp>\n\u003Ch2>影集页面\u003C\u002Fh2>\n\u003Cp>\u003Cimg src=\"https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Ftech\u002F2023\u002Fblog-upgrade\u002F03.png\" alt=\"影集详情页\">\u003C\u002Fp>\n\u003Cp>影集模块中的详情页主要用来展示照片，这里不想使用文章中把图片顺序铺排下来的方式，而是希望有一个自定义的布局，于是扩展了一个单独的\u003Ccode>GalaryLayout\u003C\u002Fcode>来展示。\u003C\u002Fp>\n\u003Cp>首先编写好布局组件，就是非常常规的Vue组件，使用\u003Ccode>useData()\u003C\u002Fcode>方法获取页面对应的数据，然后进行展示即可。为了方便，在\u003Ccode>docs\u002F.vitepress\u002Fconfig.js\u003C\u002Fcode>中的\u003Ccode>transformPageData()\u003C\u002Fcode>hook中加了一个解析页面图片的功能：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>transformPageData(pageData, ctx) {\n    if (pageData.frontmatter.category === 'galary') {\n        \u002F\u002F @ts-ignore\n        pageData.images = getImages(readFileSync(join(process.cwd(), 'docs', pageData.relativePath), 'utf8'));\n    }\n\n    return pageData;\n},\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cblockquote>\n\u003Cp>这个页面还有待进一步优化，图片的查看方式、整体视觉风格都还不够好。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch2>敏感信息擦除\u003C\u002Fh2>\n\u003Cp>因为有一些文章中有部分文字不适宜对外发布，但作为个人的文字记录，又不想删掉，于是之前就做了一个敏感信息的标记（\u003Ccode>[*敏感文字*]\u003C\u002Fcode>），在渲染的时候会擦除标记中的文字。这次迁移的时候发现VitePress不容易做这样的处理，因为它没有给出在渲染前进行原文修改的hook，只能修改渲染后的HTML代码。\u003C\u002Fp>\n\u003Cp>于是修改后的代码如下：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>export function eraseSensitiveText(text?: string): string {\n    if (!text) return '';\n    return text.replace(\u002F\\[&lt;em&gt;.*?&lt;\\\u002Fem&gt;\\]\u002Fg, '[***]');\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>在\u003Ccode>docs\u002F.vitepress\u002Fconfig.js\u003C\u002Fcode>中使用\u003Ccode>trnasformHtml()\u003C\u002Fcode>hook来调用：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>transformHtml(code, id, ctx) {\n    return eraseSensitiveText(code);\n},\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cblockquote>\n\u003Cp>写成\u003Ccode>transformHtml: eraseSensitiveText\u003C\u002Fcode>的形式也可以，但个人习惯保留回调函数的完整参数信息。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch2>评论\u003C\u002Fh2>\n\u003Cp>写作的时候Copilot提示说“评论是博客不可缺少的功能”，想了下也有几分道理，我的确是这么想的。\u003C\u002Fp>\n\u003Cp>之前使用的评论服务是Disqus，考虑到它已经被墙，且开始提供越来越多与评论无关的功能，这次打算顺手迁移一下。\u003C\u002Fp>\n\u003Cp>因为站点本身是静态的，不具备数据库和后端逻辑，因此评论数据需要托管到一个另外的地方。这里大致有几种路线：\u003C\u002Fp>\n\u003Col>\n\u003Cli>基于Github issue的评论系统\u003C\u002Fli>\n\u003Cli>基于LeanCloud之类的BaaS的评论系统\u003C\u002Fli>\n\u003Cli>基于自托管的评论系统\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>在方案选择上纠结了很久。首先个人觉得使用issue作为评论数据托管的地方，多多少少有一些滥用的嫌疑，而且Github的API现在也有频率限制，因此放弃这个路线。然后是LeanCloud之类的数据托管，有一些很私人的考虑，不太想再使用国内的服务，而国外的类似产品又不多（也可能是没有去穷尽）。\u003C\u002Fp>\n\u003Cp>最后看到了\u003Ca href=\"https:\u002F\u002Fartalk.js.org\u002F\">Artalk\u003C\u002Fa>这个产品，属于自托管的评论系统。看了一下前端界面还是挺符合需求的，但它的后端是用Go写的。我希望这个服务能部署到Cloudflare workers中去，因为刚好它还有D1（SQLite）服务，这样就可以完美解决后端程序和数据库的问题，既不用花钱，也不用自己运维。\u003C\u002Fp>\n\u003Cp>于是就翻了一下Artalk的文档和源码，自己写了一个接口完全一致的后端，部署到了Cloudflare workers中。当然，因为我只有自己使用，有一部分特性用不着，所以只实现了最核心的评论相关的特性，其它部分都未做实现。\u003C\u002Fp>\n\u003Cblockquote>\n\u003Cp>Cloudfalre有一篇官方教程，讲的就是如何为静态站点写一个评论功能，作为入门参考非常合适。\u003Ca href=\"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fd1\u002Ftutorials\u002Fbuild-a-comments-api\u002F\">https:\u002F\u002Fdevelopers.cloudflare.com\u002Fd1\u002Ftutorials\u002Fbuild-a-comments-api\u002F\u003C\u002Fa>\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>前端部分的使用和文档基本一致，需要注意的是VitePress生成的是一个SPA应用，即点击站内链接时页面不会刷新，这将导致评论内容也不更新。因此需要手工调用\u003Ccode>update()\u003C\u002Fcode>方法传入新的配置并调用\u003Ccode>reload()\u003C\u002Fcode>重新拉取评论。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>const router = useRouter()\n\nrouter.onAfterRouteChanged = async () =&gt; {\n    initArtalk();\n}\n\nlet Artalk, artalk;\n\nconst initArtalk = () =&gt; {\n\n    const conf = {\n        el: '#artalk-container',\n        pageKey: location.href.replace(\u002F\\?.*\u002F, '').replace(\u002F#.*\u002F, '').replace(\u002Fhttp:\\\u002F\\\u002Flocalhost:\\d+\u002F, 'https:\u002F\u002Fwww.toobug.net'),\n        pageTitle: document.title,\n        server: location.host.includes('localhost') ? 'http:\u002F\u002Flocalhost:8787' : 'https:\u002F\u002Fcomments.toobug.net',\n        site: 'TooBug',\n        nestMax: 2,\n        emoticons: false,\n    }\n\n    if (!artalk) {\n        artalk = Artalk.init(conf);\n    } else {\n        artalk.update(conf);\n        artalk.reload();\n    }\n\n};\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>在这个过程中还发现了一个Bug，Artalk在调用\u003Ccode>update()\u003C\u002Fcode>方法时会重复为按钮绑定事件，从而导致点击时出现重复评论的情况，目前给官方报了issue（\u003Ca href=\"https:\u002F\u002Fgithub.com\u002FArtalkJS\u002FArtalk\u002Fissues\u002F592\">https:\u002F\u002Fgithub.com\u002FArtalkJS\u002FArtalk\u002Fissues\u002F592\u003C\u002Fa>），期待解决。而我自己先加了一个初始化的标识放在\u003Ccode>dataset\u003C\u002Fcode>上，使用\u003Ccode>pnpm patch\u003C\u002Fcode>打了一个补丁作为临时解决方案：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>function submitBtn(editor) {\n  editor.refreshSendBtnText();\n  const $submitBtn = editor.getUI().$submitBtn;\n  const isInit = $submitBtn.dataset.init === &quot;1&quot;;\n  if (!isInit) {\n    $submitBtn.addEventListener(&quot;click&quot;, () =&gt; editor.submit());\n    $submitBtn.dataset.init = &quot;1&quot;;\n  }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>在评论的通知部分，Artalk原版后端应该是邮件通知的，这部分我没去做实现，只是简单接了一个Telegram机器人，这样当别人添加新的评论的时候我就能收到机器人的推送通知。\u003C\u002Fp>\n\u003Ch2>其他\u003C\u002Fh2>\n\u003Ch3>RSS\u003C\u002Fh3>\n\u003Cp>RSS基本上照抄Vue博客的代码，源码位于\u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fvuejs\u002Fblog\u002Fblob\u002Fmain\u002F.vitepress\u002FgenFeed.ts\">https:\u002F\u002Fgithub.com\u002Fvuejs\u002Fblog\u002Fblob\u002Fmain\u002F.vitepress\u002FgenFeed.ts\u003C\u002Fa>。唯一的改动是我没有输入全部文章，因此加了一个\u003Ccode>.slice()\u003C\u002Fcode>方法来限制输出的文章数量。\u003C\u002Fp>\n\u003Ch3>GA &amp; favicon\u003C\u002Fh3>\n\u003Cp>GA和favicon之类的都很简单，只需要在\u003Ccode>&lt;head&gt;\u003C\u002Fcode>中添加几行代码即可，对应到VitePress是在\u003Ccode>docs\u002F.vitepress\u002Fconfig.js\u003C\u002Fcode>中添加配置：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>head: [\n    ['link', { rel: 'icon', href: '\u002Flogo.png' }],\n    ['script', { async: '', src: 'https:\u002F\u002Fwww.googletagmanager.com\u002Fgtag\u002Fjs?id=G-XXXXXXXX' }],\n    ['script', {}, `window.dataLayer = window.dataLayer || [];function gtag(){dataLayer.push(arguments);}gtag('js', new Date());gtag('config', 'G-XXXXXXXX');`],\n],\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>首页彩蛋\u003C\u002Fh3>\n\u003Cp>\u003Cimg src=\"https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Ftech\u002F2023\u002Fblog-upgrade\u002F04.png\" alt=\"首页渐变背景\">\u003C\u002Fp>\n\u003Cp>在正式上线之前，临时为首页的模块加了一点点效果：鼠标hover到某个模块上时，会产生一个渐变的背景，并且每一次hover看到的颜色都是随机的。利益于Tailwind的封装，整体代码比较简洁：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>&lt;section class=&quot;hover:bg-gradient-to-br&quot; :class=&quot;getGradient()&quot; @mouseenter=&quot;refreshGradient&quot;&gt;\n...\n&lt;\u002Fsection&gt;\n\n&lt;script setup&gt;\nconst gradientList = {\n    light: [\n        ['from-amber-200', 'to-yellow-400'],\n        ['from-amber-500', 'to-pink-500'],\n        ['from-violet-200', 'to-pink-200'],\n        ['from-blue-200', 'to-cyan-200'],\n        ['from-teal-200', 'to-teal-500'],\n    ],\n    dark: [\n        ['from-purple-500', 'to-purple-900'],\n        ['from-blue-800', 'to-indigo-900'],\n        ['from-emerald-500', 'to-emerald-900'],\n        ['from-slate-500', 'to-slate-800'],\n        ['from-violet-600', 'to-indigo-800'],\n    ]\n};\n\nconst isDark = ref(false);\nconst checkDark = () =&gt; {\n    isDark.value = document.documentElement.classList.contains('dark');\n}\n\nconst refreshGradient = () =&gt; {\n    isDark.value = true;\n    isDark.value = false;\n    checkDark();\n}\n\nconst themeList = computed(() =&gt; {\n    const listName = isDark.value ? 'dark' : 'light';\n    return gradientList[listName];\n});\n\nconst getGradient = () =&gt; {\n    const index = Math.floor(Math.random() * themeList.value.length);\n    return themeList.value[index].join(' ')\n};\n\nonMounted(checkDark)\n&lt;\u002Fscript&gt;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>整体思路是当鼠标hover的时候重置\u003Ccode>isDark\u003C\u002Fcode>的值，然后就会依次更新\u003Ccode>themeList\u003C\u002Fcode>和\u003Ccode>getGradient()\u003C\u002Fcode>的值，从而实现更换渐变背景的效果。\u003C\u002Fp>\n\u003Ch2>小结\u003C\u002Fh2>\n\u003Cp>翻了一下提交记录，第一次安装\u003Ccode>vitepress\u003C\u002Fcode>依赖的时候是8月28日，不知不觉居然折腾了一个月了，还是花费了不少时间的。从最终结果上来看，基本上达到了我的预期，希望后续可以在这个站点上做更多的记录。\u003C\u002Fp>\n\u003Cp>这里也遗留了很多后续需要去处理的问题：\u003C\u002Fp>\n\u003Cul>\n\u003Cli>整体视觉风格的优化，包括TailwindCSS和VitePress默认主题的颜色不一致的问题处理以及暗黑模式下一些细节的优化\u003C\u002Fli>\n\u003Cli>影集模块的功能和视觉优化\u003C\u002Fli>\n\u003Cli>首页动画的微调和优化\u003C\u002Fli>\n\u003Cli>评论系统的完善，包括邮件通知、回复等功能\u003C\u002Fli>\n\u003Cli>移动端一些样式优化和交互优化\u003C\u002Fli>\n\u003Cli>构建性能的优化（目前需要3分钟）\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>希望以后有时间可以慢慢去完善。\u003C\u002Fp>\n\u003Ch3>题外话\u003C\u002Fh3>\n\u003Cp>通过这次迁移，对VitePress的认知也更深刻了。总体上来看，它的设计非常优雅，能够使用的特性并不太多，但是有足够的灵活性来满足各种各样的需求。而且因为它的最终产物是SPA，使用体验极好，相对Astro之类的框架来说，最终的使用体验是更好的。因此如果VitePress在后续的扩展和迭代中保持目前设计上的优异表现，我相信完全可以产生一个更大的生态圈。\u003C\u002Fp>\n","\u003Cp>我大概是从2007年起开始写博客的，过去这么多年中，也有更新非常频繁的时间，但总体来看最近几年的确是写得越来越少了。随着公众号、短视频的兴起，大家也不再关注博客，而我也一度不怎么更新，甚至想不起来要写一写文字。\u003C\u002Fp>\n\u003Cp>但是最近两年，我又突然发现，虽然写内容的平台到处都是，但只有博客是真正属于自己的，那种永远不会背叛你的安全感是其他平台无法比拟的。\u003C\u002Fp>\n\u003Cp>再回头看看自己的博客，发现已经很久没有更新过了，而前一版整理的\u003Ca href=\"\u002Farticle\u002Fweb\u002F2017\u002Fmigrate-blog-to-amp.html\">AMP版本\u003C\u002Fa>，现在回头看也算是掉坑里了。刚好看到勾股写的\u003Ca href=\"https:\u002F\u002Fjiongks.name\u002Fblog\u002Fmigrate-my-blog-to-vitepress\">博客站迁移至 VitePress 的备忘\u003C\u002Fa>，于是就打算自己也再整理一下，顺便做一个升级，拍个脑袋就叫“个人主页5.0”。\u003C\u002Fp>\n\u003Cp>以下为过程中的一些记录。\u003C\u002Fp>\n","https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Ftech\u002F2023\u002Fblog-upgrade\u002F01.png",{"total":24,"totalRoots":24,"comments":25,"pv":17},2,[26,34],{"id":27,"content":28,"user_id":29,"nick":30,"link":31,"date":32,"rid":17,"vote_up":17,"vote_down":17,"visible":33},161,"试试评论……发现一个 `trnasformHtml`",78,"Leo","https:\u002F\u002Fmyst729.github.io\u002F","2023-09-28 18:04:36",true,{"id":35,"content":36,"user_id":37,"nick":38,"link":39,"date":40,"rid":17,"vote_up":17,"vote_down":17,"visible":33},162,"你好，怎么去查看你这个网站的源码，想抄袭一下你的 排版",79,"nogeek","https:\u002F\u002Fnogeek.cn","2023-10-15 21:40:17"]