从 Astro 到 Cloudflare:个人博客开发复盘
整理这次博客开发中遇到的真实问题,以及 Astro、D1、R2、认证、Markdown、LaTeX 和动态样式背后值得记住的技术。
这个博客最初只是一个 Astro 静态页面,后来逐渐加入了在线文章管理、Markdown 与 PDF 导入、图片附件、子文件夹、注册审核、登录会话和评论区。功能越来越完整以后,它也不再只是一个“静态博客模板”,而成了一个很小但结构完整的内容系统。
这篇文章记录开发过程中真正遇到的问题,以及其中值得继续学习的技术。
一、静态生成与动态内容并不冲突
Astro 很适合以内容为中心的网站。页面主体可以在构建阶段生成 HTML,只在登录状态、评论、文章列表等需要更新的位置运行 JavaScript。
这次博客实际上同时存在两类文章:
- 项目目录中的 Markdown,用于版本管理和静态构建;
- Cloudflare D1 中的文章,用于在线新增、编辑与删除。
两者并不冲突,但必须明确谁是数据源。静态页面可以提供可靠的初始内容,浏览器加载后再用 API 数据更新列表。这样既保留了静态站点的简单,也获得了在线管理能力。
真正需要注意的是:如果动态数据会替换静态 HTML,那么动态生成的 DOM 必须拥有与静态内容相同的结构和样式规则。
二、Astro 局部样式与动态 DOM 的陷阱
这次最隐蔽的界面问题来自 Astro 的 scoped CSS。
Astro 会为组件中的元素增加类似 data-astro-cid-xxxx 的属性,并把样式编译成只匹配这些属性的选择器。构建阶段生成的元素没有问题,但使用 document.createElement() 创建的新元素不会自动获得这些属性。
结果就是:页面刚打开时排版正常,API 返回后文章列表被替换,样式却突然失效。
解决方法不是给每个动态元素手动复制内部属性,而是把动态区域限制在一个稳定的父容器下,再显式使用全局选择器:
:global(#latest-posts article) {
display: grid;
grid-template-columns: 150px minmax(0, 1fr);
}
这样规则仍然只影响 #latest-posts,但不再要求子元素拥有 Astro 的编译期标记。子文件夹和最近文章排版不一致,最终都是由这个问题引起的。
三、Cloudflare Pages、D1 与 R2 的职责
整个网站现在由三部分配合:
- Cloudflare Pages:托管静态页面和 Pages Functions;
- D1:保存文章、文件夹、用户、会话和评论等结构化数据;
- R2:保存图片附件等二进制文件。
D1 适合查询“某个专栏有哪些文章”“某位用户是否已通过审核”,R2 则适合保存图片本身。数据库只记录附件的名称、类型、大小和对象键,不应把大型图片直接塞进数据库字段。
把结构化数据与文件分开以后,删除、查询和扩展都会更清晰。
四、在线管理不能只考虑编辑器
一个可用的在线 CMS 不只是一个大文本框。实际使用后很快会出现更多需求:
- 上传 Markdown 或 PDF;
- 从文件头部读取标题、日期、标签与专栏;
- 上传图片并插入 Markdown;
- 用子文件夹组织同一专栏中的文章;
- 保存草稿;
- 在删除附件前明确提示引用风险。
这说明内容管理的核心不是“能够输入文字”,而是降低整理和维护内容的成本。
目前采用的图片方案是把附件上传到 R2,再在 Markdown 中插入普通图片语法:

它比把图片编码进 Markdown 更容易缓存、替换和复用,也更接近 Obsidian 的附件管理方式。
五、密码安全还要考虑运行环境
用户系统采用“注册后等待管理员审核”的流程。密码不会明文保存,而是通过 PBKDF2、随机盐和服务端 Pepper 生成摘要;登录成功后使用随机会话令牌,并通过 HttpOnly、SameSite=Strict 和 HTTPS 下的 Secure Cookie 保存。
本地开发时,PBKDF2 使用较高迭代次数可以正常运行,但部署到 Cloudflare 后出现了 500 错误。最终通过受管理员保护的诊断接口发现:Cloudflare Workers 的 Web Crypto 对 PBKDF2 迭代次数设有上限。
这件事很值得记住:
本地 API 与线上 API 名称相同,不代表所有参数范围和资源限制也完全相同。
遇到线上异常时,应当先把流程拆成“环境变量、密码派生、数据库写入”等阶段逐一验证,而不是反复猜测。
六、限流必须由数据库原子更新
注册限制要求同一 IP 一分钟最多尝试五次,并且累计不超过一百次。
如果先查询次数,再单独执行更新,并发请求可能同时读到相同值,从而绕过限制。因此计数通过 D1 的 INSERT ... ON CONFLICT DO UPDATE ... RETURNING 在一条语句中完成。
限流记录保存的是经过密钥参与哈希后的 IP,而不是直接保存原始地址。这样既能维持计数,又减少不必要的原始网络标识存储。
七、Markdown 公式需要两条渲染链路
博客同时存在静态 Markdown 和从 D1 读取的动态 Markdown,因此 LaTeX 不能只配置一次。
静态文章使用:
remark-math识别数学语法;rehype-katex生成 KaTeX HTML。
动态文章使用 marked-katex-extension 扩展浏览器端的 Marked 渲染器。两边共同加载 KaTeX 样式,才能让下面两种写法保持一致:
行内公式:。
独立公式:
只修静态构建或只修浏览器渲染,都会导致一部分文章仍然无法显示公式。
八、部署成功不等于用户立刻看到新版本
Cloudflare Pages 部署完成后,正式域名的边缘节点有时仍会短暂返回上一版静态文件。浏览器自身也可能保留旧 CSS 或恢复旧页面状态。
因此发布后的检查应分为三层:
- 本地构建产物是否包含预期代码;
- 新部署是否成功;
- 正式域名实际返回的 HTML 和 CSS 是否已经更新。
必要时使用带查询参数的地址或强制刷新,但不能把缓存问题误判为代码没有生效。
九、测试要覆盖完整用户路径
单独测试某个函数远远不够。这个博客现在会自动验证一条完整流程:
- 注册临时用户;
- 确认待审核用户不能登录;
- 管理员审核并修改用户名;
- 使用新用户名登录;
- 创建、读取和删除评论;
- 停用用户并确认会话失效;
- 测试图片、文件夹、文章、草稿和删除;
- 自动清理所有测试数据。
另外,Markdown 数学公式也有独立测试,分别确认静态与动态渲染都能生成行内和块级 KaTeX。
结语
这次开发最有价值的部分,并不是最终多了多少按钮,而是逐渐看清了一个小型网站的边界:内容、文件、身份、权限、缓存和界面并不是彼此孤立的功能。
很多问题只有在真实使用中才会出现。先完成一个可以运行的版本,再根据实际问题一点点调整,往往比一开始追求完整架构更有效。
博客仍然很小,但它已经足够用来继续学习,也足够容纳接下来真正想写的内容。
Discussion
评论区
登录并通过管理员审核后即可参与评论。
登录 / 注册