
seo-images
โ 84by seranking ยท part of seranking/seo-skills
# 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/claud
This is the playbook your agent receives when the skill activates โ you don't need to read it to use the skill, but it's here to audit before installing.
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.
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 heuristic. The LCP image is typically the first
<img>that:- Appears above the fold on a typical mobile viewport (no exact viewport without rendering; heuristic = first
<img>in the rendered DOM that is not inside a<header>/<nav>/<aside>and has noloading="lazy"ancestor), - Has a large rendered area (width ร height attributes both โฅ 300, or
<picture><source>with viewport-spanningsizes).
- Appears above the fold on a typical mobile viewport (no exact viewport without rendering; heuristic = first
- For the LCP candidate:
loading="lazy"set โimage_lcp_lazy(High). Lazy-loading the LCP image directly harms LCP.- No
fetchpriority="high"โimage_lcp_no_fetchpriority(Medium). Lighthouse'sprioritize-lcp-imageaudit; settingfetchpriority="high"moves the LCP image to the front of the browser's network queue.
- For below-fold images (not the LCP candidate, not inside the first viewport):
- Neither native
loading="lazy"nor any JS-loader signal โimage_below_fold_eager(Medium). Below-fold images should defer. - Missing
decoding="async"โimage_no_decoding_async(Low). Async decode prevents image decoding from blocking the main thread for non-LCP images.
- Neither native
- For each image, classify the lazy-loading mechanism using
-
CLS dimensions
- For each image:
- Missing both
widthandheightattributes AND no inlineaspect-ratiostyle โimage_unsized(High). The browser cannot reserve space; the image will shift content when it loads. Matches Lighthouseunsized-images. widthandheightpresent but the ratio mismatches the actual displayed ratio by >5% โimage_aspect_mismatch(Low). Layout will shift on load.
- Missing both
- The fix for both is the same: set
widthandheightattributes to the image's intrinsic dimensions, and let CSS handle responsive scaling.
- For each image:
-
File-name quality
- For each image's resolved URL, extract the filename. Flag:
- Camera-default names (
IMG_xxxx,DSC_xxxx,DSCN_xxxx,P_xxxx,Photo_xx) โimage_camera_filename(Low). - Random-hash names (
a3f9b2c.jpg,0e8d1f7.webpโ hex/base64 patterns with no human-readable tokens) โimage_hash_filename(Low). Common with image CDNs; verify there's no SEO-friendly version available. - All-uppercase or all-underscore filenames โ
image_filename_style(Low). Convention is lowercase + hyphens.
- Camera-default names (
- Don't flag every CDN-served image as a problem โ many CMSes hash filenames for cache busting and that's fine. The signal is meaningful when paired with a missing or generic
alton the same image (the page has no signal at all about what the image depicts).
- For each image's resolved URL, extract the filename. Flag:
-
ImageObjectJSON-LD: detect, validate, generate- Detect: parse every
<script type="application/ld+json">block returned by Firecrawl. Find existingImageObjectblocks โ either top-level (for image-search rich results) or nested underArticle.image,Product.image,Recipe.image, etc. - Validate against Google Images' guidelines (see
references/image-checks.mdยง ImageObject for the field list). For a top-levelImageObject:- Required:
@context,@type: ImageObject,contentUrl(the image URL),creatororcopyrightHolder. - Recommended for licensable-images rich results:
license(URL to the license terms),acquireLicensePage(URL where users can buy/license the image),creditText(how the creator should be credited). - Common mistakes:
urlinstead ofcontentUrl,authoras a bare string instead of aPerson/Organizationobject, dimensions as strings instead ofNumber.
- Required:
- Generate: for each image that doesn't already have an
ImageObjectblock AND that meets the "worth marking up" threshold (the image is the page's hero / first-fold and the page has a clear creator/owner), produce a paste-ready block fromtemplates/image-object.json, filling in fields from the live HTML. Mark unresolved fields as{REPLACE: ...}. The generated file is emitted as02-remediation/image-object.jsonld(the.jsonldextension marks it as a deliverable for<script type="application/ld+json">). - Don't generate
ImageObjectfor every<img>. It's noise. Limit to the hero image and any image that should be eligible for licensable-images rich results.
- Detect: parse every
-
Optional: PageSpeed Insights byte savings (only if
~/.config/seo-skills/google-api.jsonis present, Tier โฅ 0)- Run
python3 scripts/pagespeed_check.py "{url}" --strategy=mobile --jsonand--strategy=desktop --json(2 API calls per target URL โ within PSI's 25k/day free quota). - Pull the following audits from the JSON response and merge per-image
wastedBytesinto the remediation list:modern-image-formatsโ bytes savable by serving WebP/AVIF (overlaps with step 4; PSI's number is authoritative).uses-optimized-imagesโ bytes savable by re-compressing.uses-responsive-imagesโ bytes savable by serving size-appropriate files (overlaps with step 5).offscreen-imagesโ bytes deferrable by lazy-loading below-fold images (overlaps with step 6).unsized-imagesโ page elements missing dimensions (cross-checks step 7).prioritize-lcp-imageโ confirms or contradicts the step-6 LCP-candidate heuristic and gives PSI's authoritative LCP element.efficient-animated-contentโ confirms animated-GIF flagging from step 4.
- Each PSI audit returns
details.items[]withurlandwastedBytes. Join on image URL (resolved absolute) and tag each remediation row withpsi_wasted_bytesso the prioritised list orders by real savings, not heuristic severity alone. - If PSI is configured but returns no audits (likely a 4xx โ usually a private/protected URL Lighthouse can't load): note "PSI: could not analyse {url} ({reason})" and continue with non-PSI signals.
- Run
-
Optional: SE Ranking audit cross-reference (only if SE Ranking MCP is connected and a recent audit exists)
DATA_listAuditsโ find the most recent audit for the domain. If none exists or it's >30 days old, skip this step (don't trigger a new audit from the image skill โ that'sseo-technical-audit's call to make).- For each image-related audit code,
DATA_getAuditPagesByIssue:images_oversized(or whatever SE Ranking's current code is for "uncompressed images")images_no_altimages_broken(404 / 5xx image URLs)images_no_dimensions(CLS)
- Merge findings: for any image flagged by both the audit and this skill, elevate severity by one step. For any audit-flagged URL that the Firecrawl sample didn't include, list it under "Audit-flagged pages not in this sample" with a recommendation to re-run on those URLs specifically.
-
Synthesise
IMAGES.md. Build the remediation table sorted by:- Severity (Critical โ High โ Medium โ Low),
- Within severity: PSI
wastedBytesdescending (when PSI ran), else affected-image count descending, - Then alphabetical by issue code.
Output format
Create a folder seo-images-{target-slug}-{YYYYMMDD}/ with:
seo-images-{target-slug}-{YYYYMMDD}/
โโโ IMAGES.md (synthesised audit + remediation list โ primary deliverable)
โโโ images.csv (every image with all audit columns โ engineering pastes into Jira)
โโโ 01-inventory.md (per-page image list with raw attributes)
โโโ 02-remediation/
โ โโโ picture-snippets.md (paste-ready <picture> blocks for the top N legacy-format images)
โ โโโ alt-text-rewrites.md (suggested alts for missing / generic-text cases)
โ โโโ image-object.jsonld (generated ImageObject for the hero image, if applicable)
โโโ 03-psi-report.md (PSI image-audit breakdown โ only if Google APIs configured)
โโโ 04-audit-cross-ref.md (image-related SE Ranking audit issues โ only if step 11 ran)IMAGES.md follows this shape:
# Image SEO Audit: {URL or domain}
> Snapshot dated {YYYY-MM-DD} ยท Mode: {URL | domain-sample (n pages)} ยท Images analysed: {n}
## Coverage at a glance
| Metric | Result |
|---|---|
| Total images | {n} |
| Missing alt text | {n} ({pct}%) |
| Generic / templated alt text | {n} ({pct}%) |
| Modern format (WebP/AVIF) coverage | {pct}% direct, {pct}% via `<picture>` fallback |
| `srcset` present (responsive) | {pct}% |
| `loading` strategy detected | native: {pct}% ยท JS-loader: {pct}% ยท none: {pct}% |
| LCP image flagged | {yes/no โ element + risk} |
| Unsized (CLS risk) | {n} ({pct}%) |
| `ImageObject` JSON-LD | {present / partial / missing} |
## Top 10 remediations (severity ร byte savings)
| Rank | Issue code | Severity | Images | PSI wastedBytes | Fix | Effort |
|---|---|---|---|---|---|---|
| 1 | image_lcp_lazy | High | 1 | 480 KB | Remove `loading="lazy"`; add `fetchpriority="high"` | S |
| 2 | image_legacy_format | Medium | 14 | 2.1 MB | Convert to WebP, wrap in `<picture>` with fallback | M |
| ... |
## By category
### Alt text ({n} issues)
- {n} images missing `alt` entirely. See `02-remediation/alt-text-rewrites.md` for suggested rewrites.
- {n} images with generic alt (`image.jpg`, `photo`, "click here").
- {n} images with identical alt across multiple images (templating bug).
### Format coverage ({pct}% modern)
- {n} images stuck on legacy JPEG/PNG. See `02-remediation/picture-snippets.md`.
- {n} animated GIFs >500 KB โ recommend video.
### Responsive sizing ({pct}% have `srcset`)
- {n} images without `srcset`.
- {n} images with `srcset` but no `sizes`.
### Lazy loading & LCP
- LCP candidate: `{img src or selector}` โ {risk summary}.
- {n} below-fold images loading eagerly.
- {n} images missing `decoding="async"`.
### CLS dimensions ({n} unsized)
- {n} images without `width`/`height` attributes.
- {n} images with aspect-ratio mismatches.
### File names ({n} flagged)
- {n} camera-default names (IMG_xxxx).
- {n} hash-only filenames coupled with a missing/generic alt.
### ImageObject JSON-LD
- Currently present: {none | block-level on hero | partial}.
- Recommended additions: {none | hero-image ImageObject for licensable-images rich result}.
## Paste-ready remediations
See `02-remediation/`:
- `picture-snippets.md` โ `<picture>` blocks for the top N legacy-format images.
- `alt-text-rewrites.md` โ alt-text rewrites for missing / generic cases.
- `image-object.jsonld` โ `ImageObject` block for the hero image.
## Out of scope for this skill
- **File-level optimisation** (running `cwebp` / `exiftool` / ImageMagick / `ffmpeg` against the actual binary). This skill audits markup and references; converting and re-uploading the files is engineering work โ see the pipeline note in `references/image-checks.md` ยง Optimisation pipeline if you want a starting recipe.
- **CSS background-images.** {n} likely background-image references detected via computed style, but not audited. They don't appear in Google Images and aren't subject to the `<img>`-tag rubric.
- **Site-wide audit at >10 pages.** This is a sampled audit. For domain-level "every image on every page", run `seo-technical-audit` first to surface the audit-grade signals, then come back here for sample-level deep audit.
## Recommended next steps
- {`seo-technical-audit` if domain-wide image issues need to be quantified โ uncompressed-images counts, etc.}
- {`seo-schema` if `ImageObject` was generated and the page also needs `Article` / `Product` / etc. markup.}
- {`seo-google pagespeed` for the full Lighthouse breakdown (this skill only pulls image-specific audits).}images.csv columns: page_url,image_url,alt,alt_length,alt_issue,format,in_picture,modern_source,srcset,sizes,loading,lazy_method,fetchpriority,decoding,width,height,unsized,lcp_candidate,filename_issue,psi_wasted_bytes,severity,fix,effort.
Tips
- Default to URL mode. Single-page audits are 1 Firecrawl credit and produce a complete deliverable for the most common ask ("audit the images on /this/page"). Domain mode is for "give me a representative read on the whole site" โ it surfaces patterns (templating bugs, CMS-wide missing alts) that single-page mode misses.
<picture>is the right answer. When recommending modern formats, always recommend the<picture>element with AVIF + WebP<source>and a JPEG fallback<img>โ not raw<img src=".avif">. AVIF is at 93%+ support and WebP at 97%+, but the fallback is what makes the markup safe for older clients and crawlers.- Don't lazy-load the LCP image. This is the single most common image-SEO mistake on modern CMSes. Themes ship with site-wide
loading="lazy"defaults that apply to the hero. The skill's LCP heuristic catches the most likely culprit; PSI (step 10) confirms it authoritatively. - Empty
alt=""is correct for purely decorative images (a hairline-rule SVG, a pure background-spacer image). It tells screen readers to skip the image. Don't auto-flag every empty alt โ flag only those where the image filename and context suggest the image carries content. - Reverse the inventory if it's small. For pages with <10 images, list every image with its full audit row in
IMAGES.md's "By category" section, not just the aggregate counts. Aggregate-only output is useful when there are 100+ images; below that it hides the specifics. - PSI is rate-limited at 25k/day on the free tier but counts requests, not images. Calling PSI twice per target URL (mobile + desktop) is the default; skip desktop if you only care about Google's mobile-first ranking signal.
- CSS background-images are a real blind spot โ flag the count, but don't audit them. They're not crawled as content images by Google.
- JPEG XL is not yet shippable (Nov 2025 Chromium announcement restoring decoder support, not yet in stable Chrome). Don't recommend JPEG XL until it lands in stable. WebP and AVIF are the current safe modern formats.
- Don't auto-apply fixes. The skill diagnoses and produces paste-ready snippets; humans decide which fixes to ship and in what order.
- Verify after deploy. Re-run this skill on the same URL after the fixes ship โ the new run's "Coverage at a glance" reflects the live state and confirms the markup actually changed (vs sitting in the CMS but not pushed).
Works well with
- Predecessors:
seo-firecrawlโ when the user already scraped a page and now wants the image-specific cut.seo-technical-auditโ when a site-wide audit flagged image issues and the user wants the deep per-image rubric.seo-pageโ when a URL-level keyword/traffic verdict is "refresh" and images are part of the refresh.
- Successors:
seo-schemaโ when the page also needsArticle/Product/LocalBusinessschema beyondImageObject.seo-google pagespeedโ for the full Lighthouse report (this skill cherry-picks the image audits; PSI has 100+ more).seo-driftโ to baseline image markup and detect regressions after a CMS or theme upgrade.
npx skills add https://github.com/seranking/seo-skills --skill seo-imagesRun this in your project โ your agent picks the skill up automatically.
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.
No common issues documented yet. If you hit a problem, the repository's GitHub Issues page is the best place to look.