侧边栏

博客v0.9迭代记录

发布于 | 分类于 博客

之前的博客文章是通过hexo来管理的,其目录结构类似于

  • _drafts目录存放草稿
  • _posts目录保存了全部已发表的文章

这种平铺的结构并不是很容易直观地管理知识点,同时之前实现的SSR并不支持mdx,让一个前端开发者的博客缺失了不少乐趣。

因此我决定回归博客内容为主的本质,根据markdown frist原则、按照目录重新构建博客。

此外云服务器的续费价格变贵、免费SSL证书时效变短,感觉已经不太适合使用云服务器来部署独立域名的个人博客了,因此决定将博客构建为静态网站。

一番技术选型后,我确定使用vitepress作为开发构建工具,使用cloudflare作为部署服务器。本文将整理整个博客重构过程中的一些要点。

功能迭代

文件管理

在之前的系统中,写一篇文章的大致流程是

  • 突然有了一点想法,通过npx hexo new draft xxx创建markdown草稿
  • 不知道什么时候,终于写完了内容之后,通过npx hexo publish xxx发布到_posts文件夹下
  • 手动通过一个Nodejs脚本将数据上传到MySQL数据库中
  • 最后通过一个Node服务提供API,然后通过SSR渲染出来

整个hexo工程会单独放在一个git仓库blog-source中,用于作为最原始的博客备份数据。

shymean这个仓库只用于处理整个站点的技术框架和业务逻辑,比如后端接口服务、前端SSR等,不涉及文章数据相关的东西。

因此shymean这个仓库进行了很多次的迭代(也相当于是新技术的试验田),在本次迭代中,依旧希望保持这种结构,将博客文章和站点技术框架分离。

而vitepress本身是需要依赖md文件来生成地址和渲染页面的,如何在物理层面保证两个仓库的分隔,而在开发和构建层面可以让vitepress正常工作呢。

最简单的办法是使用软链接

首先规划整个站点的目录结构,将整个vitepress需要渲染的md文档都放在views目录下面,然后配置

ts
// .vitepres/config.mts
export default defineConfig({
	srcDir: './views',  
})

然后将blog-source仓库博客文章的目录xxxx/_posts映射到工作目录的views/article目录下面,可以使用ln -s或者NodeJS的fs.symlinkSync实现。

整个views目录文件如下图所示。

最后,vite默认是关闭了软链接访问的功能,需要打开resolve.preserveSymlinks这个配置项

ts
// .vitepres/config.mts
export default defineConfig({
  vite: {
    resolve: {
      preserveSymlinks: true,
    },
  }, 
})

现在,就可以通过artilce/xxxx.html路径来访问到_posts目录下的某个具体md文章了。

路由兼容

前面提到,hexo _posts目录默认只有一级,一个文件夹下面包含了全部的文章文件,这种平铺的结构对于管理知识点来说并不友好。

因此,本次迭代有一个最重要的目标:基于目录管理markdown形式的文章结构,方便知识点分类和梳理。

手动按目录来管理文件是比较简单的,随之而来的问题是:vitepress是默认按照文件路径来生成链接,文件目录的改动会影响最终生成的url,比如_posts/a/b.md,最后就需要通过article/a/b.html来访问。

之前的网站url都是在自定义的,比如文章详情页的url/article/:title,其中title是对应的文章标题。

这次迭代,需要兼容之前的url格式,主要原因有

  • 避免一些外部转载的文章链接失效
  • 之前的评论系统使用的leancloud脚本,其评论加载数据也是根据url来的

为了解决这个问题,需要使用vitepress提供的路由重写功能,可以将某个路径的文件映射成/article/:title格式的url。

可以编写一个脚本用于生成pathRewrites映射规则,核心逻辑如下

ts
function generateRewriteData(articles: IArticle[]) {
  const map: Record<string, string> = {}
  for (const article of articles) {
    if (article.fullPath) {
      map['article/' + article.fullPath] = 'article/' + article.title + '.md'
    }
  }
  return map
}

然后在vitepress的配置文件中配置rewrites即可。

