@softspark/ai-toolkit 2.4.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/AGENTS.md +32 -19
  2. package/CHANGELOG.md +45 -0
  3. package/README.md +13 -12
  4. package/app/.claude-plugin/plugin.json +1 -1
  5. package/app/ARCHITECTURE.md +2 -2
  6. package/app/agents/code-reviewer.md +6 -7
  7. package/app/agents/frontend-specialist.md +33 -2
  8. package/app/agents/seo-specialist.md +1 -1
  9. package/app/personas/frontend-lead.md +48 -5
  10. package/app/skills/a11y-validate/SKILL.md +377 -0
  11. package/app/skills/a11y-validate/reference/aria-patterns.md +259 -0
  12. package/app/skills/a11y-validate/reference/eaa-compliance.md +252 -0
  13. package/app/skills/a11y-validate/reference/mobile-eaa.md +329 -0
  14. package/app/skills/a11y-validate/reference/wcag-2-1-aa.md +285 -0
  15. package/app/skills/a11y-validate/reference/wcag-2-2-aa.md +221 -0
  16. package/app/skills/a11y-validate/scripts/a11y-scanner.py +639 -0
  17. package/app/skills/clean-code/reference/python.md +3 -3
  18. package/app/skills/design-engineering/SKILL.md +2 -5
  19. package/app/skills/review/SKILL.md +30 -6
  20. package/app/skills/seo-validate/SKILL.md +460 -0
  21. package/app/skills/seo-validate/reference/core-web-vitals.md +445 -0
  22. package/app/skills/seo-validate/reference/geo-aeo-patterns.md +259 -0
  23. package/app/skills/seo-validate/reference/geo-guidelines.md +248 -0
  24. package/app/skills/seo-validate/reference/schema-types.md +465 -0
  25. package/app/skills/seo-validate/reference/spa-ssg-patterns.md +351 -0
  26. package/app/skills/seo-validate/reference/w3c-guidelines.md +289 -0
  27. package/app/skills/seo-validate/scripts/seo-scanner.py +549 -0
  28. package/bin/ai-toolkit.js +32 -5
  29. package/kb/reference/architecture-overview.md +3 -3
  30. package/kb/reference/cli-reference.md +1 -1
  31. package/kb/reference/codex-cli-compatibility.md +4 -0
  32. package/kb/reference/comparison.md +1 -1
  33. package/kb/reference/extension-api.md +2 -0
  34. package/kb/reference/skills-catalog.md +3 -1
  35. package/llms-full.txt +16 -6
  36. package/manifest.json +3 -3
  37. package/package.json +2 -2
  38. package/scripts/config_cli.py +4 -10
  39. package/scripts/config_resolver.py +23 -5
  40. package/scripts/doctor.py +76 -4
  41. package/scripts/hook_sources.py +3 -0
  42. package/scripts/inject_hook_cli.py +74 -1
  43. package/scripts/install.py +34 -3
  44. package/scripts/install_steps/ai_tools.py +79 -16
  45. package/scripts/install_steps/install_state.py +25 -0
  46. package/scripts/install_steps/markers.py +2 -1
  47. package/scripts/install_steps/project_registry.py +9 -0
  48. package/scripts/plugin.py +1 -1
  49. package/scripts/propagate_global.py +92 -0
  50. package/scripts/rule_sources.py +3 -2
  51. package/scripts/update_projects.py +7 -1
  52. package/scripts/url_fetch.py +5 -0
