用Claude Code把WordPress改成静态站:我如何用一个下午重建了这个网站
- 发布日期
- 2026年7月11日
- 作者
- Jacob Lloyd —— 项目完成后,在 AI 协助下撰写
- 阅读时长
- 约 9 分钟阅读
简单来说: 这篇文章讲的是这个网站本身是怎么被一个AI编程助手在一个下午内重建的:从WordPress变成了一堆简单的文件。新网站更快、运行免费,也没有登录页面可以被黑客攻破。文章最后附上了照搬到你自己网站上的具体步骤。
这个网站以前跑在WordPress上。现在它是一个装满Markdown文件的文件夹,外加一个183行的Python脚本——由一个AI编程代理在一个下午内,根据WordPress的导出文件重建而成。下面是事情的经过,以及为什么同一套流程至今仍在维护着这个网站。
- 这是什么:一个从零搭建、取代WordPress的静态站点生成器(Python + Jinja2),外加一个把旧内容导入一次性用的导入脚本
- 花多少钱:免费。没有插件,没有主题授权费,主机还是原来那台便宜的共享主机
- 你需要什么:你的WordPress导出文件(WXR文件)、uploads文件夹,还有一个AI编程代理——我用的是Claude Code
- 最后得到什么:纯平面HTML文件,没有数据库,也没有能被暴力破解的登录页面
最终成果
先看结果——这是重建完成那天,终端里实打实敲出来的输出:
$ python3 _build/build.py
Built 15 files (7 projects, 3 pages, 4 categories).
迁移那一刻,15个HTML页面就是整个网站:首页、四个分类页、七篇项目文章、三个信息页面。全部预渲染完毕,全是纯文件,每次请求都不再需要跑任何服务端代码。
产出这些内容的WordPress导出文件有572KB、123个条目——文章、页面、图片附件记录,还有一堆废弃的草稿。真正活下来的文章只有7篇。剩下的都是导入脚本故意跳过的垃圾。
而构建这一切的机器,一张表就能装下:
| 组件 | 行数 |
|---|---|
| build.py(生成器本体) | 183 |
| migrate.py(一次性导入脚本) | 166 |
| 模板(5种页面布局) | 139 |
| 局部片段(导航、页脚、卡片) | 51 |
| site.yaml(全部站点配置) | 36 |
| serve.sh(本地预览) | 12 |
| .htaccess(服务器规则) | 57 |
| 合计 | 644 |
再加上CSS(322行)和唯一的一个JavaScript文件(31行——只是个移动端导航开关,仅此而已),整个网站不到1000行代码。没有node_modules,没有构建框架,也没有要跟你较劲的lockfile。
第一部分:逃离WordPress
这里面没有一行是我自己手写的。我把想要的东西描述给Claude Code听,它就依次写出了导入脚本、生成器、模板,而我的工作只是审查产出结果,说一句"不对,应该这样做"。文件的时间戳可以作证:从拿到导出文件,到建成一个可以浏览的网站,用了不到两个小时。是一个下午的活儿,不是一个周末的活儿。
它最终定下来的流水线是这样的:
导出文件和媒体镜像
WordPress通过工具 → 导出 → 所有内容把一切都导出来。你得到的是一个WXR文件——里面塞满了文章、页面、分类和附件记录的XML。这是导入脚本唯一的输入。
导出文件唯一不包含的东西,是你真正的图片文件本身,里面只有它们的URL。所以uploads文件夹得单独拉过来——用SFTP、文件管理器,或者一个简单的wget镜像命令都行——放进assets/uploads/,并且保持WordPress原来用的年/月目录结构,这样重写后的URL才能正确解析。
migrate.py到底做了什么
这个导入脚本只用一次,但可以安全地反复运行。没用XML库——只是拿正则表达式去匹配WXR文本,这听起来很脆弱,但你会发现WordPress导出的XML格式极其规整,用正则去抓反而比搭一个完整的解析器省代码得多。对每篇文章,它会提取标题、slug、日期、正文、分类和特色图片,然后:
- 跳过垃圾——空文章和标题为"(draft)…"的内容永远进不了content/
- 映射分类到一套干净简短的键名,而不是WordPress那套乱糟糟的nicename
- 解析特色图片——先通过附件id查找,找不到就退回到正文里第一张上传的图片
- 重写媒体URL——把
wp-content/uploads/…改成/assets/uploads/… - 重写内部链接——用一份根据所有文章和页面生成的slug对照表,把旧的永久链接换成新的干净URL
- 剥离Gutenberg区块注释,并且直接删除动态列表区块——分类列表现在由构建脚本生成,旧的那些写死的列表留着只会慢慢过期
- 自动生成摘要——去掉标签,截断到160字符,用作卡片文字和meta description
有意思的一点是:文章导出后的样子,是"一个Markdown文件里装着清理过的WordPress HTML",而不是转成了Markdown语法。渲染器直接原样透传HTML,所以旧内容没有发生任何有损转换——而新写的文章可以用纯Markdown。两种格式都能从同一个文件夹顺利构建出来。我第一次打开一个内容90%都是HTML标签的".md"文件时,还以为哪里出了问题。其实没有。
build.py做了什么
这部分是要一直跑下去的那部分。它读取site.yaml获取导航、分类和链接信息,读取content/里的所有内容,再把每个文件套进Jinja2模板,渲染成一个真正的文件夹,里面装着index.html——不需要任何服务器重写规则就能实现干净URL。除了页面本身,它还会生成:
- 由数据驱动的导航栏和页脚——加一个分类或页面,它就自动出现
- 每个页面各自的title、meta description、canonical URL、Open Graph和Twitter标签
- sitemap.xml、robots.txt,还有一份网页manifest,每次构建都会重新生成
- 一道先清理旧输出的清理步骤,这样重命名文章后不会留下一个死页面
- 零运行时依赖:自托管字体、内联SVG图标、那一个小JS文件,没有任何东西来自CDN
添加一篇文章的流程是:在content/projects/里丢一个Markdown文件,跑一次构建。整个工作流程就这么多。
先预览,再部署
本地预览只是一个脚本——先构建,再启动服务。在任何东西碰到真正的主机之前,把每个页面都点一遍。
./serve.sh
# builds, then serves the whole site at http://localhost:8099
部署是一次由清单指导的rsync推送,清单里明确列出了要发布什么(HTML、CSS、JS、图片、字体、.htaccess)和禁止发布什么(Python脚本、Markdown源文件、WordPress导出文件、模板)。服务器配置再加上压缩、缓存头、安全头,以及对任何源文件直接访问的硬性拒绝——所以就算有人知道这个生成器的存在,也没有任何东西可以下载。
第二部分:这个网站现在是怎么维护的
这也是把这次重建做扎实的真正意义所在:这个网站现在也不再靠人工维护了。建起它的那套代理工作流程,现在也在负责它的更新——包括你正在阅读的这次更新。
这次修订是从一次审查代理群开始的。一组代理核实了每个已发布页面的事实并点了每一条外部链接;另一组收集截图,并整理网站实际运作方式的确凿技术信息。所有信息汇总成一份书面规格:该做什么、不能编造数字、绝不能公开任何机密。
从那之后,工作就分发了出去——一个负责平台的代理(分类结构、第二种语言、搜索引擎相关的管道工作)、一个设计代理,以及每篇文章各配一个写作代理,这一篇也不例外。一个更快的模型根据事实文件起草每个页面初稿。一个更强的模型负责润色文风和核实准确性。然后一轮对抗式检查会试图挑出问题——把数字和链接跟原始事实重新核对一遍,重新点一遍链接,专门找那些读起来像是编造的内容。或者说得更直白点,专门找那些读起来"像AI写的"内容。这次,还真是AI写的。
把同样的活儿交给你自己的代理
手头有个老WordPress网站,不想再无止境地打补丁下去?这就是我交给我自己代理的方子。把每一步描述给你的代理听,让它去写代码。
- 导出。WordPress后台 → 工具 → 导出 → 所有内容。把WXR文件放到代理能读到的地方,比如
_build/source.xml。 - 镜像媒体文件。把旧主机上的
wp-content/uploads/复制到assets/uploads/,保持年/月目录结构。 - 要求写一个迁移脚本。解析WXR条目,跳过草稿和空内容,把分类映射成一套干净简短的列表,解析特色图片(找不到就退回正文里第一张内联图片),重写上传URL和内部链接,剥离Gutenberg区块注释,自动生成摘要,写出带frontmatter的Markdown文件。
- 要求写一个生成器。读取站点配置和内容文件夹,通过模板渲染(base、home、category、post、page,以及共用的导航/页脚/卡片局部片段),输出干净URL的文件夹,生成sitemap.xml、robots.txt和网页manifest。
- 把设计从零重做。不要移植旧主题的CSS——一份干净的样式表,自托管的开源字体,内联SVG图标,零外部请求。
- 把所有东西都预览一遍。准备一个一行命令的脚本,先构建再本地启动服务。把每个页面都点一遍。
- 写服务器配置。压缩、缓存头、安全头、拒绝访问源文件的规则,以及无扩展名URL的回退规则。
- 用清单方式部署。rsync输出内容,明确排除源文件夹和脚本,再检查线上URL是否返回200。
- 留着导出文件。WXR文件留在项目里——需要重新导入时,重新跑一次迁移脚本就能干净地重新生成内容文件夹。
完整的运行时依赖清单,全部在这里:
pip install jinja2 pyyaml markdown
就这些。
踩过的坑
- 忘了镜像uploads文件夹,不会有任何警告。构建照样成功,但每张缩略图都会404。这是吃了亏才明白的。
- 输出和源文件挤在同一个文件夹里。构建出来的HTML,和Python、Markdown源文件落在同一个项目根目录下——部署时的隔离完全靠那份allow/deny清单。一开始感觉有点别扭,但正是这一点让部署能保持是一次简单的rsync。
- 重命名分类可能会留下一个死文件夹。清理步骤里直接写死了分类名称。在配置里改了名字却忘了同步这份清单,旧文件夹就会孤零零地留在那里,继续被发布着。
- 日期是按字符串排序的,不是按日期。只要每个日期都写成
YYYY-MM-DD就没问题。写错一个,你最新的文章就会悄悄沉到最底下。