Image Alt Text Missing in Bulk: Audit, Backfill, and Lock It In

Hundreds of images with no alt attribute hurt accessibility, image search, and AdSense quality. Audit by category, backfill by traffic, then fail the build on regressions.

You run Lighthouse or axe and the accessibility report is brutal: 340 images across 180 articles have no alt attribute, or alt="" slapped on as a placeholder. Screen readers announce the raw filename (“IMG00023.JPG”) or skip silently. Google Image Search has nothing to index. AdSense quality reviewers read it as lazy authoring. This fails WCAG 2.2 success criterion 1.1.1 (Non-text Content) outright.

Fastest fix: run a categorized audit script (below), then backfill in three tiers — hand-write alt for your top traffic pages, AI-draft-then-review the middle, and mark genuinely decorative images with explicit alt="". Then wire a prebuild check that exits nonzero on any missing alt so the problem can never come back. Doing it once and locking it in is far cheaper than re-auditing forever.

Alt text was never enforced when this content was authored. Most images were dragged into the editor with the alt field left empty, and the build never complained.

Which bucket are you in?

SymptomLikely causeFix
![](image.png) — empty Markdown altEditor never required itBackfill alt inside the brackets
<img src="x.png" /> with no alt=Author switched to raw HTML for sizingAdd alt; in MDX this is a JSX element, not HTML
alt="image" / alt="screenshot"Half-hearted audit passRewrite to describe the actual content
Every pre-2025 article alt-lessCMS migration stripped altRe-import from source export, or batch backfill
Divider lines, generic illustrationsGenuinely decorativeKeep alt="" (this is correct)

The last row matters: missing alt and intentional empty alt look similar but are opposite outcomes. WCAG counts a missing alt as failure F65 (screen reader reads the filename) and alt="image"/alt="spacer" as failure F39 (a non-null alt on something that should be ignored). For a purely decorative image, alt="" is the correct answer — assistive tech skips it cleanly.

Common causes

1. Editor never required alt

The Markdown editor accepts ![](image.png) without complaint. Authors omit alt because there is no friction.

How to spot it — grep all MDX for empty Markdown 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 alt. <img src="x.png" width="600" /> is 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='

Note for MDX: inside .mdx, a raw <img> is parsed as a JSX element (mdxJsxTextElement / mdxJsxFlowElement), not as an HTML node. This bites you later when writing a lint rule (see Step 4) — a rule that only visits html nodes will silently miss every <img> in your MDX files.

3. Alt text exists but is a decorative placeholder

Authors wrote alt="image" or alt="screenshot" to pass a half-hearted audit. Technically present, semantically useless, and a WCAG F39 failure.

How to spot it:

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 dropped alt text or stored it in a sibling field your importer ignored, so every migrated image is alt-less.

How to spot it — articles authored on or before the migration date are the affected cohort. Diff that date range against your audit output to confirm the overlap.

5. Decorative images that genuinely should have empty alt

A divider line or generic illustration is pure decoration. For these, alt="" is what the W3C decorative-images tutorial prescribes — screen readers should skip them. Your audit must distinguish “missing alt” from “intentional empty alt,” or it will nag forever about images that are already correct.

Shortest path to fix

Step 1: Inventory the gap by category

Build a report that groups offenders by type, so you can triage instead of staring at one flat list:

// 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);
    // matches <img ...> that has no alt= anywhere in the tag
    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;

You now have the exact list of offenders and which category each falls in.

Step 2: Backfill by traffic, not alphabetically

Don’t AI-generate 340 alts blindly — readers and reviewers notice the generic phrasing, and Google explicitly warns against keyword-stuffed alt. Triage instead:

TierArticlesMethod
High trafficTop ~50 by Search Console impressionsHand-write real alt
Mid trafficThe bulkAI-draft, then human-review in batches of 20
Low / decorativeThe tailalt="" plus a comment marking it intentional

