How to Set Up Codex for Image SEO (Full SKILL.md Included)

Key takeaways

Audit image SEO on any page in about a minute: alt text, file size, format, responsive markup, lazy loading, and CLS — with a full Codex SKILL.md included.

What you get from this article

A skill that audits the images on any page in about a minute: alt text, file size, format, responsive markup, lazy loading, fetch priority, and layout-shift prevention, each with a flag and a fix. The scanner also fetches each image's headers to get real file sizes, so the report is based on what the browser actually downloads, not on guesses. Zero dependencies beyond python3.

Why image SEO is worth the fifty lines of script

Images have a ranking axis of their own. Google Images is still a real surface, and the alt text you write is one of the few places where you control the exact words that get indexed with a piece of content. Meanwhile, images are also the most common reason a page loses Core Web Vitals: a 500 KB hero image is slower than any script on the page, and a missing width/height attribute causes layout shift that starts before the image even loads.

The interesting part is that image problems cluster. Sites rarely miss one thing here and there; they systematically miss one kind of thing, because the CMS template or the dev handoff forgot it. Alt text in the template asset, dimensions not generated by the uploader, images served as JPEG because nobody turned on conversion. So an image audit is really a template audit, and a template audit has an outsized fix-to-effort ratio: change the template once, all pages improve.

The reference: what the scanner checks

Picture a page's images as little data records: src, alt, width, height, loading, fetchpriority, decoding, srcset, format, and actual file size. The skill grades each record against a few tables.

Alt text rules

An alt attribute is present, descriptive (not a filename and not "photo"), between 10 and 125 characters, and keyword-natural. Empty alt is legitimate only for decorative images, and you signal decay by using alt="" or role="presentation" on those. The scanner tags missing alt, alt that ends in an image extension, alt under 6 characters, and alt over 125 characters.

Size thresholds

Image category

Target

Warning

Critical

Thumbnails

< 50 KB

> 100 KB

> 200 KB

Content images

< 100 KB

> 200 KB

> 500 KB

Hero / banner

< 200 KB

> 300 KB

> 700 KB

These are targets for a fast page, not ranking thresholds. Compression has no quality cost at these levels for photographic content.

Format

WebP (97%+ browser support) is the default recommendation, AVIF (92%+) when you can take a slower encode, and JPEG only as the fallback inside a <picture> element. SVG stays reserved for icons, logos, and illustrations.

There is an emerging standard worth naming: JPEG XL shipped a Rust decoder in Chrome 145 in February 2026, behind a flag, not by default. Nobody has published a confirmed support baseline, so the practical advice stays the same as it was last year: AVIF/WebP with JPEG fallback, re-check in a few quarters.

Responsive markup

srcset plus matching sizes, with real breakpoints. The degenerate case is serving one 1200 px image to every device; the scanner notices the image exists but does not detect responsive intent. In the report, point at the pattern and reference the image's srcset field.

Lazy loading

loading="lazy" on below-the-fold images only. Above-the-fold hero images need the opposite: never lazy-load the LCP image, and give it fetchpriority="high" so the browser downloads it first. Non-LCP images also benefit from decoding="async" so decoding does not block the main thread.

Sites with a JS lazy-loader are fine; the running copy on data-src style loading is a deliberate bypass of the native attribute. The scanner reports a lazy_method so you can tell which:

lazy_method

Signal

Common stack

native

loading="lazy" attribute

plain HTML

perfmatters

data-perfmatters-src, class perfmatters-lazy

WordPress plugin

ewww

data-ewww-src, data-eio, class lazyload-eio

WordPress plugin

js-generic

data-src, data-lazy-src, data-original, class lazyload

Lazysizes and friends

none

no attribute, no class

page is not lazy-loading

Layout shift

Every <img> needs width and height attributes that match the image's real aspect ratio, or the page gets a CLS hit before the image arrives. CSS aspect-ratio works too. The scanner flags no-dimensions(CLS).

The full SKILL.md

This is the file you give to Codex. The four-tilde fence holds everything, and the whole block below it is the same file, so you can also just copy the code on the page. It is rewritten for the Codex runtime: the methodology, thresholds, and sources of the claude-seo project kept, the Claude-specific runtime removed.

markdown
---
name: codex-seo-images
description: Use when the user gives a URL or image directory and asks about image SEO, image audit, alt text, image size, WebP/AVIF conversion, image format, lazy loading, CLS from images, image metadata, or optimizing images for Google Images. Triggers on "check my images", "image optimization", "alt text audit", "images too big".
---
# Image Optimization Analysis

