跑一次 accessibility 审计——报告很难看:180 篇文章 340 张图无 alt,或更糟——alt="" 当占位符随手贴。屏幕阅读器念”image”或者悄悄跳过。Google 图片搜索没东西索引——AdSense 质量审核看到懒散作者。光是无障碍这一击就够动手;SEO 和政策含义把它推到紧急区。
写文章时根本没强制 alt——多数图被拖进编辑器、alt 字段空着。修法机械:grep-and-fix 扫一遍、prebuild 缺 alt 就 fail、MDX lint 规则——未来 PR 无法回退。一次做完锁死,比每次审计都重做便宜得多。
常见原因
1. 编辑器从未强制 alt
Markdown 编辑器(或随便什么)接受  不抱怨——作者懒得填、没摩擦。
如何判断:grep 所有 MDX 找空 alt 的 ![]。
grep -rEn '!\[\]\(' src/content/articles/ | wc -l
2. HTML img 标签没 alt
作者要尺寸控制改用裸 <img>,忘了 alt:<img src="x.png" width="600" /> 是合法 HTML 但无障碍坏了。
如何判断:grep 没 alt 属性的 img。
grep -rEn '<img [^>]*src=' src/content/articles/ | grep -v 'alt='
3. alt 存在但是装饰性占位
作者写 alt="image" 或 alt="screenshot" 来骗审计——技术上有、语义上没用。
如何判断:grep 已知坏模式。
grep -rEn 'alt="(image|screenshot|picture|img|photo)"' src/content/articles/
4. 老 CMS 批量导入剥了 alt
从 WordPress / Notion / Ghost 迁移——导出格式丢了 alt 或存在导入器忽略的兄弟字段——所有迁来的图都无 alt。
如何判断:查迁移日期——那天前的文章都是受影响群体。
5. 装饰性图——alt="" 才是对的
有些图纯装饰(分隔线、通用插画)——这些用 alt="" 才对——屏幕阅读器应跳过。审计必须区分”缺 alt”和”故意空 alt”。
如何判断:每个空 alt 个例审一遍——要么填、要么标记装饰。
最短修复路径
Step 1:把缺口盘点出来
按严重度分组的报告:
# scripts/audit-image-alt.mjs
import fs from "node:fs";
import path from "node:path";
const dirs = [
"src/content/articles/en",
"src/content/articles/zh",
];
const issues = { missingMarkdown: [], missingHtml: [], placeholder: [] };
function scan(dir) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, entry.name);
if (entry.isDirectory()) { scan(p); continue; }
if (!p.endsWith(".mdx")) continue;
const txt = fs.readFileSync(p, "utf8");
if (/!\[\]\(/.test(txt)) issues.missingMarkdown.push(p);
if (/<img(?![^>]*alt=)/.test(txt)) issues.missingHtml.push(p);
if (/alt="(image|screenshot|picture|img|photo)"/i.test(txt)) issues.placeholder.push(p);
}
}
scan("src/content/articles");
console.log(JSON.stringify(issues, null, 2));
现在你有精确的违规清单和类别。
Step 2:按流量回填最严重的
高流量文章——手写真 alt。低流量——模板化(“X dashboard 显示 Y 的截图”)或标装饰。不要一口气 AI 生 340 条——读者会注意到通用措辞。
合理分级:
- 高流量(按印象 top 50):手写
- 中流量:AI 生、20 个一批人审
- 低流量 / 装饰:alt="" 加代码注释标"故意"
Step 3:prebuild 检查缺 alt 直接 fail
把审计接到 prebuild、非零退出:
# package.json
"scripts": {
"audit:alt": "node scripts/audit-image-alt.mjs",
"prebuild": "npm run audit:content && npm run audit:alt && ..."
}
必须区分缺 alt 和故意空 alt——约定:空 alt 只在前一行有 {/* decorative */} 注释标记或匹配已知装饰资产路径模式时才合法。
Step 4:加 MDX lint 规则
持续强制——用 remark-lint-no-empty-image-alt-text + 自定义规则配 HTML img:
// .remarkrc.mjs
export default {
plugins: [
"remark-lint",
["remark-lint-no-empty-image-alt-text", "warn"],
// 自定义规则配裸 <img>
() => (tree, file) => {
visit(tree, "html", node => {
if (/<img(?![^>]*alt=)/.test(node.value)) {
file.message("img tag missing alt attribute", node);
}
});
},
],
};
在动 .mdx 的 PR 跑 CI。
Step 5:写下约定
加一段简短的作者指南:
- 真 alt:描述图显示什么、AND 在上下文里为什么重要
- 装饰图:alt="" + 前一行 {/* decorative */} 注释
- 永不用"image"/"screenshot"这类占位字符串
- UI 截图:app 名 + feature 名 + 当前相关状态
未来作者有一处可查、reviewer 有一条规则要执行。
预防
- 任何
![]或<img>缺 alt——prebuild fail,零例外 - MDX lint 规则在 PR 时抓
- 作者指南 alt 段从 PR 模板里链
- 装饰图用显式
alt=""+ 注释标记 - 季度审计重跑脚本——报告零回退
- 迁移脚本(如有)上线前验证 alt 被保留