这个博客最开始并没有什么宏大的目标。
我只是想要一个自己真正愿意长期写下去的地方:能记技术踩坑,也能写一些还没有完全想明白的东西;最好不依赖某个平台,不被特定的编辑器和发布流程绑住,哪天想搬走时,所有文章仍然只是普通的 Markdown 文件。
所以现在看到的这个站,虽然外观仍然能看出 Fuwari 的影子,但底下的工作流已经改了不少。
这篇就当作一次阶段性记录,简单拆一下它现在是怎么工作的,以及这段时间到底改了些什么。
一、先看整体架构
目前整个博客大致可以画成这样:
┌─────────────────────┐ │ GitHub Repo │ │ Markdown + Code │ └──────────┬──────────┘ │ push ▼ ┌─────────────────────┐ │ GitHub Actions │ │ install / check │ │ build / Pagefind │ └──────────┬──────────┘ │ dist ▼ ┌─────────────────────┐ │ Wrangler │ │ Direct Upload │ └──────────┬──────────┘ ▼ ┌─────────────────────┐ │ Cloudflare Pages │ │ CDN / HTTPS / 托管 │ └──────────┬──────────┘ ▼ blog.ly2u.com
后台编辑:浏览器 → Decap CMS → GitHub OAuth → GitHub Repo
图片:浏览器 → 自建图床 → 边缘加速 → Markdown 外链核心思路其实很简单:
GitHub 保存源文件,GitHub Actions 负责构建,Cloudflare 只负责托管和分发。
这和最开始直接把 GitHub 仓库接给 Cloudflare Pages 已经不太一样了。
以前每次提交代码,Cloudflare 会自己拉源码、安装依赖、重新构建;后来 GitHub Actions 本身也在做一遍 pnpm build 检查,相当于同一份代码被构建两次。
现在干脆把职责拆开:
- GitHub Actions 安装依赖;
- 运行 Astro check;
- 正式构建静态站;
- 生成 Pagefind 索引;
- 得到最终
dist/; - Wrangler 直接把成品上传到 Cloudflare Pages。
Cloudflare 不再参与源码构建,只做静态文件托管、HTTPS 和 CDN。
对一个纯静态博客来说,这套关系反而更清楚。
二、底层还是 Astro,但尽量少让前端“动起来”
博客目前基于 Astro,最初从 Fuwari 主题改出来。
我喜欢 Astro 的一个原因,是它很适合这种以阅读为主的网站:大部分内容在构建时直接生成 HTML,不需要为了显示一篇文章先加载一整个前端应用。
现阶段主要用到的东西包括:
- Astro:静态站主体;
- Tailwind CSS:页面样式;
- Svelte:少数确实需要交互的设置组件;
- Pagefind:静态全文搜索;
- Swup:站内页面切换;
- Expressive Code:桌面端代码高亮;
- PhotoSwipe:图片查看;
- OverlayScrollbars:桌面滚动体验;
- Decap CMS:网页端文章编辑后台。
不过后来的优化方向,反而一直是在做减法。
比如搜索并不会一打开网页就加载 Pagefind,而是第一次点击搜索时才加载;PhotoSwipe 也是用户真正打开图片时才引入;OverlayScrollbars 只在桌面端、精细指针设备上初始化。
右侧文章目录也是一样。以前只是用 CSS 在移动端把它隐藏,JavaScript 仍然会扫描标题、创建 Observer、计算位置。现在小屏幕下干脆连目录逻辑都不初始化。
对博客来说,很多“功能”其实并不需要一直在线。
三、真正折腾得最久的,是 iPhone 上的长文章
这个站目前改得最深的一块,其实不是外观,而是移动端代码阅读。
之前写图床那篇文章时,里面有很多代码,还有一个接近 200 行的 Go 程序。Android 和桌面都正常,但在 iPhone Safari 上快速滑动长文章时,正文会突然出现大面积空白,停下来之后才重新补绘。
最开始怀疑过很多东西:
- 右侧 TOC;
content-visibility;- 页面动画;
backdrop-filter;- 多层
overflow:hidden; - 代码块自己的滚动条。
最后做了一轮比较彻底的 A/B,才发现真正影响最大的,是长页面中常驻的 Expressive Code 复杂 DOM。
桌面端完整的代码块很好看,但一段几百行的高亮代码会生成大量 token <span>。多个代码块叠加后,iOS WebKit 在快速滚动时很容易出现绘制跟不上的情况。
最后没有继续硬调 CSS,而是换了一个思路:
正文负责阅读,代码按需查看。
现在移动端的规则是:
- 很短的代码转成轻量代码框;
- 稍长的代码在正文里变成“代码卡片”;
- 点击卡片后,再打开独立的全屏代码阅读器;
- 完整的代码 DOM 只在需要时出现,关闭阅读器后就销毁;
- 桌面端仍然保留完整 Expressive Code。
这其实比原来直接把 200 行代码铺在手机正文里更合理。
最初只是为了修一个 Safari 性能问题,最后反而变成了一个我更喜欢的移动端阅读方式。
四、Swup 也踩过一个很隐蔽的坑
另一个比较典型的问题,是桌面端点击页面时会“闪两下”。
一开始以为是两套过渡动画叠加,于是把内容入场动画、Swup fade 动画都关掉过,但问题仍然存在。更奇怪的是,录屏里连地址栏都会出现短暂的:
/目标页/ → / → /目标页/这就说明它不是单纯的视觉闪烁,而是真的发生了二次导航。
最后查到 Swup 配置要求每次同时替换:
main#toc但旧布局里 #toc 是条件渲染的:文章有目录时才存在,首页、归档、传送门等页面可能根本没有这个节点。
对于 Swup 来说,不同页面的容器结构并不稳定,于是切页时就可能出现异常回退。
最终的解决办法很简单:
所有页面始终保留
#toc容器,没有目录时只让它为空,而不是让节点本身消失。
修完以后,双跳转和闪屏都消失了。
这个问题也挺有代表性:有时候看起来像“动画问题”,真正的根因却是页面结构契约被破坏了。
五、后台并不是另一个数据库,文章仍然只是 Markdown
我不太想为了一个个人博客再维护一套数据库和复杂后端,所以后台编辑继续使用 Decap CMS。
它本质上只是一个 Git 的图形化编辑界面。
在浏览器中打开后台后,通过 GitHub OAuth 完成身份验证;文章编辑、创建、保存,最后仍然是对仓库里的 Markdown 文件提交 commit。
也就是说,无论文章是:
- 在本地编辑器里写;
- 在 GitHub 里直接改;
- 在 Decap 后台改;
- 或者由自动化工具修改;
最后落到仓库里的东西都是同一种格式。
这一点对我来说很重要,因为 Markdown 才是最终的数据源,后台只是一个入口。
目前 Decap 还做了几处自己的定制:
- 后台界面做了简单的中文化和样式整理;
- 粘贴图片时自动压缩为 WebP;
- 图片不进入 Git 仓库,而是直接上传到自建图床;
- 上传成功后自动插入 Markdown 图片链接;
- 可以从 GitHub 重新读取文章原始 Markdown;
- 保留一个快速进入外部 Markdown 排版器的入口。
这样仓库不会因为图片越来越多而迅速膨胀。
六、图片和文章分开存
图片目前走的是自建图床,而不是把所有原图都塞进博客仓库。
编辑后台里粘贴一张图片时,大致会经历:
剪贴板图片 ↓浏览器 Canvas 压缩 ↓WebP ↓自建上传接口 ↓图床存储 ↓边缘加速 ↓返回 https 图片地址 ↓插入 Markdown浏览器端会限制最大尺寸,并在上传前进行一次压缩。
这样做的好处很实际:Git 仓库里主要还是文字和代码,构建速度、clone 体积和历史记录都不会被大量二进制图片拖累。
头像后来也从仓库里的 2MB 多 PNG 改成了图床上的 WebP。
七、发布日期和最后修改日期分开了
以前文章只有 published。
但博客文章发出去之后,后面很可能还会继续修错字、补步骤、改代码。如果修改过很多次,只有一个发布日期并不能完整说明文章状态。
现在的做法是:
published继续写在 frontmatter 中,代表第一次发布;- 构建时通过 Git 历史读取这篇 Markdown 最近一次 commit 时间;
- 只有文件至少发生过第二次提交时,页面才显示“最后修改日期”;
- JSON-LD 中同时写入
dateModified。
这样不需要每次手工维护 updated 字段。
无论通过哪种方式改文章,只要最终进入 Git 历史,更新时间就会自动变化。
文章标题旁边现在也放了一个很轻的编辑图标。对普通访问者来说它只是一个入口;真正点击后仍然需要经过 GitHub OAuth 和仓库权限验证。
对自己来说,就可以从“正在看的文章”直接进入“这篇文章的后台编辑页面”。
八、从 Fuwari 到现在,主要改了什么
如果只看目录结构,这个站仍然能找到不少 Fuwari 的东西;但实际使用体验已经改了很多。
目前比较明显的变化包括:
内容层
- 删除主题自带的演示文章和示例内容;
- 重做“关于我”和“传送门”;
- 增加友链区域;
- 增加更符合自己使用习惯的站点描述和导航;
- 增加自动最后修改时间;
- 增加文章快速编辑入口。
性能层
- Pagefind 改为第一次搜索时再加载;
- PhotoSwipe 改为按需加载;
- OverlayScrollbars 只在桌面初始化;
- 移动端不初始化右侧 TOC;
- 滚动和 resize 监听做节流;
- 移动端关闭高成本导航栏模糊;
- 减少不必要的 GPU layer 和入场动画;
- 优化移动端网格,不再保留隐藏的桌面列;
- 大头像改走体积更小的 WebP 外链。
移动端代码
- 移除正文中长期存在的重型 Expressive Code DOM;
- 短代码使用轻量代码框;
- 长代码使用代码卡片;
- 点击后进入独立全屏阅读器;
- 代码阅读器保留行号、复制、横向滚动和语法高亮。
构建与发布
最早:
GitHub → Cloudflare 拉源码 → Cloudflare 构建 → Pages现在:
GitHub ↓GitHub Actions ↓pnpm installpnpm checkpnpm build ↓dist ↓Wrangler ↓Cloudflare Pages这也是我目前比较满意的一次调整:构建环境被固定在 GitHub Actions,Cloudflare 只接收最终产物。
九、为什么没有继续往“更复杂”做
如果愿意,这个博客当然还可以继续堆很多东西:数据库、评论、账号体系、动态 API、在线草稿、实时预览……
但目前我更希望它保持一种比较克制的状态。
一篇文章最核心的东西仍然是一个 Markdown 文件;网站本身可以整个重新构建;图床可以单独迁移;Cloudflare 只是托管层;后台坏了也不影响文章源文件。
换句话说,各部分之间有联系,但尽量不互相绑死。
这也是我现在越来越喜欢的一种搭建方式:
能静态就静态,能在构建阶段解决的就不要留到浏览器运行时;能保持普通文件格式的,就不要急着塞进数据库。
十、接下来还会继续改
现在这个站当然还没有“做完”。
实际上个人博客大概也不存在真正做完的时候。
后面可能还会继续调整:
- 后台编辑体验;
- 文章版本与修改记录;
- 搜索结果展示;
- 移动端代码阅读器细节;
- RSS、SEO 和结构化数据;
- 友链与独立博客之间的连接方式;
- 一些真正对写作有帮助、而不是纯粹为了“功能更多”的小工具。
不过至少到现在,底层的结构已经比较清楚了。
它不再只是“下载一个主题然后部署”的博客,而是慢慢变成了一套符合自己习惯的写作和发布工具。
某种意义上,这也正是我想做博客的原因:
不是先把一切设计完整再开始记录,而是在持续记录的过程中,让工具和自己一起慢慢成形。