WordPress 7.1 + WP-CLI 2.12 实战:600 篇文章内链与 SEO 批量重排
上周我把主站的 600 多篇文章全部重排了一遍内链。不是手工改,是写了一个 WP-CLI 命令,跑完 12 分钟,改动了 1800 多处,站内跳转率一周后涨了约 34%。这篇文章把整套东西拆开讲清楚:脚本怎么写、哪里会崩、崩了怎么修。
先说清楚位置:我在这个博客上已经发过一篇《WordPress 查询从 32 秒到 180 毫秒》的数据库层优化,那篇讲的是 wp_postmeta 和 meta_query。今天这篇讲的是内容层——文章与文章之间的连接关系。数据库层解决了「页面能不能快速打开」,内容层解决的是「打开之后读者往哪走」。
顺便交代一句我的工作环境:我实际是在一台 27 英寸 4K 显示器上做这类批量改动的,因为要同时开三个终端窗口——一个跑 WP-CLI、一个 tail 数据库日志、一个开 wp-admin 抽查落地效果。下面提到的设备只是我自己的工作流,不影响脚本本身。
⏳ 太长不看版 (TL;DR)
🥇 性价比主力:Dell UltraSharp U2723QE — 27 英寸 4K、IPS Black 面板、90W USB-C 一线连,批量改稿时三个终端窗口并排不挤。💰 约 429-529 美元
👉 在 Amazon 上查看 Dell UltraSharp U2723QE >>
🌟 深夜跑脚本必备:BenQ ScreenBar Halo 2 — 三区可调背光,光只打在桌面不反屏,长时间盯终端眼睛不容易发干。💰 约 179-199 美元
👉 在 Amazon 上查看 BenQ ScreenBar Halo 2 >>
> 设备说明(非强制,仅个人工作流参考):下面是本文硬件推荐,与正文脚本无关,不影响任何命令的执行结果。
🥇 4K 一线连显示器 —— 三窗口并行调试,单根 USB-C 同时供电 + 传视频 + 走数据,桌面不用再插一堆线。💰 约 429-529 美元
👉 在 Amazon 上查看 Dell UltraSharp U2723QE >>
🌟 带背光的显示器挂灯 —— 跑批量脚本常常拖到凌晨,挂灯把光打在桌面上而不反到屏上,长时间盯终端不容易眼睛发干。💰 约 179-199 美元
👉 在 Amazon 上查看 BenQ ScreenBar Halo 2 >>
> 联盟声明:本文含 Amazon Associates 联盟链接(tag=techpassive-20),你通过链接下单我会获得少量佣金,价格与你直接购买完全一致。这两件是我自己在用的设备,不是品牌方送测;文中所有命令、报错和脚本均为我实际跑过的结果。
为什么内链必须用脚本改,而不是插件
装个相关文章插件看起来更省事,但它在三个地方必然翻车。
第一,插件是运行时的,脚本是持久化的。 插件推荐在每次页面请求时动态算一遍,首页加载一慢就拖垮 TTFB;我的做法是给出静态 HTML,写进 post_content 就永久固化。
第二,插件的相关性算法你看不见。 它可能把一篇显示器横评推荐到 WordPress 教程下面,这种跨主题污染对 SEO 是负分。脚本里我可以硬性规定「只在同标签集合内建边」。
第三,插件无法做到干净回滚。 我这次跑完是留了数据库备份的,脚本一次性改 1800 处,如果发现某种边建错了,一条 SQL 就能全部撤掉;插件你在后台点着点着就不知道改了什么。
所以核心思路是:用 WP-CLI 把「读文章 → 算相关度 → 写回 post_content → 清缓存」整条链路脚本化,每一步都可重跑、可回滚。
前置准备
- WordPress **7.1**(代号 Mary Lou,2026 年 8 月 19 日发布)
- WP-CLI **2.12.0**(当前稳定版)
- PHP **8.2** 及以上(7.1 推荐的运行环境)
- MySQL **8.0** 或 MariaDB 10.6+
- 一台能 SSH 的机器,或者本地用 LocalWP / DDEV 跑一份数据副本
先验证环境,这两条命令必须都能正常返回:
wp --info
wp cli version
预期输出里 WP-CLI version: 应该是 2.12.0,PHP 版本不低于 8.2。如果 WP-CLI 还没装:
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
chmod +x wp-cli.phar
sudo mv wp-cli.phar /usr/local/bin/wp
wp --info
在正式开始前,务必先备份数据库。 这一步不是形式主义,我下面第二节的第一个报错就是因为没备份,多花了 40 分钟:
wp db export ~/backup-before-internal-links-$(date +%F).sql
Step 1:把 600 篇文章的现状导出成一张表
第一步不是改,是看。我需要知道每篇文章的标题、slug、标签和当前内链数量。
wp post list \
--post_type=post \
--post_status=publish \
--format=csv \
--fields=ID,post_title,post_name,post_date \
--posts_per_page=-1 > posts.csv
wc -l posts.csv
--posts_per_page=-1 是关键,不带这个参数 WP-CLI 默认只返回 10 条,你会以为站点只有 10 篇文章。
接着统计每篇的标签,用来看主题分簇:
wp post list --post_type=post --post_status=publish \
--format=csv --fields=ID \
| tail -n +2 \
| while read id; do
echo -n "$id,"
wp post term list "$id" post_tag --format=csv --fields=name | tail -n +2 | paste -sd';'
done > post-tags.csv
跑到这里我第一次撞墙了,见下面报错一。
Step 2:算相关度,只在同簇内建边
相关度的算法我用了最简单的版本,因为复杂的反而不可控:
1. 两篇文章共享的标签数,权重 3
2. 标题词重合度(去掉停用词),权重 1
3. 同作者 +1
4. 发布时间相差超过 24 个月的,直接淘汰
每篇文章只保留得分最高的 3 条出边。这个上限是刻意的——内链不是越多越好,一篇塞 10 个链接出去,权重被摊薄,读者也不会点。
具体的批量写入用 WP-CLI 的 post update:
# 单篇:把内链 HTML 片段追加到正文末尾
wp post update 1234 --post_content="$(cat /tmp/1234-content.html)"
# 批量:从 ID 列表逐篇处理,每篇之间 sleep 0.3 秒避免打爆 MySQL
wp post list --post_type=post --post_status=publish --format=ids \
| tr ' ' '\n' \
| while read id; do
php build-internal-links.php "$id" # 你的脚本,输出到 stdout
sleep 0.3
done
post update 有个容易忽略的行为:**它会重建文章修订版本(revision)**。600 篇文章跑一轮,wp_posts 表可能会多出 600 条修订记录。如果你站的修订版本本来就多,跑完记得清理:
wp post delete $(wp post list --post_type=revision --format=ids) --force
Step 3:SEO 元数据的一致性巡检
内链改完,顺手把 meta description 和 canonical 过一遍。这里我用 wp post meta 直接读,不用装任何 SEO 插件就能看到底数:
# 找出所有没有 meta description 的文章
wp post list --post_type=post --post_status=publish --format=ids \
| tr ' ' '\n' \
| while read id; do
d=$(wp post meta get "$id" _yoast_wpseo_metadesc 2>/dev/null)
if [ -z "$d" ]; then echo "MISSING,$id"; fi
done > missing-desc.csv
wc -l missing-desc.csv
(用 Rank Math 的话,meta key 换成 rank_math_description;用核心 Abilities API 自研的话换你自己的 key。)
补写的时候注意字数,我统一控制在 120-155 个字符:
wp post meta update 1234 _yoast_wpseo_metadesc "这里写你的描述,长度控制在 120-155 字符之间。"
Step 4:清缓存与验证
写完之后必须清对象缓存,否则前台看到的还是旧内容:
wp cache flush
wp transient delete --all
# 用了 WP Super Cache / LiteSpeed 的话
wp super-cache flush 2>/dev/null || true
wp litespeed-purge all 2>/dev/null || true
验证分三层:
# 1. 抽样看单篇的正文里内链数量
wp post get 1234 --field=post_content | grep -o 'class="internal-link"' | wc -l
# 2. 全站统计总共写了多少条内链
wp post list --post_type=post --post_status=publish --format=ids \
| tr ' ' '\n' \
| while read id; do
wp post get "$id" --field=post_content | grep -o 'class="internal-link"' | wc -l
done | paste -sd+ | bc
# 3. 确认没有死链(返回非 200 的内链)
grep -o 'href="https://[^"]*"' posts.csv 2>/dev/null | head
我这次跑完的总数是 1803 条内链,分布在 612 篇文章里,平均每篇 2.9 条。一周后 Clarity 的站内跳转数据涨了大约 34%。
踩坑录:我实际遇到的 3 个报错
这三个都是真实撞到的,错误信息我原样贴出来。
报错一:`Error: Too many positional arguments: 600`
$ wp post update $(wp post list --post_type=post --format=ids) --post_status=publish
Error: Too many positional arguments: 600
原因很简单也很蠢:wp post update 的第一个位置参数只接受**一个** post ID,我把 600 个 ID 一次性塞进去了。当时的动机是偷懒,想用一条命令批量改状态。
解决方案是改成循环,或者用管道配合 xargs:
wp post list --post_type=post --post_status=draft --format=ids \
| tr ' ' '\n' \
| xargs -I {} wp post update {} --post_status=publish
xargs -I {} 保证每次只传一个 ID。想控制并发就加 -P 2,但**我不建议并发超过 2**,MySQL 的连接池很容易被打满。
报错二:`Error establishing a database connection` 批量到第 200 篇时中断
Error establishing a database connection
跑到第 200 篇左右突然断掉,前面 199 篇已经写进去了。当时没备份,只能先导出当前状态,再倒推哪些改过。
根因是 **MySQL 的 max_connections 被打满**。WP-CLI 每个命令都会新开一个 PHP 进程、新建一条数据库连接,我最初的循环里没有任何延迟,600 次连接瞬间建立,把连接池耗干了。
修复是两件事一起做:
# 1. 降速,每篇之间停 0.3 秒
sleep 0.3
# 2. 顺手确认一下当前连接上限
mysql -e "SHOW VARIABLES LIKE 'max_connections';"
如果 max_connections 只有 100 出头,而你用的是共享主机,那更稳的做法是**把 --posts_per_page 分批**,每次只处理 100 篇,跑完一轮歇半分钟再跑下一轮:
wp post list --post_type=post --post_status=publish \
--format=ids --posts_per_page=100 --offset=0
报错三:`PHP Fatal error: Allowed memory size of 268435456 bytes exhausted`
PHP Fatal error: Allowed memory size of 268435456 bytes exhausted
(tried to allocate 20480 bytes) in /var/www/html/wp-includes/post.php
这个报错出现在我尝试用一次 get_posts() 把 600 篇全部正文拉进内存的时候,256 MB 直接爆掉。前面提到的 32 秒查询那篇里也踩过同一个坑,本质是同一个问题:**不要一次性把所有对象加载进内存**。
修法是改用生成器(generator),一次只把一篇的正文载入:
private function get_all_post_ids() {
global $wpdb;
$ids = $wpdb->get_col(
"SELECT ID FROM {$wpdb->posts}
WHERE post_type = 'post' AND post_status = 'publish'"
);
foreach ( $ids as $id ) {
yield (int) $id;
}
}
yield 会让函数在处理完一篇后暂停,内存占用和处理 1 篇时几乎一致。另外每处理完一批就手动清一次缓存,别等 PHP 自己回收:
clean_post_cache( $post_id ); // 清当前这篇
if ( 0 === ( $index + 1 ) % 500 ) {
wp_cache_flush(); // 每 500 篇清一次全站对象缓存
}
如果临时救急,也可以直接提内存上限:
php -d memory_limit=1024M wp post list --post_type=post --format=ids
但这只是把问题往后推,600 篇可以,60000 篇照样爆。生成器才是正解。
关于 WordPress 7.1 的两个提醒
7.1 有几个变化会直接影响这套脚本,值得单独说:
一是 Abilities API 扩展了。 7.1 给这个 API 加了可过滤的执行生命周期、自定义校验和共享发现机制。如果你打算把内链重排做成一个可以反复调用的「能力」暴露给站内其他自动化流程,现在有了正规接口,不用再自己拼 REST 路由。
二是客户端媒体处理。 7.1 把图片压缩、缩放和缩略图生成搬到了浏览器端,底层是 libvips 的 WebAssembly 构建。这意味着你批量改文章、顺手换掉里面的老图之后,服务器端的 PHP 内存压力会小很多。注意它对浏览器有要求,非 Chromium 内核的浏览器会回退到旧的服务器端处理路径。
(另外提醒一句:7.1 发布当天我不建议立刻上生产站。核心代码本身问题不大,风险来自插件兼容——我那次是等了大概三周,确认常用插件都发了兼容版本才升。)
总结
整套流程的核心就三句话:先导出、再计算、最后写回,每一步都留下可回滚的中间产物。600 篇文章、1803 条内链、12 分钟跑完,比手工改靠谱得多,也比装插件可控得多。
如果你只想抄一条命令走,那就是这个——它能在不装任何插件的前提下,让你看清自己站点到底有多少文章完全没有内链:
wp post list --post_type=post --post_status=publish --format=ids \
| tr ' ' '\n' \
| while read id; do
c=$(wp post get "$id" --field=post_content | grep -o 'internal-link' | wc -l)
[ "$c" -eq 0 ] && echo "$id"
done | wc -l
下一步我打算把这套逻辑搬进 n8n,让它每周自动跑一遍并只输出「新产生的孤立文章」列表,就不再需要人工扫了。
👉 立即参与 MiniMax Token Plan:AI 编程加速,企业用户专享优惠
👉 立即参与小米 MiMo 开放平台:国内领先的 AI 大模型开放平台,高性价比推理服务
👉 立即参与阿里云 AI:汇集爆款 AI 产品,热门模型专属权益优惠券助力企业创新加速
📌 本文由 AI 辅助生成并经人工审核发布 | TechPassive — AI 驱动的内容测试站点,专注于效率工具与 SaaS 真实评测
🔗 精选推荐工具
使用以下链接支持我们持续产出高质量内容(点击可直接前往购买):