内容资产要握在自己手里:我怎么搭这个数字空间
一个要维护 5~10 年的个人站,真正的资产是 Git 里的 Markdown、标准 SQL 和 S3 里的对象。平台只是当前的运行环境。
AI 摘要
面向长期维护的个人站,将数字数据区分为内容资产与运行时数据,分别以Git+MDX和对象存储存放,运行时数据可丢可重建;约束正文不进CMS、URL不写死、业务代码不直连平台API,并基于Astro静态输出+Cloudflare免费额度降级策略实现「十年后仍可带走」。
这个站点本身是我对“长期可维护”这个命题的一次实践。目标很明确:五年后我还能改它,十年后我还能带走它。
这个目标一旦写下来,很多技术选择就自动被排除了。
先定义什么叫“资产”
我把数字空间里的数据分成两类:
内容资产——文章正文、笔记、项目介绍、图片。特征是:创作成本高,格式稳定,丢了就真没了。
运行时数据——评论、点赞、阅读量、用户、会话。特征是:可重建,丢了会心疼但不致命。
这个区分决定了整个架构:内容资产必须存在我能完全控制、且格式开放的地方;运行时数据可以放在任何方便的地方。
于是:
| 数据 | 存放 | 格式 | 迁移成本 |
|---|---|---|---|
| 文章正文 | Git | MDX | 复制文件 |
| 图片资源 | 对象存储 | S3 object key | rclone |
| 评论点赞 | D1 | SQLite / SQL | wrangler d1 export |
| 缓存 | KV | 可丢弃 | 不需要迁移 |
| 站点配置 | 代码里 | TypeScript | 就是代码 |
三条硬约束
一、正文必须在 Git 里
不放在 CMS 里,不放在数据库里,不放在任何“需要登录才能批量导出”的地方。
src/content/
├── blog/
├── notes/
├── projects/
├── timeline/
└── pages/
每篇文章是一个 .mdx 文件。改一个错别字是一次 git commit,能看到完整的修改历史。想备份?git clone 或者 git push --mirror 到第二个 remote。
这带来的一个直接好处:数据库挂了,博客一篇文章都不会少。
架构上这句话是有实际含义的——站点第一阶段是纯静态输出,所有文章在构建期就变成了 HTML。运行时的评论、点赞、统计接口全部失败,首页、文章、项目、关于、RSS、Sitemap 照样正常。
反过来,如果文章存在数据库里,那数据库一挂,整个站就没了。这对我这种“可能半年不更新一次,但希望十年后还在”的个人站来说,是不可接受的风险。
二、不把任何一家的 URL 写进内容
这条约束看起来很小,实际最容易破。
错误做法——把 CDN 地址写死在文章里:

三年后换存储商,这行链接就死了。更糟的是你有 200 篇文章,每篇 3 张图。
正确做法——只存 object key:

URL 在渲染时由 StorageProvider 生成。换存储商只改一个实现:
export interface StorageProvider {
upload(key: string, body: ArrayBuffer, contentType: string): Promise<void>;
delete(key: string): Promise<void>;
getUrl(key: string): string;
}
R2、S3、MinIO、Backblaze B2 都是 S3 兼容接口,换起来就是改 endpoint 和凭据。
三、业务代码不许碰平台 API
这一条最容易被违反,因为“直接调 env.DB“实在太顺手了。
// 不要这样
const rows = await env.DB.prepare("SELECT * FROM comments WHERE post_id = ?").bind(id).all();
一旦这么写,D1 就长进了业务代码里。以后要换 PostgreSQL,你得改每一个调用点。
应该这样:
// 业务层只认识接口
const comments = await commentRepository.findByPostId(postId);
// 接口定义
export interface CommentRepository {
findByPostId(postId: string): Promise<Comment[]>;
create(input: CreateCommentInput): Promise<Comment>;
moderate(id: string, status: CommentStatus): Promise<void>;
}
// 实现可以随时换
class D1CommentRepository implements CommentRepository { /* ... */ }
class SqliteCommentRepository implements CommentRepository { /* ... */ }
class PostgresCommentRepository implements CommentRepository { /* ... */ }
业务层完全不知道底下是谁。这不是过度设计——这是“换运行环境”这件事的成本,从“重写整个后端”降到“写一个新实现 + 改一行工厂函数”。
为什么选 Astro
前端框架选型的决定性因素是:它必须能输出纯静态 HTML。
React 生态里的 Next.js、Remix 都能做 SSR,但它们的默认心智模型是“应用”。Astro 的默认心智模型是“内容站”——默认输出静态,需要动态时再加 adapter。
这跟我的需求对上了:
- 文章、笔记、项目全是静态页面,构建期就生成好
- 搜索用 Pagefind,索引在 build 阶段从 HTML 里提取,不需要任何后端服务
- 评论、点赞这类真正动态的功能,用 Server Islands 或 API 路由单独加
所以“后端全挂”的最坏情况是什么?搜索还能用(索引是静态的),文章还能看(HTML 是静态的),只是评论发不出去。这个降级曲线我很满意。
免费额度不是免费午餐
Cloudflare 的免费额度对个人站很够用,但有个前提:你得假设它随时会没。
Workers Free 每天 10 万请求、D1 每天 500 万行读、KV 每天 10 万次读。正常个人站远远用不完——除非你的某篇文章突然上热搜。
所以我给自己定的降级策略:
正常 → 80% 告警 → 90% 降级 → 100% 静态模式
降级的意思是:关掉评论、点赞、统计这些消耗写额度的功能,但文章照常可读。站点从“能互动的博客”降级成“纯静态页面”,而不是变成 500 错误页。
这个策略的前提就是前面的架构:内容本来就是静态的,动态功能本来就是可插拔的装饰。
什么情况下这套是过度设计
如果你的目标是“先上线一个博客,看看能不能坚持写三个月”,那上面全是废话。用 WordPress、用 Notion、用任何让你最快开始写东西的工具。第一个月最大的风险是不写,不是平台跑路。
我这套设计的前提是:我已经确定要长期写,愿意为“五年后还能改”付前期成本。
判断标准:你能想象自己在三年后还在维护这个东西吗? 能,就值得把地基打牢;不能,就别浪费时间。
Related · AI
相关阅读
基于语义相似度推荐