ts
export default defineConfig({
  cleanUrls: true,
  rewrites: {
    ...pathRewrites,
  },
}

此外,vitepress的url默认会携带.html后缀,要保持之前无后缀风格的url,可以配置一下cleanUrls

注意cleanUrls在会影响最终访问的url,因此在部署的时候还需要考虑资源命中的问题,在后续的部署章节会单独介绍。

自定义主题

vitepress默认的主题已经很不错了,大部分文档站点都可以直接开箱即用。但作为个人博客,还是希望有一点自己的风格,因此需要实现一套自定义主题。

关于自定义主题,官网文档说的比较详细了,也可以参考vitepress内置的default-theme实现,这里只是简单整理一下。

.vitepress/theme/index.ts入口文件处,指定Layout配置项

ts
import DefaultTheme from 'vitepress/theme'
import { registerGlobalComponent, Layout } from '@/theme'

export default {
  extends: DefaultTheme, // 继承默认主题的样式
  Layout, // 自己实现的布局
  enhanceApp({ app, router, siteData }) {
    registerGlobalComponent(app)
  }
} as Theme

这个Layout就相当于Vue引用的App根组件,然后在组件内根据frontmatter.layout来渲染对应的组件

vue
<template>
  <div class="min-h-100vh flex flex-col pt-70px">
    <Header class="fixed top-0 left-0 w-full bg-[#f5f5f5] h-60px z-9" />
    <main class="sm:w-full md:w-700px lg:w-900px mx-auto mt-50px mb-30px">
      <Content class="vp-doc" v-if="frontmatter.layout === 'page'" />
      <Article v-else></Article>
    </main>
    <Footer class="mt-auto" />
  </div>
</template>

<script setup>
import { useData } from 'vitepress'
import Footer from './layout/Footer.vue'
import Header from './layout/Header.vue'
import Article from './layout/Article.vue'

const { page, frontmatter } = useData()
</script>

在某个页面对应的md文件中,可以在头部的frontmatter指定对应的布局名称,然后根据frontmatter.layout渲染对应的页面组件

---
layout: page
---

vitepress的默认主题就是这样处理的,通过componentis动态渲染layout

另外一种实现自定义页面的方式是通过直接引入Vue组件。由于vitepress支持mdx,也就是在markdown文件中写引入Vue组件,因此对于自定义的页面,可以直接引入全局组件

---
layout: page
---

<layout-article-feed :page="1"/>

在主题配置文件中enhanceApp可以调用这个registerGlobalComponent注册全局组件

ts
export function registerGlobalComponent(app: App) {
  app.component('user-comment', Comment)
  app.component('layout-archive', LayoutArchive)
  app.component('layout-tags', LayoutTags)
  app.component('layout-article-feed', LayoutArticleFeed)
  app.component('layout-archive-search', LayoutArchiveSearch)
}

由于之前的博客文档都是hexo风格的markdown文档,其frontmatter即里面没有指定layout,且此类文章数量最多。

因此views/article目录下面的md文档,会在Layout中作为兜底渲染Article组件,其余自定义页面均声明为layout: page,然后通过引入全局组件的方式进行渲染。

暗黑模式

由于整个主题继承至DefaultTheme,同时引入了unocss原子类工具,因此实现暗黑模式就非常简单。

vue
<template>
  <button class="VPSwitch" type="button" role="switch" :class="{dark:isDark}"  :aria-checked="isDark" @click="onChange">
    <span class="check">
      <span class="icon" >
        <span class="vpi-sun sun" />
        <span class="vpi-moon moon" />
      </span>
    </span>
  </button>
</template>

<script setup lang="ts">
import { useDark, useToggle } from '@vueuse/core'

const isDark = useDark()
const toggleDark = useToggle(isDark)

function onChange(){
  toggleDark()
}
</script>

然后在需要切换暗色样式的地方使用dark:bg-xxx dark:text-xxx来编写原子类即可。

你可以点击页面顶部的导航栏,或者下面这个按钮直接查看效果。

发布时间时区问题

article页面是上展示的创建时间是直接使用的frontmatter.date这个数据,vitepress自己使用gray-matter解析的md文件头部配置项,该数据可以通过useData拿到。

js
const { frontmatter } = useData()
console.log(frontmatter.date)

之前的文章,都是通过hexo publish自动创建的,其date没有携带时区,默认为2024-05-06 11:06:48形式的字符串。

gray-matter在解析这些日期数据的时候会自动转成utc 0时区,即2024-05-06T11:06:48Z,导致在页面上展示的发布时间都增加了8个小时(本地是北京时间)。

首先想到的是能不能禁用这个行为,查阅资料发现这个issue的讨论:Disable date parsing,该行为的具体原因是内置使用的js-yaml解析导致的。

因此,没法直接通过配置来处理这个问题,只有手动修改每个md文件frontmatter中的date字段以带上时区,这个任务交给了一个NodeJS脚本来处理。

SEO TDK

之前博客一直没有怎么搞SEO,这次决定借助GPT等工具一起格式化一下。

vitepress的frontmatter 配置中提供了head这个配置项,可以设置页面级别的head标签,其格式如下所示。

---
head:
  - - meta
    - name: description
      content: hello
  - - meta
    - name: keywords
      content: super duper SEO
---

由于历史文件一直没有优化SEO,导致每篇文章的descriptionkeywords都是缺失的,使用了文章的开头和标签来替代。

在这次迭代中,编写了一个脚本调用kimi的接口,为每篇文章编写了独立的描述和关键字。

核心脚本代码为

ts
export async function updateArticleSEO(filePath: string, seo: { description: string; keywords: string }) {
  const fileContent = await fs.readFile(filePath, 'utf8')
  const { content, data } = matter(fileContent)
  const file = matter.stringify(content, {
    ...data,
    head: [
      ['meta', { name: 'description', content: seo.description }],
      ['meta', { name: 'keywords', content: seo.keywords }],
    ],
  })
  await fs.writeFile(filePath, file)
}

其中的seo参数即通过kimi返回的文章描述和关键字。当然也可以选用其他平台的API接口,这里不在展开

由于目前kimi的账号权限不高,RPM(1分钟内可以通过API发送的请求)只有5,需要手动编写一个队列来处理请求任务,总共两百五十多篇文章需要比较长的时间才能跑完,平均价格大概是0.04元/1篇文章,由于kimi初始账号里面有15元,应该是可以跑完的。

至于具体的SEO效果还需要观察一段时间(由于部署服务器的问题,对于国内搜索引擎的抓取还要打一个问号。)

托管至cloudflare

博客不再使用云服务器部署,因此决定使用免费的静态网站服务商托管,最终选择了cloudefare

DNS解析

cloudflare 可以直接通过CNAME部署二级域名,但是如果要部署根域名,需要将域名托管至cloudflare进行解析。

域名注册商一般都提供了修改该域名DNS服务器的功能,需要参考购买域名所在平台对应的文档,由于我的域名是在阿里云万网上面购买的,所以参考相关操作流程:

大致流程就是,登录域名管理后台-域名管理-DNS管理-修改DNS服务器。

这个过程需要等待一段时间(我等了大概十分钟左右),修改成功之后cf也会发邮件通知。

然后在workers和pages的自定义域名下面关联一下,整个解析过程就算完成了。

此外,cloudflare还自动开启了免费的SSL,不需要每三个月去阿里云搞一个免费的证书放在服务器上面了~

部署

接下来就是部署静态站点的工作

执行npm run build命令,通过vitepress构建静态站点,输出产物在.vitepress/dist目录下面,接着将整个dist文件夹拖动到cf进行上传

这个文件体积25M的限制,一般的静态站点页面肯定是够了。

上传完成之后还需要等待一会,整个部署就完了,这一步貌似也可以通过脚本或者github web hook来实现,后面再研究。

最后,删除浏览器DNS缓存,访问shymean.com,就可以看见更新后的博客了,大工告成。

web analytics

博客之前一直使用的友盟统计访问数据,2022年之后友盟不再提供免费的web统计分析服务了,我自己对于这些数据也不是很关心,因此之后一直没有再接入其他埋点SDK。

在部署至cloudflare的时候发现它也提供了免费的web analytics服务,因此决定试一下。

在vitepress配置文件的head中插入一个script脚本即可,对应的token可以再cf控制台查看

ts

export default defineConfig({
  head:[
    //Cloudflare Web Analytics
    ['script',{src:"https://static.cloudflareinsights.com/beacon.min.js",'data-cf-beacon':'{"token": "xxx"}'}]
  ],
}

根据Web Analytics for Single Page Applications (SPAs)这篇文档介绍,该sdk会自动监听pushStateonpopstate等SPA应用的路由切换,因此不需要单独处理vitepress构建站点的页面上报了。

小结

至此,整个博客的v0.9.0版本重构已基本完成,还剩下了一些零碎的工作待完成,包括

  • 移动端适配

  • 自动化部署

  • 历史文章重新整理归纳

等有时间了再把这些TODO划掉~

你要请我喝一杯奶茶?

版权声明:自由转载-非商用-保持署名和原文链接。

本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。