Image Alt Text Missing in Bulk: Hundreds of Images, Zero Accessibility

Articles use img tags with no alt attribute — bad for accessibility, bad for image search, bad for AdSense quality signals. Audit, backfill, enforce via MDX lint.

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 ![](image.png) 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

Tags: #Content ops #Site quality #Site audit #Troubleshooting #alt-text