用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网站,不想再无止境地打补丁下去?这就是我交给我自己代理的方子。把每一步描述给你的代理听,让它去写代码。

  1. 导出。WordPress后台 → 工具 → 导出 → 所有内容。把WXR文件放到代理能读到的地方,比如_build/source.xml
  2. 镜像媒体文件。把旧主机上的wp-content/uploads/复制到assets/uploads/,保持年/月目录结构。
  3. 要求写一个迁移脚本。解析WXR条目,跳过草稿和空内容,把分类映射成一套干净简短的列表,解析特色图片(找不到就退回正文里第一张内联图片),重写上传URL和内部链接,剥离Gutenberg区块注释,自动生成摘要,写出带frontmatter的Markdown文件。
  4. 要求写一个生成器。读取站点配置和内容文件夹,通过模板渲染(base、home、category、post、page,以及共用的导航/页脚/卡片局部片段),输出干净URL的文件夹,生成sitemap.xml、robots.txt和网页manifest。
  5. 把设计从零重做。不要移植旧主题的CSS——一份干净的样式表,自托管的开源字体,内联SVG图标,零外部请求。
  6. 把所有东西都预览一遍。准备一个一行命令的脚本,先构建再本地启动服务。把每个页面都点一遍。
  7. 写服务器配置。压缩、缓存头、安全头、拒绝访问源文件的规则,以及无扩展名URL的回退规则。
  8. 用清单方式部署。rsync输出内容,明确排除源文件夹和脚本,再检查线上URL是否返回200。
  9. 留着导出文件。WXR文件留在项目里——需要重新导入时,重新跑一次迁移脚本就能干净地重新生成内容文件夹。

完整的运行时依赖清单,全部在这里:

pip install jinja2 pyyaml markdown

就这些。

踩过的坑

  • 忘了镜像uploads文件夹,不会有任何警告。构建照样成功,但每张缩略图都会404。这是吃了亏才明白的。
  • 输出和源文件挤在同一个文件夹里。构建出来的HTML,和Python、Markdown源文件落在同一个项目根目录下——部署时的隔离完全靠那份allow/deny清单。一开始感觉有点别扭,但正是这一点让部署能保持是一次简单的rsync。
  • 重命名分类可能会留下一个死文件夹。清理步骤里直接写死了分类名称。在配置里改了名字却忘了同步这份清单,旧文件夹就会孤零零地留在那里,继续被发布着。
  • 日期是按字符串排序的,不是按日期。只要每个日期都写成YYYY-MM-DD就没问题。写错一个,你最新的文章就会悄悄沉到最底下。

← 更多AI 与本地 LLM