图片 alt 文本批量缺失:审计、回填,并用构建检查锁死

几百张图没有 alt 属性,砸了无障碍、图片搜索和 AdSense 质量信号。按类别审计、按流量回填,再让构建在回退时直接失败。

你跑了一次 Lighthouse 或 axe,无障碍报告很难看:180 篇文章里 340 张图没有 alt 属性,或者随手贴了个 alt="" 当占位符。屏幕阅读器要么把原始文件名念出来(“IMG00023.JPG”),要么默默跳过。Google 图片搜索没有东西可索引。AdSense 质量审核会把这读成作者偷懒。这直接违反 WCAG 2.2 成功标准 1.1.1(非文本内容)。

最快修法: 先跑一个按类别分组的审计脚本(见下文),然后分三档回填——流量最高的页面手写 alt,中间的用 AI 起草再人工审,真正的装饰性图片用显式 alt="" 标掉。接着把一个 prebuild 检查接进来,只要还有缺 alt 就让构建非零退出,问题就再也回不来了。一次做完锁死,比每次都重新审计便宜得多。

写这批内容时根本没强制过 alt。多数图片是被拖进编辑器、alt 字段空着的,而构建从来不抱怨。

你属于哪一类?

症状可能原因修法
![](image.png) —— Markdown alt 为空编辑器从未要求在方括号里回填 alt
<img src="x.png" /> 没有 alt=作者为控制尺寸改用裸 HTML补 alt;在 MDX 里它是 JSX 元素,不是 HTML
alt="image" / alt="screenshot"应付式审计改写成描述真实内容
2025 年前的文章全无 altCMS 迁移剥掉了 alt从源导出重新导入,或批量回填
分隔线、通用插画确实是装饰性的保留 alt=""(这是对的)

最后一行很关键:缺 alt 和故意空 alt 看着像,结果却相反。WCAG 把缺少 alt 记为失败 F65(屏幕阅读器读文件名),把 alt="image" / alt="spacer" 这种本应被忽略却给了非空 alt 记为失败 F39。对纯装饰性图片,alt="" 才是对的答案——辅助技术会干净地跳过它。

常见原因

1. 编辑器从未要求 alt

Markdown 编辑器接受 ![](image.png) 而不抱怨。作者省掉 alt,因为没有任何摩擦。

如何判断——grep 所有 MDX 找空的 Markdown 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='

MDX 注意点:在 .mdx 里,裸 <img> 被解析成 JSX 元素(mdxJsxTextElement / mdxJsxFlowElement),而不是 HTML 节点。这在后面写 lint 规则时会咬你一口(见 Step 4)——一个只访问 html 节点的规则,会默默漏掉你 MDX 文件里的每一个 <img>

3. alt 存在但只是装饰性占位

作者写了 alt="image"alt="screenshot" 来糊弄一次审计。技术上有、语义上没用,而且是 WCAG 的 F39 失败。

如何判断:

grep -rEn 'alt="(image|screenshot|picture|img|photo)"' src/content/articles/

4. 老 CMS 批量导入剥掉了 alt

你从 WordPress / Notion / Ghost 迁移过来。导出格式丢了 alt,或者把它存在导入器忽略的兄弟字段里,于是所有迁来的图都没 alt。

如何判断——在迁移日期当天或之前发布的文章,就是受影响的那一批。把这个日期区间和审计输出对一遍,确认重叠。

5. 确实应该用空 alt 的装饰性图片

分隔线或通用插画就是纯装饰。对这些,W3C 装饰性图片教程 规定用 alt=""——屏幕阅读器应该跳过它们。你的审计必须区分”缺 alt”和”故意空 alt”,否则它会对一批本来就正确的图片唠叨个没完。

最短修复路径

Step 1:按类别盘点缺口

做一份按类型分组的报告,这样你可以分级处理,而不是盯着一个扁平的大清单:

// scripts/audit-image-alt.mjs
import fs from "node:fs";
import path from "node:path";

const ROOT = "src/content/articles";
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);
    // 匹配整个标签里都没有 alt= 的 <img ...>
    if (/<img(?:(?!alt=)[^>])*>/i.test(txt)) issues.missingHtml.push(p);
    if (/alt="(image|screenshot|picture|img|photo)"/i.test(txt)) {
      issues.placeholder.push(p);
    }
  }
}
scan(ROOT);

const total = Object.values(issues).reduce((n, a) => n + a.length, 0);
console.log(JSON.stringify(issues, null, 2));
console.log(`\n${total} files with alt problems`);
process.exitCode = total > 0 ? 1 : 0;

现在你有了精确的违规清单,以及每一条属于哪个类别。

Step 2:按流量回填,别按字母序

不要一口气 AI 生成 340 条 alt——读者和审核都会注意到那种通用措辞,而且 Google 明确反对堆关键词的 alt。改成分级处理:

档位文章方法
高流量按 Search Console 展示量 top 50 左右手写真实 alt
中流量大头AI 起草,再每 20 篇一批人工审
低流量 / 装饰长尾alt="" 加一条注释标”故意”

好的 alt 文本长什么样,参照 Google 图片 SEO 文档:在上下文里描述主体,不堆关键词。Google 自己的例子里,alt="Dalmatian puppy playing fetch" 排在 alt="puppy" 之上,两者又都好过空 alt。控制在一两句话即可——足够描述主体和它的作用,又不会把屏幕阅读器用户埋掉(有些屏幕阅读器会在约 125 个字符处截断,所以越简洁越好)。UI 截图就把 app 名、功能名和当前状态写清楚:alt="Cursor settings panel with the Composer model set to Sonnet 4.6"alt="settings" 强得多。

