---
description: Production-grade SEO setup; analyze project first, then ask before writing. Executor, not decision-maker.
globs:
- "**/*"
alwaysApply: false
---
# seo-pro-max
> A cross-IDE SEO skill. Works with **Claude Code**, **Cursor**, **Windsurf**, **Cline / Roo Code**, **GitHub Copilot Chat**, **Continue.dev**, **Aider**, **Zed AI**, and any agent that can read a Markdown rules file.
## Core Principle
**This skill does not decide. The user decides; this skill executes.**
- The skill MUST NOT silently pick frameworks, file locations, database tables, or admin-panel placements.
- The skill MUST present options and wait for the user before writing files.
- The skill MUST analyze the existing project before suggesting anything.
If the agent is tempted to "just pick the obvious one," it must stop and ask instead.
---
## Operating Protocol (Mandatory Order)
### Phase 0 — Read the project
Before any question, the agent must inventory:
1. Framework / runtime — look for `package.json`, `composer.json`, `requirements.txt`, `pyproject.toml`, `Gemfile`, `go.mod`, `pubspec.yaml`, `next.config.*`, `nuxt.config.*`, `astro.config.*`, `vite.config.*`, `remix.config.*`, `svelte.config.*`, `artisan`, `manage.py`.
2. Rendering model — SSR / SSG / SPA / hybrid / RSC / ISR.
3. Routing convention — file-based (`app/`, `pages/`, `src/routes/`, `src/pages/`) vs. controller-based (Laravel `routes/web.php`, Django `urls.py`, Express routers).
4. Existing SEO surface — any current `
` management, `metadata` exports, `Helmet`, `next/head`, `useSeoMeta`, blade `@section('meta')`, etc.
5. Admin panel — presence of a backoffice (`app/(admin)`, `admin/`, `/dashboard`, Filament, Nova, Django admin, Strapi, custom).
6. Database — ORM (Prisma, Drizzle, Sequelize, TypeORM, Eloquent, Django ORM), migrations folder, existing settings/options table.
7. i18n — `next-intl`, `next-i18next`, `vue-i18n`, `nuxt-i18n`, Laravel locales, Django i18n.
8. Hosting hints — `vercel.json`, `netlify.toml`, `wrangler.toml`, `Dockerfile`, nginx configs.
The agent reports findings as a short summary, then proceeds to Phase 1.
### Phase 1 — Scope intake (ask, don't assume)
The agent asks the user, one block at a time, which SEO surfaces are in scope. **Default to none.** Each surface is opt-in.
```
Which SEO surfaces should I set up?
[ ] Meta basics (title, description, keywords, language, viewport, charset, theme-color)
[ ] Indexing controls (robots meta, X-Robots-Tag, canonical)
[ ] robots.txt
[ ] XML sitemap (single / index / per-locale)
[ ] Open Graph (Facebook, LinkedIn, WhatsApp, Slack, Discord previews)
[ ] Twitter / X Cards
[ ] Schema.org JSON-LD (Organization, WebSite, Article, Product, BreadcrumbList, FAQ, LocalBusiness, ...)
[ ] Favicons & PWA manifest (favicon.ico, apple-touch-icon, manifest.webmanifest, theme-color)
[ ] Search engine verification (Google Search Console, Bing Webmaster, Yandex)
[ ] llms.txt (see note below)
[ ] Admin-panel UI for SEO data
[ ] Database schema for SEO data
[ ] Per-page overrides (slug-level metadata)
[ ] Language, i18n & hreflang (BCP 47, RTL, bidirectional hreflang, x-default)
[ ] Accessibility / WCAG 2.2 AA (image alt-text enforcement, contrast, keyboard, landmarks, heading hierarchy)
[ ] OG image generation (static / dynamic)
[ ] Core Web Vitals & page performance (LCP, CLS, INP, font, third-party scripts)
[ ] Image optimization (srcset, sizes, AVIF/WebP, lazy, CLS prevention)
[ ] URL structure & slug rules (case, separator, Unicode/Turkish policy, length)
[ ] Internal linking & orphan detection (anchor text, link depth, faceted nav)
[ ] Security & Lighthouse headers (HSTS, CSP, Referrer-Policy, Permissions-Policy)
[ ] Search engine submission & IndexNow (GSC, Bing, Yandex, Naver, Baidu)
[ ] SPA hydration & client-side meta updates
```
### Phase 2 — Per-surface drill-down
For every selected surface, the agent asks the specific questions in the matching section below. The agent **does not pick defaults** — if the user says "you choose," the agent presents 2–3 options with trade-offs and waits.
### Phase 3 — Plan & confirm
The agent prints a written plan: files to create, files to modify, migrations to add, routes to register, admin pages to add. **No writes** until the user types "go" / "ok" / explicit confirmation.
### Phase 4 — Implement
Execute the confirmed plan. One commit-sized chunk at a time. After each chunk, report path + 1-line description.
### Phase 5 — Verify
- Lint / typecheck pass.
- Build pass.
- `curl -I` the homepage and one inner route; show response headers.
- Fetch HTML, grep for required tags, report counts.
- **HTTP status verification** (Surface 2 policy):
- `curl -sI ` → expect `200`.
- `curl -sI ` → expect `200`.
- `curl -sI '/__definitely-does-not-exist-'` → expect `404`, NOT `200`. Print `HTTP/1.1 200` here as a **FAIL** with the soft-404 explanation.
- `curl -sI '/sitemap.xml'` → expect `200` and `Content-Type: application/xml`.
- `curl -sI '/robots.txt'` → expect `200` and `Content-Type: text/plain`.
- `curl -sIL ''` → expect first hop `301` to HTTPS, second hop `200`.
- `curl -sIL '/About'` (mixed case) → expect `301` to `/about`, then `200`.
- `curl -sIL '/about/'` (trailing slash variant) → expect `301` to the canonical form, then `200`.
- For any route the user marked as "retired" → expect `410` (or `301` to its replacement).
- For any auth-walled route hit without credentials → expect `401` or `302` to login, never `200` with a login form rendered to bots.
- For sitemaps and `robots.txt`: hit the public URL and show the body.
- For JSON-LD: validate JSON parses; warn if Google's required fields are missing.
- For accessibility (Surface 15): run `axe-core` against the homepage and one inner route; print a count of violations grouped by impact (critical / serious / moderate / minor). Run the alt-text auditor: grep all `` tags in the built HTML and report any without `alt=` or with auto-generated placeholder alt. Run the heading auditor: per route, print `h1` count and the first skipped-level jump (if any).
---
## Surface 1 — Meta Basics
### What it covers
``, ``, `` (see policy below), ``, ``, ``, ``, ``, ``.
### `` policy (must tell the user verbatim before emitting)
> The behaviour of search engines toward the `keywords` meta tag is **not uniform** — and that matters when the site targets non-Western markets.
>
> - **Google** — does not use it for ranking. Stated publicly since 2009. Source: https://developers.google.com/search/blog/2009/09/google-does-not-use-keywords-meta-tag
> - **Bing** — does not use it for ranking. Has stated it can be a quality signal *against* the page when stuffed.
> - **DuckDuckGo / Brave Search** — inherit Bing's signal in practice; do not use it.
> - **Yandex** — historically uses it as a weak hint; current public guidance is unclear, but the field is still parsed.
> - **Baidu** — still reads it as a signal in China.
> - **Naver (KR)** — does not use it.
>
> Decision rule the skill applies:
>
> - Site's target market is Western (US/EU/TR/LATAM) only → **omit** `keywords`. Adding it is dead weight and clutters the head.
> - Site targets RU/CIS (Yandex) or CN (Baidu) → emit `keywords` with 3–6 honest, comma-separated terms per page; never repeat the same set site-wide; never stuff.
> - The skill never fabricates keywords from the slug or body. If the editor cannot provide them, omit the tag.
### Questions to ask
1. Site name? Tagline?
2. Title strategy — `Page | Site` / `Site — Page` / page only / custom template?
3. Title max length policy? (Recommend 50–60 chars but ask.)
4. Description length policy? (Recommend 140–160 but ask.)
5. Default language code (`lang="?"`)?
6. Theme color (light / dark variants)?
7. Where do per-page overrides live — at the route file, in a CMS, in the database, or all three?
8. **`` ↔ `
` relationship.** The page `
` should mirror or paraphrase the `` (same topic, different phrasing allowed) — they are read together as the page's identity signal by Google. Confirm both are sourced from the same DB field (cheap; recommended) or kept as separate fields (more editorial control; more drift risk). The heading hierarchy rules live in Surface 15 — do not bypass them just because Surface 1 owns the ``.
9. **`keywords` meta decision** — per the policy above, confirm: target markets, emit or omit, and if emit, source of the per-page keyword list.
### Output locations (framework-dependent)
- **Next.js App Router** → `export const metadata` / `generateMetadata` in `layout.tsx` and per `page.tsx`.
- **Next.js Pages Router** → `next/head` or `next-seo`.
- **Nuxt 3** → `useSeoMeta()` / `definePageMeta()` / `app.head` in `nuxt.config`.
- **Astro** → `` slot in layout + per-page frontmatter.
- **SvelteKit** → `` in `+layout.svelte` and `+page.svelte`.
- **Remix** → `meta` export per route.
- **Laravel + Blade** → `@stack('meta')` and a `` component.
- **Django** → `{% block meta %}` in `base.html`.
- **WordPress** → `wp_head` action hook in theme.
- **Plain HTML** → `` of each template.
---
## Surface 2 — Indexing Controls
### What it covers
``, `X-Robots-Tag` HTTP header, ``, alternate hreflang, pagination (`rel="prev/next"` is deprecated by Google but ask).
### Questions
1. Which routes are `index, follow` vs. `noindex` / `nofollow`?
2. Should auth-walled pages, drafts, search-result pages, and faceted URLs be `noindex`?
3. Canonical strategy — self-canonical on every page, or canonical to a parent for paginated/faceted?
4. Are there cross-domain canonicals (mirror sites, syndicated content)?
5. Strip query strings from canonical? Which ones (`utm_*`, `ref`, `fbclid`)?
6. hreflang pairs — confirm the locale list and the URL pattern (`/en/`, `?lang=en`, `en.example.com`).
7. **HTTP status policy** — confirm the response codes the framework will emit. See "HTTP status code policy" below; non-negotiable rules apply.
### HTTP status code policy (mandatory; verified in Phase 5)
Wrong status codes are an SEO-critical defect. Google indexes by status; a `200 OK` page that visually says "not found" (a **soft 404**) gets indexed as a valid page and wastes crawl budget, then later gets demoted with a vague "Soft 404" Search Console warning. The skill enforces real status codes at the framework layer.
Required behavior per route class:
| Route class | Status | Headers / extras |
|--------------------------------------------------------------------|----------|-------------------------------------------------------------------------------|
| Existing content | `200` | `Cache-Control` set; `Content-Type` correct. |
| Unknown URL / missing resource | `404` | Real 404, never a 200 with a "not found" UI. Page should still render the branded 404 UI. |
| Permanently removed (gone for good, no replacement) | `410` | Use 410 instead of 404 when the URL is intentionally retired and you want Google to drop it faster. |
| Permanent move to new URL | `301` | `Location:` header; one hop only; never chain. |
| Temporary move / A-B test / region routing | `302` / `307` | 307 preserves method; 302 may downgrade `POST` to `GET`. |
| Permanent move that must preserve method (POST → POST) | `308` | Strict permanent redirect. |
| Auth-walled page hit unauthenticated | `401` | Not `200` with a login form rendered as the page body for bots. |
| Forbidden (logged in but not allowed) | `403` | Same rule: real status. |
| Rate-limited | `429` | `Retry-After:` header. |
| Scheduled maintenance / planned downtime | `503` | `Retry-After:` header. Never use `200` with a maintenance page — Googlebot will index the maintenance message. |
| Server error (uncaught) | `5xx` | Default framework behavior; verify it's actually 500-class, not 200. |
| Empty-state inside an existing valid route (e.g. empty search) | `200` | This is NOT a 404 — the route exists, the result set is empty. Render a clear empty-state UI; `noindex` is acceptable.|
Anti-patterns the skill refuses:
- Returning `200 OK` from a custom `not-found.tsx` / `404.vue` / `404.blade.php` / custom 404 view when the framework would have emitted 404. Confirm the status is preserved by the framework wrapper (Next App Router `notFound()`, Nuxt `throw createError({ statusCode: 404 })`, SvelteKit `error(404, ...)`, Laravel `abort(404)`, Django `Http404`, Astro `Astro.response.status = 404`).
- "Catch-all" routes that swallow unknown slugs and render a default page with 200.
- Redirect chains (`/a` → `/b` → `/c`). Collapse to one hop.
- Mixed-case duplication (`/About` and `/about` both `200`). Pick one canonical case and 301 the other.
- Trailing-slash duplication (`/about` and `/about/` both `200`). Pick one and 301 the other.
- HTTPS upgrade serving `200` on HTTP — must be `301` from `http://` to `https://`.
- `www` vs apex both `200`. One canonical, the other `301`.
Per-framework wiring the skill must verify:
- **Next.js App Router** — `notFound()` from a server component triggers `not-found.tsx`; confirm the response status is 404 in Vercel/CDN headers, not 200.
- **Next.js Pages Router** — `getStaticProps` / `getServerSideProps` returning `{ notFound: true }` produces a real 404.
- **Nuxt 3** — `throw createError({ statusCode: 404, fatal: true })`.
- **Astro** — `Astro.response.status = 404;` inside a `[...slug].astro` catch-all when the slug is unknown.
- **SvelteKit** — `import { error } from '@sveltejs/kit'; error(404, 'Not Found');`.
- **Remix** — `throw new Response(null, { status: 404 })`.
- **Laravel** — `abort(404)`; views in `resources/views/errors/404.blade.php` keep the status.
- **Django** — `raise Http404` or return `HttpResponseNotFound`.
- **Express / Fastify** — `res.status(404).render(...)`, not `res.render(...)` alone.
- **Static hosts** — Netlify `_redirects` (`/* /404.html 404`), Vercel `not-found.html` with explicit status, Cloudflare Pages `_redirects` syntax, S3+CloudFront error mapping.
Sitemap / canonical interplay:
- A URL emitting `404` / `410` / `5xx` MUST NOT appear in `sitemap.xml`. Sitemap generators (Surface 4) query live content; the skill verifies that 404'd URLs are absent.
- A URL with a `canonical` pointing elsewhere should still return `200`. If the page is retired, use `301` / `410`, not canonical-as-redirect.
- A `noindex` page should still return `200` (or the appropriate real status); `noindex` is not a substitute for `404` / `410`.
### Pagination canonical policy (Google deprecated `rel="prev/next"` in 2019)
Google announced in March 2019 that it no longer uses `` / `` as an indexing signal. Most sites still emit them incorrectly. The skill's policy:
- **Do not emit** `` / `` for the purpose of helping Google. They are harmless but dead weight. Bing still reads them weakly; emit only if the user explicitly wants Bing-paginated handling.
- Each paginated page **self-canonicals** to itself (`/blog?page=2` → canonical `/blog?page=2`). Do not canonical page 2+ back to page 1 — that hides the deeper pages from Google and they will not be crawled.
- Paginated pages should be `index, follow`, not `noindex`, unless the content is fully duplicated elsewhere.
- The "view all" pattern is acceptable if the all-results page loads in reasonable time; in that case, paginated pages canonical to the view-all URL.
- For infinite-scroll, ensure the same content is reachable via real paginated URLs (`?page=N`) — Google will not execute scroll-triggered loading.
### 404 page UX policy (the friendly-404 pattern)
Google's documentation calls helpful 404s a positive UX signal and an "important part of a healthy site." The page still returns the real `404` status (per status policy above), but its body is designed for the user:
- Branded header / footer (same as the rest of the site so the user knows they're not on a different domain).
- Clear, human heading — `
`Page not found`
` (per Surface 15 heading rules — yes, the 404 page MUST have an `
`).
- A site search box (if site has search).
- 3–6 popular / recommended links: home, top-level sections, last-published posts/products.
- Optional: report-broken-link form (`mailto:` or a real endpoint).
- Anti-pattern: meta refresh redirect to homepage. Banned by the skill.
- Anti-pattern: showing the 404 UI but returning `200` (covered above as soft-404).
- The 404 page itself is `noindex, follow`.
---
## Surface 3 — robots.txt
### Questions
1. Allow all by default, then disallow listed paths — or disallow all and allow listed?
2. Disallow patterns — `/admin`, `/api`, `/_next`, `/cdn-cgi`, `/cart`, `/checkout`, search pages?
3. Different rules per user-agent? (e.g. block `GPTBot`, `CCBot`, `ClaudeBot`, `Google-Extended`, `anthropic-ai`, `PerplexityBot`, `Bytespider` — ask explicitly, this is a content-licensing decision.)
4. Sitemap URL(s) to advertise — single, multiple, sitemap index?
5. Crawl-delay? (Google ignores it; Yandex and Bing honor it.)
### Output locations
- Static: `public/robots.txt` (Next, Vite, Nuxt, Astro), `static/robots.txt` (SvelteKit), `public/robots.txt` (Laravel).
- Dynamic: Next App Router `app/robots.ts`, Nuxt module, Express route, Django view, Laravel route returning `text/plain`.
---
## Surface 4 — XML Sitemap
### Questions
1. One sitemap or sitemap index?
2. Per-locale split? Per content type (pages / posts / products)?
3. Source of URLs — file system routes, database query, headless CMS, or hybrid?
4. Include `lastmod`, `changefreq`, `priority`? (`changefreq`/`priority` are mostly ignored by Google — ask anyway.)
5. Cache strategy — regenerate on each request, on cron, on content update (webhook / hook)?
6. Image sitemap entries? Video sitemap entries? News sitemap?
7. Max URLs per file (cap is 50,000 / 50 MB — split before that).
### Output locations
- Next App Router → `app/sitemap.ts` (returns `MetadataRoute.Sitemap`).
- Nuxt → `@nuxtjs/sitemap` or custom server route.
- Laravel → `spatie/laravel-sitemap` or a controller returning XML.
- Django → `django.contrib.sitemaps`.
- Static → generated at build time and written to `public/sitemap.xml`.
---
## Surface 5 — Open Graph
### What it covers
`og:title`, `og:description`, `og:type`, `og:url`, `og:site_name`, `og:locale`, `og:locale:alternate`, `og:image`, `og:image:width`, `og:image:height`, `og:image:alt`, `og:image:type`, plus type-specific (`article:published_time`, `article:author`, `product:price:amount`, etc.).
### Questions
1. Default OG image — single static asset or dynamic per page?
2. If dynamic: rendered at build, rendered on request (edge function / `@vercel/og` / `satori`), or pre-uploaded per record?
3. Image dimensions — `1200×630` (standard), or also `1080×1080` for Instagram/LinkedIn square previews?
4. Fall-back chain when a page lacks its own OG image?
5. `og:type` per route group (website / article / product / profile / book / video).
6. Per-locale alternates required?
7. `og:image:alt` policy — every OG image MUST emit `og:image:alt` (see Surface 15 alt-text policy). Where does the alt string come from — same DB column as the image's primary `alt`, or a separate `og_alt`?
---
## Surface 6 — Twitter / X Cards
### What it covers
`twitter:card` (`summary` / `summary_large_image` / `app` / `player`), `twitter:site`, `twitter:creator`, `twitter:title`, `twitter:description`, `twitter:image`, `twitter:image:alt`.
### Questions
1. Card type per route group?
2. `@handle` for site and for author?
3. Reuse OG image or have a separate Twitter image (the platforms now share image fields, but ask)?
4. `twitter:image:alt` — same alt-text policy as Surface 15 applies. Never omit; never auto-fill from filename.
---
## Surface 7 — Schema.org / JSON-LD
### What it covers (only enable types the user confirms)
`Organization`, `WebSite` (+ `SearchAction`), `WebPage`, `BreadcrumbList`, `Article` / `NewsArticle` / `BlogPosting`, `Product` + `Offer` + `AggregateRating` + `Review`, `QAPage`, `HowTo`, `Recipe`, `Event`, `LocalBusiness` (and subtypes), `Person`, `VideoObject`, `ImageObject`, `Course`, `JobPosting`, `SoftwareApplication`, `Dataset`, `SoftwareSourceCode`, `Book`, `MusicRecording`, `MovieSeries`, `TVSeries`, `RealEstateListing`, `MedicalEntity`, `Service`.
### Schema recommendation engine (Phase 0 output drives this)
The skill **proposes** types based on what Phase 0 detected in the project. The user always confirms. Use this matrix as the suggestion source — never silently apply.
| Detected in project | Suggest these types | Notes |
|---------------------------------------------------------------------------|---------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------|
| Any production site (root layout) | `Organization` + `WebSite` (+ `SearchAction` if search route exists) | One per site, emitted on homepage / root layout only. |
| Breadcrumb component, nested routes, or sitemap with depth > 1 | `BreadcrumbList` | Required when nav has visible breadcrumbs; Google won't show them otherwise. |
| Blog routes (`/blog`, `/posts`, `app/blog/[slug]`, CMS post collection) | `BlogPosting` (or `Article` if generic) | `headline` ≤ 110 chars; `image` ≥ 1200px wide; require `datePublished` + `dateModified` + `author`. |
| News routes / press section | `NewsArticle` | Stricter than `Article`; also register with Google News if applicable. |
| E-commerce (cart, checkout, product detail routes, product table) | `Product` + `Offer` (+ `AggregateRating` + `Review` only with real data) | Never fabricate ratings. Must include `price`, `priceCurrency`, `availability`. |
| Job listing routes / `jobs` table | `JobPosting` | `datePosted`, `validThrough`, `hiringOrganization`, `jobLocation` required. |
| Recipe site / `recipes` table | `Recipe` | Needs `recipeIngredient`, `recipeInstructions`, `cookTime`, `nutrition` for full eligibility. |
| Event listings / calendar / ticketing | `Event` (+ subtypes: `MusicEvent`, `BusinessEvent`, …) | `startDate`, `location`, `offers` recommended. |
| Physical address in footer / `contact` page / multi-branch | `LocalBusiness` (use the most specific subtype: `Restaurant`, `Dentist`, `Store`, …) | Include `geo`, `openingHoursSpecification`, `priceRange`. One per branch. |
| Course/lesson content | `Course` (+ `CourseInstance`) | Required for Google's Course rich result. |
| Video player on routes / `