Audit and improve image assets for search, image SERP, and Core Web Vitals.
The scanner collects per-image signals; you rate against the tables and write
the fix list.

## Run

```bash
python3 ~/.codex/skills/codex-seo-images/scripts/image_scan.py <url> [--json]
```

The scanner reports total images, picture elements, and a summary of missing
alt, missing dimensions, lazy-load hints, oversized files, and non-WebP/AVIF
formats, then one line per image with its flags.

## Checks

### Alt text

- Present on every `<img>`, including images inside a `<picture>`'s `<img>` tag
- Descriptive: tells a human what the image shows, not the filename
- Length 10-125 characters
- Keyword use natural, not stuffed; decorative images use `alt=""` or
  `role="presentation"`

Good: "Professional plumber repairing kitchen sink faucet"
Bad: "image.jpg" (filename, not a description)
Bad: "plumber plumbing plumber services" (stuffing)
Bad: "Click here" (not descriptive)

### File size

| Image category | Target | Warning | Critical |
|----------------|--------|---------|----------|
| Thumbnails | < 50 KB | > 100 KB | > 200 KB |
| Content images | < 100 KB | > 200 KB | > 500 KB |
| Hero / banner | < 200 KB | > 300 KB | > 700 KB |

Compress to target without visible quality loss; the scanner flags anything
over 200 KB as heavy and over 500 KB as oversized.

### Format

| Format | Browser support | Use case |
|--------|-----------------|----------|
| WebP | 97%+ | Default recommendation |
| AVIF | 92%+ | Best compression, newer |
| JPEG | 100% | Photo fallback |
| PNG | 100% | Graphics with transparency |
| SVG | 100% | Icons, logos, illustrations |

Serve AVIF or WebP with a JPEG fallback:

```html
<picture>
  <source srcset="image.avif" type="image/avif">
  <source srcset="image.webp" type="image/webp">
  <img src="image.jpg" alt="Descriptive alt text" width="800" height="600"
       loading="lazy" decoding="async">
</picture>
```

JPEG XL: a decoder shipped in Chrome 145 (Feb 2026) behind a flag, not on by
default. No confirmed production standard yet; keep AVIF/WebP plus JPEG.

### Responsive images

- `srcset` with multiple widths and `sizes` matching breakpoints
- Not the same width served to desktop and mobile

```html
<img src="image-800.jpg"
     srcset="image-400.jpg 400w, image-800.jpg 800w, image-1200.jpg 1200w"
     sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"
     alt="Description">
```

### Lazy loading

- `loading="lazy"` on below-the-fold images
- Never lazy-load the hero/LCP image - that directly harms LCP
- JS-driven lazy loaders are common; the scanner reports `lazy_method`:

| `lazy_method` | Signal | Stack |
|---|---|---|
| native | `loading="lazy"` attribute | modern browsers |
| perfmatters | `data-perfmatters-src` / class `perfmatters-lazy` | WordPress + Perfmatters |
| ewww | `data-ewww-src` / `data-eio` / class `lazyload-eio` | WordPress + EWWW |
| js-generic | `data-src` / `data-lazy-src` / `data-original` / class `lazyload` | Lazysizes, vanilla lazyload, jQuery |
| none | no attribute or class signal | page is not lazy-loading |

If `lazy_method` is a JS loader, the missing native attribute is not a
regression - say so in the report instead of flagging it.

### LCP and CLS

- Hero image gets `fetchpriority="high"`:
  `<img src="hero.webp" fetchpriority="high" alt="..." width="1200" height="630">`
- Non-LCP images get `decoding="async"` so decode does not block the main thread
- Every image carries `width` and `height` (or CSS `aspect-ratio`) or the page
  gets layout shift: the scanner flags `no-dimensions(CLS)`
- Prefer real dimensions over hard-coded CSS that lies about the real ratio

### Filenames

- Descriptive and hyphenated, lowercase: `blue-running-shoes.webp`, not `IMG_1234.jpg`
- No spaces, no special characters

### CDN

- Images on a separate CDN hostname with edge caching headers is fine and
  expected for image-heavy sites; only flag if origin serves images and the
  site has no cache layer at all

## Output

### Image audit summary

