@ctxr/skill-frontend-excellence 0.1.1

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.
@@ -0,0 +1,422 @@
1
+ # SEO Playbook
2
+
3
+ On-page and technical SEO that hits Lighthouse 100 and earns rankings. For AI search optimization (AEO/GEO/LLMO) and programmatic SEO at scale, treat this as the foundation; specialized strategies build on top.
4
+
5
+ ## Priority Order
6
+
7
+ 1. **Crawlability and indexation.** If Google can't find or can't index it, nothing else matters.
8
+ 2. **Technical foundations.** Speed, mobile, HTTPS, structure.
9
+ 3. **On-page optimization.** Title, description, headings, content, internal links.
10
+ 4. **Content quality.** E-E-A-T, depth, intent match.
11
+ 5. **Authority and links.** Off-page; outside the scope of this skill.
12
+
13
+ ## Indexability
14
+
15
+ ### Robots.txt
16
+
17
+ Lives at `/robots.txt`. Returns 200. Format:
18
+
19
+ ```
20
+ User-agent: *
21
+ Allow: /
22
+ Disallow: /api/
23
+ Disallow: /private/
24
+
25
+ Sitemap: https://example.com/sitemap.xml
26
+ ```
27
+
28
+ Common mistakes:
29
+
30
+ - Disallowing `/_next/` or `/static/` (blocks asset crawl, hurts rendering)
31
+ - Disallowing the entire site by accident (`Disallow: /`)
32
+ - Forgetting the `Sitemap:` line
33
+
34
+ ### XML Sitemap
35
+
36
+ Lives at `/sitemap.xml` (or `/sitemap-index.xml` for large sites with sub-sitemaps). Lists only:
37
+
38
+ - Canonical URLs
39
+ - 200-status URLs
40
+ - Indexable URLs (no `noindex`)
41
+ - URLs the site actually owns (no third parties)
42
+
43
+ For large sites, split into sub-sitemaps of 50,000 URLs each, referenced from `sitemap-index.xml`.
44
+
45
+ ```xml
46
+ <?xml version="1.0" encoding="UTF-8"?>
47
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
48
+ <url>
49
+ <loc>https://example.com/</loc>
50
+ <lastmod>2026-05-08</lastmod>
51
+ <changefreq>weekly</changefreq>
52
+ <priority>1.0</priority>
53
+ </url>
54
+ </urlset>
55
+ ```
56
+
57
+ `changefreq` and `priority` are advisory; Google mostly ignores them. `lastmod` is honored when accurate.
58
+
59
+ Submit to Search Console. Verify it's accessible from `robots.txt`.
60
+
61
+ ### Meta Robots
62
+
63
+ Per page:
64
+
65
+ ```html
66
+ <meta name="robots" content="index, follow" />
67
+ ```
68
+
69
+ Use `noindex, nofollow` on:
70
+
71
+ - Pages behind authentication
72
+ - Internal search result pages with no indexable value
73
+ - Duplicate or near-duplicate pages
74
+ - Thin content (tags, archives, pagination beyond page 1 in some cases)
75
+ - Print stylesheets and embed-only pages
76
+
77
+ X-Robots-Tag HTTP header is equivalent and works for non-HTML resources (PDFs, images):
78
+
79
+ ```
80
+ X-Robots-Tag: noindex, nofollow
81
+ ```
82
+
83
+ ### Canonical Tag
84
+
85
+ Every indexable page has a self-referencing canonical:
86
+
87
+ ```html
88
+ <link rel="canonical" href="https://example.com/pricing" />
89
+ ```
90
+
91
+ Rules:
92
+
93
+ - The URL is absolute, including protocol and domain.
94
+ - Lowercased, with the consistent trailing-slash convention you've chosen.
95
+ - Without tracking params (utm, fbclid, gclid).
96
+ - For paginated lists: each page canonicals to itself; do not canonicalize page 2 to page 1.
97
+ - For filter/facet variants of a category page: canonicalize variants to the unfiltered root only when the filter doesn't change indexable content.
98
+
99
+ ## Titles and Descriptions
100
+
101
+ ### Title Tag
102
+
103
+ ```html
104
+ <title>Page topic - Brand</title>
105
+ ```
106
+
107
+ Rules:
108
+
109
+ - Unique per page across the entire site.
110
+ - 50-60 characters before truncation in the SERP.
111
+ - Primary intent term near the beginning when natural.
112
+ - Brand name at the end (separator: pipe `|` or hyphen). Some sources advise omitting the brand on home/category pages where the SERP already attaches it.
113
+ - No keyword stuffing, no all caps, no emoji unless the brand allows.
114
+
115
+ Common failures:
116
+
117
+ - Same title on every page.
118
+ - Title longer than 60 chars (truncated).
119
+ - Title that reads like a slug (`pricing-page-brand`).
120
+ - Auto-generated `Home | Brand` on every page.
121
+
122
+ ### Meta Description
123
+
124
+ ```html
125
+ <meta name="description" content="One sentence stating what this page covers and the value the reader gets, in plain language, with one explicit or implicit call to action." />
126
+ ```
127
+
128
+ Rules:
129
+
130
+ - Unique per page.
131
+ - 140-160 characters.
132
+ - Primary intent term used naturally.
133
+ - Clear value proposition.
134
+ - Implicit or explicit call to action.
135
+
136
+ ## Heading Structure
137
+
138
+ - One `<h1>` per page. The H1 is the primary intent.
139
+ - Sequential `<h2>` -> `<h3>` -> ... no skipped levels.
140
+ - Headings describe content, not styling.
141
+ - Sections labeled by their heading via `aria-labelledby` for accessibility (also reinforces structure for some bots).
142
+
143
+ ```html
144
+ <main>
145
+ <h1>Page primary intent (one H1)</h1>
146
+ <section aria-labelledby="section-1">
147
+ <h2 id="section-1">First section heading</h2>
148
+ ...
149
+ </section>
150
+ <section aria-labelledby="section-2">
151
+ <h2 id="section-2">Second section heading</h2>
152
+ <h3>Sub-section</h3>
153
+ ...
154
+ </section>
155
+ </main>
156
+ ```
157
+
158
+ ## URL Structure
159
+
160
+ - Lowercase.
161
+ - Hyphen-separated.
162
+ - Descriptive, keyword-rich where natural.
163
+ - No tracking params in the canonical.
164
+ - Consistent trailing-slash policy across the site.
165
+ - Short. Prefer `/topic/specific-slug` over `/2026/05/08/post-id-123`.
166
+ - Stable. Once published, URLs don't change. If a URL must change, return 301 from the old to the new.
167
+
168
+ ## Open Graph and Twitter Cards
169
+
170
+ Every public page exposes:
171
+
172
+ ```html
173
+ <meta property="og:type" content="website" />
174
+ <meta property="og:title" content="Page topic - Brand" />
175
+ <meta property="og:description" content="..." />
176
+ <meta property="og:url" content="https://example.com/page-path" />
177
+ <meta property="og:image" content="https://example.com/og/page.png" />
178
+ <meta property="og:image:width" content="1200" />
179
+ <meta property="og:image:height" content="630" />
180
+ <meta property="og:site_name" content="Brand" />
181
+ <meta property="og:locale" content="en_US" />
182
+
183
+ <meta name="twitter:card" content="summary_large_image" />
184
+ <meta name="twitter:title" content="..." />
185
+ <meta name="twitter:description" content="..." />
186
+ <meta name="twitter:image" content="https://example.com/og/page.png" />
187
+ <meta name="twitter:site" content="@brand" />
188
+ ```
189
+
190
+ Image rules:
191
+
192
+ - 1200x630 PNG or JPEG.
193
+ - Under 5 MB. Aim for under 500 KB.
194
+ - Text in the image legible at thumbnail size.
195
+ - Avoid rendering critical info only in the image; the description should stand on its own.
196
+
197
+ ## Structured Data (JSON-LD)
198
+
199
+ Add to `<head>`. One `<script type="application/ld+json">` block per type, or a graph block with multiple types.
200
+
201
+ ### Organization (site-wide, on home and about)
202
+
203
+ ```html
204
+ <script type="application/ld+json">
205
+ {
206
+ "@context": "https://schema.org",
207
+ "@type": "Organization",
208
+ "name": "Brand",
209
+ "url": "https://example.com",
210
+ "logo": "https://example.com/logo.png",
211
+ "sameAs": [
212
+ "https://twitter.com/brand",
213
+ "https://github.com/brand",
214
+ "https://linkedin.com/company/brand"
215
+ ]
216
+ }
217
+ </script>
218
+ ```
219
+
220
+ ### WebSite with SearchAction (home only)
221
+
222
+ ```html
223
+ <script type="application/ld+json">
224
+ {
225
+ "@context": "https://schema.org",
226
+ "@type": "WebSite",
227
+ "url": "https://example.com",
228
+ "potentialAction": {
229
+ "@type": "SearchAction",
230
+ "target": "https://example.com/search?q={search_term_string}",
231
+ "query-input": "required name=search_term_string"
232
+ }
233
+ }
234
+ </script>
235
+ ```
236
+
237
+ ### BreadcrumbList (every interior page)
238
+
239
+ ```html
240
+ <script type="application/ld+json">
241
+ {
242
+ "@context": "https://schema.org",
243
+ "@type": "BreadcrumbList",
244
+ "itemListElement": [
245
+ { "@type": "ListItem", "position": 1, "name": "Home", "item": "https://example.com" },
246
+ { "@type": "ListItem", "position": 2, "name": "Section", "item": "https://example.com/section" },
247
+ { "@type": "ListItem", "position": 3, "name": "Current page" }
248
+ ]
249
+ }
250
+ </script>
251
+ ```
252
+
253
+ ### Article (blog posts, news)
254
+
255
+ ```html
256
+ <script type="application/ld+json">
257
+ {
258
+ "@context": "https://schema.org",
259
+ "@type": "Article",
260
+ "headline": "...",
261
+ "description": "...",
262
+ "image": "https://...",
263
+ "datePublished": "2026-05-08",
264
+ "dateModified": "2026-05-09",
265
+ "author": { "@type": "Person", "name": "...", "url": "..." },
266
+ "publisher": { "@type": "Organization", "name": "Brand", "logo": { "@type": "ImageObject", "url": "..." } },
267
+ "mainEntityOfPage": "https://..."
268
+ }
269
+ </script>
270
+ ```
271
+
272
+ ### FAQPage (FAQ sections)
273
+
274
+ ```html
275
+ <script type="application/ld+json">
276
+ {
277
+ "@context": "https://schema.org",
278
+ "@type": "FAQPage",
279
+ "mainEntity": [
280
+ {
281
+ "@type": "Question",
282
+ "name": "Question text exactly as displayed?",
283
+ "acceptedAnswer": { "@type": "Answer", "text": "Answer text exactly as displayed." }
284
+ }
285
+ ]
286
+ }
287
+ </script>
288
+ ```
289
+
290
+ Only use FAQPage when the FAQ is genuinely on the page and visible to users.
291
+
292
+ ### Product / SoftwareApplication / HowTo / Recipe / Event / etc.
293
+
294
+ Use the schema.org type that best matches the page content. Validate every JSON-LD with the Rich Results Test before declaring it complete.
295
+
296
+ ### Validation
297
+
298
+ - **Rich Results Test** (https://search.google.com/test/rich-results) renders JS, finds JSON-LD, and shows what Google can extract.
299
+ - **Schema.org Validator** (https://validator.schema.org/) for structural validation.
300
+ - The Lighthouse SEO category does not validate JSON-LD content; use the dedicated tools.
301
+
302
+ ## Image SEO
303
+
304
+ - Descriptive file names: `topic-comparison-chart.png`, not `IMG_2034.png`.
305
+ - `alt` text describing the image (for accessibility AND SEO).
306
+ - Compress to AVIF/WebP per [performance.md](performance.md).
307
+ - For images that are themselves content (infographics, charts), provide a long-form description nearby.
308
+ - For decorative images, `alt=""`. Search engines understand this signal.
309
+
310
+ ## Internal Linking
311
+
312
+ - Every important page should be reachable within 3 clicks of the home page.
313
+ - Use descriptive anchor text. Not "click here", "read more", "learn more". Use the destination's title or a topical phrase.
314
+ - Avoid orphan pages (no internal links pointing to them).
315
+ - Avoid over-linking (every navigation item duplicated 5x in body).
316
+ - Use a hub-and-spoke model: cluster pages link to a central topical hub.
317
+
318
+ ## Hreflang (Multilingual)
319
+
320
+ For each locale variant of a page, list all variants including the page itself:
321
+
322
+ ```html
323
+ <link rel="alternate" href="https://example.com/pricing" hreflang="en" />
324
+ <link rel="alternate" href="https://example.com/de/pricing" hreflang="de" />
325
+ <link rel="alternate" href="https://example.com/ja/pricing" hreflang="ja" />
326
+ <link rel="alternate" href="https://example.com/pricing" hreflang="x-default" />
327
+ ```
328
+
329
+ Rules:
330
+
331
+ - Mutual: every variant lists every other variant.
332
+ - Self-referencing: each variant lists itself.
333
+ - `x-default` for the unmatched/default version.
334
+ - Use valid BCP 47 codes (`en`, `en-US`, `de`, `pt-BR`).
335
+
336
+ ## Mobile-Friendly
337
+
338
+ - Responsive design (no separate `m.` site).
339
+ - `<meta name="viewport" content="width=device-width, initial-scale=1">`.
340
+ - No horizontal scroll at 320px width.
341
+ - Touch targets >= 44x44 CSS pixels.
342
+ - Body text >= 16px on mobile (avoids iOS auto-zoom).
343
+ - Same content as desktop (mobile-first indexing).
344
+
345
+ ## Page Speed (CrUX, not Lighthouse)
346
+
347
+ Search Console uses CrUX (Chrome User Experience Report) p75 over 28 days for the Page Experience signal. Lighthouse is a lab proxy; CrUX is the real signal.
348
+
349
+ Targets (CrUX p75):
350
+
351
+ - LCP <= 2.5s: "Good"
352
+ - INP <= 200ms: "Good"
353
+ - CLS <= 0.1: "Good"
354
+
355
+ A page can have a Lighthouse 95 in the lab and still be "Needs Improvement" in CrUX if real users have slower devices/networks. Instrument the field with `web-vitals` and track p75.
356
+
357
+ ## Content Quality (E-E-A-T)
358
+
359
+ Google's quality raters apply the E-E-A-T framework:
360
+
361
+ - **Experience**: first-hand experience visible (case studies, original screenshots, "I tested this and...").
362
+ - **Expertise**: author has demonstrable subject knowledge.
363
+ - **Authoritativeness**: cited by others, recognized in the space.
364
+ - **Trustworthiness**: accurate facts, transparent business, contact info, privacy policy, secure (HTTPS).
365
+
366
+ For YMYL (Your Money Your Life) topics (medical, legal, financial), expertise and trustworthiness signals are critical:
367
+
368
+ - Author bios with credentials.
369
+ - Editorial policy.
370
+ - Sources cited inline.
371
+ - Updated/published dates.
372
+ - Contact and "About" pages.
373
+
374
+ For non-YMYL, expertise is still useful but not as strict.
375
+
376
+ ## Common SEO Mistakes
377
+
378
+ - Same title on every page.
379
+ - Same description on every page.
380
+ - No canonical (or canonical pointing to a different URL by mistake).
381
+ - Multiple H1s per page.
382
+ - Skipped heading levels.
383
+ - Important content hidden behind JS that doesn't render server-side.
384
+ - Important content in images without alt text.
385
+ - Slow LCP because the hero image isn't optimized.
386
+ - CLS because of late-loading hero or font swap.
387
+ - Internal links with anchor text "click here" or "read more".
388
+ - Sitemap listing 404 or `noindex` URLs.
389
+ - robots.txt blocking CSS or JS (Google needs to render the page).
390
+ - `noindex` on a page Google should index (typo or stale config).
391
+ - Tracking params in the canonical (creates infinite duplicates).
392
+ - Migrating URLs without 301 redirects.
393
+ - Blocking the staging domain in robots.txt while allowing production by mistake.
394
+
395
+ ## Pre-Publish SEO Checklist
396
+
397
+ For every public-visible page:
398
+
399
+ - [ ] Unique `<title>` 50-60 chars
400
+ - [ ] Unique `<meta name="description">` 140-160 chars
401
+ - [ ] One `<h1>` matching primary intent
402
+ - [ ] Sequential headings, no skipped levels
403
+ - [ ] Self-referencing `<link rel="canonical">`
404
+ - [ ] `<meta name="robots" content="index, follow">` if indexable; `noindex` if not
405
+ - [ ] Open Graph tags (og:title, og:description, og:image, og:url, og:type)
406
+ - [ ] Twitter card tags
407
+ - [ ] Lang attribute on `<html>`
408
+ - [ ] Structured data validated via Rich Results Test (where applicable)
409
+ - [ ] Image alt text on every meaningful image
410
+ - [ ] Internal links use descriptive anchor text
411
+ - [ ] Page reachable from home in <= 3 clicks
412
+ - [ ] Listed in sitemap.xml (if indexable)
413
+ - [ ] HTTPS, no mixed content
414
+ - [ ] Mobile-friendly (responsive, viewport meta, no horizontal scroll)
415
+ - [ ] CrUX p75 LCP/INP/CLS in "Good" zone (verify after 28 days of traffic)
416
+ - [ ] No render-blocking content critical to indexing (Google renders JS, but slowly; SSR/SSG preferred for primary content)
417
+
418
+ ## See Also
419
+
420
+ - [lighthouse.md](lighthouse.md) for the SEO category audits in detail
421
+ - [performance.md](performance.md) for Core Web Vitals optimization
422
+ - [accessibility.md](accessibility.md) for the accessibility/SEO overlap (alt text, headings, labels)