用 Cloudflare Pages 部署一个零依赖静态博客
为什么要"零依赖"
大部分静态博客生成器(Hugo、Hexo、Astro)都要先装几百 MB 的依赖,升级一次 Node 版本就可能全线崩溃。而对于一个只写文章的博客来说,真正的需求其实只有三件事:
- 把 Markdown 变成 HTML
- 套上一份好看的 CSS
- 把生成的文件夹推到 CDN 上
这三件事,用一个不到 500 行的 Node 脚本就能全部搞定,而且不需要任何第三方包。
依赖越少,能坏的地方就越少。一个三年后还能
node build.mjs就跑起来的博客,比一个功能齐全但需要考古的博客有价值得多。
目录结构
blog/
├── build.mjs # 构建器:读 content/ 输出 public/
├── site.config.mjs # 站点配置:站名、域名、导航
├── lib/
│ └── markdown.mjs # 自己实现的 Markdown 渲染器
├── content/ # 文章源文件(Markdown)
│ ├── about.md
│ └── *.md
├── static/ # 原样拷贝的静态资源
│ ├── favicon.svg
│ └── assets/
│ ├── style.css
│ └── main.js
└── public/ # 构建产物(部署这个目录)自己写一个 Markdown 渲染器
核心难点是既要支持语法,又不能引入 XSS。策略是:所有文本先做 HTML 转义,只让解析器自己生成白名单标签。
const escapeHtml = (s) =>
String(s)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>');
function inline(text) {
// 1) 先保护行内代码,避免被后续规则误伤
const codes = [];
let s = text.replace(/(`+)([\s\S]*?)\1/g, (_m, _t, code) => {
codes.push(`<code>${escapeHtml(code.trim())}</code>`);
return `\u0001${codes.length - 1}\u0001`;
});
// 2) 整体转义
s = escapeHtml(s);
// 3) 再处理粗体、链接、图片……
s = s.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
// 4) 最后还原代码
return s.replace(/\u0001(\d+)\u0001/g, (_m, i) => codes[Number(i)]);
}这里的顺序很关键。如果先处理 **粗体** 再转义,用户写 **<script>** 就会出问题;如果先转义再提取行内代码,代码里的 < 又会显示成字面量。
块级解析的状态机
块级元素(列表、引用、代码块)需要维护一个列表栈,否则嵌套列表的 <ul> 标签会错位:
| 情况 | 处理方式 |
|---|---|
| 缩进变深 | 入栈,输出新的 <ul> / <ol> |
| 缩进变浅 | 出栈直到匹配层级 |
| 空行 | 向后预读:若下一行仍是列表项,保持列表开启 |
| 类型切换 | 关掉当前列表,重新开一个 |
构建流程
整个 build() 函数就是这么直白:
function build() {
rmrf(OUT); // 清空 public/
mkdir(OUT);
const posts = loadPosts(); // 读取并排序文章
write('index.html', pageIndex(posts, 1)); // 首页
posts.forEach((p) => write( // 每篇文章
path.join(p.url.replace(/^\//, ''), 'index.html'),
pagePost(p)
));
write('archive/index.html', pageArchive(posts)); // 归档
write('rss.xml', rss(posts)); // 订阅源
write('sitemap.xml', sitemap(posts)); // 站点地图
write('search-index.json', JSON.stringify(index)); // 离线搜索索引
copyDir(STATIC, OUT); // 静态资源
}没有增量构建、没有缓存、没有插件系统。全量构建 30 篇文章只需要几十毫秒,任何优化都是多余的。
前端搜索
因为站点是纯静态的,搜索只能在浏览器里做。构建时导出一份精简索引:
[
{
"t": "用 Cloudflare Pages 部署一个零依赖静态博客",
"u": "/posts/2026/03/cloudflare-pages-blog/",
"d": "2026-03-18",
"s": "不装框架、不引打包器,只用一个 Node 脚本……",
"g": ["Cloudflare", "前端", "部署"],
"m": 6
}
]搜索时按字段加权打分——标题命中权重最高,其次是标签,最后是摘要:
const score = (item, terms) => {
let s = 0;
for (const t of terms) {
if (item.t.toLowerCase().includes(t)) s += 10;
if (item.g.join(' ').toLowerCase().includes(t)) s += 5;
if (item.s.toLowerCase().includes(t)) s += 2;
}
return s;
};几十篇文章的索引只有几 KB,全量加载毫无压力,还完全不依赖任何后端服务。
部署到 Cloudflare Pages
Cloudflare Pages 的免费额度对个人博客来说绰绰有余:
- 每月 500 次构建,每次构建 20 分钟上限
- 无限带宽,不限请求数
- 全球 300+ 节点,自动签发并续期 HTTPS 证书
- 每个分支自动生成预览环境,比如
dev.your-site.pages.dev
部署命令只有一行:
wrangler pages deploy public --project-name my-blog首次部署完成后,你会拿到一个 https://<项目名>.pages.dev 的地址,立刻就可以访问,不需要买域名、不需要配 DNS、不需要等证书签发。
加一条缓存规则
静态资源的文件名不会变,所以要用 _headers 文件告诉 Cloudflare 怎么缓存:
/assets/*
Cache-Control: public, max-age=31536000, immutable
/*.html
Cache-Control: public, max-age=0, must-revalidate把 assets/ 下的 CSS 和 JS 设成一年强缓存,HTML 设成每次校验——这样用户第二次访问时几乎瞬间加载,而内容更新又能马上生效。
关于成本
这一切的总成本是 0 元:
| 项目 | 费用 |
|---|---|
| Cloudflare Pages 托管 | 免费 |
| 带宽与请求 | 免费且无限 |
| HTTPS 证书 | 免费自动续期 |
.pages.dev 子域名 | 免费 |
| 自定义域名(可选) | 域名本身的年费 |
唯一的"代价"是你要接受一个事实:你不再需要为了写博客而维护一套软件。
小结
- 博客的本质是「Markdown → HTML → CDN」,中间的框架都是可选的
- 自己实现渲染器时,转义顺序决定了安全性
- 列表嵌套要用栈来管理缩进层级
- 静态站点的搜索可以完全放在浏览器端,索引几 KB 而已
- Cloudflare Pages 的免费额度足够个人博客长期使用
下一步可以做的事:把文章源文件放进 Git 仓库,用 Cloudflare 的 Git 集成做自动构建,这样每次 git push 就等于一次发布。