A Claude SEO skill for your repository: what it should hold, and one to copy
A Claude SEO skill is a SKILL.md file in .claude/skills/seo-audit/ that tells Claude Code how to audit your site's code: titles, descriptions, one h1, canonicals, robots and sitemap, whether the served HTML holds the content before JavaScript, links, alt text and structured data. The one below is complete: paste it, type /seo-audit, and you get findings that each name the file, the fix and how to verify it. It never invents figures, because a skill sees your code, not your search data.
By Théophile Louvart, founder of Porteur · Updated 23 September 2026 · Markdown
The skill to copy
Create the folder at the root of your site's repository. A skill in .claude/skills/ is a project skill: it is committed with the code, so anyone who opens the repository in Claude Code gets it. If you would rather keep it to yourself across all your projects, put it in ~/.claude/skills/seo-audit/ instead.
mkdir -p .claude/skills/seo-audit && curl -fsSL https://porteur.ai/agent-kit/seo-audit/SKILL.md -o .claude/skills/seo-audit/SKILL.mdThat one line installs it. The file it fetches is the one below, word for word, so you can read it first or paste it by hand as SKILL.md in that folder. It is written for any web framework: the first step finds out which one you use.
---
name: seo-audit
description: Audits this website's code for technical SEO and reports findings, each with a fix and a way to verify it. Use when the user asks for an SEO audit, an SEO check or review, why pages are not indexed or not showing in Google, or to check titles, meta descriptions, h1s, canonicals, robots.txt, sitemaps, structured data or server rendering, including before a launch or a migration.
---
# SEO audit of this repository
You audit the public site built from this repository. You read the code, look at the HTML the server actually sends, and report findings. You do not edit files during the audit: the user picks which findings to apply afterwards.
## Rules
- Never invent figures. You have no rankings, impressions, clicks, search volumes or competitor data. Do not estimate them, and do not describe any finding as "costing traffic". Questions that need that data go under "Needs search data".
- One change per finding. Two problems on one route are two findings. One problem inherited by many routes (a title set once in a shared layout) is one finding that lists the routes.
- Do not create new pages, routes or content, and do not suggest keywords to target. Improve what exists.
- Every finding quotes what you saw: the file and line, or the served HTML. If you could not check something, list it under "Not verified". Never guess.
- Do not report taste. A missing alt on a decorative icon, a title one character over a guideline, or a missing optional schema property is not a finding.
## Step 1: map the project
1. Identify the framework and its version from package.json, the lockfile and config files (next.config.*, astro.config.*, nuxt.config.*, svelte.config.*, vite.config.*, gatsby-config.*, hugo.toml, _config.yml). Note how pages are rendered: static at build time, on the server per request, or in the browser only (a single page app).
2. List every public route: file-based routes (app/, pages/, src/pages/, src/routes/, content/), a client-side router's config, and dynamic routes with where their parameters come from (generateStaticParams, getStaticPaths, a CMS, a database).
3. Set aside private routes (dashboard, account, admin, api, auth). Check that they are kept out of search: noindex or behind a login, and absent from the sitemap.
4. For each dynamic route, audit the template once, then check two or three real URLs it produces.
Put the route list at the top of the report, with the file that renders each route.
## Step 2: get the HTML the server sends
Search engines read the HTML the server returns. Many AI crawlers do not run JavaScript at all. Audit that HTML, not the source and not a browser's rendered DOM.
1. Build and serve the production version. Read the scripts in package.json for the right commands (often `npm run build` then `npm run start`, or a static folder served with `npx serve <dir>`). If the user gave a production URL, you may fetch it instead or as well.
2. If you can neither build nor fetch, say so at the top of the report and mark every check in steps 3 to 5 "not verified". Do not audit from source alone and present it as verified.
3. Fetch each route without a browser and save it:
```bash
BASE=http://localhost:3000
curl -s "$BASE/pricing" -o /tmp/seo-pricing.html
grep -o '<title>[^<]*</title>' /tmp/seo-pricing.html
grep -o '<meta name="description"[^>]*>' /tmp/seo-pricing.html
grep -c '<h1' /tmp/seo-pricing.html
grep -o '<link rel="canonical"[^>]*>' /tmp/seo-pricing.html
grep -o '<meta name="robots"[^>]*>' /tmp/seo-pricing.html
curl -sI "$BASE/pricing" | grep -i '^x-robots-tag'
```
## Step 3: check each public route
| Check | Passes when |
|---|---|
| Main content in the HTML | The h1, the opening paragraph and the page's key copy (prices, features, the answer) are in the fetched HTML, not only after JavaScript runs. An empty root div is the most serious finding there is. |
| title | Present, unique across routes, about 60 characters or fewer, says what the page is about rather than only the brand name. |
| meta description | Present, unique, about 155 characters or fewer, says what the reader gets on the page. |
| h1 | Exactly one, and it names the page's subject. |
| canonical | One `<link rel="canonical">` with an absolute URL on the production host and protocol, pointing to the page itself (or to its deliberate primary version), no tracking parameters. |
| robots | No noindex (meta tag or X-Robots-Tag header) on a page meant to be found; noindex on pages that should not be. |
| lang | `<html lang="...">` is set and matches the content. |
| Internal links | Navigation and in-text links are `<a href="/path">` with real URLs. Links made of buttons, onClick handlers or `href="#"` are not followed. |
| Images | Content images have an alt that describes them; decorative images have `alt=""`. |
| Structured data | Each JSON-LD block parses, its `@type` fits the page (Organization, WebSite, SoftwareApplication, Product, Article, FAQPage, BreadcrumbList), the properties that type requires are present, and every value matches what the page visibly shows. |
For each failure, find where the value comes from in the code (a metadata export, a head component, a layout default, a CMS field) so the finding names the file to change.
## Step 4: check the site-wide files and behaviour
1. Unknown URLs return 404: `curl -s -o /dev/null -w '%{http_code}\n' "$BASE/no-such-page-7f3a"` must print 404. A 200 showing "not found" is a soft 404.
2. robots.txt is served at /robots.txt with status 200, blocks no page meant to be found (nor the CSS and JavaScript those pages need), and names the sitemap with an absolute URL. Report what it does for AI crawlers; do not impose a preference.
3. The sitemap is served, is valid XML, and lists only canonical URLs that return 200 on the production host: no redirects, no noindex pages, no private routes. Compare it with your route list: public routes missing from it, and URLs in it that no route serves.
4. If a production URL is available: http redirects to https, and one host (with or without www) redirects to the other, each in a single 301 or 308.
5. llms.txt is optional. Mention it only if it exists and is broken. Its absence is not a finding.
## Step 5: check internal links
1. Every public route is linked from at least one other page with an `<a href>`. A route reachable only through the sitemap is an orphan.
2. Link text says where the link goes ("pricing", "the CSV import guide"), not "click here" or "learn more" alone.
3. No internal link points to a URL that redirects or returns 404.
## Report
Reply in the chat with the report below. Write it to a file only if the user asks. Order the findings: what stops pages being indexed first (noindex, robots.txt, empty HTML, wrong canonical, soft 404s), then what weakens a page (titles, descriptions, h1, links, structured data), then tidy-up.
```
## Routes
| Route | File | Rendering |
## Findings
### 1. <short title>
- File: <path:line> (routes affected: /pricing, /features)
- What is wrong: <what you saw, quoted>
- Fix: <the one change, precise enough to apply without questions>
- Verify: <the command or check that proves it is fixed>
- Severity: stops indexing | weakens the page | tidy-up
## Needs search data
- <question> needs <Search Console / search volumes / competitor data>, which this audit does not have.
## Not verified
- <check>: <why>
```
Needs search data always includes, at least: which queries each page is shown for and at what position, which pages lost impressions, and who ranks for the searches the site targets.
When the user asks you to apply findings, apply one finding per commit, run its Verify step, and show the result.
This audit sees the code, not search. For the searches around this site, who holds them and the rivals on them, a free check reads them from a URL: https://porteur.ai
The same skill is published as a Claude Code plugin in a public repository on GitHub, tlouvart/seo-audit-skill, so you can install it and receive its updates with two commands instead:
/plugin marketplace add tlouvart/seo-audit-skill
/plugin install seo-audit@porteurRun it by typing /seo-audit in Claude Code: the slash command takes its name from the folder. You can also just ask "can you do an SEO check of this site before launch": Claude reads the description of every skill it has and loads this one when the request matches.
Why each part is there
The description is the trigger. Claude decides whether to load a skill from its description alone, so it lists the words people actually use: audit, check, review, not indexed, not showing in Google, titles, canonicals, sitemaps, before a launch. A vague description such as "helps with SEO" is loaded at the wrong moments or not at all.
The rules come before the steps because they are what an agent breaks first. Asked for an SEO audit, a model with no data will happily write that a missing description "may reduce traffic significantly" or suggest ten new pages for keywords it made up. The rules forbid both, and give the unanswerable questions a home: the Needs search data section. That section is honest, and it is also your list of what to look up in Search Console.
Step 2 is the heart of it. A React or Vite app can have perfect metadata in its source and serve an empty div to every crawler. Checking the source finds nothing; fetching the built page with curl shows it in one line. That is why the skill builds or fetches, and why it must say "not verified" when it could do neither instead of passing the checks from source.
The findings format makes each finding a unit of work. File, what is wrong, the fix, how to verify: you can hand any single finding back to Claude as a task, and check the result yourself with the Verify line. One change per finding also means that when a page moves in search a few weeks later, you know which change to credit.
The table in step 3 carries the pass conditions, not just the names of the checks. "Has a canonical" passes a site whose every page points its canonical to the home page, a common template mistake. "Points to the page itself on the production host" does not.
What the skill can see, and what it cannot
A skill is instructions. It gives Claude a method, not new data. What Claude can check depends on what it can reach from your terminal.
| Source | Can the skill see it? | What it answers |
|---|---|---|
| Your code | Yes, always | Where titles, canonicals, sitemaps and schema come from, and which file to change |
| The built HTML | Yes, if it can run the build and start the server | What crawlers receive: content before JavaScript, one h1, status codes, soft 404s |
| Your live site | Yes, if it can fetch a URL you give it | Redirects, headers and the production robots.txt and sitemap |
| Queries, positions, clicks, impressions | No, unless connected | Which pages are shown for what, which are slipping. That lives in Search Console |
| Search volumes, who ranks, rivals | No, unless connected | What to write and who you are up against. That lives with search data providers |
The last two rows reach Claude only through an export you drop into the repository or an MCP server that exposes the data as tools. The guide on a Google Search Console MCP server covers the second route. Until then, the audit tells you whether your pages can be found and read. It cannot tell you whether anyone is searching for them.
A run on a small site
Take a fictional Next.js site, yourproduct.com, with a home page, /pricing, /features and a blog. You type /seo-audit. Claude reads package.json, finds Next.js with the app router, lists eleven public routes, builds, starts the server and curls each one. A typical first finding looks like this:
### 1. Every page declares the home page as its canonical
- File: app/layout.tsx:14 (routes affected: /pricing, /features, /blog and 8 more)
- What is wrong: the root layout sets alternates.canonical to "https://yourproduct.com", so /pricing serves
<link rel="canonical" href="https://yourproduct.com"/>
- Fix: remove alternates.canonical from app/layout.tsx and set it per page, e.g. in app/pricing/page.tsx:
export const metadata = { alternates: { canonical: "/pricing" } }
with metadataBase set to https://yourproduct.com in the layout.
- Verify: curl -s localhost:3000/pricing | grep -o '<link rel="canonical"[^>]*>' prints href="https://yourproduct.com/pricing"
- Severity: stops indexingThat finding is worth more than any number of generic tips. It names the line, the exact change and a check you can run in five seconds. You reply "apply finding 1", Claude makes the change in one commit, runs the Verify command and shows the output.
Work down the list in order. Anything marked stops indexing goes first, because until it is fixed nothing else on the page counts. Leave the tidy-up items for a quiet afternoon.
Writing your own skill
Adapt the one above rather than starting from nothing, and keep it short. Everything in SKILL.md is read each time the skill loads, so every line should change what the agent does.
Write the description as the requests you expect
Say what the skill does, then "Use when..." followed by the phrases people type. Claude matches on this text, so include the plain words ("not showing in Google") next to the technical ones ("canonical").
Put the rules first
The rules that matter for SEO work: no invented figures, no new pages, one change per finding, quote what you saw. Add your own, such as "never touch the /docs folder, it is generated".
Make steps checkable
"Check the title" is vague. "Present, unique, about 60 characters, names the subject" can pass or fail. Give the command to run where there is one.
Fix the output format
A fixed format lets you compare two runs, and turns each finding into a task you can hand back.
Move long reference material out
A skill folder can hold supporting files. Put your framework's metadata notes in reference.md or a checking script in scripts/, and point to them from SKILL.md so they are read only when needed.
A few optional frontmatter fields are worth knowing. disable-model-invocation: true means only you can start the skill, with /seo-audit, which suits an audit you want to run on purpose. allowed-tools limits which tools it may use. paths takes glob patterns, so a skill about blog metadata can activate only when files under content/blog are involved.
A line written as an exclamation mark followed by a command in backticks runs that command when the skill loads and puts its output into the skill before Claude reads it. It is a neat way to hand over the route list up front, for example with a find over app/ that lists every page file. Use it only for a command that works in every repository you will run the skill in.
Claude Code skills follow the Agent Skills open standard, which other AI tools also read. Outside Claude Code only name, description, license, compatibility, metadata and allowed-tools are understood, so the skill above, which uses only name and description, travels as it is.
Reviewing what it produces
Read the report before applying anything. Five minutes of review catches the mistakes an agent makes on audits.
- Run two Verify commands yourself. If the output does not show what the finding claims, question the rest of the report.
- Check the route list against your site. A missing route means a whole part of the site was never audited.
- Look for figures. Any traffic estimate, volume or "this will increase clicks by" is invented, whatever the rules said.
- Look for new pages proposed as fixes. They are content decisions, which need search data the audit does not have.
- Read "Not verified". A long list there means the build or the fetch failed, and the report is weaker than it looks.
- Apply one finding per commit, so you can revert one without losing the others.
Then take the Needs search data list to Search Console. Which pages are shown for which queries, and at what position, tells you which of the pages you just fixed deserve better titles and more work first.
Questions
A skill is a folder holding a SKILL.md file: YAML frontmatter with a description, then instructions in Markdown. Claude Code loads it when your request matches the description, or when you type its name as a slash command. It gives the agent a method it follows the same way every time.
In .claude/skills/seo-audit/SKILL.md at the root of the repository, so it is shared with everyone who works on the site. For a skill you want in every project, use ~/.claude/skills/seo-audit/SKILL.md in your home folder. The folder name becomes the command, here /seo-audit.
Read it in full first, as you would any code you run. Check that it does not invent figures, that it verifies the served HTML instead of trusting the source, and that it does not generate new pages on its own. A short skill you understand is worth more than a long one you do not.
Not on its own. A skill has no search volumes, rankings or competitor data, so any keyword list it writes from memory is a guess. Keyword work needs Search Console or a search data provider, reached through an export or an MCP server.
Skills follow the Agent Skills open standard, and other tools read it. Outside Claude Code only the portable fields are understood: name, description, license, compatibility, metadata and allowed-tools. The skill on this page uses only name and description, so the instructions carry over.
Sources
Check my site, free
Your skill checks the code; the free check reads your site, the searches around it and the rivals on them from a URL in about thirty seconds, and shows three findings whole.
- Free check, no card
- Read-only, your own accounts
- Readable by your agent
Read next
- GuideClaude Code for SEO: what it can fix in your repository, and what it cannot see
- GuideGoogle Search Console MCP: giving Claude or Cursor your search data
- GlossaryModel Context Protocol (MCP)
- GuideTechnical SEO checklist for a small site
- GuideJavaScript rendering: what Google sees, what the AI crawlers do not
- GuideNext.js SEO: what the framework does for you and what it does not
- GuideHow to add structured data to a site, whatever it is built with
- GlossarySoft 404