给站点接上自己的域名:一天的 Cloudflare 配置记录
A Day with Cloudflare: Notes on Wiring Up a Domain
站点本身怎么搭的,写在《这个站点是怎么搭的》里,这里不重复。它一直挂在 用户名.github.io 这个地址上;今天把自己的域名接上了,顺带配了 Cloudflare 的一整套东西。操作本身不复杂,值得记的是几个不照着文档走就会卡住的地方。
为什么值得买一个域名
这是今天唯一花钱的部分,一年十来美元。
理由不是速度,也不是好看。个人站点相对平台的核心优势是复利——三年前的文章今天仍然在被搜到、被链接、被引用,而一条社交媒体动态四十八小时后就死了。但这条复利完全建立在 URL 稳定的前提上。
用户名.github.io 把你绑在了 GitHub 上。有了自己的域名之后,托管商、CDN、生成器将来都可以换,而那个地址可以跟着走。买的其实是可携带性,不是域名本身。
后缀我最后选了 .com。考虑过 .io,因为它在技术圈观感好,但查清楚之后放弃了:它原本是英属印度洋领地的国家代码域名,而该领地的主权状态正在变动。为一个要持有二十年的地址,去选一个后缀存续性存疑的域名,和买它的初衷相悖。
反直觉之一:签证书必须先把 CDN 关掉
这是今天卡得最久的一步。
Cloudflare 的 DNS 记录有个代理开关,橙云表示流量经过 Cloudflare,灰云表示它只做解析、不碰流量。直觉上橙云功能更多,应该一开始就打开。
但打开之后,GitHub Pages 的 Enforce HTTPS 这个选项一直是灰的,提示域名未正确配置以支持 HTTPS。
原因是签发证书和使用证书是两回事。GitHub 要向 Let's Encrypt 申请证书,而 Let's Encrypt 不能随便发——否则任何人都能申请别人域名的证书。它要求申请者先证明控制权:在服务器上放一个特定文件,然后自己去访问 http://你的域名/某个路径,看能不能读到。
开着橙云时,这个验证请求到达的是 Cloudflare 的边缘节点,那里没有那个文件,验证失败。
所以正确顺序是:灰云 → 等证书签发 → 再改橙云。证书一旦到手就是一份存在服务器上的文件,使用它不需要任何外部验证,这时候 Cloudflare 插在中间毫无问题。
这个模式值得记住,它不只出现在这里:验证所有权需要直连,提供服务不需要。以后配邮箱域名验证、申请泛域名证书、接入某些第三方服务,大概率还会遇到"先把代理临时关掉"这种要求。
顺带两个相关的坑:
- 改回橙云之后,SSL/TLS 加密模式必须设成 Full (strict)。默认可能是 Flexible,那个模式下 Cloudflare 用 HTTP 回源,而 GitHub Pages 会强制跳转到 HTTPS,两边互踢,形成无限重定向循环。
- HSTS 我没开。它的特性是不可撤销——浏览器记住之后,在有效期内即使你关掉它也无法回退。对一个没有登录、没有表单、内容本来就公开的站点,它防的那点风险和它带来的故障可能性不成比例。
反直觉之二:统计脚本可以不经过仓库
接 Cloudflare Web Analytics 时,我原本以为必须手动把那段 JS 写进模板,因为站点托管在 GitHub 而不是 Cloudflare 自己的产品上。实际上只要开了橙云代理就能自动注入,源站在哪不重要。
原理比结论有意思。橙云状态下 Cloudflare 不只是转发,它可以修改经过的响应内容:从 GitHub 拿到原始 HTML 之后,在 </body> 前插入信标脚本,再发给访客。
所以仓库里的文件从头到尾没变。git clone 下来搜不到那段脚本,浏览器查看源码却能看到——这两个"源码"根本不是同一份东西。
这也顺带解释了另一件事:如果响应头里有 Cache-Control: public, no-transform,注入会失败。no-transform 的字面意思就是禁止中间代理改写内容,Cloudflare 遵守它。
值得意识到的一点是:这个能力意味着 Cloudflare 能读到并修改所有经过的内容。对公开博客无所谓,但它不是一个被动的管道,而是一个有完全访问权的中间人。灰云时它是电话簿,橙云时它是代理人。
同一个错误,一天出现三次
今天最有价值的收获不是某个具体配置,而是一个反复出现的模式。
试着把站点迁到 Cloudflare Pages 做备选方案时,构建失败了:
Invalid US-ASCII character "\xE2" on line 5
\xE2 是 UTF-8 里通用标点的首字节——破折号、弯引号这类字符。文件本身没问题,是构建容器的 locale 不是 UTF-8,Ruby 默认按 ASCII 读文件,遇到多字节字符就崩了。
加两条环境变量解决:
LANG=C.UTF-8
LC_ALL=C.UTF-8
用 C.UTF-8 而不是 en_US.UTF-8,因为前者在任何 glibc 系统上都存在,后者需要镜像里预先生成过。
关键在于,同一类问题今天已经是第三次出现:
- 在云服务器上装环境时,最小化镜像默认 locale 是 POSIX,Jekyll 处理中文正文直接抛异常
- 在 VS Code 集成终端里,环境变量因为非交互式 shell 提前 return 而没被读到
- 现在是云端构建容器
三次之后它就不是偶然了。对写中文的人来说,任何新的执行环境都要先确认 locale 是 UTF-8,这应该是默认动作,不是出了问题再排查的事项。
一个小教训:入口点错了
配 Pages 时最开始进错了界面。Cloudflare 把 Workers 和 Pages 放在同一个菜单下,两者入口挨得很近。
识别方法:如果界面上出现「部署命令」并预填了 npx wrangler deploy,那是 Workers——用来部署 JavaScript 函数的。Pages 的界面上应该是「框架预设」「构建命令」「构建输出目录」这三栏。
静态站点要走 Pages。Jekyll 那几项填构建命令 bundle exec jekyll build、输出目录 _site,再加一条 RUBY_VERSION——这些和本地构建是一回事,《这个站点是怎么搭的》里写过。
现在的架构
域名所有权 Cloudflare Registrar
DNS 解析 Cloudflare
源码和版本 GitHub
构建和托管 GitHub Pages
CDN / HTTPS Cloudflare
访问统计 Cloudflare Web Analytics
三家产品拼在一起,各管一段。这也是为什么配置要在两个后台来回切——不是哪里搞错了,是架构本来如此。
值得说的是这个结构的可替换性:域名在自己名下、源码在 Git 历史里、内容是纯 Markdown。任何一层想换,改几处配置就能走,不会被锁死在谁手里。
这正是今天那十几美元买到的东西。