What good alt text looks like, per Google’s image SEO docs: describe the subject in context, no keyword stuffing. Google’s own example ranks alt="Dalmatian puppy playing fetch" above a bare alt="puppy", and both above an empty alt. Keep it to roughly a sentence or two — enough to describe the subject and its role without burying a screen-reader user (some screen readers truncate around 125 characters, so brevity helps). For UI screenshots, name the app, the feature, and the relevant state: alt="Cursor settings panel with the Composer model set to Sonnet 4.6" beats alt="settings".

Step 3: Fail the build on missing alt

Wire the audit into prebuild and let its nonzero exit code stop the build:

"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 workable convention: empty alt is allowed only when the previous line carries a marker comment like {/* decorative */}, or when the asset path matches a known-decorative pattern (for example /decor/ or *-divider.svg). Encode that exception in the script so it does not flag correct decorative images.

Step 4: Add a lint rule that actually sees MDX images

For PR-time enforcement, combine remark-lint-no-empty-image-alt-text (Markdown ![]() syntax) with a custom rule for raw <img>. The trap: in MDX a raw <img> is a JSX node, so the rule must visit mdxJsxFlowElement and mdxJsxTextElement, not html. The original “visit html” approach misses every 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"],
    // custom rule: raw <img> in MDX is a JSX element, not 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);
      });
    },
  ],
};

Run it in CI on PRs that touch .mdx files. Use remark-lint-alt-text too if you also want to catch unhelpful placeholder strings automatically.

Step 5: Document the convention

Add a short authoring-guide section so future authors and reviewers share one rule:

- Real alt: describe what the image shows AND why it matters in context (a sentence or two)
- Decorative images: alt="" plus a {/* decorative */} comment on the prior line
- Never use placeholder strings like "image" or "screenshot"
- UI screenshots: name the app, the feature, and the relevant state
- Filenames: descriptive and hyphenated (account-settings.png, not IMG00023.JPG)

How to confirm it’s fixed

  1. Re-run npm run audit:alt — it should print 0 files with alt problems and exit 0.
  2. Re-run Lighthouse (or npx @axe-core/cli https://yoursite.com/some-article/) on a few rebuilt pages; the “Image elements do not have [alt] attributes” check should pass.
  3. Open one fixed page in a screen reader (VoiceOver: Cmd+F5 on macOS; NVDA on Windows) and arrow through the images — each should read its description, decorative ones should be silent, none should read a filename.
  4. In Google Search Console, watch the Images report over the next few weeks for impressions on pages you backfilled.

Prevention

  • Prebuild fails on any ![] or <img> missing alt — no exceptions.
  • MDX lint rule (visiting mdxJsxFlowElement, not html) catches alt issues at PR time.
  • The authoring-guide alt section is linked from the PR template.
  • Decorative images use explicit alt="" plus a {/* decorative */} marker.
  • Quarterly audit re-runs the script and reports zero regressions.
  • Any migration script is validated to preserve alt before the next import.

FAQ

Is a missing alt worse than an empty alt=""? Yes, for non-decorative images. With no alt attribute, many screen readers fall back to reading the filename (WCAG failure F65). An empty alt="" at least stays silent — but on a meaningful image that is wrong too, because the information is lost. Use a real description for content images and alt="" only for decoration.

Will AI-generated alt text hurt my AdSense or SEO? Only if it is generic or stuffed. Google judges alt on usefulness and context, not on who wrote it. AI-drafted alt that a human edits to be specific is fine; 340 identical “screenshot of the dashboard” lines are the problem. Review before shipping.

How long should alt text be? Aim for a sentence or two — long enough to describe the subject and its role, short enough not to fatigue a screen-reader user. There is no hard HTML limit, but some screen readers truncate around 125 characters, so keep it concise; very long alt is a usability problem.

Do decorative images really need alt="" instead of no attribute at all? Yes. Omitting the attribute entirely can make some screen readers announce the filename. An explicit alt="" is the documented way to tell assistive tech to skip the image (W3C decorative-images guidance).

Should I also fix image filenames and formats? It helps. Google prefers descriptive hyphenated filenames (account-settings.png over IMG00023.JPG) and serves WebP/AVIF well. That is a separate optimization, but a good thing to batch into the same sweep since you are already touching every image.

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