Astro 博客迁移小记
重复造轮
花了两周多的时间,把博客完整地迁移到了 Astro 上。作为新博客的第一篇文章,就简单地小记一下这次迁移吧。
动机
一直以来都有自己动手搭建博客的想法。在静态生成博客这条路上,已经有了 11ty、Hexo 和 Hugo 等优秀框架,但同时这些框架也各有各的局限、或者大概不是局限但我用不懂的地方。11ty 配置繁杂,难以集成现代 JS 框架;Hexo 强制使用 ejs,不够灵活;而 Hugo 直接脱离了我熟悉的 JS 领域。
于是,大约一年前,我自己动手写了一个静态网页生成,试图实现我想要有的所有功能:模块化、多语言、分页、分类、完全静态。最后写是写出来了,但是架构上的问题不小(不如说其实根本没有架构),后续也基本无法维护,于是就荒废在那里了。
后来听说了 Astro,看了一眼,最大的感想是这个框架把我想做的东西给做完了。于是趁着刚入职没有很忙,就开始动手迁移了。
步骤
重构的目标还是和之前一样,支持多语言、分页、分类和完全静态。虽然其实多语言和分页并不太必要,因为很难说这个博客还会有中文以外的语言的文章,也很难说文章总数能够超过一页,但本着先做了比以后再加要方便的想法,还是做了。
1. 添加集成
说是完全静态,但实际上这个博客的背景就显而易见地使用了 JavaScript 进行随机生成。当时是用了 Mithril.js 这个小众框架写的这个背景生成,但是 Astro 并没有官方的 Mithril.js 集成,为了避免麻烦就使用了 Vue。再加上能够对 Markdown 文档进行更精确的控制的 MDX 和其实意义并不太大的 Sitemap,这方面就算是完成了
2. 迁移页面
这一步相对比较轻松,因为总的而言,我不需要再从零重新设计一套页面,直接把之前写的 HTML 模板粘贴到 .astro
文件里就完成了 70% 的工作。而且因为原架构的模板本来就是组件化的,所以也省去了大量的重拆复用组件的时间。
不过另一方面,我还是会忍不住去调整与修改这些页面,比如大量的 CSS class 重命名。
3. 路由与多语言
其实这一点也是我当初自己手写静态生成的最大原因,因为我发现,想用市面上存在的静态网页生成工具同时实现多语言和分页(甚至再加上分类),其实是相当繁琐的。
那 Astro 在这方面做得怎么样呢?说老实话,一般。文档里的 这篇指南 中提供了一个实现多语言的思路,同时推荐了一些社区插件,但因为 Astro 自身对于页面渲染的一些限制,不管哪个插件对于多语言的支持手段都是比较“脏”的。这也使得我最后还是选择基于指南的内容,自己重造一套多语言相关的工具。
Astro 的路由策略是基于文件的路由,这个策略简化了配置,但是增加了对于路由生成的理解成本。这个博客就是将页面布局(layout
)和路由(pages
)完全分开了,layout
只处理布局,而 pages
只处理由 getStaticPaths
生成路由及准备数据的过程。
如果去看这个博客的源码,能看到在 pages
文件夹下的 .astro
模板文件里,每个 getStaticPaths
里都有很长一串。毕竟,当语言、分类、分页三个维度同时交织在一起的时候,这种繁杂就是不可避免的了:文集先被语言和分类条件分别聚合,再被分页拆分,而每个语言、每个分类的每一页,都对应着一个实际的页面。
不过至少,这种多维度的路由生成需求在 Astro 里是可实现、且可以逻辑清晰地实现的。题外话,在我重构博客的时候,Object.groupBy
已经进入了实验阶段,可能再过个几年能用上这个方法的时候,相关的逻辑又能简化一点。
4. 完善功能
布局是迁移完了,但是这不代表所有的功能都回来了。如果说路由生成是博客迁移过程中最麻烦的地方,那么将文章页的功能全部重现则是难度最高的地方。而且,其中一些功能甚至是用 cheerio 直接拼凑 HTML 实现的,先不说效率如何,至少在 Astro 这个框架下肯定是无法直接复用逻辑的。
不过最后,这些功能都还是基本通过其他的手段实现了:
- 分体式目录:原本用 cheerio 检索标题(
h1, h2, ..., h6
)后将标题内容汇总,再喂给相关组件,Astro 在通过entry.render()
渲染 Markdown 内容后会自动生成headings
标题列表,可以用同样的逻辑处理并渲染。 - 标题链接:原本也是用 cheerio 检索标题,然后直接将原有 HTML 块用自定义字符串包了一层,并在标题前加上图片。Astro 中使用了
rehype-autolink-headings
这个 rehype 插件实现 - 图片 / 表格样式:原本用
markdown-it-attrs
往渲染后的元素上挂 CSS class 实现。现在换用了 MDX,可以直接引用自制 Astro 组件解决这个问题。 - 代码块高亮:原本用 Prism 渲染。Astro 会在零配置下,自动对代码块使用 Shiki 进行高亮渲染,然后就没有然后了。
- 代码块复制:原本用 cheerio 检索
pre code
选择器,然后外包含有复制按钮的 HTML 代码。对于这样的功能, rehype 生态圈没有(也不会有)直接可用的插件,所以手写了一个一次性简易 rehype 插件对渲染后的 HTML 抽象树(即 hast)进行了类似的操作。 - 文章评论:之前使用了 Utterances 作为简易的评论功能,但由于其似乎不再维护,因而转到了 Giscus。
不是小结
作为迁移完博客的感想,总体而言,Astro 的设计还是不错的,不然我也不会费那么大力重启博客,并迁移至 Astro 了。
然而,在使用 Astro 的过程中也不可避免地遇到了一些小坑,比如:
- 当前,Astro 的 VSCode 扩展并没有对
.astro
组件内的 Sass 样式块进行支持, - 仅在内容集合(
src/content
)的 MDX 文章内引用了的组件,其style
和script
块中的内容不会被正确提取至网页。(astro#7761)- 临时的解决方案是在用到了这些组件的布局中,用一个不可见的 HTML 重复引入这些元素。
- 由于
getStaticPaths
本质是一个独立的代码域,并优先于组件内的其他代码执行,所以在其外部定义的局部变量是无法被它捕获到的。- 这一点与其说是坑,不如说是设计如此。只是说对于初次接触 Astro 的人而言,这一点确实比较难理解。
- 不过,由于
import
是静态分析,通过import
引入的内容(例如常用的getCollection
)可以正常使用。
- Astro 的
RehypePlugin
类型和 unified(即 rehype 全家桶)提供的Plugin
类型是冲突的。- 在使用了
astro.config.ts
的情况下,如果使用的 rehype 插件为 JS 编写,还可以as RehypePlugin
解决;但如果是 TS 编写,并使用了Plugin
接口,就只能使用丑陋的as unknown as RehypePlugin
了。 - 不过,鉴于 unified 自身也是 JS +
d.ts
形式提供的类型,估计这个问题也并没有那样的解决方法,可能最后还是该用as
就用as
吧。
- 在使用了
在这里也稍微列一下这些点,说不定能帮到一些并不存在的路人。
以后?
其实,重启博客的原因一方面是遇到了心仪的轮子,而另一方面更是出于最后还是能够在互联网上留下点什么的念想。不过说老实话,我也是个典型懒人,精力被工作偷掉之后也就不剩多少给兴趣爱好,更别提写小文章了。所以念想大概率也只是个念想。
想记的其实很多,从代码经到一些创作感想再到手搓小游戏开发日记之类的东西都想记一记,这样,日后别人问起我做过些什么,还可以潇洒地甩个链接过去。倒是说,以前还挂记着可能博客放在网上会有人看一眼,现在也没有那样的执念了,只能说深刻地认识到自己自我品牌打造(Self Branding)能力的孱弱。
话虽如词,既然好不容易重写了博客,随意塞点东西也是好的。俗话说得好,独乐乐不如众乐乐嘛。