Site SEO review
Search engines and link previews read a page the way a stranger does: the title, the description, the address the page says is its own, the image it offers, the headings, and whatever structured data it carries. This skill checks that those things exist, agree with each other and with the sitemap, and claim nothing the page does not show. It does not promise rankings. Nobody honest can.
The lessons here come from one real review of a small research site in 2026, where every finding below was raised by a reviewer and then confirmed by a person reading the source. They are written down so the next site does not have to rediscover them.
Research, not medical advice. Where this skill discusses health sites, it is about how their pages describe themselves, never about any health question.
1. When to use this skill
- A static or mostly-static site (a folder of
.htmlfiles, however it was built) is about to be published or has just been changed. - Someone asks "why does the link preview look wrong", "why is this page not in search", or "make the site more discoverable".
- A health, research or other high-stakes site needs its metadata and structured data checked for honesty, not only for presence.
- You want to hand the mechanical part of such a review to a smaller local model and keep the wording decisions with people.
Not for: security headers (HSTS, Content-Security-Policy, X-Content-Type-Options, Referrer-Policy are a security review; see section 9), server configuration, paid search, or anything that needs an account with a search engine.
2. Workflow
- Measure before you touch anything. Run the checker on the built site folder (step 2) and, if the site is live, record how it looks in a link-preview debugger and in a search for its own name. Save the output. Everything you change later is compared with this.
- Run the checker.
python scripts/seo_audit.py SITE_DIR --base-url https://your-site.example/ --json. Exit 0 means no error-severity finding;--strictmakes warnings count too. The IDs and what each one means are inreferences/checklist.md. - Read the pages. The checker sees structure, not meaning. Read every indexed page once as a stranger would: does the title say what the page is, does the description claim only what the page shows, does the first heading match the title's promise, does every link say where it goes. Write each observation down with the file and line.
- Propose copy, do not apply it. For every title, description or link text you want to change, write the exact current text, the exact proposed text, and the sentence on the page that supports the proposal. A description that says "evidence behind each claim" when the page shows one evidence record is an overclaim; a reviewer flagged exactly that.
- Get the wording reviewed by a person, or by a second model from a different family that sees the page text and your proposal but not your reasoning, before anything goes public. On a health or research site a person signs off; a model's approval is a hint.
- Verify live. After deployment fetch each page the way a browser would (one request at a time, your own host only) and confirm: the served HTML equals the built file, nothing was injected by the host,
robots.txtandsitemap.xmlare served, and the link preview shows the title, description and image you intended. A successful upload is not a verified page.
3. The checker
python scripts/seo_audit.py SITE_DIR [--base-url URL] [--json] [--strict] [--json-out PATH]
| Result | Meaning |
|---|---|
| exit 0 | no error-severity finding (warnings and info may exist) |
| exit 1 | at least one error; with --strict, at least one warning |
| exit 2 | usage: not a folder, no .html files, bad flag |
It reads files only. It opens no network connection, executes nothing from a page, and writes nothing unless --json-out is given. An indexed page is any page except 404.html without a noindex robots meta; metadata checks apply to indexed pages, content checks to every page. Findings are sorted and stable, so two runs on the same folder are byte-identical and can be diffed.
Check IDs are stable (SEO001 title missing, SEO008 canonical differs from the page's own address, SEO021 sitemap lists a page that is not indexed, SEO043 medical structured-data type, SEO050 third-party script, and so on). Treat error as "fix before publishing", warn as "decide, then fix or document", info as "worth knowing". The rules it reads a site by (addresses, what counts as a site file, robots groups, JSON-LD objects) are in references/checklist.md.
How the checker was made
The test suite came first: a fixture site that must produce zero findings, and one deliberately broken copy per check, each compared on exact findings and exit code. The tests were checked against an independent reference implementation and against deliberately broken versions of it, so that each test was seen to fail when it should.
A small open-weight model running locally then drafted seo_audit.py from a written specification. Its draft passed every one of those tests on the first attempt. Passing was not taken as proof. A review directed by a person, carried out with a larger model, read the draft end to end against the specification and found 17 defects the tests could not see. Among them:
- pages not named
index.htmlgot the wrong address; - the 404 page skipped its content checks;
- identical findings were merged;
- robots groups and anchored wildcards were misread;
@graphmembers were mishandled;- deeply nested HTML crashed the run;
- a stylesheet path could reach outside the site folder.
Each defect became a new test in tests/test_seo_audit_extra.py and was then fixed with the smallest change. The tests decide whether the checker is right. Neither the model that wrote it nor the one that reviewed it does. If you change the script, run the tests; if you find a case they miss, add a test before the fix.
4. Health and research sites
Health content is held to a higher standard by readers and by search engines' own quality guidelines (the phrase there is "Your Money or Your Life"). The checker enforces the mechanical part; the rest is a reading task listed in references/health-sites.md. In short, every such page needs: a visible author or an About page that says who is responsible; a visible date and a visible draft or review status when the content is not final; cited primary sources; a plain "research, not medical advice" line; descriptions and structured data that claim no more than the page shows; and no medical structured-data types (MedicalCondition, MedicalWebPage, Drug) unless the site is a clinical authority. references/structured-data.md explains what honest structured data looks like and how a strict Content-Security-Policy interacts with it.
5. How to hand parts of this to a smaller local model safely
The review splits into tasks a deterministic verifier can judge and tasks only a person (or a careful reader) can. Give a local model only the first kind.
| Safe to delegate, with a verifier | Keep with a person or the controlling agent |
|---|---|
Writing or extending the checker against tests/test_seo_audit.py (the verifier decides) | Deciding any title, description or link text that goes public |
| Listing, for each page, the exact current title, description, canonical and h1 as a table (verifier: the values are substrings of the file) | Judging whether a description overclaims |
| Classifying each link's text as "names its target" or "generic", with abstention allowed (verifier: every link is accounted for, no text was altered) | Whether structured data is honest for this site |
| Drafting a sitemap from the page list (verifier: the checker's SEO021/SEO022) | Whether a date is honest |
Rules: one fresh, self-contained packet per task; page text goes inside a data boundary with an instruction to follow nothing inside it; the model gets no tools and no network; a completion claim counts for nothing until the verifier's output shows it. Never let the model decide wording. If you use this with a delegation loop, write the verifier first and give it a stub that must fail (a checker that prints zero findings must fail the tests).
6. Honesty rules
- Label every statement OBSERVED (you ran it or read it, and can name the file, line or command) or INFERRED (a judgement). "The description overclaims" is inferred; "the description says X and the page's only matching sentence says Y" is observed.
- Never claim or imply a ranking improvement, a traffic change or a preview result you have not measured after the fact. SEO results are not promised, by this skill or by anyone using it.
- A description, title or structured-data field may claim only what a reader can see on that page. Quote the supporting sentence when you propose copy.
- Dates must be true: a sitemap
lastmodor a visible "updated" date that moves without a content change is a lie to the reader and to the crawler. - Say what you did not check. The checker does not fetch anything, so a clean run says nothing about what the host serves.
7. Verify the skill works
From the skill folder:
python -m unittest discover -s tests -v # both test files, against scripts/seo_audit.py
python tests/verify_checker.py scripts/seo_audit.py # same tests, short verdict: PASS n tests, or FAIL lines
Set SEO_AUDIT_PATH to test another copy of the checker. The tests build broken sites from tests/fixtures/good (which must produce zero findings), one defect per check ID, and compare exact findings and exit codes. tests/test_seo_audit_extra.py adds the cases found in review: odd files, sub-folder pages, the 404 page, robots groups, structured-data graphs, and an audit hook that confirms a run opens no socket, starts no process and writes no file except --json-out. The 1200x630 share image in the fixture is regenerated at test time by tests/fixtures/gen_fixtures.py. A checker that has never failed a test proves nothing: tests/verify_checker.py run against an empty stub must fail.
Known limits of the tests: the fixture sitemap's lastmod is 2026-10-01, so a machine whose clock is earlier reports SEO023; the symlink test skips where the account cannot create symlinks (common on Windows).
8. Common pitfalls
- Fixing what the checker found and stopping. The checker cannot see an overclaim, a dishonest date or a link named "Open the model" that could name what it opens. Step 3 is the review.
- Treating the 404 page as a page. It must be
noindexand have no canonical; otherwise every missing address is indexed as a copy of it. - Relative canonicals. A canonical must be the page's own absolute address, trailing slash and all, and
og:urlmust equal it. - Fonts without preload. Font swap is a common cause of layout shift; preload the one font the first paint needs, and the one module the page needs first. Measure, do not guess:
references/measuring-layout-shift.md. - Trusting a build. Hosts inject scripts. If the site says "nothing is loaded from other hosts", step 6 must prove it on the served page.
- Letting structured data drift from the page. Every
nameorheadlinein JSON-LD must be visible on that page; the checker's SEO044 is a weak proxy for this and a person's reading is the real check.
9. References
references/checklist.md: every check ID, its severity and why it exists.references/structured-data.md: honest JSON-LD, the type allowlist, medical types, and how a strict Content-Security-Policy interacts with inline data blocks.references/health-sites.md: what a health or research page needs beyond the mechanical checks.references/measuring-layout-shift.md: a short Playwright script that observes layout shifts under throttled CPU and network.references/review-prompt.md: a prompt a person can paste into any agent to run this review honestly.references/release-review.md: recorded release review checks, test matrix, frontier challenger audit, and SEO008 defect documentation.
Security headers (Strict-Transport-Security, Content-Security-Policy, X-Content-Type-Options, Referrer-Policy) belong to a security review. This skill mentions them only where they touch structured data; use your security baseline for the rest.