@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.
- package/CHANGELOG.md +37 -0
- package/LICENSE +21 -0
- package/README.md +114 -0
- package/SKILL.md +227 -0
- package/package.json +63 -0
- package/references/accessibility.md +396 -0
- package/references/audit-workflow.md +390 -0
- package/references/components.md +247 -0
- package/references/data-viz.md +457 -0
- package/references/defects.md +152 -0
- package/references/design.md +513 -0
- package/references/forms.md +485 -0
- package/references/lighthouse.md +242 -0
- package/references/motion.md +642 -0
- package/references/performance.md +416 -0
- package/references/pre-launch.md +342 -0
- package/references/responsive.md +519 -0
- package/references/seo.md +422 -0
- package/references/ui-ux.md +565 -0
- package/scripts/check-no-dashes.mjs +90 -0
|
@@ -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)
|