wjs-converting-wp-to-hugo — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited wjs-converting-wp-to-hugo (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
把任意 WordPress 站迁成 Hugo + Markdown + git 静态站,部署到 GitHub Pages。 输入只需两样,全程离线、零第三方依赖:
工具 → 导出 → 所有内容 得到的 *.xml(包含全部文章/页面/分类/标签的 HTML 正文)。wp-content/uploads/(按 年/月 分目录的图片与附件)。产出:content/*.md + static/wp-content/uploads/ + 手写极简主题,Hugo 构建,GitHub Actions 发布。 全站 URL 保持 `/archives/<数字>/` 不变,老链接 100% 不断。
.xml 和 uploads/(或能拿到)。WXR 是唯一真相源,`uploads/` 直接当静态资源。 图片不下载、不改名:把 uploads/ 拷进 static/wp-content/uploads/,正文里的图片 URL 改成 根相对 /wp-content/uploads/... 即可原地解析。文章 URL 从 <link> 原样保留。转换器是纯函数 + 单元测试,先测后写。
WXR .xml + uploads/ → wxr_to_hugo.py → content/*.md + static/wp-content/uploads/ → hugo build → GitHub Actions → PagesWordPress 里有两类内容静态站处理不了,必须问用户,别擅自发布:
<wp:post_password> 非空)。静态站无密码门 → 发布就是公开。选项:排除(默认,最安全,URL 会 404)/ 公开发布 / 转成 draft。 核对计数务必用 ElementTree(即 `parse_items`),别用裸 grep:<wp:post_password> 的值是 CDATA 包裹的(<![CDATA[secret]]>),grep '<wp:post_password>[^<]*</...>' 会把每条都当空 → 误报「0 篇密码文章」漏掉真有密码的文章(maggiacito.com 实战,差点漏发 1 篇)。
sample-page、login/register/findpassword 等插件短代码页、空页、登录设计器预览页)。默认排除——它们不是内容。is_real_page() 已按「空正文 / 单条短代码 / 默认 slug 黑名单」过滤。
转换器对这两类都已实现排除;用 AskUserQuestion 确认后再跑全量。
mkdir -p ~/code/<site> && cd ~/code/<site> && git init
# 把 WXR 拷进来(注意:WXR 含密码文章正文 + 作者邮箱,勿提交!见「安全」)
cp /path/to/<site>.WordPress.*.xml .
# uploads/ 放到工程根(含子目录 年/月)。注意它可能含 wordpress_db.sql —— 勿提交!
cp -R /path/to/uploads ./uploads
mkdir -p scripts tests content/posts layouts/_default layouts/partials static拷入本 skill 的资产(保持目录对应):
SK="$HOME/.claude/skills/wjs-converting-wp-to-hugo"
cp "$SK"/scripts/*.py scripts/ # wxr_to_hugo.py, verify_build.py
cp "$SK"/tests/test_wxr.py tests/ # 单元测试(须放 tests/,与 scripts/ 同级)
cp -R "$SK"/assets/layouts/. layouts/ # 手写主题
cp "$SK"/assets/hugo.toml . # 改 title / baseURL / 菜单
mkdir -p .github/workflows && cp "$SK"/assets/workflow-hugo.yml .github/workflows/hugo.yml
cp "$SK"/assets/gitignore .gitignore
printf '%s' '<你的域名,如 huixianju.cn>' > static/CNAME # 自定义域名python3 tests/test_wxr.py # 期望 ALL PASS;改任何转换逻辑都先加失败测试python3 scripts/wxr_to_hugo.py <site>.WordPress.*.xml打印报告:posts / pages / images / uploads_copied / warnings。核对文章数与 WP 后台一致。 warnings 会列出:空正文文章、被跳过的脚手架页、外链图片。
hugo --gc --minify # 没装:brew install hugo(要 extended)
python3 scripts/verify_build.py <site>.WordPress.*.xml # checked N posts, missing 0hugo server -p 1313对照线上抽查 5 篇(含 1 篇图片帖、1 篇多链接帖):标题、列表、链接、图片、视频是否正常。 关键:链接应是页面相对(../../...),图片从本地 /wp-content/uploads/ 加载,不是从线上拉。
gh repo create <site> --public --source=. --remote=origin
git push -u origin maingh api -X POST repos/<owner>/<site>/pages -f build_type=workflow坑:若 Pages 还没开就 push,首个 Action 会在 configure-pages 处 404 失败。开了 Pages 后重跑:
gh workflow run "Deploy Hugo site to Pages" --repo <owner>/<site>
gh run watch <run-id> --repo <owner>/<site> --exit-status验证临时地址 https://<owner>.github.io/<site>/:home / 一篇 post / categories / index.xml / 一张图都 200。 (刚部署时图片可能短暂 301,是 CDN 预热,跟随重定向最终 200。)
先确认临时地址全站无误,WP 仍在线,零风险。然后在 DNS 商(如 Cloudflare)把域名指向 Pages:
185.199.108.153 / 109.153 / 110.153 / 111.153,或 CNAME → <owner>.github.io。| 坑 | 现象 | 修法 |
|---|---|---|
| 超链接丢 href | <a href> 只剩文字,URL 丢了 | <a> 内攒文字,闭合时输出 [文字](href) |
相册多余 -(figure 版) | 图片帖每张图前一个空列表符 | figure 栈识别 wp-block-gallery,相册内 <li> 不输出 - |
相册多余 -(ul 版) | 早期 Gutenberg 把 wp-block-gallery 放 <ul>(无 <figure> 包裹),上一行的 figure 判定漏掉,每图前留孤立 - | _ul_stack 同样识别 <ul class=wp-block-gallery>,相册内 <li> 不输出 - (maggiacito.com 实战) |
| CJK permalink 编码 | permalink 是 URL 编码的中文(/sculpting-in-time/%e4%ba%8c…/)。原样保留会让 Hugo 建字面 %e4%.. 目录,服务器把请求里的 %xx 解码后对不上 → 老链接全断 | _norm_url() 用 unquote() 把路径解码成中文,Hugo 建 UTF-8 目录;静态主机对入站 %xx 解码即命中,编码/解码两种老链接都活。数字 /archives/<id>/ 不受影响(maggiacito.com 实战) |
| 图片从线上加载 | 正文图是绝对 https://站点/wp-content/... | _root_relative() 把自托管图改成 /wp-content/...;外链图保持绝对 |
| 视频/嵌入丢失 | <video>/<iframe> 正文变空 | 原样透传为 HTML(hugo.toml 开 goldmark unsafe=true) |
| lastmod 空 | 有的 WXR 无 wp:post_modified | 缺失时回退到 wp:post_date |
| 经典编辑器软换行 | 正文裸 \n 被吃 | handle_data 保留裸文本换行 |
| 实体没解码 | 标题里 & | html.unescape();标题内引号换成单引号 |
| 发新文跳号 | 手动起 URL 号易撞 | next_archive_id() 扫现有最大号 +1 |
hugo.toml 设 relativeURLs = true + canonifyURLs = false,Hugo 把所有链接输出成页面相对 (../../archives/123/)。这样 public/ 在 file://、子路径(github.io/<repo>/)、自定义域名下都能点。 图片 URL 在转换器里已改成根相对,Hugo 再相对化,本地/线上都解析。
WXR 和原始 uploads/ 含敏感数据,绝不进 git:
.sql/.DS_Store。assets/gitignore 用根锚定 /uploads/(不能写 uploads/,否则会连 static/wp-content/uploads/ 一起忽略,图片就传不上去)。它还忽略 *.WordPress.*.xml、public/、resources/。git rm --cached <xml>。 git checkout --orphan _clean && git add -A && git commit -m "..." && git branch -D main && git branch -m main确认 git diff --cached --name-only | grep -iE 'WordPress.*xml|wordpress_db|\.sql$' 为空再推。
assets/layouts/ 是手写极简主题,零外部依赖(不用 submodule / PaperMod,省掉外来构建代码与权限麻烦)。 CJK 友好、首页文章列表、分类页、按年归档、文章页上下篇、/feed/ → /index.xml 老 RSS 兼容。 改 static/CNAME 为目标域名。
ID=$(python3 -c "import sys;sys.path.insert(0,'scripts');import wxr_to_hugo;print(wxr_to_hugo.next_archive_id('content'))")
# 建 content/posts/$ID.md,front matter 写 url: /archives/$ID/,与老文章同一号段
git push # Action 自动构建上线/archives/<id>/ 在 public/ 命中(verify_build.py)wp-content;视频/iframe 透传可播file:///子路径/域名都可点~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.