Cloudflare Pages 迁移实录

将个人网站从本地 Python 服务器搬到 Cloudflare 免费云托管

MiaoXiyuan · 云托管改造全记录 · 2026-07-09

一、为什么要迁移

1.1 我的困境

我的个人网站一直跑在自己电脑的本地服务器上,用 Python 的 serve.py 提供 HTTP 服务。在学校的时候一切正常,但放假回家后——服务器关了,网站就打不开了。

所以我需要一个免费、稳定、全球可用的托管方案,而且最好能让我在任何设备上直接在线编辑和管理项目。

1.2 网站长什么样

在动手之前,先理清楚我的网站到底有哪些东西:

文件作用类型
about.html个人主页,有粒子动画和滚动效果纯静态
documents.html文档列表页,需要动态渲染卡片动态渲染
PROJECT_REFERENCE.html项目优化计划页面纯静态
html/*.html8 篇论文/文档/技术文章纯静态
metadata.json每篇文档的标题、描述、日期、slug配置文件
serve.pyPython 后端:路由 + 动态渲染 + 字数统计后端逻辑

95% 的内容是静态的,只有 /documents 页面需要后端动态渲染(读取 metadata.json、统计字数、填充卡片模板)。这意味着我需要一个能托管静态文件、又能跑少量服务端逻辑的平台。

1.3 为何选择了 Cloudflare Pages

调研了一圈之后,我选定了 Cloudflare Pages,原因很简单:

  • 免费:静态资源请求无限,Functions 每天 10 万次,对个人站绰绰有余
  • 全球 CDN:Cloudflare 的边缘节点遍布全球,访问速度快
  • Git 集成:推送到 GitHub 就自动部署,还能在 GitHub 网页上直接编辑文件
  • Pages Functions:底层就是 Workers,可以跑 JavaScript 服务端代码,完美替代我的 Python 后端
  • 自动 HTTPS:不用自己折腾证书
最终效果:月成本 $0,全球可用,git push 自动部署,换设备只需要 git clone + git push。

二、迁移方案规划

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 的内容:

WebProject/ ├── public/ ← 部署到 Cloudflare Pages 的目录 │ ├── about.html ← 个人主页 │ ├── documents.html ← 文档列表模板(含 {{CARDS}} 占位符) │ ├── PROJECT_REFERENCE.html ← 项目优化计划 │ ├── avatar.jpg ← favicon 图片,虽然实际上是硬编码的 │ ├── background.png ← 背景图,也是硬编码的,但是先放这 │ ├── metadata.json ← 构建时自动同步 │ ├── word-counts.json ← 构建时自动生成 │ ├── _redirects ← 构建时自动生成 │ ├── _headers ← HTTP 缓存策略 │ └── html/ ← 所有文档 HTML │ ├── functions/ ← Pages Functions(也部署到 Cloudflare) │ ├── _middleware.js ← 路由重写(/ → about, /reference 等) │ ├── documents.js ← /documents 动态渲染 │ ├── [slug].js ← /{slug} 文档路由 │ └── [fallback].js ← 兜底:未匹配 → error 页 │ ├── scripts/ ← 构建脚本(Cloudflare 构建时运行) │ ├── count-words.js → public/word-counts.json │ ├── generate-redirects.js → public/_redirects │ └── sync-metadata.js → public/metadata.json │ ├── about.html ← 源文件(本地开发用) ├── html/ ← 源文件(本地开发用) ├── metadata.json ← 文章的一些描述 ├── serve.py / serve.bat ← 本地开发服务器 ├── package.json ← 构建命令配置 └── .gitignore ← Git 忽略规则

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
/documentsdocuments.js动态渲染文档列表
其他未匹配路径[fallback].js302 → 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
关键点:word-counts.json、_redirects、metadata.json 这三个文件都是构建时自动生成的,不需要手动编辑它们。只需要维护根目录的 metadata.jsonhtml/ 目录。

3.2 部署流程

整个部署流程完全自动化:

  1. 在本地修改代码(或直接在 GitHub 网页上编辑)
  2. git push 推送到 GitHub
  3. Cloudflare Pages 检测到 push,自动拉取代码
  4. 运行构建命令(三个 Node.js 脚本)
  5. 编译 Functions(functions/ 目录下的 JS 文件)
  6. 部署 public/ 目录到全球 CDN
  7. 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 的 /* 兜底规则拦截了所有请求

现象:部署成功后,访问任何页面都直接跳转到 error.miaoxiyuan.com。

原因:_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 路由不工作

现象:about 和 documents 页面正常了,但点击任何文档卡片都跳转到 error 页面。

原因:虽然 _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:本地操作

  1. 把新的 HTML 文件放到 html/ 目录(源文件)
  2. 同时复制到 public/html/ 目录(部署用)
  3. 编辑根目录的 metadata.json,添加新条目:
    "新文件名.html": {
      "title": "文章标题",
      "description": "文章描述",
      "date": "2026-07-09",
      "slug": "url-friendly-slug"
    }
  4. 推送代码:
    git add .
    git commit -m "Add new document: xxx"
    git push
  5. 等 1-2 分钟,Cloudflare 自动构建部署

方式 B:GitHub 网页操作

  1. 打开 https://github.com/MiaoXiyuan/miaoxiyuan-self-web
  2. 进入 html/ 目录 → Add fileUpload files → 上传新 HTML
  3. 同样上传到 public/html/ 目录
  4. 回到根目录 → 点击 metadata.json → 点右上角铅笔图标编辑 → 添加新条目
  5. Commit changes
  6. 等 1-2 分钟,自动部署完成
不需要手动改的东西:_redirectsword-counts.jsonpublic/metadata.json 都会在构建时自动生成,只需要维护 metadata.jsonhtml/ 目录。

5.2 修改 about.html(个人主页)

其实操作一和上述一样

方式 A:本地操作

  1. 编辑根目录的 about.html
  2. 复制到 public/about.html(或者用命令:Copy-Item about.html public/about.html
  3. 推送:git add . && git commit -m "Update about page" && git push

方式 B:GitHub 网页操作

  1. 在 GitHub 上分别编辑 about.html(根目录)和 public/about.html
  2. 两次编辑可以放在同一个 commit 里

5.3 修改 documents.html(文档列表页模板)

一样,也需要同时更新根目录和 public/ 下的副本。

注意:documents.html 里有 {{COUNT}}{{CARDS}} 占位符,这些是给 documents.js Function 用的,不要删掉。

5.4 修改某篇文档的内容

方式 A:本地操作

  1. 编辑 html/文件名.html
  2. 复制到 public/html/文件名.html
  3. 推送

方式 B:GitHub 网页操作

  1. 分别编辑 html/文件名.htmlpublic/html/文件名.html
  2. 提交
如果只是修改文档内容(不改 metadata):字数统计会在构建时自动重新计算,我不需要手动更新 word-counts.json

5.5 修改文档的元数据(标题、描述等)

  1. 编辑根目录的 metadata.json
  2. 推送

构建时会自动同步到 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.jsonscripts/count-words.js
public/_redirectsscripts/generate-redirects.js
public/metadata.jsonscripts/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].jscatch-all 兜底:未匹配的请求 302 重定向到 error.miaoxiyuan.com

6.4 配置和脚本文件

文件作用
public/_headersHTTP 缓存策略(静态文件 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.batPython 本地开发服务器,不影响线上部署
public/ 下的副本文件部署用的文件副本,需要和根目录保持同步

七、迁移前后对比

对比项迁移前(本地服务器)迁移后(Cloudflare Pages)
可用性依赖本地服务器在线全球 CDN,99.99% 可用
访问速度取决于家庭网络全球边缘节点,低延迟
成本电费 + 网络费$0(免费计划)
HTTPS需自行配置证书自动 HTTPS
部署手动启动 serve.pygit push 自动部署
在线编辑不支持GitHub 网页编辑器
版本控制Git 完整历史
后端语言PythonJavaScript (Workers)
换设备需要重新搭建环境只需 git clone + git push