| Metric | Status |
|--------|--------|
| Total images | XX |
| Missing alt text | XX |
| Oversized (>200 KB) | XX |
| Wrong format (not WebP/AVIF) | XX |
| No dimensions | XX |
| Not lazy-hinted | XX |

### Prioritized optimization list

Sorted by largest file-size savings first, each row: image, current size,
format, issues, estimated savings. Then recommendations in this priority
order: convert to WebP/AVIF, add alt text, add dimensions, enable lazy load
below fold, compress oversize, fix filenames.

## Local file optimization (optional, no installs promised)

If the user hands you image files instead of a URL, check `which exiftool
cwebp convert ffmpeg` first. Use whatever exists:

```bash
cwebp -q 82 -metadata all input.jpg -o output.webp          # WebP
convert input.jpg -resize 800x -quality 82 image-800.webp   # responsive variant
ffmpeg -i input.jpg -c:v libaom-av1 -crf 30 -still-picture 1 output.avif  # AVIF
```

Two things to know:

- IPTC Creator/Copyright/By-line can show in Google Images display and brand
  attribution; it is display only, not a ranking factor. WebP carries EXIF/XMP
  but not IPTC; use XMP fields for WebP.
- If images are AI-generated product photos, Google Merchant Center requires
  the IPTC `DigitalSourceType: TrainedAlgorithmicMedia` label on feed imagery;
  unlabeled AI imagery can be disapproved. That is a feed-layer policy, not a
  ranking factor - flag it for Merchant Center feeds, not for the page.

## What matters vs what doesn't

| Factor | Impact | Where to set |
|--------|--------|--------------|
| Alt text | Critical (ranking) | HTML `<img alt="">` |
| Filename | High (ranking) | descriptive, hyphenated |
| Page context | High (ranking) | surrounding HTML content |
| File size / speed | Medium (via CWV) | compression + format |
| IPTC Creator/Copyright | Low (display only) | image metadata |
| EXIF camera data | None | irrelevant |
| IPTC keywords | None | Google ignores |

## Errors

| Scenario | Action |
|---|---|
| URL unreachable | Report connection error and status; check auth |
| No images found | Report none detected; suggest JS/CSS background images and asking for rendered HTML |
| Images behind CDN/auth | Report markup signals only; note sizes could not be fetched |
| No conversion tools | Skip local optimization; give online converters or design feedback instead |

The evidence collector

The scanner is a single file, standard library only. It has three parts:

  1. Fetch the page, strip script and style blocks so nothing in JavaScript code

gets counted as an image.

  1. Pull every <img> and its attributes (alt, width, height, loading,

fetchpriority, decoding, srcset), plus a count of <picture> elements.

  1. For the first 25 images, do an HTTP HEAD and read the content-length and

content-type, so the report shows real kilobytes and the real format.

The flag logic is where the thresholds live: missing alt, alt that is a filename, alt too long, no dimensions, no lazy hint, no fetch priority, heavy or oversized by the category thresholds, and anything that is not WebP/AVIF. Image ordering in HTML matters, so the first image is treated as the likely hero in the interpretation step.

python
#!/usr/bin/env python3
"""Collect per-image signals from a page: alt, size, format, lazy loading, CLS.
Standard library only. Usage:
  python3 image_scan.py <url> [--json]
"""
import json
import html as htmlmod
import re
import sys
import urllib.error
import urllib.request
import urllib.parse

TIMEOUT = 12
UA = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
      "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36 "
      "codex-seo-images/1.0")


def fetch(url, timeout=TIMEOUT):
    req = urllib.request.Request(url, headers={"User-Agent": UA})
    try:
        resp = urllib.request.urlopen(req, timeout=timeout)
        return resp.status, resp.geturl(), resp.read().decode("utf-8", "replace")
    except urllib.error.HTTPError as e:
        return e.code, url, ""
    except Exception as e:
        return "ERR:" + str(e)[:120], url, ""


def header(url):
    """HEAD or ranged GET for size/type; returns (size, ctype) or (None, None)."""
    req = urllib.request.Request(url, method="HEAD", headers={"User-Agent": UA})
    try:
        resp = urllib.request.urlopen(req, timeout=TIMEOUT)
        ct = resp.headers.get("content-type", "")
        sl = resp.headers.get("content-length")
        size = int(sl) if sl and sl.isdigit() else None
        return size, ct
    except Exception:
        return None, None


