关于 wp-static-clone
将 WordPress 网站转为可部署在静态主机上的 HTML 副本,面向迁移或归档网站的操作者。它以 XML sitemap 收集页面,并提供路径重写、WordPress 运行时清理及部署检查流程。
主要功能
- 01从 XML sitemap 获取页面
- 02一次抓取并改写跨页链接
- 03转换为根相对资源路径
- 04清理评论表单等运行时标记
- 05自托管 Gravatar 头像
- 06提供静态托管部署与验证流程
适用场景
- 01将 WordPress 网站迁移到静态托管
- 02保存网站页面为静态 HTML
- 03检查静态副本的链接和资源
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ jdevalk/ skills --skill wp-static-clone安装后在 Claude Code 中输入/wp-static-clone确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
AGENTS.mdREADME.mdscripts/insert-banner.pyrewrite-paths.pyselfhost-gravatars.pystrip-wp-runtime.py详细介绍
wp-static-clone
将在线 WordPress 网站转换为静态 HTML 克隆,可部署到任意静态托管平台。根据网站的 XML 站点地图进行处理。可应对常规 wget 操作容易遗漏的 WordPress 特有问题——Cloudflare 机器人防护、抓取过程中的链接重写、代理分析、转移到 R2 的上传文件、评论表单运行时、Yoast 署名以及 Gravatar 隐私。
操作指南见 AGENTS.md;可复用脚本见 scripts/。 本文件介绍工作流、注意事项和输出结构。
适用场景
遇到以下请求时使用:
- “抓取这个 WordPress 网站并部署到 [host]”
- “将 [domain] 冻结为静态 HTML”
- “从这个站点地图拉取所有页面并转换成静态文件”
- “将这个 WP 网站迁移到 [host],不需要构建步骤”
整体流程(站点地图 → wget → 根相对路径 → 静态托管)也适用于任何能生成标准 XML 站点地图的 CMS。运行时清理(评论表单、Plausible 代理、Gravatar、Yoast)则是 WordPress 特有的。
工作流
阶段 0 — 确认意图
开始抓取前,确认以下事项:
- 源 URL(在线网站)。
- 目标托管平台——Cloudflare Pages、Netlify、Vercel 或通用静态托管平台。这会影响阶段 9。
- 目标域名是否相同。这决定
og:url、<link rel="canonical">和 JSON-LD@id应保留为绝对 URL(同域名——符合正确的 SEO 行为),还是进行重写(不同域名)。 - 如何处理分析和表单。 WP 插件无法在静态网站上运行。Plausible 会替换为标准跟踪器(操作指南见
AGENTS.md);联系和搜索表单则要么移除,要么通过 Pages Functions / Formspree / Netlify Forms 接入——具体取决于托管平台。
阶段 1 — 发现 URL 并拉取 XML 站点地图
获取站点地图索引。先尝试 <root>/sitemap_index.xml(Yoast 约定),再尝试 <root>/sitemap.xml。如果索引引用了子站点地图(page-sitemap.xml、post-sitemap.xml 等),则获取每个子站点地图,并将 <loc> 值合并到 urls.txt。跳过图片站点地图条目。
同时获取 XML 站点地图本身和 Yoast XSL 样式表(操作指南见 AGENTS.md)——它们没有链接到 HTML 中,因此稍后 wget -p 不会找到它们。
阶段 2 — 一次性抓取
关键: 在单次 wget 调用中抓取所有 URL,这样 --convert-links 处理时就能看到所有已下载文件,并正确重写跨页面链接。分开运行抓取会导致先抓或后抓的页面残留绝对链接。操作指南见 AGENTS.md。
阶段 3 — 拉取页面资源抓取未覆盖的资源
有些资源不会通过 -p 跟随抓取,因为它们只出现在 og:image、apple-touch-icon、JSON-LD image/logo、msapplication-TileImage 或 <link rel="modulepreload"> 中。检查并获取遗漏的资源。操作指南见 AGENTS.md,涵盖三个资源根目录(uploads、themes、plugins)。
阶段 4 — 将路径转换为根相对路径
wget -k 会生成混合路径:../wp-content/...(相对于目录深度)和不带前缀的 wp-content/...(首页)。两者在本地都可用,但页面一旦移动就会失效。请将所有路径转换为根相对路径 /wp-content/...:
python3 scripts/rewrite-paths.py output/ urls.txt --source-domain example.com
脚本会根据 urls.txt 而不是遍历目录来派生页面 slug 列表——否则,wget 抓取的 category/、feed/、author/、wp-json/ 等归档目录会被错误地归类为页面,其页面间链接也会被错误重写。
脚本默认使用 WordPress 资源根目录(wp-content、wp-includes)。对于非 WP 来源,请通过 --asset-roots 覆盖,例如 Drupal 使用 --asset-roots sites/default/files,sites/default/themes,Ghost 使用 --asset-roots content/images。其余重写逻辑与 CMS 无关。
阶段 5 — 为静态输出添加标识
这样,未来的你(或任何查看源代码的人)都能一眼看出这是静态克隆,而不是在线 WP 安装:
python3 scripts/insert-banner.py output/
在每个页面的 <!DOCTYPE html> 后插入 HTML 注释。可重复执行而不会产生重复内容。然后替换 wp-content/plugins/wordpress-seo/css/main-sitemap.xsl 中的 “Generated by Yoast SEO” 署名——操作指南见 AGENTS.md。
阶段 6 — 替换 WP 运行时钩子
后端移除后会失效的三类仅适用于 WP 的标记:
1. 评论表单、回复链接和失效的 head 标签。 一次处理:
python3 scripts/strip-wp-runtime.py output/
移除 <div id="respond"> 块(评论表单)、区块主题和经典主题变体中的 comment-reply-link 锚点、comment-reply-js 脚本标签及其底层文件,以及失效的 <head> 标签(REST API 发现、RSD、oEmbed alternate、RSS alternate、归档 next 链接)。始终按 class 匹配,不依赖链接文本的语言。
2. Plausible 分析代理。 WP 插件通过 /wp-content/uploads/<hash>/pa-XXX.js 代理脚本,并将事件发回 /wp-json/...。这两个端点都会消失。将这段双脚本代码块替换为标准跟踪代码——配方见 AGENTS.md。
3. Gravatar 头像。 将每个不同的 (hash, size) 组合自行托管,移除发往第三方的 ?s=N&d=mm 请求:
python3 scripts/selfhost-gravatars.py output/
将文件保存到 avatars/ 并重写每个引用。根据响应字节检测扩展名(PNG 兜底,真实头像为 JPEG),并为不同尺寸保留单独文件(?s=40 和 ?s=80 对应不同文件)。
运行脚本后,审查剩余的绝对源域名 URL(配方见 AGENTS.md),并按情况分类处理:作者归档页 → 移除 <a> 包装标签,服务器渲染的 iframe → 移除外层 <p>,Gravity Forms 脚本块 → 提及 gform 时移除,等等。
阶段 7 — 复制 robots.txt
HTML 中没有链接到它;请显式获取。如果部署后的网站地图路径与源路径不同,请调整 Sitemap: 引用。
阶段 8 — 在本地验证
使用 python3 -m http.server 从 output/ 提供服务,然后运行 AGENTS.md 中的验证清单:
urls.txt中的每个 URL 都能解析到文件(没有遗漏页面)。- 规范 URL /
og:url/ JSON-LD 允许列表以外,不再有https://<source-domain>/。 wget --spider未发现损坏的内部链接。- 在浏览器中抽查首页和一个深层页面。留意 srcset 图片、侧边栏小工具和页眉横幅——遗漏时这些问题会悄无声息地出现。
阶段 9 — 部署
主机专属配方见 AGENTS.md:
- Cloudflare Pages —
_redirects、_headers、“无构建命令、无输出目录”的默认设置。 - Netlify — 同样的
_redirects/_headers语法,以及netlify.toml。 - Vercel — 使用
redirects/headers的vercel.json。 - 通用 — nginx
try_files、ApacheOptions +MultiViews。
易踩问题
以下是我们遇到的问题。请勿重蹈覆辙。
-
Cloudflare 的机器人防护会对默认的
Wget/1.xUA 返回 403。 始终设置真实浏览器 UA 和Accept/Accept-Language标头(参见操作步骤)。如果在一阵密集请求后看到403 Forbidden,就是这个原因——暂停请求、更换 UA,然后重试。 -
只有在单次 wget 调用中,跨页面链接重写才有效。 wget 的
-k只会将链接重写为当前运行中看到的本地路径。如果某个页面是在单独一次调用中下载的(例如,为恢复某个 URL 的 403 而单独下载),其中指向其他页面的链接就会保持绝对路径。解决方法:设置好正确的 UA 后,重新完整抓取一次。不要分批零敲碎打。如果要大规模抓取(10K+ 个 URL),且无法在一次运行中完成,请分批抓取,然后重新运行scripts/rewrite-paths.py,将其作为最终统一处理步骤——此时-k的输出就多余了。 -
不同主机的默认发布目录。 如果未配置构建命令,Cloudflare Pages 会提供仓库根目录的内容。Netlify 和 Vercel 的默认目录也都是根目录。如果抓取到
output/,要么将文件移到仓库根目录(git mv output/* .),要么配置主机从output/发布。Pages 配置错误时的表现:每个 URL 都返回 404,并带有 R2 风格的标头(access-control-allow-origin: *、cache-control: no-store),而不是带有 Pages 品牌标识的 404。 -
WordPress Offload Media 插件会将
/wp-content/uploads/路由到 R2 / S3 存储桶。即使之后直接访问会返回 404,wget 仍可能成功获取图片(存储桶同步可能是间歇性的或不完整)。请相信本地副本——这正是我们抓取并自行托管的原因。 -
Sitemap 和 Yoast XSL 不会从 HTML 中链接。 wget
-p找不到它们。请在第 1 阶段显式获取。 -
带有
?ver=...查询字符串的文件名。 wget 会将其保留为字面文件名;HTML 则使用%3F编码。标准服务器(Pages、Netlify、Vercel、python -m http.server)会进行 URL 解码并正确提供文件。除非确实出现问题,否则不要尝试“清理这些文件名”。 -
og:url、canonical 和 JSON-LD 保持绝对形式。 它们用于标识规范资源;如果重新部署到同一域名,这样写就是正确的。只有更换域名时才需要重写。 -
sed -i ''仅适用于 macOS / BSD。 GNU sed 需要使用sed -i(不带空字符串参数)。AGENTS.md中的操作步骤会标出 macOS 特有用法;如果有选择,默认使用 Python 脚本——它们具有可移植性。
输出结构
<repo-root>/
index.html ← homepage
<slug>/index.html ← one per URL from sitemap
wp-content/ ← assets (themes, uploads, plugins)
wp-includes/ ← block library CSS, et al.
avatars/ ← self-hosted Gravatars (Phase 6)
sitemap_index.xml
page-sitemap.xml ← + any other child sitemaps
wp-content/plugins/wordpress-seo/css/main-sitemap.xsl
robots.txt
_redirects ← optional, host-specific
_headers ← optional, host-specific
推送到 git 主机,并连接到静态主机;不设置构建命令,也不设置构建输出目录——默认设置即可。