You run an accessibility audit and the report is brutal: 340 images across 180 articles have no alt attribute, or worse, alt="" slapped on as a placeholder. Screen readers announce “image” or skip silently. Google Image Search has nothing to index. AdSense quality reviewers see lazy authoring. The accessibility hit alone should motivate the fix; the SEO and policy implications make it urgent.
Alt text was never enforced when the content was authored — most images were dragged into the editor and the alt field left empty. The fix is mechanical: grep-and-fix sweep, prebuild check that fails on missing alt, MDX lint rule so future PRs cannot regress. Doing it once and locking it in is far cheaper than doing it on every audit forever.
Common causes
1. Editor never required alt
The Markdown editor (or whatever you use) accepts  without complaint. Authors omit alt because there is no friction.
How to spot it: grep all MDX for ![] with empty alt.
grep -rEn '!\[\]\(' src/content/articles/ | wc -l
2. HTML img tag with no alt attribute
When authors needed sizing control, they switched to raw <img> and forgot to include alt. <img src="x.png" width="600" /> is fully valid HTML but accessibility-broken.
How to spot it: grep for img tags without an alt attribute.
grep -rEn '<img [^>]*src=' src/content/articles/ | grep -v 'alt='
3. Alt text exists but is decorative placeholder
Authors wrote alt="image" or alt="screenshot" to pass a half-hearted audit. Technically present, semantically useless.
How to spot it: grep for known bad patterns.
grep -rEn 'alt="(image|screenshot|picture|img|photo)"' src/content/articles/
4. Bulk import from old CMS stripped alt
You migrated from WordPress / Notion / Ghost. The export format dropped alt text or stored it in a sibling field that your importer ignored. All migrated images now alt-less.
How to spot it: check the migration date — articles authored on/before that date are the affected cohort.
5. Decorative images that genuinely should have empty alt
Some images are pure decoration (a divider line, a generic illustration). For these, alt="" is the correct answer — screen readers should skip them. The audit must distinguish “missing alt” from “intentional empty alt.”
How to spot it: review every empty-alt case individually; either fill in or annotate as decorative.
Shortest path to fix
Step 1: Inventory the gap
Build a report grouping by missing-alt severity:
# 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));
You now know the exact list of offenders and the category.
Step 2: Backfill the worst offenders by traffic
For high-traffic articles, hand-write real alt text. For low-traffic articles, batch through a templated description (“Screenshot of the X dashboard showing Y”) or mark as decorative. Don’t AI-generate 340 alts blindly — readers will notice the generic phrasing.
A reasonable triage:
- High-traffic (top 50 by impressions): hand-write alts
- Mid-traffic: AI-generate, human review in batches of 20
- Low-traffic / decorative: alt="" with a code comment marking intentional
Step 3: Add a prebuild check that fails on missing alt
Wire the audit script into prebuild and make it exit nonzero:
# package.json
"scripts": {
"audit:alt": "node scripts/audit-image-alt.mjs",
"prebuild": "npm run audit:content && npm run audit:alt && ..."
}
The check must distinguish missing alt from intentionally empty alt. A convention: empty alt is fine only when preceded by a code-comment marker like {/* decorative */} on the previous line, OR when matching a known-decorative asset path pattern.
Step 4: Add an MDX lint rule
For ongoing enforcement, use remark-lint-no-empty-image-alt-text and a custom rule for HTML img tags:
// .remarkrc.mjs
export default {
plugins: [
"remark-lint",
["remark-lint-no-empty-image-alt-text", "warn"],
// custom rule for raw <img>
() => (tree, file) => {
visit(tree, "html", node => {
if (/<img(?![^>]*alt=)/.test(node.value)) {
file.message("img tag missing alt attribute", node);
}
});
},
],
};
Run in CI on PRs touching .mdx files.
Step 5: Document the convention
Add a short authoring guide section:
- Real alt text: describe what the image shows AND why it matters in context
- Decorative images: alt="" plus comment {/* decorative */} on prior line
- Never use placeholder strings like "image" or "screenshot"
- For screenshots of UI: name the app, name the feature, name the relevant state
Future authors have one place to look; reviewers have one rule to enforce.
Prevention
- Prebuild fails on any
![]or<img>missing alt — no exceptions - MDX lint rule catches alt issues at PR time
- Authoring guide section on alt text is linked from PR template
- Decorative images use explicit
alt=""plus a comment marker - Quarterly audit re-runs the script and reports zero regressions
- Migration scripts (if any) validated to preserve alt text before import
Related
- Content Site Broken Internal Link Rot
- Content Site Translation Pages Mismatched
- Outdated Screenshots in Tutorials
- Content Site FAQ Schema Not Extracted
- AdSense Low Value Content
- Content Site Publish Date Stuck in Past
Tags: #Content ops #Site quality #Site audit #Troubleshooting #alt-text