def lazy_method(img, lower_src):
    cls = img.get("class", "")
    srcs = lower_src
    if '"perfmatters' in srcs or "data-perfmatters" in lower_src:
        return "perfmatters (JS plugin)"
    if "data-ewww" in lower_src or "data-eio" in lower_src or "lazyload-eio" in cls:
        return "ewww (JS plugin)"
    if "data-src" in lower_src or "data-lazy-src" in lower_src or "data-original" in lower_src or \
            "lazyload" in cls or re.search(r'\bdata-srcset', lower_src):
        return "js-generic (lazy loader)"
    if "loading=\"lazy\"" in json.dumps(img) or 'loading="lazy"' in json.dumps(img):
        return "native"
    return "none (not lazy-loaded)"


def main():
    url = sys.argv[1] if len(sys.argv) > 1 else ""
    if not url:
        print("usage: python3 image_scan.py <url> [--json]")
        sys.exit(1)
    want_json = "--json" in sys.argv
    if not url.startswith("http"):
        url = "https://" + url
    status, final, html = fetch(url)
    if not html:
        print({"url": url, "status": status, "error": "no HTML body"})
        sys.exit(0 if status == 200 else 1)
    base = final
    # strip <script>/<style> contents before extracting <img> to avoid false hits
    html = re.sub(r"<(script|style)[^>]*>.*?</\1>", " ", html, flags=re.S | re.I)

    imgs = []
    for m in re.finditer(r"<img\b[^>]*>", html, re.I):
        tag = m.group(0)
        attrs = {}
        for a in re.finditer(r'([\w-]+)\s*=\s*"([^"]*)"', tag, re.I):
            attrs[a.group(1).lower()] = htmlmod.unescape(a.group(2))
        src = attrs.get("src", "")
        abs_src = urllib.parse.urljoin(base, src) if src else ""
        alt = attrs.get("alt", "")
        imgs.append({
            "src": abs_src,
            "alt": alt,
            "alt_len": len(alt),
            "has_width": "width" in attrs,
            "has_height": "height" in attrs,
            "loading": attrs.get("loading", ""),
            "fetchpriority": attrs.get("fetchpriority", ""),
            "decoding": attrs.get("decoding", ""),
            "srcset": attrs.get("srcset", ""),
            "lazy_method": lazy_method(attrs, json.dumps(attrs).lower()),
        })

    picture_count = len(re.findall(r"<picture[^>]*>", html, re.I))

    # size/format from HEAD on first 25 images (progress: 1 per image, sequential)
    for img in imgs[:25]:
        if not img["src"].startswith("http"):
            img["size_kb"] = None
            img["ctype"] = ""
            continue
        size, ct = header(img["src"])
        img["size_kb"] = round(size / 1024, 1) if size else None
        img["ctype"] = ct.split(";")[0] if ct else ""

    def flag(img):
        f = []
        if img["src"].startswith("data:"):
            f.append("inline-base64:" + str(len(img["src"]))[:3] + "KB")
        if not img["alt"]:
            f.append("missing-alt")
        elif not img["alt"].startswith(("image", "pict", "photo")):
            if img["alt_len"] < 6:
                f.append("alt-too-short(%d)" % img["alt_len"])
        if img["alt"].lower().endswith((".jpg", ".jpeg", ".png", ".gif", ".webp", ".avif")):
            f.append("alt-is-filename")
        if img["alt_len"] > 125:
            f.append("alt-too-long(%d)" % img["alt_len"])
        if not img["has_width"] and not img["has_height"]:
            f.append("no-dimensions(CLS)")
        if img["loading"] == "lazy" and (img["alt"] or "") and img.get("src", ""):
            pass  # lazy is fine; above-fold flags are for humans + see note
        if img["loading"] not in ("lazy", "eager"):
            f.append("no-loading-hint")
        if img["fetchpriority"] not in ("high", "low"):
            f.append("no-fetchpriority")
        if img["size_kb"] is not None:
            if img["size_kb"] > 500:
                f.append("oversized-%dkb" % img["size_kb"])
            elif img["size_kb"] > 200:
                f.append("heavy-%dkb" % img["size_kb"])
        if img["ctype"] and img["ctype"] not in ("image/webp", "image/avif", "image/svg+xml"):
            f.append("format-" + img["ctype"].replace("image/", ""))
        return "; ".join(f)

    for img in imgs:
        img["flags"] = flag(img)

    r = {
        "url": url, "status": status,
        "total_images": len(imgs),
        "picture_elements": picture_count,
        "summary": {
            "missing_alt": sum(1 for i in imgs if not i["alt"]),
            "no_dimensions": sum(1 for i in imgs if not i["has_width"] and not i["has_height"]),
            "no_lazy_hint": sum(1 for i in imgs if i["loading"] not in ("lazy", "eager")),
            "oversized_over_200kb": sum(1 for i in imgs if (i["size_kb"] or 0) > 200),
            "not_webp_avif": sum(1 for i in imgs if i["ctype"] and i["ctype"] not in ("image/webp", "image/avif", "image/svg+xml")),
        },
        "images": imgs,
    }

    if want_json:
        print(json.dumps(r, indent=2))
        return
    print("image_scan %s  status %s  images %d  picture %d" % (r["url"], r["status"], r["total_images"], r["picture_elements"]))
    print("  summary: %d missing alt / %d no dimensions / %d not lazy-hinted / %d >200KB / %d not webp-avif" % (
        r["summary"]["missing_alt"], r["summary"]["no_dimensions"],
        r["summary"]["no_lazy_hint"], r["summary"]["oversized_over_200kb"],
        r["summary"]["not_webp_avif"]))
    for i, img in enumerate(imgs[:15], 1):
        size = "%skb" % img["size_kb"] if img["size_kb"] is not None else "-"
        short = (img["src"][:70] or "(empty)")
        print("  %2d. %-72s %-8s %s" % (i, short, size, img["flags"] or "ok"))


