Changelog - Introducing XenT

回顾这个 blog 的历史,一开始是基于 Jekyll,后来因为太慢了,换成了 Hexo。今年我又折腾了一下 Hexo,例如加了中文字体切割,把 Related Posts 换成了基于 embedding model 的 recommendation,但是换来换去感觉还是基于别人的 theme 做。我还粗略看了一下其他的 SSG,例如 Hugo 的 PaperMod,但是感觉还是不如 hexo-theme-next

后来也趁着 vibe coding 这么流行,我也在想要不要借此机会试试 vibe coding。之前的 feature 也都是 vibe code 出来的,我懒得去看 Hexo 的文档了(而且有些地方文档还不清楚)。这次就趁着大改 theme 的机会多试试 vibe coding(主要使用 Claude Code)。

Framework 和 Theme

为什么不继续用 Hexo 了呢?有好几个点总觉得不太舒服:

  • extension 用的是他自己的 tag plugin,我在写 tag plugin 的时候就感觉里面 magic 好多;
  • asset managementhexojs/hexo#3245:我不能搞一个文件夹叫做 post-title/ 里面放所有的 asset (例如图片)加一个 index.md,必须建一个 post-title/ 文件夹和 post-title.md 文件,引用图片要么用 {% asset_img %} 插件,要么自己手动输入绝对路径,用户体验很不好。
  • 开发感觉已经接近停滞了……

第一步肯定是考察用哪个 framework,我就在 GitHub 上看看 #static-site-generate 这个 tag 里有哪些 project 比较 promising。由于不知道怎么看流行度,我只好用 star history 来估计一下。Next.js/Nuxt 我感觉比较重,不是很适合 blog,我就不考虑了。

从图上分析:

  • 论绝对数量,老牌框架 Hugo 一骑绝尘,并且增长势头还在持续;
  • Jekyll/Gatsby/Hexo 这三家虽然绝对数量很多,但是有点增长乏力了;
  • Astro 算得上是异军突起,虽然势头没一开始那么强了,但是还是显著比其他几家快;
  • Eleventy/Zola 感觉一直有点不温不火?

基本上就是 Hugo 和 Astro 中间选一个了……

  • Hugo 我看了一下,感觉不是很喜欢。例如 extension 用的是他自己 shortcode 系统,这个就和 Hexo 的风格更接近了。它比较好的一点就是 single binary,不像 node.js 那样一装就是一堆包,而且 node.js 的 ecosystem 很容易被 supply chain attack Popular Tinycolor npm Package Compromised in Supply Chain Attack Affecting 40+ PackagesDuckDB NPM packages 1.3.3 and 1.29.2 compromised with malware
  • Astro 更像是为前端人准备的,甚至我还从中知道了 MDX 这个文件格式,可以在 Markdown 中写调用新的 component。这样 Astro 里面有两种 markdown 扩展方式:
    1. 通过 MDX:我去写 Component,然后用户可以调用 component。这种解决方案更加前端。
    2. 写 remark/rehype 插件:例如使用 remark-directive 插件,可以轻松实现 shortcode/tag 这样的扩展方式。这样和 Hugo/Hexo 的 compatibility 都可以搞定了。

所以我思考了一下,选择了 Astro。

至于 theme,我有两个选择,一个是用 Astro 的一个 theme,另一个是自己从头造一个 theme。由于我一开始尤为钟爱 hexo-theme-next,我决定把 hexo-theme-next 迁移到 Astro 上面去,这就是为什么代码里一开始有很多的 NexT 相关代码。可是后来写着写着就感觉不行我要做很多优化、删减,再到后来发现已经完全变样了,那就索性自己开始 innovate 了……

至于名字呢,既然原来的主题叫做 NexT,我也玩玩梗,改成 XenT,然后 icon 就变成两个 distribution,这不就是 cross entropy 了吗哈哈哈哈哈……XenT 的 sample website 有两个,一个是我开发时用来测试的网站,另一个就是这个 blog。至于代码嘛,我暂时还没有放出来,主要是感觉还有好些东西可以调……

