用 Cloudflare Pages 部署一个零依赖静态博客

为什么要"零依赖"

大部分静态博客生成器(Hugo、Hexo、Astro)都要先装几百 MB 的依赖,升级一次 Node 版本就可能全线崩溃。而对于一个只写文章的博客来说,真正的需求其实只有三件事:

  1. 把 Markdown 变成 HTML
  2. 套上一份好看的 CSS
  3. 把生成的文件夹推到 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 转义,只让解析器自己生成白名单标签。

js
const escapeHtml = (s) =>
  String(s)
    .replace(/&/g, '&')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;');

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>** 就会出问题;如果先转义再提取行内代码,代码里的 &lt; 又会显示成字面量。

块级解析的状态机

块级元素(列表、引用、代码块)需要维护一个列表栈,否则嵌套列表的 <ul> 标签会错位:

情况处理方式
缩进变深入栈,输出新的 <ul> / <ol>
缩进变浅出栈直到匹配层级
空行向后预读:若下一行仍是列表项,保持列表开启
类型切换关掉当前列表,重新开一个

构建流程

整个 build() 函数就是这么直白:

js
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 篇文章只需要几十毫秒,任何优化都是多余的。

前端搜索

因为站点是纯静态的,搜索只能在浏览器里做。构建时导出一份精简索引:

json
[
  {
    "t": "用 Cloudflare Pages 部署一个零依赖静态博客",
    "u": "/posts/2026/03/cloudflare-pages-blog/",
    "d": "2026-03-18",
    "s": "不装框架、不引打包器,只用一个 Node 脚本……",
    "g": ["Cloudflare", "前端", "部署"],
    "m": 6
  }
]

搜索时按字段加权打分——标题命中权重最高,其次是标签,最后是摘要:

js
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

部署命令只有一行:

bash
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 就等于一次发布。