if __name__ == "__main__":
    main()

Install it in three commands

bash
mkdir -p ~/.codex/skills/codex-seo-images/scripts
# save the two files above at:
#   ~/.codex/skills/codex-seo-images/SKILL.md
#   ~/.codex/skills/codex-seo-images/scripts/image_scan.py
python3 ~/.codex/skills/codex-seo-images/scripts/image_scan.py https://example.com

The last command is your sanity check. Expect image_scan https://example.com status 200 images N picture N and a summary line. If it prints images 0, the page you picked really has no <img> tags (see the failure table).

Run it

bash
python3 ~/.codex/skills/codex-seo-images/scripts/image_scan.py https://yoursite.com/blog/your-article

Read the output in this order: the summary line first (what kind of problem dominates), then the rows the summary points at, then any row flagged weird on its own. A page with 40 images and 2 flagged rows is healthy. A page with 12 images and 9 missing-alt rows is a template issue: finding the template line that drops alt text is worth more than hand-editing nine markup chunks.

A real scan

Here is the scanner on a published article from this very series, https://auspia.ai/blog/codex-seo-technical-audit. The scan is a few days old and the image set has grown since; the signal still holds.

Code
image_scan https://auspia.ai/blog/codex-seo-technical-audit  status 200  images 16  picture 0
  summary: 14 missing alt / 3 no dimensions / 2 not lazy-hinted / 1 >200KB / 1 not webp-avif
   1. https://auspia.ai/blog/auspia-logo.svg                  11.3kb   no-dimensions(CLS); no-loading-hint; no-fetchpriority
   2. https://auspia.ai/_image?href=https%3A%2F%2Fauspia.ai%2F_emdash...   60.2kb   missing-alt
   3. https://auspia.ai/_emdash/api/media/file/01M13Y5HP...jpg           267.7kb  alt-too-long(213); no-dimensions(CLS); no-fetchpriority; heavy-267kb; format-jpeg
   4. https://auspia.ai/_image?href=https%3A%2F%2Fauspia.ai%2F_emdash...  -        missing-alt; no-fetchpriority

Three findings worth a template change:

  • 14 of 16 images serve as an optimized proxy (/_image?href=...), and those

proxy images come through without alt text. The original file names carry the description, but the optimized path drops it.

  • Image 3 is a 267.7 KB JPEG with a 213-character alt: it is the inline

diagram, and the alt is nine lines long. Great fallback for a blind reader, but it blew both the alt-length and the content-image size target.

  • The SVG logo has no width and no height, which is the only CLS candidate

that predates every feature of this skill.

Interpretation matters here: the 14 flags on proxy images are all the same root cause. One template fix (carry alt through the optimization proxy), and the missing-alt count goes to zero. That is precisely the cluster effect mentioned above, and it is why the fix list starts with templates and not with fourteen edits.

