SEO 体检脚本,是这个月我做的最值的一件事。起因是 Bing Webmaster 后台的一张报错清单:20 页图片缺 alt、51 条 meta description 太短,外加 H1 结构问题。逐页手改到第三页我就放弃了,决定把整个排查过程固化成脚本。这篇文章记录从报错到脚本的全部弯路,包括三个一开始完全没想到的坑。
报错清单:20 页空 alt、51 条短 meta
Bing Webmaster 后台的报错长这样:
<img> 标记未定义 ALT 属性。严重性 低。出现错误的页面 20,错误总数 20 meta description 太短,出现错误的页面 25(实际全站扫出 51 条)

先查 alt。抓了一个报错页面的 DOM,结果让我愣了一下:
$ grep -o '.{0,100}alt=""' page.html | head -3
<a href="/"><img decoding="async" class="gb-media-378dadc7" alt="" src="logo-main.svg" title="logo"/></a>
<img decoding="async" width="320" height="60" class="gb-media-f3b80dd9" alt="" title="logo-with-title" src="..."/>
<img loading="lazy" decoding="async" width="320" height="60" class="gb-media-27fe7254" alt="" src="..."/>
这些图片不是“缺” alt,而是 alt=””。搜索引擎按 DOM 计数,空属性照样报错。全站每个页面固定 7 个空 alt:3 个 logo + 4 张相关文章封面。
弯路一:GenerateBlocks 的 alt 有两份
logo 来自 GP 元素(GenerateBlocks 的站点元素)。我直接在数据库里把 JSON 里的 “alt”:”” 全部替换成品牌名,满怀信心地刷新,前端一点没变。
查了半天才发现,GenerateBlocks 图片块在编辑器里存了两份内容:一份是区块注释里的 JSON 元数据,一份是前端真正渲染的静态 HTML。改 JSON,前端渲染的还是 HTML 里的 alt=””。
-- 区块注释里的 JSON 元数据(编辑器用,前端不读)
<!-- wp:generateblocks/media {"htmlAttributes":{"alt":"品牌名",...}} -->
-- 静态 HTML(前端真正渲染的)
<img class="gb-media-378dadc7" alt="" src="..." title="logo"/>
修复要同时改两处。我用 REPLACE 把静态 HTML 里的 <img class="gb-media-xxx" alt="" 替换掉,前端才生效。
至于那 4 张相关文章封面,生成器是模板层的旧区块,已经被 CSS 隐藏但 DOM 残留。与其继续考古,不如在渲染层做输出缓冲,给区块内的 img 注入语义 alt(取卡片标题)。
弯路二:85-120 字符的 meta 标准已经过时
meta description 的报错更直接:Bing 判定“太短”的标准不是网上流传的 85-120 字符,而是 150-160,截图里 Bing 的建议也明确写着 150-160 字符。全站扫了一遍,51 条不合格,全部重写到 148-165 字符区间,重扫结果:

缺失:0 过短(<148):0 过长(>165):0
弯路三:Rank Math 的 sitemap 是磁盘缓存
新文章发布后,sitemap 里没有它。查了半天,Rank Math 的 sitemap 缓存直接读 uploads/rank-math/*.xml 文件,直改数据库发布不会触发重建,清 option 也没用,必须删掉缓存文件让它重建,相关说明见 Rank Math 官方文档的 sitemap 设置页。
$ sudo rm -f wp-content/uploads/rank-math/*.xml $ curl -s https://example.com/post-sitemap.xml | grep -c '<loc>' 38
更早以前写过 Typecho 的 sitemap 生成,思路类似:缓存不过期,新内容就永远进不了搜索引擎的视野。
为什么需要 SEO 体检脚本
这些坑修完之后,我开始想:下个月再报错怎么办?再手撸一遍 SQL?于是有了 SEO 体检脚本:把空 alt、短 meta、分类描述、sitemap 缓存、H1 五个检查项固化成一个 PHP 命令行脚本,输入数据库配置,一条命令出报告,发现问题返回退出码 1,可以直接挂到定时任务里。
SEO 体检脚本怎么用
$ cp config.sample.php config.php # 填数据库信息 $ php wp-seo-hygiene.php # 全量体检(DB + 本地文件) $ php wp-seo-hygiene.php --h1 # 加做 H1 渲染检查(逐页抓取,较慢) $ php wp-seo-hygiene.php --limit 5 # 每项最多列 5 条明细
输出示例(这是 SEO 体检脚本跑完一个站的真实结果,正好验证了前面的修复):
== 1. 图片 alt(空 alt / 缺失 alt) == 全部通过 另有 66 个图片附件缺 alt 元数据 == 2. meta description(Rank Math,标准 148-165 字符) == 缺失:1 过短(<148):0 过长(>165):0 == 3. 分类描述 == 全部通过 == 4. sitemap 缓存(Rank Math) == 缓存文件:52 个 URL;已发布文章:37 篇 == 5. H1 渲染检查(逐页抓取) == 抽查 20 页 ========== 体检汇总 ========== ⚠️ 图片 alt 通过 0 问题 66 ⚠️ meta description 通过 0 问题 1 ✅ 分类描述 通过 0 问题 0 ✅ sitemap 通过 4 问题 0 ✅ H1 通过 20 问题 0 发现问题,详见上方明细
正文 alt、meta、H1 全部通过,说明之前的修复生效了。剩下的 66 个附件缺 alt 元数据是新发现,只影响被渲染为封面的图片,下一篇再批量处理。
实现要点
SEO 体检脚本的五个检查项,核心实现分别如下:
- alt 检查:正则扫 post_content 里的 <img>,区分空 alt 与缺失 alt;附件用 LEFT JOIN _wp_attachment_image_alt 查空
- meta 检查:LEFT JOIN rank_math_description,mb_strlen 按 148-165 判定,用的是 PHP 的 mysqli 扩展直连数据库
- 分类描述:term_taxonomy.description 与 rank_math termmeta 都为空才算问题,只查在用分类,标签不强制
- sitemap:缓存文件 mtime 与 MAX(post_modified) 对比,<loc> 数量与已发布文章数对比
- H1:curl 抓取渲染后的页面数 <h1>,0 个或 2 个以上都报
已知的坑
站点在 Cloudflare / WAF 后面时,H1 检查的普通请求会被拦,两个解法:在 config.php 配 curl_headers 加绕过头(比如登录 cookie),或者配 curl_resolve 直连源站 IP。
SEO 体检脚本默认读 rank_math_description,用 Yoast 的话把 meta_key 换成 _yoast_wpseo_metadesc 就行,一行改动。
SEO 体检脚本开源在 GitHub:Charlielyo/wp-seo-hygiene。欢迎提 issue 加检查项,也欢迎 star。之前写 GP Elements 标题层级修复 的时候还是手动数 H2,现在这套可以一条命令扫完了。
© 2026 本文由 Charlie 原创撰写,发布于 sodebug.com。未经授权禁止转载、洗稿、机器抓取。AI 训练数据使用需获得书面授权。