用 SvelteKit 重写了博客

警告 本文为草稿,内容可能不完整或有所变动。

Transitional Apps

起因是发现了 siYoutube Transitional Apps 架构——它的工作方式是页面第一次加载时是服务端发送的渲染好的 HTML,加载完后再通过 Hydrate 变成一个类似 SPA 的网页,页面切换由 JavaScript 完成。

Transitional Apps 把传统 SPASSG 的优点结合起来了:

  • 有和 SSG 一样很快的 FCP 速度
  • 有和 SPA 一样很快的切换页面速度
  • 禁用 JavaScript 后网站也能正常显示静态内容,非常适合交互性比较低的的网页

Next.js、Nuxt、SvelteKit 和很多框架都支持这种方式渲染网页。

于是我就瞬间被吸引到了,花了一两天时间用 SvelteKit 重写了一下博客。

Hugo

之前博客用的是 Hugo,痛点有好几处:

  • 没有后端
  • HTML 里写 Go Template 真的很…
  • 用 npm 的依赖很费劲

换成 SvelteKit 后除了解决了以上问题,还有了:

  • 强大的 Vite & Rolldown

败笔

还有一点是用了 MDSveX,类似 MDX,可以在 Markdown 里写 Svelte,但是写的时候发现 MDSveX 的生态实在是太烂了

  • Visual Studio Code 里没有高亮和补全
  • 没有 Prettier 的格式化插件
  • @sveltejs/enhanced-img 的兼容性也很糟糕,两个 preprocessor 会打架
  • 想用 footnote 只能用一个奇怪版本remark-footnotes
  • 没有迁移到 Svelte 5 什么的,log 里会有奇怪的 deprecation notice。

View Transition API

之前看到 Naman Goel 的博客有一个特别帅的动画。点进一篇文章后文章列表的文章标题会有一个动画平移到文章的标题。用到了最近才 baselineView Transition API

于是我也尝试模仿了一下,效果意外的好,实现起来也非常简单。目前这个特性发现的唯一缺点就是有点掉帧。


好像动画越加越多了,有点上小学时做的 PowerPoint 那种感觉,反正就是很多很多平移。

重影

发现从主页进到文章正文时标题会重影,研究了好久,最后发现是 TailwindCSS 的 text-4xl 会设置 line-height: calc(2.5/2.25),然后主页的文章列表用的是默认的 line-height: 1.5,正文标题的 line-height 更低一点,导致 bounding box 不太一样。

因为默认动画的 anchor 在元素的左上方,在 bounding box 不一样的情况下会被对齐到左上角。

头像

发现友链页面很多人的头像分辨率都特别大,有的甚至用了 1024px * 1024px,于是花了半天时间用 siNpm sharp 代理了一下图片请求。

同时顺便

  • 把头像转换到了 WebP 格式,图片更小,加载速度更快
  • 解決了一些站点 Cache-Controlmax-age 太低的问题
  • 解決了 avatars.githubusercontents.com 在国内的访问问题

但是写完了之后才意识到 Cloudflare Workers 压根用不了 sharp 依赖的 libvips 🤡

目录

用了 siNpm svelte-toc 这个 ToC 实现,默认 style 有点丑,但是改了改后还不错。

很可惜不支持 SSR,水合时会闪一下,后面再研究下其他方法。


SSR 终于写好了,简单说就是把 .svx 文件用 siNpm remark 跑一遍,再把从 mdsvex parse 到的 heading 的信息传给 <Toc />

// src/routes/posts/[slug]/+page.server.ts

function extractHeadings(markdown: string) {
	// NOTE: we're ignoring svelte syntaxes inside mdsvex
	const tree = remark().use(remarkFrontmatter).parse(markdown);
	const slugger = new GithubSlugger();
	const headings: { level: number; text: string; id: string }[] = [];

	visit(tree, 'heading', (node) => {
		const text = toString(node);
		headings.push({ level: node.depth, text, id: slugger.slug(text) });
	});

	return headings;
}

其他

同时也重新设计了一遍博客的样式,这次很大程度上参考了朋友的博客,用了浅黄色的主题和衬线字体。

干掉了 i18n 和 tags,感觉前者太麻烦了,后者也似乎没有什么实际用处。