XenT 开发笔记

XenT 的视觉风格用我自己的话说是 clean, minimal, yet functional,就是能简化的地方都想简化一下,不会加奇奇怪怪的元素,东西都是必须要出现了才出现。

我考虑过用 Tailwind.css 或者 shadcn/ui,但是感觉这样对于一个 static blog 是不是太 heavy 了……

抄 NexT:sidebar 和顶部进度条

XenT 里很多地方借鉴了 NexT。一开始开发的时候基于 NexT-Gemini,但是后来又借鉴了一些 NexT-Mist,所以有可能有点不伦不类的……

这个 sidebar 就是学习 NexT-Gemini 的,但是我把它做成了一个可开关的,开关在导航栏右上角。sidebar 里的 TOC 就是把 NexT-Gemini 抄了过来,我很喜欢的一点是自动跟随,自动展开,尽量少给信息,但是必要的信息会给足。移动端上,我就把 sidebar 改成了 overlay 而不是 block。NexT-Gemini 里的 sidebar 还有个 Site Overview,我觉得意义不大,就扔了。

网站最顶端有一个 progress bar 来提示看了多少了,这个我觉得也挺好的就抄过来了。

配色系统

我之前的 blog 的配色被我自己大改了一发,用的是 Nord 这个 color palette,但是其实还有一些颜色没改成,例如 callout 的颜色。这次我也想延续这个做法,甚至想更进一步,可以在前端切换 colortheme。我和 ChatGPT/Claude 聊了很久,最后整出了一套方案,大意是:

  • 第一层:每个 color theme 有 16 个核心颜色,例如 --primitive-danger: #dc3545;
  • 第二层:我们定义了一些 semantic token,例如 --interactive-primary: var(--primitive-brand);
  • 第三层:每个 component 用 component token,例如 --navbar-bg: color-mix(in srgb, var(--surface-page) 95%, transparent);

至于切换的话,在 root 上打一个 data-theme="nord" 就行了。

抄 Tufte CSS:sidenote

我之前无意中看到了 Tufte CSS,很喜欢 sidenote 这个想法,比 footnote 看起来舒服多了,不用跳来跳去,Thinking Machines Lab 的文章也是这么搞的。所以我就去研究了一下,把所有的 footnote 换成了 sidenote。这篇文章之前出现过 footnote,应该已经有印象了。

移动端上,label 可以点击,点击之后就可以看到 note。Thinking Machines Lab 的方式是不需要点击,我觉得这个问题都不大。

至于 sidenote 的实现,可以参考 Gwern,他这网站可做的太 fancy 了……

没啥好说的,感觉很标准……就是左边 title,中间是主要内容,右边放一些小工具。右边的小工具包括:RSS,sidebar 开关,和一个颜色切换开关。

navbar 的一个问题是怎么在小屏幕上显示。我现在的做法是屏幕小了就把文字省掉,但是感觉更好的做法是用一个 dropdown 显示出来……

行高

我个人喜欢高一点的行高,看起来更加 relax 一些。但是试了一下后发现,中文和英文需要的行高不一样。所以我就在渲染的时候检测是否有 CJK 字符,有的话就把行高调高一点。我一开始想的方法是自动检测语言,但是后来发现这个检测还是太菜了,索性简单一点检测字符了。

抄 Astro-m2dx:找文章概览

首页里我们可以预览一个 post,那么这个预览怎么写呢?Hexo 里的解决方案是用 <!-- more --> 来自动隐藏后面的。我一开始的想法是用 LLM 来 summarize 一下,但是这个暂时还比较 heavy。我不太想用 Hexo 这个解决方案了,后来在网上冲浪的时候发现了 Astro-m2dx,做法是文章开头到第一个 heading 之间的内容就是概览,我一听觉得太 make sense 来。但是我的实现和 Astro-m2dx 稍有不同,我会把内容也渲染出来(所以渲染主页会比较慢……)

