最近投入了大量摸鱼时间重构博客。现在大概告一段落了,向大家介绍一下整体的技术选型和具体实现的简要思路。
TLDR:JAMStack 实践,使用最新最潮的前端元框架 Astro,魔改 Astro Paper 主题,搭配 Headless CMS Directus,直通对接思源笔记内容同步,自建 SeaweedFS 分布式文件系统暴露 S3 API 作为图床后端,使用 WebP Server 作为图床前端反代,部署 Cloudflare Pages,实现高度客制化的博客体验。
你可以从这里开始访问我的博客:https://clouder0.com/zh-cn/posts/my-new-blog
Introduction
一路走来,我的博客架构换了一茬又一茬,虽然内容好像更新地也不是很勤快……不,其实还是有持续输出内容的吧!
但是,我现在越来越忙了,也可能是越来越懒了,手动更新博客显然不是什么好的方案,毕竟我目前的内容创作主要集中于:
- 在思源笔记中写一点为自己记录的内容。顺便,写作是思考的媒介,我也很习惯边写东西边做研究。
- 写完之后,可能随手复制粘贴发到知乎上。
复制粘贴发送到知乎上,和复制粘贴发送到博客上有什么区别呢,好像也没什么区别,但如果想要支援多平台的话,感觉就很麻烦了。而且我确实很讨厌把差不多的事情重复做很多次。
所以还是来点纯粹的自动化吧!
History
那么讲一讲我博客的历史……曾经用过的方案包括:
-
WordPress,感觉已经是上个时代的产物了。庞大、臃肿,功能过于丰富,破事很多,成本也不低。而且商业化气息浓厚,各种付费主题插件之类的,总之对个人博客场景感觉不是很好的选择。
- BTW,我对 PHP 没什么好感(
-
typecho,相比 WordPress 当然是清爽简洁了许多,但是……动态博客对个人场景来说还是比较 overhead,因为主体静态内容明明是可以随便 CDN 分发的才对。而且感觉功能又有点过于简陋了。
- 同样也是上个时代的产物了。唉,PHP.
-
Hexo,静态博客是一阵风啊,突如其来地席卷了大江南北。当然,Hexo 是好的,但当初还是遇到了一些问题,主要是:构建时间长。
- 对于现在的我来说,不使用 Hexo 的主要原因可能是……定制化没那么方便?基于模板的方案,不是很自由。
- 主要就是不是很自由吧。
-
Hugo,构建速度非常的神清气爽啊,上个博客就是这个版本的。
- 主要问题还是定制不太方便,用的是 Golang 的模板语法,原来尝试改过一点主题,真是令人痛苦万分啊。
Arch
那么,归纳一下我的需求:
- 以静态为主,但要能够结合一些动态内容。最好能直接渲染成 HTML,但支持在里面内嵌一些 Component,走前后端分离架构那套玩法来添加一点动态内容。
- 高度可定制化,需要什么小功能就能自己搓。
当初我听说过有一个框架叫做 Gatsby,不过现在更流行的是 Astro. Astro 虽然可以用来做博客,不过更多时候会拿来做一些企业的 Landing Page. 它的设计架构非常的漂亮,整体上说:
- 同时支持 Server Component 和 Client Component,可以混合动静态内容。
- 使用类似 JSX 的语法,写起来令人倍感亲切,比学奇奇怪怪的模板方便多了。
- 在 Static Site Generation 阶段,可以直接执行 JavaScript 代码,调用给出的 API 即可,非常非常灵活啊。
是的,所以理论上在 Astro 里面我们可以做到这样的事情:
- 在 build 阶段,调用 API 拉取 CMS 的内容。根据 API 调用返回值直接构建静态网站。
而如果你不想静态构建的话,Server Side Rendering 也是改个配置的事情。非常好文明。
Implementation
在实际的搭建过程中,顺序大概是这样的:
- 先用 Astro 把博客的前端跑起来。
- 搭建 Directus,折腾 Astro 成功对接 Directus 的后端内容。
- 手搓一个小工具,实现思源笔记导出内容至 Directus 中。
- 手搓博客的 i18n、评论区、RSS、OgImage 等功能。
- 跑 SeaweedFS,跑 WebP Server 把图床跑起来。
- 使用思源笔记的发布插件对接 S3 Backend 方便上传图片。
Astro
虽然 Astro 挺好的, 主要是它可以非常方便地结合前端技术栈,爱用啥用啥。但还是有一些 Drawbacks,目前我注意到的几点不足是:
-
拉取 Remote Content 的 Content Layer API 还不是很成熟,许多文档缺失。虽然已经可以用了,但官方没有暴露方便的 Markdown Render 接口,导致拉取了远端的 Markdown 内容还要自己想办法渲染。
- 渲染 Remote Markdown 最后的解决方案是我在官方 Discord Channel 的聊天记录里找到的…
-
Incremental Build 虽然有,但只支持本地 Markdown 文件,只要打开增量构建,Remote Content 会直接消失。果然是非常 Experimental 的 Feature 啊。
-
默认没有异步构建,或者说默认的 Concurrent Number 为 1,导致如果你的 Single Page Build 过程中有阻塞工作,总构建时长就会非常令人感叹。
-
开放的 API 感觉缺了不少东西,比如说缺少为使用者提供的 Cache API 之类的,只能自己脏脏地乱写。
-
没有足够开箱即用的 i18n 支持,虽然有 i18n router,但是灵活度差强人意,而且文档中给的方案也并不好。
Whatever,里面的大部分问题都被我想办法解决掉了。
i18n
具体探索掠过不谈,直接讲目前的 Solution:
使用 paraglidejs,一个 i18n 库,在 yml 中写翻译,然后 compile 成 js 文件,就可以直接在 Astro 中调用了。
但你依然需要为不同的语言生成不同的 static files,要做到这一点的话,可以添加一层路由:
然后在 export static path 的时候:
import {
availableLanguageTags, languageTag } from "paraglide/runtime";
import * as m from "paraglide/messages.js";
const posts = [
...(await getCollection("blog")),
...(await getCollection("directus_blog")),
];
const tags = getUniqueTags(posts.filter(p => p.data.lang === languageTag()));
export function getStaticPaths() {
return availableLanguageTags.map(lang => ({
params: {
lang } }));
}
大概是这么个意思,为不同的 language 生成不同的路由。
需要注意:Astro 默认的 Content 必须有 unique slug,而这意味着 [lang]/[slug]
这样的路由会导致 zh-cn/mypost
和 en/mypost
只能指向同一个 mypost.md
,这个时候一个解决方案是:写文件的时候把 slug 开头加上 [lang]-
,然后生成路由的时候删掉。
for (const lang of ["zh-cn", "en"]) {
if