Step 3:缺 alt 就让构建失败

把审计接进 prebuild,让它的非零退出码挡住构建:

"scripts": {
  "audit:alt": "node scripts/audit-image-alt.mjs",
  "prebuild": "npm run audit:content && npm run audit:alt"
}

这个检查必须区分 alt 和故意空 alt。一个可行的约定:只有当上一行带有标记注释 {/* decorative */},或者资产路径匹配已知装饰模式(比如 /decor/*-divider.svg)时,才允许空 alt。把这个例外写进脚本,它就不会再误报正确的装饰性图片。

Step 4:加一条真能看到 MDX 图片的 lint 规则

PR 时强制——把 remark-lint-no-empty-image-alt-text(管 Markdown 的 ![]() 语法)和一条针对裸 <img> 的自定义规则配在一起。陷阱在于:MDX 里裸 <img> 是 JSX 节点,所以规则必须访问 mdxJsxFlowElementmdxJsxTextElement,而不是 html。原来那种”访问 html”的写法会漏掉每一个 MDX <img>

// .remarkrc.mjs
import { visit } from "unist-util-visit";

export default {
  plugins: [
    "remark-lint",
    "remark-mdx",
    ["remark-lint-no-empty-image-alt-text", "warn"],
    // 自定义规则:MDX 里的裸 <img> 是 JSX 元素,不是 html
    () => (tree, file) => {
      visit(tree, ["mdxJsxFlowElement", "mdxJsxTextElement"], (node) => {
        if (node.name !== "img") return;
        const alt = node.attributes?.find(
          (a) => a.type === "mdxJsxAttribute" && a.name === "alt"
        );
        if (!alt) file.message("img tag missing alt attribute", node);
      });
    },
  ],
};

在改动 .mdx 的 PR 上跑 CI。如果你还想自动抓出无用的占位字符串,可以再加上 remark-lint-alt-text

Step 5:把约定写下来

加一段简短的作者指南,让未来的作者和审稿人共用同一条规则:

- 真实 alt:描述图片显示什么 AND 它在上下文里为何重要(一两句话)
- 装饰性图片:alt="" 加上上一行的 {/* decorative */} 注释
- 永远不用 "image"、"screenshot" 这类占位字符串
- UI 截图:写清 app 名、功能名和当前相关状态
- 文件名:描述性、用连字符(account-settings.png,不是 IMG00023.JPG)

如何确认已修好

  1. 重跑 npm run audit:alt——它应打印 0 files with alt problems 并以 0 退出。
  2. 在几个重建后的页面上重跑 Lighthouse(或 npx @axe-core/cli https://yoursite.com/some-article/);“Image elements do not have [alt] attributes” 这一项应通过。
  3. 用屏幕阅读器打开一个修好的页面(macOS VoiceOver:Cmd+F5;Windows 用 NVDA),逐张图箭头走一遍——每张都应念出描述,装饰性的应保持沉默,没有一张该读文件名。
  4. 在 Google Search Console 里,接下来几周观察”图片”报告,看你回填过的页面有没有展示量。

预防

  • 任何 ![] 或缺 alt 的 <img>——prebuild 直接失败,零例外。
  • MDX lint 规则(访问 mdxJsxFlowElement,而非 html)在 PR 时就抓。
  • 作者指南的 alt 段从 PR 模板里链过去。
  • 装饰性图片用显式 alt=""{/* decorative */} 标记。
  • 季度审计重跑脚本,报告零回退。
  • 任何迁移脚本,都在下次导入前验证 alt 被保留。

常见问题

alt 比空 alt="" 更糟吗? 对非装饰性图片,是的。完全没有 alt 属性时,很多屏幕阅读器会退而读文件名(WCAG 失败 F65)。空 alt="" 至少保持沉默——但对有意义的图片这同样错,因为信息丢了。内容图片用真实描述,alt="" 只留给装饰。

AI 生成的 alt 会伤 AdSense 或 SEO 吗? 只有当它通用或堆关键词时才会。Google 衡量 alt 看的是有用性和上下文,不看是谁写的。AI 起草、人工改得具体的 alt 没问题;340 条一模一样的 “screenshot of the dashboard” 才是问题。上线前先审。

alt 文本该多长? 控制在一两句话——足够描述主体和它的作用,又短到不会拖累屏幕阅读器用户。HTML 没有硬性长度上限,但有些屏幕阅读器会在约 125 个字符处截断,所以尽量精简;过长的 alt 是个可用性问题。

装饰性图片真的要 alt="",而不是干脆不写属性吗? 是的。完全省掉属性,会让某些屏幕阅读器把文件名念出来。显式的 alt="" 才是有文档支持的、告诉辅助技术跳过这张图的方式(W3C 装饰性图片指南)。

我是不是也该顺手修文件名和格式? 有帮助。Google 偏好描述性、连字符的文件名(account-settings.png 好过 IMG00023.JPG),也能很好地处理 WebP/AVIF。这是另一项优化,但既然你已经在动每一张图,顺手批进同一轮里是好事。

相关

标签: #内容运营 #站点质量 #站点审计 #排查 #alt-text