博客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
目录下面,然后配置
// .vitepres/config.mts
export default defineConfig({
srcDir: './views',
})
然后将blog-source
仓库博客文章的目录xxxx/_posts
映射到工作目录的views/article
目录下面,可以使用ln -s
或者NodeJS的fs.symlinkSync
实现。
整个views
目录文件如下图所示。
最后,vite默认是关闭了软链接访问的功能,需要打开resolve.preserveSymlinks这个配置项
// .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
映射规则,核心逻辑如下
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
即可。
export default defineConfig({
cleanUrls: true,
rewrites: {
...pathRewrites,
},
}
此外,vitepress的url默认会携带.html
后缀,要保持之前无后缀风格的url
,可以配置一下cleanUrls
。
注意cleanUrls
在会影响最终访问的url,因此在部署的时候还需要考虑资源命中的问题,在后续的部署章节会单独介绍。
自定义主题
vitepress默认的主题已经很不错了,大部分文档站点都可以直接开箱即用。但作为个人博客,还是希望有一点自己的风格,因此需要实现一套自定义主题。
关于自定义主题,官网文档说的比较详细了,也可以参考vitepress内置的default-theme实现,这里只是简单整理一下。
在.vitepress/theme/index.ts
入口文件处,指定Layout
配置项
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
来渲染对应的组件
<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的默认主题就是这样处理的,通过component
的is
动态渲染layout
。
另外一种实现自定义页面的方式是通过直接引入Vue组件。由于vitepress支持mdx
,也就是在markdown文件中写引入Vue组件,因此对于自定义的页面,可以直接引入全局组件
---
layout: page
---
<layout-article-feed :page="1"/>
在主题配置文件中enhanceApp
可以调用这个registerGlobalComponent
注册全局组件
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
原子类工具,因此实现暗黑模式就非常简单。
<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
拿到。
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,导致每篇文章的description
和keywords
都是缺失的,使用了文章的开头和标签来替代。
在这次迭代中,编写了一个脚本调用kimi的接口,为每篇文章编写了独立的描述和关键字。
核心脚本代码为
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控制台查看
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会自动监听pushState
和onpopstate
等SPA应用的路由切换,因此不需要单独处理vitepress构建站点的页面上报了。
小结
至此,整个博客的v0.9.0版本重构已基本完成,还剩下了一些零碎的工作待完成,包括
移动端适配
自动化部署
历史文章重新整理归纳
等有时间了再把这些TODO划掉~
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。