用 Cloudflare Workers + D1 + R2 手写一个极简博客

为什么不用现成框架

看过一圈静态博客生成器(Hexo、Hugo、Astro、Next.js),总觉得太重:本地要装 Node 环境,写一篇文章要跑 build,部署一堆文件,想改个样式还要翻主题文档。

最终想要的是:

  • 写文章 = 在浏览器后台粘贴 Markdown,点发布
  • 访问 = 边缘节点直接吐 HTML,毫秒级
  • 数据库 = 不用自己管服务器
  • 图片 = 不用自己管存储
  • 成本 = 趋近于零

Cloudflare 的 Workers + D1 + R2 组合刚好满足。Workers 每天 10 万次免费请求,D1 5GB 存储,R2 流量免费(不收出口费),个人博客根本用不完。

技术选型

最终方案是 Worker 手写模板渲染 + 边缘缓存,不上 SSR/SSG 框架:

  • 后端:单个 Worker 脚本,处理路由、API、管理面板
  • 数据库:D1(SQLite 语法),存文章、标签、分类、设置、媒体元数据
  • 文件存储:R2,存上传的图片
  • 前端:Worker 直接返回渲染好的 HTML,配合 Cache-Control: s-maxage 让 Cloudflare CDN 缓存
  • Markdown 渲染:marked,在 Worker 端编译成 HTML

这样每次请求:命中缓存就直接返回静态 HTML,没命中才查 D1 渲染——既有 SSG 的速度,又不用本地 build。

数据库表结构

建了八张表:

  • posts:文章(标题、slug、Markdown 正文、HTML 缓存、封面、发布时间)
  • categories:分类
  • tags:标签
  • post_tags:文章-标签多对多
  • media:媒体库(文件名、R2 key、mime、大小、上传时间)
  • settings:站点配置(站名、副标题、SEO、自定义 CSS)
  • reactions:点赞(文章 ID + emoji 类型 + 计数)
  • admins:管理员账号

实现的功能

前台

  • 时间轴首页,最新文章在最上面,滑到底自动加载旧文章(Intersection Observer)
  • 文章独立页,带目录和 emoji 点赞
  • 标签页、归档页、RSS、sitemap
  • 完整 SEO:title、description、Open Graph、JSON-LD

后台

  • 密码登录(HttpOnly Cookie)
  • 文章编辑器(Markdown,实时预览)
  • 分类标签管理
  • 媒体库:上传图片到 R2,网格缩略图、复制链接、删除
  • 站点设置:站名、副标题、SEO、自定义 CSS 直接覆盖
  • 点赞数据查看

设计风格

最终选了锤子笔记那种纸感拟物风:

  • 暖纸色背景 #f4f1ea,带细微噪点纹理
  • 米色卡片 #fffdf8,14px 圆角,柔和投影
  • 橙红强调色 #c8503c
  • 橙色圆点时间轴,虚线分隔
  • 拟物胶囊点赞按钮(渐变 + 内阴影)
  • 深色代码块

没有用图标库,所有装饰都是 CSS 画的。

踩过的坑

1. 缓存函数类型不匹配

一开始写了个 cacheable() 包装函数想统一加 Cache-Control 头,但内部渲染函数返回的是字符串,直接当 Response 传给缓存 API 导致首页返回空 body(Content-Length: 0)。改成先 new Response(html) 再包装就好了。

2. Wrangler 静态资源配置坑

想把 favicon 放项目根目录通过 [assets] 配置暴露,结果请求一直超时。最后改成直接在 Worker 代码里加路由,SVG 内联字符串、PNG 转 base64 解码返回——小文件这样最稳,还省一次静态资源部署。

3. R2 媒体索引

参考了一个 R2 文件管理 Worker,它把文件列表存在 R2 里一个 file-list.txt。我改成把元数据写进 D1,这样后台媒体库能直接查 SQL,按时间排序、搜索都方便,删文件时同时删 R2 object 和 D1 记录。

成本

上线跑了一周:

  • Workers 请求:每天几百次,远低于免费额度
  • D1:几 MB 数据
  • R2:几张图,几 MB
  • 月成本:0 元

想做还没做的

  • 自定义域名绑定后自动更新 og:image 的绝对 URL
  • 文章草稿/定时发布
  • 简单的访问统计

部署在 Cloudflare 边缘网络,数据库是 D1,图片存在 R2,没有服务器,没有构建步骤。

觉得有用就点一下: