Cloudflare Pages 迁移实录
将个人网站从本地 Python 服务器搬到 Cloudflare 免费云托管
MiaoXiyuan · 云托管改造全记录 · 2026-07-09
一、为什么要迁移
1.1 我的困境
我的个人网站一直跑在自己电脑的本地服务器上,用 Python 的 serve.py 提供 HTTP 服务。在学校的时候一切正常,但放假回家后——服务器关了,网站就打不开了。
所以我需要一个免费、稳定、全球可用的托管方案,而且最好能让我在任何设备上直接在线编辑和管理项目。
1.2 网站长什么样
在动手之前,先理清楚我的网站到底有哪些东西:
| 文件 | 作用 | 类型 |
|---|---|---|
about.html | 个人主页,有粒子动画和滚动效果 | 纯静态 |
documents.html | 文档列表页,需要动态渲染卡片 | 动态渲染 |
PROJECT_REFERENCE.html | 项目优化计划页面 | 纯静态 |
html/*.html | 8 篇论文/文档/技术文章 | 纯静态 |
metadata.json | 每篇文档的标题、描述、日期、slug | 配置文件 |
serve.py | Python 后端:路由 + 动态渲染 + 字数统计 | 后端逻辑 |
95% 的内容是静态的,只有 /documents 页面需要后端动态渲染(读取 metadata.json、统计字数、填充卡片模板)。这意味着我需要一个能托管静态文件、又能跑少量服务端逻辑的平台。
1.3 为何选择了 Cloudflare Pages
调研了一圈之后,我选定了 Cloudflare Pages,原因很简单:
- 免费:静态资源请求无限,Functions 每天 10 万次,对个人站绰绰有余
- 全球 CDN:Cloudflare 的边缘节点遍布全球,访问速度快
- Git 集成:推送到 GitHub 就自动部署,还能在 GitHub 网页上直接编辑文件
- Pages Functions:底层就是 Workers,可以跑 JavaScript 服务端代码,完美替代我的 Python 后端
- 自动 HTTPS:不用自己折腾证书
二、迁移方案规划
2.1 整体思路
核心思路是把 serve.py 里的 Python 逻辑翻译成 Cloudflare Pages Functions(JavaScript),静态文件直接丢到 CDN 上。
| 原来的方式(serve.py) | 迁移后的方式 |
|---|---|
Python http.server 处理所有请求 | Cloudflare CDN 直接返回静态文件 |
/ → 返回 about.html | _middleware.js 重写路径 |
/documents → Python 动态渲染 | documents.js Function 动态渲染 |
/{slug} → Python 查 metadata 映射 | [slug].js Function 查 metadata 映射 |
/favicon.ico → 返回 avatar.jpg | _middleware.js 重写路径 |
| 未匹配路径 → 302 到 error 页 | [fallback].js catch-all 重定向 |
| 字数统计 → Python 启动时计算 | count-words.js 构建时预计算 |
2.2 项目目录设计
迁移后的目录结构如下。根目录保留源文件供本地 serve.py 开发使用,public/ 目录是部署到 Cloudflare 的内容:
2.3 请求处理流程
Cloudflare Pages 收到请求后,按以下顺序处理:
| 请求路径 | 谁处理 | 结果 |
|---|---|---|
/binary-tree-huffman 等 slug | _redirects 静态映射 | 直接返回对应 HTML 文件 |
/about.html 等静态文件 | CDN 直接返回 | 直接返回文件 |
/ 或 /about | _middleware.js | 重写为 /about.html |
/reference | _middleware.js | 重写为 /PROJECT_REFERENCE.html |
/favicon.ico | _middleware.js | 返回 /avatar.jpg |
/documents | documents.js | 动态渲染文档列表 |
| 其他未匹配路径 | [fallback].js | 302 → error.miaoxiyuan.com |
三、构建流程与自动化
3.1 构建命令
在 Cloudflare Pages 的项目设置中,配置了以下构建命令:
node scripts/count-words.js && node scripts/generate-redirects.js && node scripts/sync-metadata.js
每次 git push 后,Cloudflare 会自动运行这三个脚本:
| 脚本 | 做什么 | 输出 |
|---|---|---|
count-words.js | 遍历 html/ 目录,统计每篇文档的有效字符数(去掉 HTML 标签、标点等) | public/word-counts.json |
generate-redirects.js | 读取 metadata.json,为每个 slug 生成路由映射 | public/_redirects |
sync-metadata.js | 把根目录的 metadata.json 复制到 public/ | public/metadata.json |
metadata.json 和 html/ 目录。
3.2 部署流程
整个部署流程完全自动化:
- 在本地修改代码(或直接在 GitHub 网页上编辑)
git push推送到 GitHub- Cloudflare Pages 检测到 push,自动拉取代码
- 运行构建命令(三个 Node.js 脚本)
- 编译 Functions(
functions/目录下的 JS 文件) - 部署
public/目录到全球 CDN - 1-2 分钟后网站更新完成
四、踩坑记录
迁移过程并不顺利,使用LLM进行Vibe Coding也遇到了好几个坑。以下是完整的问题排查和解决过程。
坑 1:wrangler.toml 导致部署失败
Missing entry-point to Worker script。
原因:项目里有一个 wrangler.toml 文件(本来是给本地开发用的)。Cloudflare Pages 的构建系统看到它后,错误地尝试用 wrangler deploy 命令部署,而不是正常的 Pages 部署流程。
解决:直接删除 wrangler.toml。Pages 通过 Git 集成部署时完全不需要这个文件。
Remove-Item wrangler.toml
git add . && git commit -m "Remove wrangler.toml" && git push
坑 2:创建项目时选错了类型
原因:在 Cloudflare Dashboard 创建项目时,因为沟槽的界面默认是创建Workers,所以一没注意就默认创建了 Workers 而不是 Pages(这真的是我这次迁移中犯得最蠢的问题了)。Workers 和 Pages 的设置界面完全不同:
| Pages 项目 | Workers 项目 |
|---|---|
| 有 Build command + Build output directory | 有 Deploy command + Version command |
| 自动部署 public/ 目录 | 需要手动指定入口文件 |
解决:删除错误的项目,重新创建。这次确保选了 Pages 标签页。
坑 3:_redirects 的 /* 兜底规则拦截了所有请求
原因:在 _redirects 文件最后写了一条兜底规则:
/* https://error.miaoxiyuan.com 302
问题是 Cloudflare Pages 的处理顺序是:_redirects 先于 Functions 执行。/* 匹配所有路径,导致 /、/documents 等请求在到达 Functions 之前就被 302 重定向了。
解决:把兜底逻辑从 _redirects 移到 functions/[fallback].js。Cloudflare Pages 的 [fallback].js 是特殊的 catch-all 路由,只在没有其他规则和 Function 匹配时才执行。
// functions/[fallback].js
const ERROR_URL = 'https://error.miaoxiyuan.com';
export async function onRequest(context) {
return Response.redirect(ERROR_URL, 302);
}
坑 4:fetch(url, request) 导致页面空白
/ 返回空白页面,没有任何内容。
原因:在 _middleware.js 里用了 fetch(url.toString(), request),把原始请求对象作为 fetch 的第二个参数传入。在 Cloudflare Workers 环境中,这会导致请求行为异常,返回空响应。
解决:改用 env.ASSETS.fetch() 来读取静态文件。这是 Pages Functions 访问部署资源的正确方式,直接从 CDN 读取文件,不走 HTTP 子请求。
// 错误写法
const resp = await fetch(new URL('/about.html', url).toString(), request);
// 正确写法
const resp = await env.ASSETS.fetch(new URL('/about.html', url));
坑 5:文档 slug 路由不工作
原因:虽然 _redirects 里有 slug → HTML 的映射(如 /binary-tree-huffman → /html/数据结构与算法.html),但 Cloudflare Pages 的 rewrite(200 状态码)在某些情况下没有正确工作,请求直接落到了 [fallback].js。
解决:新增 functions/[slug].js,用 Function 来处理 slug 路由。这个 Function 会读取 metadata.json,找到 slug 对应的 HTML 文件名,然后通过 env.ASSETS 返回文件内容。
// functions/[slug].js
export async function onRequestGet(context) {
const { request, env } = context;
const url = new URL(request.url);
const slug = decodeURIComponent(url.pathname.slice(1));
// 跳过非 slug 路径
if (!slug || slug.includes('/') || slug.includes('.')) {
return context.next();
}
// 从 metadata.json 查找对应文件
const metadataResp = await env.ASSETS.fetch(new URL('/metadata.json', url));
const metadata = await metadataResp.json();
for (const [filename, meta] of Object.entries(metadata)) {
if (meta.slug === slug) {
const htmlResp = await env.ASSETS.fetch(new URL('/html/' + filename, url));
return new Response(htmlResp.body, {
status: 200,
headers: { 'Content-Type': 'text/html; charset=utf-8' }
});
}
}
return context.next(); // 没找到,交给 [fallback].js
}
五、日常维护操作手册
在日常使用中需要做以下的各种操作来实现维护和更新。主要分为本地操作和GitHub 网页操作两种方式,可以根据当前环境选择。
5.1 添加新文档
方式 A:本地操作
- 把新的 HTML 文件放到
html/目录(源文件) - 同时复制到
public/html/目录(部署用) - 编辑根目录的
metadata.json,添加新条目:"新文件名.html": { "title": "文章标题", "description": "文章描述", "date": "2026-07-09", "slug": "url-friendly-slug" } - 推送代码:
git add . git commit -m "Add new document: xxx" git push - 等 1-2 分钟,Cloudflare 自动构建部署
方式 B:GitHub 网页操作
- 打开
https://github.com/MiaoXiyuan/miaoxiyuan-self-web - 进入
html/目录 → Add file → Upload files → 上传新 HTML - 同样上传到
public/html/目录 - 回到根目录 → 点击
metadata.json→ 点右上角铅笔图标编辑 → 添加新条目 - 点 Commit changes
- 等 1-2 分钟,自动部署完成
_redirects、word-counts.json、public/metadata.json 都会在构建时自动生成,只需要维护 metadata.json 和 html/ 目录。
5.2 修改 about.html(个人主页)
其实操作一和上述一样
方式 A:本地操作
- 编辑根目录的
about.html - 复制到
public/about.html(或者用命令:Copy-Item about.html public/about.html) - 推送:
git add . && git commit -m "Update about page" && git push
方式 B:GitHub 网页操作
- 在 GitHub 上分别编辑
about.html(根目录)和public/about.html - 两次编辑可以放在同一个 commit 里
5.3 修改 documents.html(文档列表页模板)
一样,也需要同时更新根目录和 public/ 下的副本。
documents.html 里有 {{COUNT}} 和 {{CARDS}} 占位符,这些是给 documents.js Function 用的,不要删掉。
5.4 修改某篇文档的内容
方式 A:本地操作
- 编辑
html/文件名.html - 复制到
public/html/文件名.html - 推送
方式 B:GitHub 网页操作
- 分别编辑
html/文件名.html和public/html/文件名.html - 提交
word-counts.json。
5.5 修改文档的元数据(标题、描述等)
- 编辑根目录的
metadata.json - 推送
构建时会自动同步到 public/metadata.json 并重新生成 _redirects。如果改了 slug,_redirects 里的路由也会自动更新。
5.6 回滚到之前的版本
有两种方式:
- Git 回滚:在 GitHub 上找到对应的 commit,点 Revert 或手动
git revert,push 后自动重新部署 - Cloudflare 回滚:Dashboard → Pages 项目 → Deployments → 找到想回滚的版本 → 点 Rollback
5.7 换设备后的操作
如果使用其他设备进行更新和维护,只需要:
# 1. 克隆仓库
git clone https://github.com/MiaoXiyuan/miaoxiyuan-self-web.git
cd miaoxiyuan-self-web
# 2. 配置 Git(如果新设备没配过)
git config --global user.name "MiaoXiyuan"
git config --global user.email "3108401169@qq.com"
# 3. 改完代码后正常推送
git add .
git commit -m "xxx"
git push
或者直接在GitHub网页进行修改
不需要安装 Node.js、不需要安装 Wrangler、不需要重新配置 Cloudflare。构建脚本在 Cloudflare 的服务器上运行,我本地只需要 Git。
六、文件作用速查表
6.1 需要编辑的文件
| 文件 | 什么时候改 |
|---|---|
metadata.json(根目录) | 添加/修改文档的标题、描述、日期、slug |
html/*.html(根目录 + public/html/) | 写新文章或修改已有文章 |
about.html(根目录 + public/) | 修改个人主页内容 |
documents.html(根目录 + public/) | 修改文档列表页的样式或模板 |
PROJECT_REFERENCE.html(根目录 + public/) | 修改项目参考页面 |
6.2 自动生成的文件(不用手动改)
| 文件 | 由谁生成 |
|---|---|
public/word-counts.json | scripts/count-words.js |
public/_redirects | scripts/generate-redirects.js |
public/metadata.json | scripts/sync-metadata.js |
6.3 Cloudflare Functions(一般不需要改)
| 文件 | 作用 |
|---|---|
functions/_middleware.js | 路由重写:/→about、/reference→PROJECT_REFERENCE、/favicon.ico→avatar.jpg |
functions/documents.js | 拦截 /documents,读取 metadata + 字数 + 模板,动态渲染文档列表 |
functions/[slug].js | 拦截 /{slug},从 metadata 查找对应 HTML 文件并返回 |
functions/[fallback].js | catch-all 兜底:未匹配的请求 302 重定向到 error.miaoxiyuan.com |
6.4 配置和脚本文件
| 文件 | 作用 |
|---|---|
public/_headers | HTTP 缓存策略(静态文件 1 天,图片 7 天) |
package.json | 项目配置,定义构建命令 |
.gitignore | 排除 .venv/、.idea/ 等不需要提交的文件 |
scripts/count-words.js | 字数统计脚本 |
scripts/generate-redirects.js | 从 metadata 生成 _redirects |
scripts/sync-metadata.js | 同步 metadata.json 到 public/ |
6.5 本地开发文件(保留不动)
| 文件 | 作用 |
|---|---|
serve.py / serve.bat | Python 本地开发服务器,不影响线上部署 |
public/ 下的副本文件 | 部署用的文件副本,需要和根目录保持同步 |
七、迁移前后对比
| 对比项 | 迁移前(本地服务器) | 迁移后(Cloudflare Pages) |
|---|---|---|
| 可用性 | 依赖本地服务器在线 | 全球 CDN,99.99% 可用 |
| 访问速度 | 取决于家庭网络 | 全球边缘节点,低延迟 |
| 成本 | 电费 + 网络费 | $0(免费计划) |
| HTTPS | 需自行配置证书 | 自动 HTTPS |
| 部署 | 手动启动 serve.py | git push 自动部署 |
| 在线编辑 | 不支持 | GitHub 网页编辑器 |
| 版本控制 | 无 | Git 完整历史 |
| 后端语言 | Python | JavaScript (Workers) |
| 换设备 | 需要重新搭建环境 | 只需 git clone + git push |