@@ -0,0 +1,351 @@
1
+ # SPA / SSR / SSG / CSR / ISR Patterns
2
+
3
+ Reference for `seo-validate` Category 7. How to detect rendering mode, identify SPA pitfalls, and flag crawlability issues per framework.
4
+
5
+ ## Rendering Mode Decision Tree
6
+
7
+ ```
8
+ Is content server-rendered before the client gets it?
9
+ ├── YES (server returns meaningful HTML)
10
+ │ ├── At request time? → SSR (Server-Side Rendering)
11
+ │ ├── At build time? → SSG (Static Site Generation)
12
+ │ └── Cached + revalidated periodically? → ISR (Incremental Static Regeneration)
13
+ └── NO (server returns a shell; client renders)
14
+ └── CSR (Client-Side Rendering) / SPA — CRAWLABILITY RISK
15
+ ```
16
+
17
+ For SEO:
18
+ - **SSG / SSR / ISR**: safe — crawlers see content immediately.
19
+ - **CSR / SPA**: risky — most crawlers see an empty shell.
20
+ - **Hybrid** (SSR/SSG for public routes, CSR for dashboards): check each route type.
21
+
22
+ ---
23
+
24
+ ## Why CSR SPAs Fail SEO
25
+
26
+ Modern crawlers (Googlebot) can execute JS, but:
27
+
28
+ 1. **Other engines lag**: Bing, DuckDuckGo, Yandex, Baidu have limited/no JS execution.
29
+ 2. **LLM answer engines** (ChatGPT, Perplexity, Bing Copilot) typically ingest raw HTML — they don't wait for hydration.
30
+ 3. **Social scrapers** (Facebook, LinkedIn, Slack, Twitter/X) don't execute JS — OG tags in `<head>` must be present at initial response, not injected by React.
31
+ 4. **Googlebot delays JS rendering** — two-wave indexing can delay discovery by days/weeks.
32
+ 5. **Hash routes (`/#/about`)** are fragments — not indexed as separate URLs.
33
+ 6. **Unique per-route meta is impossible without SSR/SSG/prerender** — the static `index.html` has the same `<title>` and `<meta description>` for every route.
34
+
35
+ **Bottom line**: a CSR-only SPA with public marketing content is effectively invisible to half the SEO ecosystem. This is a HIGH-severity finding.
36
+
37
+ ---
38
+
39
+ ## Framework Detection Patterns
40
+
41
+ ### Next.js
42
+
43
+ Detection: `next` in `package.json` deps.
44
+
45
+ Config: `next.config.js` / `next.config.mjs` / `next.config.ts`.
46
+
47
+ Rendering per route (App Router, Next.js 13+):
48
+ - Static by default (SSG).
49
+ - `export const dynamic = 'force-dynamic'` → SSR.
50
+ - `export const revalidate = 60` → ISR.
51
+ - `'use client'` at top of component → component runs client-side, but page can still be SSR/SSG.
52
+ - `dynamic(() => import(...), { ssr: false })` → that specific component is CSR only.
53
+
54
+ Flags:
55
+ - `next.config.*` with `output: 'export'` → forced SSG. Dynamic features (API routes, ISR, middleware) won't work.
56
+ - Page file with `'use client'` at top: page still pre-renders but becomes a client component boundary.
57
+ - `dynamic(..., { ssr: false })` wrapping LCP element → HIGH severity.
58
+
59
+ Per-route metadata:
60
+ - App Router: `export const metadata = { ... }` or `export async function generateMetadata() { ... }`.
61
+ - Pages Router: `<Head>` from `next/head`.
62
+
63
+ ### Nuxt
64
+
65
+ Detection: `nuxt` in `package.json` deps.
66
+
67
+ Config: `nuxt.config.ts`.
68
+
69
+ Rendering:
70
+ - Default: SSR.
71
+ - `ssr: false` → SPA mode (dangerous for SEO).
72
+ - `nitro.prerender.routes: [...]` → SSG routes.
73
+ - `nuxt generate` command → full SSG export.
74
+
75
+ Per-route metadata:
76
+ - `useHead({ title: ..., meta: [...] })` composable.
77
+ - `definePageMeta({ ... })`.
78
+
79
+ ### Astro
80
+
81
+ Detection: `astro` in `package.json` deps.
82
+
83
+ Config: `astro.config.mjs`.
84
+
85
+ Rendering:
86
+ - `output: 'static'` (default) → SSG.
87
+ - `output: 'server'` → SSR (with adapter like `@astrojs/node`, `@astrojs/vercel`).
88
+ - `output: 'hybrid'` → mostly static, opt-in SSR per page.
89
+
90
+ Component-level:
91
+ - `client:load` / `client:idle` / `client:visible` → islands hydrate on client, but initial HTML is rendered.
92
+ - `client:only="react"` → component is CSR only. Flag when on hero/above-the-fold content.
93
+
94
+ Per-page metadata: direct `<meta>` tags in layout or frontmatter.
95
+
96
+ ### Gatsby
97
+
98
+ Detection: `gatsby` in `package.json` deps.
99
+
100
+ Config: `gatsby-config.js`.
101
+
102
+ Rendering: SSG by default. `gatsby build` creates static HTML per page.
103
+
104
+ Modern Gatsby (4+) supports SSR (`getServerData`) and DSG (Deferred Static Generation).
105
+
106
+ Per-page metadata:
107
+ - `react-helmet` or `gatsby-plugin-react-helmet`.
108
+ - Gatsby Head API: `export const Head = () => (<><title>...</title></>)`.
109
+
110
+ ### SvelteKit
111
+
112
+ Detection: `@sveltejs/kit` in `package.json` deps.
113
+
114
+ Config: `svelte.config.js`.
115
+
116
+ Rendering per route:
117
+ - `export const ssr = true` (default) — SSR.
118
+ - `export const ssr = false` — CSR only.
119
+ - `export const prerender = true` — SSG for that route.
120
+ - `export const prerender = 'auto'` — prerender unless dynamic data.
121
+
122
+ Adapter determines deployment:
123
+ - `@sveltejs/adapter-static` → pure SSG.
124
+ - `@sveltejs/adapter-node` → Node server, SSR.
125
+ - `@sveltejs/adapter-vercel` / `@sveltejs/adapter-cloudflare` → serverless.
126
+
127
+ Per-route metadata: `<svelte:head>` block.
128
+
129
+ ### Remix
130
+
131
+ Detection: `@remix-run/*` in `package.json` deps.
132
+
133
+ Rendering: SSR by default. Loader functions run server-side, deliver HTML.
134
+
135
+ Per-route metadata: `export const meta: MetaFunction = () => [...]`.
136
+
137
+ No built-in SSG — always SSR.
138
+
139
+ ### Angular
140
+
141
+ Detection: `@angular/core` in `package.json` deps.
142
+
143
+ Rendering:
144
+ - Default: CSR only → HIGH-severity for content sites.
145
+ - With `@angular/ssr` (Angular 17+) or `@nguniversal/*` (older): SSR.
146
+
147
+ Check `angular.json` for `ssr` builder config.
148
+
149
+ Per-page metadata: inject `Meta` and `Title` services from `@angular/platform-browser`.
150
+
151
+ ### Vue (non-Nuxt SPA)
152
+
153
+ Detection: `vue` in deps without `nuxt`.
154
+
155
+ Rendering: CSR unless using:
156
+ - Vite SSR (`vite-plugin-ssr` → now `vike`).
157
+ - Vue Storefront or similar custom SSR.
158
+ - `@vue/server-renderer` explicitly.
159
+
160
+ Default Vue SPA from `create-vue` / `@vue/cli` is CSR-only → HIGH severity for content sites.
161
+
162
+ ### React SPA (Vite)
163
+
164
+ Detection: `react` + `vite` in deps, no `next`, no `@remix-run/*`.
165
+
166
+ Rendering: CSR only unless:
167
+ - `vike` (formerly vite-plugin-ssr) → SSR.
168
+ - `vite-plugin-prerender` / `@preact/preset-vite` with prerender option → build-time prerender.
169
+ - `@tanstack/router` with SSR plugin.
170
+
171
+ Default Vite + React is CSR → HIGH severity for content sites.
172
+
173
+ ### Create React App (CRA)
174
+
175
+ Detection: `react-scripts` in deps.
176
+
177
+ Rendering: CSR only.
178
+
179
+ Mitigations (rare — CRA is deprecated):
180
+ - `react-snap` plugin → build-time prerender crawl.
181
+
182
+ Default CRA is CSR → HIGH severity. Also: recommend migrating to Next.js / Remix / Vite+Vike.
183
+
184
+ ---
185
+
186
+ ## Detection Signals
187
+
188
+ ### Entry HTML inspection
189
+
190
+ Read `public/index.html`, `index.html`, `src/app.html`, or framework entry:
191
+
192
+ **Empty mount point (SPA signal)**:
193
+ ```html
194
+ <body>
195
+ <div id="root"></div>
196
+ <script src="/main.js"></script>
197
+ </body>
198
+ ```
199
+
200
+ **Prerendered content (SSG/SSR signal)**:
201
+ ```html
202
+ <body>
203
+ <div id="root">
204
+ <main>
205
+ <h1>Actual content here</h1>
206
+ ...
207
+ </main>
208
+ </div>
209
+ <script src="/main.js"></script>
210
+ </body>
211
+ ```
212
+
213
+ Rule of thumb: if `<div id="root">` / `<div id="app">` / `<div id="__next">` contains only whitespace or tiny loader markup, it's a SPA shell — flag.
214
+
215
+ ### Runtime-only meta detection
216
+
217
+ Look for:
218
+ - `react-helmet-async` usage without `HelmetProvider` in SSR server entry.
219
+ - `vue-meta` / `@vueuse/head` without SSR plugin.
220
+ - Direct `document.title = ...` / `document.querySelector('meta[name=description]').content = ...` writes.
221
+
222
+ If meta is only set at runtime, it won't be in the initial response → crawlers miss it.
223
+
224
+ ### HashRouter detection
225
+
226
+ ```jsx
227
+ // React Router HashRouter
228
+ import { HashRouter } from 'react-router-dom';
229
+ <HashRouter>...</HashRouter>
230
+
231
+ // Vue Router hash mode
232
+ createRouter({ history: createWebHashHistory(), ... })
233
+ ```
234
+
235
+ Hash routes like `/#/about` are fragments — Google does NOT index them as separate URLs.
236
+
237
+ Always use `BrowserRouter` / `createWebHistory()` for public routes.
238
+
239
+ ### Hydration mismatch signals
240
+
241
+ ```jsx
242
+ // Red flag: different output based on environment
243
+ function Component() {
244
+ if (typeof window !== 'undefined') {
245
+ return <ClientVariant />;
246
+ }
247
+ return <ServerVariant />;
248
+ }
249
+
250
+ // Red flag: overuse of suppressHydrationWarning
251
+ <div suppressHydrationWarning>
252
+ {Math.random()} {/* Masking a real bug */}
253
+ </div>
254
+ ```
255
+
256
+ The skill flags `>3` occurrences of `suppressHydrationWarning` in the codebase as a heuristic WARN.
257
+
258
+ ---
259
+
260
+ ## Prerendering Strategies for SPAs
261
+
262
+ When migration to Next.js / Remix isn't feasible, SPA prerendering is the fallback:
263
+
264
+ ### react-snap (CRA, Vite-React)
265
+
266
+ Runs a headless browser against the SPA at build time, snapshots the DOM per route, writes static HTML.
267
+
268
+ ```json
269
+ // package.json
270
+ {
271
+ "scripts": {
272
+ "postbuild": "react-snap"
273
+ },
274
+ "reactSnap": {
275
+ "source": "build",
276
+ "include": ["/", "/about", "/pricing"]
277
+ }
278
+ }
279
+ ```
280
+
281
+ Works for: static content sites. Breaks on: dynamic/authenticated routes.
282
+
283
+ ### vite-plugin-prerender
284
+
285
+ Same approach for Vite projects.
286
+
287
+ ### vike (formerly vite-plugin-ssr)
288
+
289
+ Full SSR for Vite projects. More invasive than prerender plugins but more capable.
290
+
291
+ ### prerender-spa-plugin (Webpack)
292
+
293
+ Older Webpack-based solution for Vue/React SPAs.
294
+
295
+ ### Dynamic rendering (legacy — Google deprecated)
296
+
297
+ `prerender.io`, `rendertron`: intercept requests from crawler user agents, serve prerendered HTML.
298
+
299
+ Google's recommendation as of 2024: use SSR/SSG instead. Dynamic rendering is a fallback, not a solution — detected as INFO severity, not HIGH.
300
+
301
+ ---
302
+
303
+ ## Quick Detection Reference (for seo-validate Category 7)
304
+
305
+ | Check | Signal | Severity |
306
+ |-------|--------|----------|
307
+ | `public/index.html` mount div empty | CSR confirmed | HIGH (if content site) |
308
+ | `package.json` has `react-scripts` + no `react-snap` | CRA SPA no prerender | HIGH |
309
+ | `react` + `vite` + no `vike`/`vite-plugin-ssr`/prerender | Vite SPA no prerender | HIGH |
310
+ | `@angular/core` + no `@angular/ssr` + no `@nguniversal/*` | Angular SPA no SSR | HIGH |
311
+ | `vue` + no `nuxt` + no SSR plugin | Vue SPA no SSR | HIGH |
312
+ | `HashRouter` or `createWebHashHistory` on public route | Hash routing | HIGH |
313
+ | `dynamic(..., { ssr: false })` on hero component | LCP blocked + CSR | HIGH |
314
+ | `ssr: false` in `nuxt.config` | Nuxt SPA mode | WARN |
315
+ | `export const ssr = false` in SvelteKit route | Route is CSR | WARN |
316
+ | `client:only` on Astro hero component | Component is CSR | WARN |
317
+ | `'use client'` at top of Next.js page with no server-side logic | Forced CSR boundary | WARN |
318
+ | `react-helmet-async` without `HelmetProvider` in server entry | Meta only client-side | HIGH |
319
+ | `document.title = ...` in component code | Runtime-only title | HIGH |
320
+ | `suppressHydrationWarning` ≥4 occurrences | Likely masked mismatch | WARN |
321
+ | `prerender.io` / `rendertron` config | Legacy dynamic rendering | INFO |
322
+
323
+ ---
324
+
325
+ ## When to NOT Flag
326
+
327
+ Not every SPA needs SEO. The skill should NOT flag rendering-mode HIGH for:
328
+
329
+ - Auth-gated dashboards, admin panels (no public routes to index).
330
+ - Internal tools, intranet apps.
331
+ - Mobile app backends with no web UI.
332
+ - Electron/desktop apps.
333
+
334
+ Heuristic: if no public routes exist (check `robots.txt`, presence of marketing pages, landing page in a layout), downgrade SPA findings to INFO.
335
+
336
+ For mixed cases (Next.js app with marketing routes + dashboard routes), flag per-route: marketing routes should be SSG/SSR; dashboard can be CSR-heavy.
337
+
338
+ ---
339
+
340
+ ## References
341
+
342
+ - Google Search Central on JavaScript SEO: https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics
343
+ - web.dev rendering patterns: https://web.dev/rendering-on-the-web/
344
+ - Next.js rendering: https://nextjs.org/docs/app/building-your-application/rendering
345
+ - Nuxt rendering: https://nuxt.com/docs/guide/concepts/rendering
346
+ - Astro rendering: https://docs.astro.build/en/guides/rendering/
347
+ - Gatsby rendering: https://www.gatsbyjs.com/docs/conceptual/rendering-options/
348
+ - SvelteKit rendering: https://kit.svelte.dev/docs/page-options
349
+ - Angular SSR: https://angular.dev/guide/ssr
350
+ - vike (vite-plugin-ssr): https://vike.dev/
351
+ - react-snap: https://github.com/stereobooster/react-snap
@@ -0,0 +1,289 @@
1
+ # W3C HTML Guidelines (SEO-Relevant Subset)
2
+
3
+ Reference for `seo-validate` Category 1 (HTML Semantics & W3C) and Category 2 (Meta & Open Graph).
4
+
5
+ Sources: W3C HTML Living Standard (HTML5), WCAG 2.2, IETF RFC 5646 (BCP 47), Open Graph Protocol, Twitter Card spec.
6
+
7
+ ## Document Structure
8
+
9
+ ### DOCTYPE
10
+
11
+ HTML5 §8.1.1 — the document MUST begin with `<!DOCTYPE html>` (case-insensitive). Missing doctype triggers quirks mode in older browsers and breaks modern CSS behavior — effectively a HIGH-severity issue.
12
+
13
+ ```html
14
+ <!DOCTYPE html>
15
+ <html lang="en">
16
+ <head>...</head>
17
+ <body>...</body>
18
+ </html>
19
+ ```
20
+
21
+ ### lang attribute
22
+
23
+ HTML5 §3.2.6 — the `<html>` element SHOULD have a `lang` attribute using a BCP 47 language tag.
24
+
25
+ - Required for screen reader pronunciation (WCAG 3.1.1).
26
+ - Signals primary language to search engines.
27
+ - Feeds into hreflang correctness checks.
28
+
29
+ Valid examples:
30
+ - `lang="en"` — English (generic)
31
+ - `lang="en-US"` — English (United States)
32
+ - `lang="pl"` — Polish
33
+ - `lang="zh-Hant"` — Chinese (Traditional)
34
+
35
+ Invalid examples (flag these):
36
+ - `lang="en_US"` — underscore, not hyphen
37
+ - `lang="english"` — not an ISO code
38
+ - `lang="EN"` — case irrelevant per RFC but inconsistent; prefer lowercase
39
+
40
+ ### charset
41
+
42
+ HTML5 §4.2.5.5 — `<meta charset="utf-8">` MUST appear in `<head>` as early as possible, within the first 1024 bytes. UTF-8 is the only accepted encoding for new content.
43
+
44
+ ```html
45
+ <head>
46
+ <meta charset="utf-8">
47
+ <!-- other meta tags follow -->
48
+ </head>
49
+ ```
50
+
51
+ ### viewport
52
+
53
+ Not formally W3C but required for mobile-first indexing:
54
+
55
+ ```html
56
+ <meta name="viewport" content="width=device-width, initial-scale=1">
57
+ ```
58
+
59
+ Missing viewport = mobile rendering breaks = Google ranks the page lower.
60
+
61
+ ## Document Outline
62
+
63
+ ### Heading hierarchy
64
+
65
+ HTML5 §4.3.6 defines heading semantics. Current best practice:
66
+
67
+ - **Exactly one `<h1>`** per page/route component.
68
+ - **Sequential levels**: `h1` → `h2` → `h3`, never skip (e.g., `h1` → `h3`).
69
+ - **Semantic, not decorative**: heading level should reflect content hierarchy, not visual size. Use CSS for styling.
70
+
71
+ ### Landmarks
72
+
73
+ HTML5 §4.3 + ARIA Landmarks. Every page should have at minimum:
74
+
75
+ - `<header>` — site/page header
76
+ - `<nav>` — primary navigation
77
+ - `<main>` — main content (exactly one per page)
78
+ - `<footer>` — site/page footer
79
+
80
+ Optionally: `<aside>`, `<section>` (requires accessible name), `<article>`.
81
+
82
+ Using `<div>` for these is a WARN — semantic HTML aids both screen readers and AI/LLM parsers.
83
+
84
+ ## Meta Tags
85
+
86
+ ### Title
87
+
88
+ HTML5 §4.2.2. Required element inside `<head>`.
89
+
90
+ - Recommended length: **50–60 characters** including brand/separator. Google truncates at ~600 pixels (~50–60 chars).
91
+ - Unique per page.
92
+ - Format: `Primary Keyword - Brand` or `Primary Keyword | Secondary | Brand`.
93
+ - Avoid keyword stuffing.
94
+
95
+ ```html
96
+ <title>SEO Audit Tool - Free Analyzer | ExampleCo</title>
97
+ ```
98
+
99
+ ### Description
100
+
101
+ HTML `<meta name="description">`.
102
+
103
+ - Recommended length: **150–160 characters**. Google truncates at ~155 chars (mobile narrower).
104
+ - Unique per page.
105
+ - Include primary keyword naturally.
106
+ - Write as a SERP call-to-action.
107
+
108
+ ```html
109
+ <meta name="description" content="Free SEO audit tool. Instantly analyze your site's title tags, meta descriptions, and Core Web Vitals. Fix issues before Google indexes them.">
110
+ ```
111
+
112
+ ### Canonical
113
+
114
+ HTML5 §4.2.4 + Google Search Central canonicalization.
115
+
116
+ - Required on indexable pages.
117
+ - Must be an absolute URL.
118
+ - Self-referencing canonical on canonical version.
119
+ - Points to preferred version when multiple URLs return the same content (tracking params, pagination, sort orders).
120
+
121
+ ```html
122
+ <link rel="canonical" href="https://example.com/blog/post-slug">
123
+ ```
124
+
125
+ #### Canonical + URL parameters
126
+
127
+ Query parameters create duplicate URLs unless handled explicitly. After Google deprecated the Search Console URL Parameters tool (April 2022), the only remaining signals are `rel="canonical"`, `robots.txt`, and `<meta name="robots">`.
128
+
129
+ **Tracking parameters** (`utm_source`, `utm_medium`, `utm_campaign`, `gclid`, `fbclid`, `msclkid`, `ref`):
130
+
131
+ Every variant must canonical to the clean URL:
132
+
133
+ ```html
134
+ <!-- User lands on: https://example.com/page?utm_source=newsletter&utm_campaign=apr26 -->
135
+ <link rel="canonical" href="https://example.com/page">
136
+ ```
137
+
138
+ **Faceted navigation** (filters, sort, pagination on category/product listing pages):
139
+
140
+ ```html
141
+ <!-- URL: /products?category=shoes&color=red&sort=price-asc&page=2 -->
142
+ <!-- Strategy A: canonical to clean base, let Google dedup -->
143
+ <link rel="canonical" href="https://example.com/products">
144
+
145
+ <!-- Strategy B: canonical to first page + rel=prev/next (deprecated as signal but still used by some engines) -->
146
+ <link rel="canonical" href="https://example.com/products?category=shoes">
147
+ ```
148
+
149
+ For facet combinations that are NOT genuinely different content (e.g., `?sort=name` vs `?sort=price`), always canonical to the clean URL. For facets that ARE different content (e.g., `/products?category=shoes` is a distinct shoe listing), the facet URL itself is canonical.
150
+
151
+ **Search result pages (SRPs)**:
152
+
153
+ Internal search results are thin, user-specific content. Google's Search Essentials explicitly lists SRPs as content to keep out of the index.
154
+
155
+ Two valid strategies:
156
+
157
+ ```html
158
+ <!-- Strategy A: noindex, allow crawl (discovers outbound links) -->
159
+ <meta name="robots" content="noindex, follow">
160
+ <link rel="canonical" href="https://example.com/search">
161
+ ```
162
+
163
+ ```
164
+ # Strategy B: robots.txt Disallow — blocks crawl entirely
165
+ User-agent: *
166
+ Disallow: /search?*
167
+ Disallow: /*?q=*
168
+ ```
169
+
170
+ Never combine Disallow + noindex — a disallowed URL will never be fetched, so the noindex directive is never read.
171
+
172
+ **Pagination**:
173
+
174
+ Modern Google treats paginated content as individual URLs. Each page self-canonicals:
175
+
176
+ ```html
177
+ <!-- /blog?page=2 -->
178
+ <link rel="canonical" href="https://example.com/blog?page=2">
179
+ ```
180
+
181
+ `rel="prev"` and `rel="next"` are deprecated as a Google signal (2019) but remain valid HTML and are used by other engines/accessibility tools.
182
+
183
+ ### Robots
184
+
185
+ Controls indexing and following per-page.
186
+
187
+ ```html
188
+ <!-- Indexable, links followed (default — usually omitted) -->
189
+ <meta name="robots" content="index, follow">
190
+
191
+ <!-- Block indexing (confirm intent!) -->
192
+ <meta name="robots" content="noindex, nofollow">
193
+
194
+ <!-- Common production mistake: leftover from dev -->
195
+ <meta name="robots" content="noindex">
196
+ ```
197
+
198
+ `noindex` on production routes is a HIGH-severity finding unless explicitly intentional (admin pages, thank-you pages, search-results pages).
199
+
200
+ ## Open Graph Protocol
201
+
202
+ `https://ogp.me/` — used by Facebook, LinkedIn, Slack, Discord, Teams, WhatsApp.
203
+
204
+ Required for rich social cards:
205
+
206
+ ```html
207
+ <meta property="og:title" content="Page Title">
208
+ <meta property="og:description" content="Page description">
209
+ <meta property="og:image" content="https://example.com/og-image.jpg"> <!-- ABSOLUTE URL -->
210
+ <meta property="og:url" content="https://example.com/page">
211
+ <meta property="og:type" content="article"> <!-- or website, product, profile -->
212
+ <meta property="og:site_name" content="ExampleCo">
213
+ ```
214
+
215
+ Image specs:
216
+ - Recommended: 1200×630 px (1.91:1 aspect ratio).
217
+ - Maximum: 8 MB.
218
+ - JPEG or PNG. WebP has limited platform support for OG.
219
+
220
+ Relative URLs for `og:image` are a definitive WARN — many scrapers don't resolve them.
221
+
222
+ ## Twitter Card
223
+
224
+ ```html
225
+ <meta name="twitter:card" content="summary_large_image">
226
+ <meta name="twitter:site" content="@example">
227
+ <meta name="twitter:creator" content="@author">
228
+ <meta name="twitter:title" content="Page Title">
229
+ <meta name="twitter:description" content="Page description">
230
+ <meta name="twitter:image" content="https://example.com/twitter-image.jpg">
231
+ ```
232
+
233
+ When OG tags are present, Twitter falls back to them for most fields — but `twitter:card` is still required.
234
+
235
+ ## hreflang
236
+
237
+ RFC 5646 (BCP 47) language tags + ISO 3166-1 alpha-2 region codes.
238
+
239
+ ### Format
240
+
241
+ ```
242
+ <language-subtag>[-<region-subtag>]
243
+ ```
244
+
245
+ Valid:
246
+ - `en`, `en-US`, `en-GB`, `pl`, `pt-BR`, `zh-Hant`, `sr-Cyrl`
247
+
248
+ Invalid:
249
+ - `en_US` (underscore)
250
+ - `EN-us` (case inconsistent — prefer lowercase language, uppercase region)
251
+ - `english` (not a code)
252
+ - `uk` is **Ukrainian**, not "UK English" (common confusion — `en-GB` is correct for British English)
253
+
254
+ ### Implementation
255
+
256
+ Every locale version MUST include hreflang for every other locale, including itself, plus `x-default`:
257
+
258
+ ```html
259
+ <link rel="alternate" hreflang="en-US" href="https://example.com/en-us/page">
260
+ <link rel="alternate" hreflang="en-GB" href="https://example.com/en-gb/page">
261
+ <link rel="alternate" hreflang="pl" href="https://example.com/pl/page">
262
+ <link rel="alternate" hreflang="x-default" href="https://example.com/en-us/page">
263
+ ```
264
+
265
+ ### Bidirectionality
266
+
267
+ If page A links to page B via hreflang, page B MUST link back to page A. Broken bidirectionality is ignored by Google — a HIGH-severity issue.
268
+
269
+ ### x-default
270
+
271
+ Fallback for users whose language doesn't match any listed locale. Usually points to the default/main version (often English).
272
+
273
+ ## Validation
274
+
275
+ The W3C provides an online validator: https://validator.w3.org/ — but the `seo-validate` skill targets static source analysis, not rendered HTML validation.
276
+
277
+ For runtime validation, use:
278
+ - `html-validate` (npm)
279
+ - `htmlhint`
280
+ - Built-in linters in Next.js, Nuxt, Astro
281
+
282
+ ## References
283
+
284
+ - HTML Living Standard: https://html.spec.whatwg.org/
285
+ - WCAG 2.2: https://www.w3.org/TR/WCAG22/
286
+ - BCP 47 (RFC 5646): https://datatracker.ietf.org/doc/html/rfc5646
287
+ - ISO 3166-1: https://www.iso.org/iso-3166-country-codes.html
288
+ - Open Graph Protocol: https://ogp.me/
289
+ - Google Search Central (title/description/canonical): https://developers.google.com/search/docs