数学公式

由于我有很多 post 涉及大量公式,所以我必须要在文章中加入数学公式的支持。一种方案自然就是和之前一样,在浏览器上跑一个 MathJax/KaTeX,由他们去渲染。但是这是 Astro 啊,前端发展了这么久,自然是有一些东西了。所以我学到了一个东西叫做 SSG,Static Site Generation,直接在 markdown 生成 html 的时候把公式全部解析了,都不需要浏览器上跑 js 了!

这里我用的 KaTeX,没有用之前的 MathJax 了,不过差别不大……

理论上我还想用 typst 的数学模式,语法比 LaTeX 不知道好到哪里去了。虽然 typst 也支持 SSG,但是有两个问题:

  • Typst 现在官方还不支持 math mode 下 export to html typst/typst#5512,没有 MathML,只能生成 SVG;虽然有一些第三方的 converter,但是总感觉有点不放心。
  • 因为语法太不一样了,我们现在需要支持 per-post remark plugin 的支持,这个看起来只能让我们自己写一个 meta plugin 来搞了。

评论系统

虽然我加入了 isso 的支持,但是我现在暂时把评论功能禁掉了,因为我现在还没想好到底该怎么支持评论。显然,Disqus 是不能用了Goodbye Disqus - Your injected ads are horrible,isso 其实问题不大,但是少了很多东西(例如提醒),而且我现在 url 也变了,估计得操作一下 isso 的数据库才能转换过来。

后来在 Hacker News 看到了一个很有趣的想法:用 Mastodon 或者 BlueSky 来做评论 Client-side comments with Mastodon on a static Jekyll websiteOn Mastodon-powered Blog CommentsAdding comments to your static blog with Mastodon。我觉得似乎还挺 make sense 的……但是我懒得去实现它,就先放这里吧 →_→

扩展语法

之前也提到了我们有两种扩展方式:remark/rehype 插件和 MDX component。

我自己写了两个 Component:

  • <Gallery>:主要用来展示一堆图片,例子见这里。基本上就是几个库合在一起:用 justified-layout 来计算每张图片最后大小应该是多大,然后用 PhotoSwipe 来展示所有图片。下面会提到我对图片做的一些优化。之前的 blog 用的是 Carousel from Fancyapps 来展示图片,后来才意识到这实际上是一个收费的库……
  • <YouTube>:这个就很简单了,插入一个 YouTube 视频……

至于 remark/rehype 插件,我用了好几个:

  • r4ai/remark-callout:实现类似 gfm/obsidian 的 callout

  • remarkjs/remark-github:把指向 GitHub 的一些链接变得好看点

  • remarkjs/remark-directive:支持一些 directives,例如我加了两个 directive,::youtube:::gallery ,这样可以不写 MDX 语法了 23333

    ::youtube[dQw4w9WgXcQ]
    
    :::gallery
    ![](img1.jpg "title 1")
    ![](img2.jpg "title 2")
    ![](img3.jpg "title 3")
    :::
  • remarkjs/remark-math:把数学公式 wrap 一下,之后就可以交给 mattfeng/rehype-katex 来做 SSG 了

  • rehypejs/rehype-external-links:自动识别 external links,然后加上一个 .external-link 的 class

  • 自己写了几个小插件,用来:

    • 把 footnote 变成 sidenote
    • 计算每个 post 的阅读时间

图片优化

我之前一直想做的一件事是自动优化图片。优化主要分为两类:

  • 图片格式:我的一些图片是 JPG/PNG,而现在 WebP 几乎所有现代一点的浏览器都支持了,AVIF 的支持率也很高https://en.wikipedia.org/wiki/AVIF#Web_browsers,所以我们可以直接使用这些更为先进的格式来省空间,甚至 repo 里面可以直接放 iOS 导出的 HEIC 了。

  • 分辨率:<Gallery> 中用到的 PhotoSwipe 支持缩略图,那么在文章中,我们应该先载入缩略图,用户点击了图片之后再载入原图。甚至,我们真的是否需要载入原图?PhotoSwipe 自己都说

    PhotoSwipe is not designed to display very large images.

    我最后的解决方案是:缩略图我会 resize 到 100k pixels,原图我会 resize 到 2M pixels(约为 1920*1080)。