How to read the flags

Flag

What it means

Fix

missing-alt

no descriptive text

add alt, 10-125 chars

alt-is-filename

alt equals photo.jpg style

write a real description

alt-too-long

over 125 characters

trim the caption of the image

no-dimensions(CLS)

no width/height

add both attributes

no-loading-hint

no loading attribute at all

add lazy below fold, eager above

no-fetchpriority

hero not prioritized

fetchpriority="high" on LCP image

heavy/oversized

over 200/500 KB as served

convert to WebP, resize

format-jpeg/png

not WebP/AVIF

convert, keep JPEG fallback

Local file optimization without installing anything

The scanner audits a live URL. If you hand Codex a folder of image files instead, the skill will first check what conversion tools exist on your machine (which exiftool cwebp convert ffmpeg). Any that exist get used; if none exists, the skill says so and gives the browser-friendly alternatives instead of pretending it can convert. Two facts worth knowing before you go down this road: IPTC metadata (Creator, By-line, Copyright) can enhance how a licensable image shows in Google Images display, but it is display only, not a ranking factor. And if you run AI-generated product photos through a Merchant Center feed, Google requires an IPTC DigitalSourceType label on those images (the policy is documented at support.google.com/merchants); an unlabeled feed can get the images disapproved. That is a feed-layer requirement, not a page-layer ranking signal.

Failure table

Scenario

What happens

What to do

images 0

page genuinely has no <img>

fine; check for CSS backgrounds, note it and move on

ERR: in status

TLS or network error

check the URL, try the http version, check VPN

All sizes show -

HEAD was blocked (auth/CDN botwall)

report markup-only findings; sizes unavailable

no-loading-hint spam

CMS template omits loading everywhere

fix the template, or add a streaming picture component

Site behind login

scanner sees the login page

use the public URL, or save the rendered HTML and point Codex at it

Install this skill by pasting to Codex

Copy everything between the two four-tilde lines at the top of this article into a message to Codex (or just select from "## The full SKILL.md" through the end of the code blocks). Paste this prompt:

Code
Read this message. Create the skill below at
~/.codex/skills/codex-seo-images/ by following the two code blocks in it:
1. The markdown block is SKILL.md.
2. The python block is scripts/image_scan.py.
Then run python3 ~/.codex/skills/codex-seo-images/scripts/image_scan.py
https://example.com as a self-test and report the output (or the error) back
to me, along with what you would fix first if I point you at my own site.
Do not run any other commands. Do not modify any other files.

Codex will create the folder, write the two files, run the self-test, and give you the verdict. If image_scan.py does not run, the failure table above covers the usual causes.

FAQ

Is alt text still a ranking factor? In the direction of an image's own indexing, yes. Where it matters most is image search and accessibility, both of which are worth getting right for reasons beyond pure ranking.

Should every image have alt? No. Decorative images should have alt="" or role="presentation". The scanner counts empty alt as missing, so in the report differentiate: empty alt is planful, absent alt is not.

What about Google's "visual search" directions? Image discovery through visual search (scene and object understanding in multilanguage search surfaces) is expanding. The published levers are what they always were: descriptive alt text, clean file structure, and correct context around the image. Don't chase a new format.

Do I still need the `<picture>` element if my images are WebP? Yes, for browser baselines that don't support it. WebP is at 97%+ support, so the fallback is thin, but there is no cost to shipping <picture> with a JPEG fallback on anything that matters to you.

Where do I pin the percentages in the article from? Browser support figures come from published compatibility data and are stable enough to quote; the JPEG XL point is flagged as observed in Chrome 145 without a confirmed production baseline.

Next in the series (post 07 of 20): [How to Set Up Codex for XML Sitemap Checks (Full SKILL.md Included)](https://auspia.ai/blog/codex-seo-sitemap) - sitemap discovery, validation, generation.

Previous in the series: How to Set Up Codex for Schema Markup (Full SKILL.md Included). The full series roadmap lists all 20 posts.

Author: Clara Bennett, 10-Year Content Strategy Practitioner at Auspia. Clara writes about content operations, editorial systems, and keeping content assets fast, findable, and useful.

Based on the open-source claude-seo project by AgriciDaniel (MIT license, GitHub). This series adapts it for the Codex runtime: rewritten methodology, a Codex-native SKILL.md, and new evidence collectors written from scratch for this series.

Explore this topic

Keep following the same growth thread