Image SEO Audit
A focused, page-level (or domain-sample) audit of every <img> and <picture> on the target. Surfaces alt-text issues, format gaps (WebP/AVIF coverage), responsive-image gaps (srcset / sizes), LCP and CLS risk, and missing ImageObject markup. Output is a prioritised remediation list plus paste-ready <picture> and JSON-LD snippets.
Adapted from
AgriciDaniel/claude-seo'sseo-imagesskill (MIT). Rubric, lazy-loader taxonomy, and severity ladder track the upstream implementation; data sources are wired to this catalogue's SE Ranking / Firecrawl / Google APIs stack.
Prerequisites
- Required for inventory:
mcp__firecrawl-mcp__firecrawl_scrape(raw HTML access).WebFetchreturns markdown only — every<img>attribute (srcset,sizes,loading,fetchpriority,width,height,data-src*lazy variants) is stripped before the skill ever sees it. Without Firecrawl the audit cannot run. Install viabash extensions/firecrawl/install.sh. - Optional (PSI byte-saving estimates):
google-api.jsonconfigured (Tier 0 — API key only). When present, step 9 runs and adds real LighthousewastedBytesper image to the remediation list. - Optional (SE Ranking audit cross-reference): SE Ranking MCP server connected and a recent audit for the domain. When present, step 10 elevates image-related audit issues onto the same remediation list.
- User provides: a target URL (single-page audit) or a domain (sampled audit). For domains, the skill confirms how many pages to sample before spending Firecrawl credits.
Process
-
Validate target & preflight. Normalise the URL (strip trailing slash, decode IDN). Resolve mode:
- URL mode (default for inputs that look like a single page): the target is one URL. Cost: 1 Firecrawl credit + optional PSI calls.
- Domain mode (input is a bare domain or the user explicitly asks for a domain-wide audit): map first, then scrape a sample.
- Preflight checks (mirror
skills/seo-firecrawl/references/preflight.mdwhere it applies):- Confirm Firecrawl is connected. If not, abort with the install command and stop.
- If Google APIs are wired up (
~/.config/seo-skills/google-api.jsonpresent), record the detected tier; step 9 will use it. If not, mark step 9 as skipped. - If SE Ranking MCP is connected and a recent audit exists for the domain, record the audit ID; step 10 will use it. If not, mark step 10 as skipped.
-
Gather image inventory
mcp__firecrawl-mcp__firecrawl_scrape(URL mode) orfirecrawl_map+firecrawl_scrape(domain mode)- URL mode: scrape the target with
formats: ["html", "markdown"]andonlyMainContent: false(we want nav/footer images too — hero logo, footer trust badges, decorative imagery all matter for the audit). For SPAs, passwaitFor: 2000so lazy-injected images appear in the rendered DOM. Parse every<img>and every<picture>from the returnedhtml. Capture per image:src,srcset,sizes,alt,loading,fetchpriority,decoding,width,height,role,aria-hidden- Lazy-loader attributes:
data-src,data-srcset,data-lazy-src,data-original,data-perfmatters-src,data-perfmatters-srcset,data-ewww-src,data-eio - Class signals:
lazyload,lazyloaded,lazy,perfmatters-lazy,lazyload-eio - Parent
<picture><source>entries:type,srcset,media - Resolved absolute URL (for cross-origin / CDN detection)
- Domain mode: run
firecrawl_map(defaultlimit: 500, hard cap; cost: ~0.5 credit per discovered URL — surface the estimate before running). From the URL list, select a sample of up to 10 pages: homepage, plus the top traffic landing pages (fromDATA_getDomainKeywords's page aggregation if SE Ranking is connected, otherwise the deepest-nested URLs found in the sitemap — these are usually the content pages, not category indexes). Confirm the sample list and credit cost before scraping. Then scrape each (1 credit per page). Inventory is the union of every image on the sampled pages. - CSS background-images: flag as a known blind spot. We don't audit
background-image: url(...)in stylesheets — those are not crawlable as content images by Google and don't get image-search visibility. Surface "{n} likely background-images detected (computed style references) — out of scope for this audit; review separately if hero/feature images are CSS-based" in the synthesis.
- URL mode: scrape the target with
-
Alt-text audit
- Load
references/image-checks.md§ Alt text. For each image:- Presence: missing
alt(notalt=""— the empty-string form is valid for purely decorative images). Severity High. - Decorative-but-not-marked:
alt=""is fine only if the image is genuinely decorative. Flag images withalt=""that also have a non-decorativesrc(e.g. product photo path, hero image path) as "verify decorative intent" (Medium). - Generic text:
altvalue matches a generic pattern — bare filename (image.jpg,IMG_1234.png), single generic noun (photo,picture,image,banner), CTA copy (click here,read more,learn more). Severity High. - Length:
altoutside the 10–125 character window. Below 10 → Medium (probably not descriptive). Above 125 → Low (likely too verbose; screen readers truncate around there). - Keyword stuffing: the same keyword token appears 3+ times in the alt, or the alt is >50% keyword tokens. Severity Medium.
- Identical alt across multiple images on the page: flag as a templating bug (Medium) — every product photo on a PDP should not share the same alt.
- Presence: missing
- Load
-
Format coverage (WebP / AVIF)
- For each image, classify its served format from
srcextension (.webp,.avif,.jpg/.jpeg,.png,.gif,.svg) and<picture><source>typeattributes (image/avif,image/webp). - Compute three coverage metrics for the page (or domain sample):
- % images served as WebP or AVIF directly (via the chosen
<img src>or chosen<picture><source>). - % images wrapped in
<picture>with at least one modern-format<source>(progressive enhancement — fallback chain). - % images stuck on legacy formats (JPEG / PNG / GIF) with no modern alternative.
- % images served as WebP or AVIF directly (via the chosen
- Per-image flags:
- Legacy format with no
<picture>modern alternative →image_legacy_format(Medium). - Animated GIF over 500 KB → recommend video (
<video autoplay muted loop playsinline>) instead (Medium — performance + LCP impact). Source: Google PSIefficient-animated-contentaudit. - SVG used for photographic content →
image_svg_misuse(Low — file size will be enormous; SVG is for icons/illustrations).
- Legacy format with no
- JPEG XL note. Chromium announced restoration of JPEG XL decoding (Rust-based) in November 2025 but it's not yet in Chrome stable. Surface as a Tips note: not actionable today, monitor for 2026.
- For each image, classify its served format from
-
Responsive coverage (
srcset/sizes)- For each non-SVG raster image:
- Missing
srcset→image_no_srcset(Medium). Browser cannot pick a size-appropriate file; mobile users download desktop-sized images. srcsetpresent but nosizesand not inside<picture>→image_no_sizes(Medium). Browser falls back to viewport width assumptions and can pick the wrong candidate.srcsetdeclared but all candidates are the same width descriptor (1xonly, or allwvalues within 100 px of each other) →image_srcset_useless(Low).
- Missing
- For each non-SVG raster image:
-
Lazy loading & LCP signals
- For each image, classify the lazy-loading mechanism using
references/lazy-loaders.md's taxonomy:native/perfmatters/ewww/js-generic/none. Reportlazy_methodalongsideloadingso a JS-loader-driven page isn't mis-flagged for missingloading="lazy"(the native attribute is intentionally absent there — the loader handles it). - **LCP-candidate he
- For each image, classify the lazy-loading mechanism using