说起来 Astro 要支持 HEIC 还有点麻烦,我鼓捣了一下才搞好。

  1. 不能使用预编译的 sharp,因为它默认带的 libvips 没有 HEIC 解码器。所以我们需要:
    1. 在系统里安装 libvips,例如 brew install libvips
    2. 安装编译需要的 node-gypnode-addon-api
    3. 安装 sharpnpm install sharp
  2. 改一下 Astro 的文件让它不会报错:改 node_modules/astro/dist/assets/consts.js 这个文件,在 VALID_INPUT_FORMATSVALID_SUPPORTED_FORMATS 这两个 list 内加入 "heif"

检索系统

之前 hexo-theme-next 有一页来显示所有 posts,再搞了一个插件来做 local search,还有专门的页面来显示每个 tag/category 有哪些 posts。我在这里 innovate 了一下,核心 idea 是:刚才这几个 feature 本质上都是显示通过某种方式 filter 后的 post 列表,那么我为啥不直接用一个网页搞定呢?

这里的 /posts 可以做好几种 filtering,例如按照 category/tag 分,在标题中搜索,在内容中搜索等等。我用了 Fuse.js 来做这件事,甚至还支持一定程度的 fuzzy search。不过这里的一个问题就是文章标题是 markdown,没法渲染 LaTeX\LaTeX 了,因为我们用的 SSG。

在 taxonomy 这方面,我思考了一下,感觉不需要支持 hierarchical categories,所以我又把所有的 category flatten 掉了。

文件格式

最常见的文件格式就是 Markdown,已经成为了 de facto,基本上所有的静态博客 framework 都会支持 Markdown。但是 Markdown 的问题就是扩展性,各个 framework 有自己的自定义语法,例如 Hexo 是 {% tag %} Hugo 是 {{< shortcode >}} 。Astro 里面通过 remark/rehype 插件来扩展,但是如果你用 remark-directive 的话,是不是也是一种自定义语法了呢……你也可以继续 argue 说现在大家都可以用 remark 这套 ecosystem,所以这种 extension 是更加合理的,但是这样的话 pandoc 为啥不是呢……

Astro 这里对 MDX 有原生支持,比起 Markdown 来说,扩展的方式更加 principle 一些,也更符合前端的人设一些。我用下来感觉 MDX 没啥雷点。

另外一个我正在观察的文件格式是 typst。typst 好处都有啥?谁说对了就给他!typst 有几个我很喜欢的 feature:

  • typst 对数学公式的支持太 native 了,而且语法不知道比 LaTeX\LaTeX 好到哪里去了,自己就可以做 SSG,可以跳过 rehype-katex 了。
  • typst 能够 scripting,文档即代码。
  • typst 有三个 mode:script mode, text mode, math mode,三者的切换非常顺畅,我用之前没料到会这么顺畅。

但 typst 的问题是它太早期了,ecosystem 不成熟,甚至 1.0 都还没发布,html export 都是 experimental 的。之前也说了 typst 的数学公式只能输出成 SVG,而且 typst 虽然通过 html export 支持了写前端,但是真要想用 typst 写 html 结构,估计能把自己恶心死……如果 typst 专门有个 html mode,或许也能成为写前端的一把好手呢 →_→

另外还有 Quarkdown,但是一来我不喜欢 .func 这个格式(就不能用一个更加少见的 identifier 吗, :func 都好啊),二来……它 compiler 是 Kotlin 写的,意味着我还得装一堆 Java 东西才能跑起来……

参考