@softspark/ai-toolkit 2.4.1 → 2.6.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/AGENTS.md +33 -20
- package/CHANGELOG.md +57 -0
- package/README.md +29 -13
- package/app/.claude-plugin/plugin.json +3 -2
- package/app/ARCHITECTURE.md +11 -0
- package/app/agents/code-reviewer.md +6 -7
- package/app/agents/frontend-specialist.md +33 -2
- package/app/agents/seo-specialist.md +1 -1
- package/app/personas/frontend-lead.md +48 -5
- package/app/skills/a11y-validate/SKILL.md +377 -0
- package/app/skills/a11y-validate/reference/aria-patterns.md +259 -0
- package/app/skills/a11y-validate/reference/eaa-compliance.md +252 -0
- package/app/skills/a11y-validate/reference/mobile-eaa.md +329 -0
- package/app/skills/a11y-validate/reference/wcag-2-1-aa.md +285 -0
- package/app/skills/a11y-validate/reference/wcag-2-2-aa.md +221 -0
- package/app/skills/a11y-validate/scripts/a11y-scanner.py +639 -0
- package/app/skills/clean-code/reference/python.md +3 -3
- package/app/skills/design-engineering/SKILL.md +2 -5
- package/app/skills/hipaa-validate/SKILL.md +39 -23
- package/app/skills/hipaa-validate/scripts/hipaa_scan.py +64 -7
- package/app/skills/review/SKILL.md +30 -6
- package/app/skills/seo-validate/SKILL.md +460 -0
- package/app/skills/seo-validate/reference/core-web-vitals.md +445 -0
- package/app/skills/seo-validate/reference/geo-aeo-patterns.md +259 -0
- package/app/skills/seo-validate/reference/geo-guidelines.md +248 -0
- package/app/skills/seo-validate/reference/schema-types.md +465 -0
- package/app/skills/seo-validate/reference/spa-ssg-patterns.md +351 -0
- package/app/skills/seo-validate/reference/w3c-guidelines.md +289 -0
- package/app/skills/seo-validate/scripts/seo-scanner.py +549 -0
- package/bin/ai-toolkit.js +24 -9
- package/kb/reference/architecture-overview.md +1 -1
- package/kb/reference/comparison.md +1 -1
- package/kb/reference/opencode-compatibility.md +161 -0
- package/kb/reference/skills-catalog.md +3 -1
- package/llms-full.txt +177 -6
- package/llms.txt +1 -0
- package/manifest.json +3 -3
- package/package.json +6 -3
- package/scripts/config_cli.py +4 -10
- package/scripts/doctor.py +3 -3
- package/scripts/generate_opencode.py +117 -0
- package/scripts/generate_opencode_agents.py +126 -0
- package/scripts/generate_opencode_commands.py +158 -0
- package/scripts/generate_opencode_json.py +133 -0
- package/scripts/generate_opencode_plugin.py +169 -0
- package/scripts/install_steps/ai_tools.py +117 -1
- package/scripts/install_steps/install_state.py +1 -1
- package/scripts/plugin.py +1 -1
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
# Core Web Vitals Reference
|
|
2
|
+
|
|
3
|
+
Reference for `seo-validate` Category 5. Static-analysis patterns that cause LCP/INP/CLS regressions, plus resource hint guidance and above-the-fold heuristics.
|
|
4
|
+
|
|
5
|
+
Source: web.dev Core Web Vitals, HTML Living Standard `<link>` rel types, W3C Resource Hints, per-framework image component documentation.
|
|
6
|
+
|
|
7
|
+
## Thresholds
|
|
8
|
+
|
|
9
|
+
| Metric | Good | Needs Improvement | Poor |
|
|
10
|
+
|--------|------|-------------------|------|
|
|
11
|
+
| LCP (Largest Contentful Paint) | <2.5s | 2.5–4.0s | >4.0s |
|
|
12
|
+
| INP (Interaction to Next Paint) | <200ms | 200–500ms | >500ms |
|
|
13
|
+
| CLS (Cumulative Layout Shift) | <0.1 | 0.1–0.25 | >0.25 |
|
|
14
|
+
|
|
15
|
+
75th-percentile mobile values count for ranking. The skill can't measure these at runtime — it flags code patterns known to cause regressions.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## LCP (Largest Contentful Paint)
|
|
20
|
+
|
|
21
|
+
The LCP element is typically the hero image or H1 text in the first viewport. Goal: render it as fast as possible.
|
|
22
|
+
|
|
23
|
+
### Image dimensions
|
|
24
|
+
|
|
25
|
+
Every `<img>` and `<video>` must declare `width` and `height` (or `aspect-ratio` CSS). Missing dimensions cause:
|
|
26
|
+
- CLS as the image loads.
|
|
27
|
+
- LCP delay (browser can't reserve space / estimate priority).
|
|
28
|
+
|
|
29
|
+
```html
|
|
30
|
+
<!-- Good -->
|
|
31
|
+
<img src="hero.jpg" width="1200" height="630" alt="...">
|
|
32
|
+
|
|
33
|
+
<!-- Bad (CLS + LCP) -->
|
|
34
|
+
<img src="hero.jpg" alt="...">
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### fetchpriority
|
|
38
|
+
|
|
39
|
+
`fetchpriority="high"` on the LCP image tells the browser to fetch it before other resources. Introduced in Chrome 101 (May 2022).
|
|
40
|
+
|
|
41
|
+
```html
|
|
42
|
+
<img src="hero.jpg" width="1200" height="630" fetchpriority="high" alt="...">
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`fetchpriority="low"` on below-the-fold images frees bandwidth for critical resources.
|
|
46
|
+
|
|
47
|
+
### loading="lazy" on LCP = BAD
|
|
48
|
+
|
|
49
|
+
`loading="lazy"` delays the image load until it's near the viewport. Applied to the LCP element, this is a HIGH-severity anti-pattern.
|
|
50
|
+
|
|
51
|
+
Only apply `loading="lazy"` to images **below the fold**.
|
|
52
|
+
|
|
53
|
+
```html
|
|
54
|
+
<!-- Bad: LCP element marked lazy -->
|
|
55
|
+
<img src="hero.jpg" loading="lazy" alt="Hero">
|
|
56
|
+
|
|
57
|
+
<!-- Good: LCP element eager + high priority -->
|
|
58
|
+
<img src="hero.jpg" fetchpriority="high" alt="Hero">
|
|
59
|
+
|
|
60
|
+
<!-- Good: below-the-fold image lazy -->
|
|
61
|
+
<img src="testimonial.jpg" loading="lazy" decoding="async" alt="Jane, happy customer">
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### decoding="async"
|
|
65
|
+
|
|
66
|
+
Signals that image decoding can happen off the main thread. Apply to non-critical images.
|
|
67
|
+
|
|
68
|
+
```html
|
|
69
|
+
<img src="sidebar.jpg" loading="lazy" decoding="async" alt="...">
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Don't apply `decoding="async"` to the LCP image — it can delay paint. Use `decoding="sync"` or omit.
|
|
73
|
+
|
|
74
|
+
### Preloading the LCP image
|
|
75
|
+
|
|
76
|
+
When the LCP image URL is known at build time (hero background, article cover), preload it:
|
|
77
|
+
|
|
78
|
+
```html
|
|
79
|
+
<link rel="preload" as="image" href="/images/hero.jpg"
|
|
80
|
+
imagesrcset="/images/hero-640.jpg 640w, /images/hero-1280.jpg 1280w"
|
|
81
|
+
imagesizes="100vw">
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Font loading
|
|
85
|
+
|
|
86
|
+
Fonts frequently gate LCP when text is the LCP element.
|
|
87
|
+
|
|
88
|
+
```css
|
|
89
|
+
/* Good: text renders in fallback, swaps when webfont loads */
|
|
90
|
+
@font-face {
|
|
91
|
+
font-family: 'Inter';
|
|
92
|
+
src: url('/fonts/inter.woff2') format('woff2');
|
|
93
|
+
font-display: swap; /* or 'optional' for aggressive LCP optimization */
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* Bad: FOIT (Flash of Invisible Text) blocks paint */
|
|
97
|
+
@font-face {
|
|
98
|
+
font-family: 'Inter';
|
|
99
|
+
src: url('/fonts/inter.woff2') format('woff2');
|
|
100
|
+
/* font-display defaults to 'auto', which most browsers treat as 'block' = FOIT */
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Preload webfonts used above the fold:
|
|
105
|
+
|
|
106
|
+
```html
|
|
107
|
+
<link rel="preload" as="font" type="font/woff2"
|
|
108
|
+
href="/fonts/inter-var.woff2" crossorigin>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The `crossorigin` attribute is required even for same-origin fonts — fonts are always fetched with CORS.
|
|
112
|
+
|
|
113
|
+
### Responsive images
|
|
114
|
+
|
|
115
|
+
Images wider than ~600px should use `srcset` + `sizes` or `<picture>`:
|
|
116
|
+
|
|
117
|
+
```html
|
|
118
|
+
<img
|
|
119
|
+
src="hero-1280.jpg"
|
|
120
|
+
srcset="hero-640.jpg 640w, hero-1280.jpg 1280w, hero-2560.jpg 2560w"
|
|
121
|
+
sizes="(max-width: 768px) 100vw, 50vw"
|
|
122
|
+
width="1280" height="720" alt="...">
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Over-fetching a 2560px image to a 375px mobile viewport wastes bytes and delays LCP.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## INP (Interaction to Next Paint)
|
|
130
|
+
|
|
131
|
+
Replaces FID (First Input Delay) as the CWV interaction metric in March 2024. Measures longest interaction latency across the session.
|
|
132
|
+
|
|
133
|
+
### Render-blocking scripts
|
|
134
|
+
|
|
135
|
+
Scripts in `<head>` without `async` or `defer` block parsing:
|
|
136
|
+
|
|
137
|
+
```html
|
|
138
|
+
<!-- Bad: blocks parser + CSSOM -->
|
|
139
|
+
<script src="/analytics.js"></script>
|
|
140
|
+
|
|
141
|
+
<!-- Good: doesn't block parser, runs after parse but before DOMContentLoaded -->
|
|
142
|
+
<script defer src="/app.js"></script>
|
|
143
|
+
|
|
144
|
+
<!-- Good: downloads in parallel, runs ASAP (order not guaranteed) -->
|
|
145
|
+
<script async src="/analytics.js"></script>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Third-party scripts
|
|
149
|
+
|
|
150
|
+
Analytics, chat widgets, ads, A/B test SDKs commonly cause INP regressions.
|
|
151
|
+
|
|
152
|
+
Patterns to flag:
|
|
153
|
+
- `<script src="https://www.googletagmanager.com/...">` without `async`
|
|
154
|
+
- Intercom, Drift, HubSpot widgets loaded synchronously
|
|
155
|
+
- Ads via direct `<script>` (most use `async` by default, verify)
|
|
156
|
+
|
|
157
|
+
Next.js `<Script>` component with strategy:
|
|
158
|
+
```jsx
|
|
159
|
+
<Script src="https://analytics.example.com" strategy="lazyOnload" />
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Strategies:
|
|
163
|
+
- `beforeInteractive` — blocks hydration, rarely needed.
|
|
164
|
+
- `afterInteractive` (default) — loads after hydration.
|
|
165
|
+
- `lazyOnload` — loads during idle time. Best for analytics/chat.
|
|
166
|
+
- `worker` — runs in Web Worker (Partytown). Experimental.
|
|
167
|
+
|
|
168
|
+
### document.write
|
|
169
|
+
|
|
170
|
+
Always HIGH. Modern browsers disable `document.write` for scripts loaded over 2G-like networks. Never use.
|
|
171
|
+
|
|
172
|
+
### Hydration cost
|
|
173
|
+
|
|
174
|
+
Heavy synchronous work during hydration blocks INP. Red flags:
|
|
175
|
+
|
|
176
|
+
```jsx
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
// Large sync block — splits Long Tasks
|
|
179
|
+
heavyComputation();
|
|
180
|
+
anotherHeavyFunction();
|
|
181
|
+
yetAnotherOne();
|
|
182
|
+
}, []);
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Mitigation: `requestIdleCallback`, `scheduler.postTask`, or split across effects.
|
|
186
|
+
|
|
187
|
+
### Bundle size
|
|
188
|
+
|
|
189
|
+
Bundles >300KB gzipped gating interaction = INP risk. Check:
|
|
190
|
+
- Imports of entire lodash / moment / date-fns (use tree-shaking or day.js)
|
|
191
|
+
- Icon libraries imported wholesale (import only what's used)
|
|
192
|
+
- PDF/charting libraries loaded eagerly (dynamic import)
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## CLS (Cumulative Layout Shift)
|
|
197
|
+
|
|
198
|
+
### Image/video/iframe dimensions
|
|
199
|
+
|
|
200
|
+
Covered above — always set `width`/`height` or `aspect-ratio`.
|
|
201
|
+
|
|
202
|
+
### Iframes
|
|
203
|
+
|
|
204
|
+
YouTube embeds, maps, Twitter embeds cause CLS without reserved space:
|
|
205
|
+
|
|
206
|
+
```html
|
|
207
|
+
<!-- Bad -->
|
|
208
|
+
<iframe src="https://www.youtube.com/embed/..."></iframe>
|
|
209
|
+
|
|
210
|
+
<!-- Good: fixed container -->
|
|
211
|
+
<div style="aspect-ratio: 16/9;">
|
|
212
|
+
<iframe src="..." width="100%" height="100%"></iframe>
|
|
213
|
+
</div>
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Dynamic content
|
|
217
|
+
|
|
218
|
+
Ads, embeds, and "related content" widgets injected after paint cause CLS. Reserve space with CSS:
|
|
219
|
+
|
|
220
|
+
```css
|
|
221
|
+
.ad-slot {
|
|
222
|
+
min-height: 250px; /* Or aspect-ratio tied to ad size */
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Font swap shifts
|
|
227
|
+
|
|
228
|
+
`font-display: swap` causes a visible shift when the webfont loads (FOUT — Flash of Unstyled Text). Minimize by:
|
|
229
|
+
- Using system-ui fonts as fallback with matching metrics (`size-adjust`, `ascent-override`).
|
|
230
|
+
- Using `font-display: optional` (accepts missing-font in exchange for zero shift).
|
|
231
|
+
|
|
232
|
+
### Hydration mismatches
|
|
233
|
+
|
|
234
|
+
Server renders one thing, client renders another — content "flashes" or "jumps":
|
|
235
|
+
|
|
236
|
+
```jsx
|
|
237
|
+
// Bad — hydration mismatch potential
|
|
238
|
+
function Component() {
|
|
239
|
+
if (typeof window !== 'undefined') {
|
|
240
|
+
return <ClientOnlyContent />;
|
|
241
|
+
}
|
|
242
|
+
return <ServerContent />;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Good — use useEffect with loading state
|
|
246
|
+
function Component() {
|
|
247
|
+
const [mounted, setMounted] = useState(false);
|
|
248
|
+
useEffect(() => setMounted(true), []);
|
|
249
|
+
return mounted ? <ClientOnlyContent /> : <Skeleton />;
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Resource Hints
|
|
256
|
+
|
|
257
|
+
HTML Living Standard + W3C Resource Hints. Hints are suggestions — browsers can ignore them.
|
|
258
|
+
|
|
259
|
+
### preload
|
|
260
|
+
|
|
261
|
+
High-priority fetch for critical, known resources.
|
|
262
|
+
|
|
263
|
+
```html
|
|
264
|
+
<link rel="preload" as="image" href="/hero.jpg">
|
|
265
|
+
<link rel="preload" as="font" type="font/woff2" href="/fonts/inter.woff2" crossorigin>
|
|
266
|
+
<link rel="preload" as="style" href="/critical.css">
|
|
267
|
+
<link rel="preload" as="script" href="/critical.js">
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Rules:
|
|
271
|
+
- Must include `as` attribute matching the resource type.
|
|
272
|
+
- Must appear BEFORE the resource uses it in document order.
|
|
273
|
+
- Don't over-use — >6 preloads starts to hurt.
|
|
274
|
+
- Preloaded resource not used within a few seconds → browser console warning.
|
|
275
|
+
|
|
276
|
+
### prefetch
|
|
277
|
+
|
|
278
|
+
Low-priority fetch for probable-next-navigation resources.
|
|
279
|
+
|
|
280
|
+
```html
|
|
281
|
+
<link rel="prefetch" href="/next-page.js">
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Framework-integrated prefetching:
|
|
285
|
+
- Next.js `<Link>` auto-prefetches visible links in production.
|
|
286
|
+
- Nuxt `<NuxtLink>` with `prefetch` prop.
|
|
287
|
+
- SvelteKit: `data-sveltekit-preload-data="hover"` or `"tap"`.
|
|
288
|
+
- Remix: `<Link prefetch="intent">`.
|
|
289
|
+
|
|
290
|
+
### preconnect
|
|
291
|
+
|
|
292
|
+
Establishes early connection (DNS + TCP + TLS) to a 3rd-party origin.
|
|
293
|
+
|
|
294
|
+
```html
|
|
295
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
296
|
+
<link rel="preconnect" href="https://www.google-analytics.com">
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Use for critical 3rd-party origins: font hosts, CDNs, analytics. Overuse contends with critical-path connections — limit to 3–4.
|
|
300
|
+
|
|
301
|
+
### dns-prefetch
|
|
302
|
+
|
|
303
|
+
Lightweight DNS-only resolution. Use for less-critical origins or as fallback for browsers without preconnect support.
|
|
304
|
+
|
|
305
|
+
```html
|
|
306
|
+
<link rel="dns-prefetch" href="https://some-analytics.example.com">
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### modulepreload
|
|
310
|
+
|
|
311
|
+
Preload ESM modules on the critical path (bypasses discovery cost for import graphs).
|
|
312
|
+
|
|
313
|
+
```html
|
|
314
|
+
<link rel="modulepreload" href="/build/chunk-main.js">
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Build tools can generate these automatically (Vite, esbuild, Rollup plugins).
|
|
318
|
+
|
|
319
|
+
### Ordering
|
|
320
|
+
|
|
321
|
+
Preload/preconnect MUST appear before the resource that uses them — typically at the top of `<head>`, right after charset/viewport.
|
|
322
|
+
|
|
323
|
+
```html
|
|
324
|
+
<head>
|
|
325
|
+
<meta charset="utf-8">
|
|
326
|
+
<meta name="viewport" content="...">
|
|
327
|
+
|
|
328
|
+
<!-- Resource hints first -->
|
|
329
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
330
|
+
<link rel="preload" as="image" href="/hero.jpg" fetchpriority="high">
|
|
331
|
+
<link rel="preload" as="font" type="font/woff2" href="/fonts/inter.woff2" crossorigin>
|
|
332
|
+
|
|
333
|
+
<!-- Then the resource (stylesheets) -->
|
|
334
|
+
<link rel="stylesheet" href="/styles.css">
|
|
335
|
+
|
|
336
|
+
<!-- Metadata -->
|
|
337
|
+
<title>...</title>
|
|
338
|
+
<meta name="description" content="...">
|
|
339
|
+
</head>
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Anti-patterns
|
|
343
|
+
|
|
344
|
+
- Preloading non-critical resources (every icon, every font weight).
|
|
345
|
+
- Preload without `as` attribute — browser issues warning, may not prioritize.
|
|
346
|
+
- Multiple `preload`s for images that aren't the LCP element.
|
|
347
|
+
- Preload after stylesheets/scripts that already request the resource.
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## Above-the-Fold Heuristics
|
|
352
|
+
|
|
353
|
+
True "above-the-fold" detection requires rendering. In static analysis, treat these as ATF candidates:
|
|
354
|
+
|
|
355
|
+
1. **First `<img>` / `<Image>` / `<NuxtImg>` / `<GatsbyImage>` / `<Image from 'astro:assets'>`** in a route/page component.
|
|
356
|
+
2. **First child of `<main>`**, `<section>`, or `<header>`.
|
|
357
|
+
3. **Components named** `Hero`, `HeroSection`, `Banner`, `Masthead`, `Jumbotron`, `CoverImage`, `SplashImage`, `HeaderImage`.
|
|
358
|
+
4. **`<Image>` with `priority` prop** (Next.js) — developer has already declared ATF intent.
|
|
359
|
+
|
|
360
|
+
For ATF elements, required:
|
|
361
|
+
- `width` + `height` declared.
|
|
362
|
+
- `fetchpriority="high"` (or framework `priority` equivalent).
|
|
363
|
+
- NO `loading="lazy"`.
|
|
364
|
+
- Matching `<link rel="preload">` if URL known at build.
|
|
365
|
+
|
|
366
|
+
For below-the-fold images, recommended:
|
|
367
|
+
- `loading="lazy"`.
|
|
368
|
+
- `decoding="async"`.
|
|
369
|
+
- `fetchpriority="low"` (optional).
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
## Framework-Specific Image Components
|
|
374
|
+
|
|
375
|
+
### Next.js — `next/image`
|
|
376
|
+
|
|
377
|
+
```jsx
|
|
378
|
+
import Image from 'next/image';
|
|
379
|
+
|
|
380
|
+
// LCP image
|
|
381
|
+
<Image src="/hero.jpg" alt="..." width={1200} height={630} priority />
|
|
382
|
+
|
|
383
|
+
// Below fold
|
|
384
|
+
<Image src="/thumb.jpg" alt="..." width={400} height={300} loading="lazy" />
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
Benefits: automatic WebP/AVIF, responsive srcset, lazy by default, supports `fill` + `sizes`.
|
|
388
|
+
|
|
389
|
+
Flags:
|
|
390
|
+
- `<img>` used instead of `<Image>` in route components → WARN (unless explicit raw HTML is needed).
|
|
391
|
+
- `<Image>` without `priority` on LCP element → HIGH.
|
|
392
|
+
- `<Image>` without `sizes` when using `fill` → WARN.
|
|
393
|
+
|
|
394
|
+
### Nuxt — `<NuxtImg>` / `<NuxtPicture>`
|
|
395
|
+
|
|
396
|
+
```vue
|
|
397
|
+
<NuxtImg src="/hero.jpg" width="1200" height="630" preload />
|
|
398
|
+
<NuxtImg src="/thumb.jpg" width="400" height="300" loading="lazy" />
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
Requires `@nuxt/image` module.
|
|
402
|
+
|
|
403
|
+
### Astro — `<Image>` from `astro:assets`
|
|
404
|
+
|
|
405
|
+
```astro
|
|
406
|
+
---
|
|
407
|
+
import { Image } from 'astro:assets';
|
|
408
|
+
import hero from '../assets/hero.jpg';
|
|
409
|
+
---
|
|
410
|
+
<Image src={hero} alt="..." width={1200} height={630} loading="eager" />
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### Gatsby — `GatsbyImage`
|
|
414
|
+
|
|
415
|
+
```jsx
|
|
416
|
+
import { GatsbyImage, getImage } from 'gatsby-plugin-image';
|
|
417
|
+
|
|
418
|
+
const image = getImage(data.file);
|
|
419
|
+
<GatsbyImage image={image} alt="..." loading="eager" />
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### SvelteKit — `@sveltejs/enhanced-img`
|
|
423
|
+
|
|
424
|
+
```svelte
|
|
425
|
+
<script>
|
|
426
|
+
import heroImg from '$lib/images/hero.jpg?enhanced';
|
|
427
|
+
</script>
|
|
428
|
+
<enhanced:img src={heroImg} alt="..." />
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
## References
|
|
434
|
+
|
|
435
|
+
- web.dev LCP: https://web.dev/lcp/
|
|
436
|
+
- web.dev INP: https://web.dev/inp/
|
|
437
|
+
- web.dev CLS: https://web.dev/cls/
|
|
438
|
+
- Resource Hints: https://www.w3.org/TR/resource-hints/
|
|
439
|
+
- HTML Living Standard `<link>` rel: https://html.spec.whatwg.org/multipage/links.html#linkTypes
|
|
440
|
+
- fetchpriority: https://web.dev/fetch-priority/
|
|
441
|
+
- font-display: https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display
|
|
442
|
+
- Next.js Image: https://nextjs.org/docs/app/api-reference/components/image
|
|
443
|
+
- Nuxt Image: https://image.nuxt.com/
|
|
444
|
+
- Astro Image: https://docs.astro.build/en/guides/images/
|
|
445
|
+
- Gatsby Image: https://www.gatsbyjs.com/docs/reference/built-in-components/gatsby-plugin-image/
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# AEO — Answer Engine Optimization
|
|
2
|
+
|
|
3
|
+
Reference for `seo-validate` Category 6 (GEO/AEO). Companion to [geo-guidelines.md](geo-guidelines.md). Focuses on optimizing content to appear in AI-generated answers — not just ranked pages.
|
|
4
|
+
|
|
5
|
+
All AEO findings are severity `INFO` — emerging practice, no known penalties.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. What is AEO
|
|
10
|
+
|
|
11
|
+
Traditional SEO ranks pages. AEO targets AI-synthesized answers that cite or paraphrase your content without requiring a click.
|
|
12
|
+
|
|
13
|
+
**Key answer engines:**
|
|
14
|
+
|
|
15
|
+
| Engine | Mechanism |
|
|
16
|
+
|--------|-----------|
|
|
17
|
+
| Google AI Overviews | Retrieval-augmented summarization over organic index |
|
|
18
|
+
| Bing Copilot | GPT-4 grounded on Bing index; inline citations |
|
|
19
|
+
| Perplexity | Real-time retrieval + LLM synthesis; explicit source cards |
|
|
20
|
+
| ChatGPT Search | OpenAI web retrieval; used in ChatGPT Plus |
|
|
21
|
+
|
|
22
|
+
**Difference from classic SEO:** AI engines do not rank pages against each other — they extract the most useful fragment. A page ranked #8 with a direct-answer paragraph can beat #1 in AI responses.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 2. Content Structure for AI Consumption
|
|
27
|
+
|
|
28
|
+
### Direct-answer paragraphs (first-sentence-answer pattern)
|
|
29
|
+
|
|
30
|
+
AI engines extract the first 1–3 sentences after a heading as the answer to the implied question. Put the fact first.
|
|
31
|
+
|
|
32
|
+
```html
|
|
33
|
+
<!-- Good -->
|
|
34
|
+
<h2>What is a canonical URL?</h2>
|
|
35
|
+
<p>A canonical URL is the preferred page version search engines should index when near-duplicates exist. Specify it with <code><link rel="canonical" href="..."></code> in <code><head></code>.</p>
|
|
36
|
+
|
|
37
|
+
<!-- Bad: answer buried after filler -->
|
|
38
|
+
<h2>What is a canonical URL?</h2>
|
|
39
|
+
<p>When building websites, you often encounter situations where multiple URLs serve similar content...</p>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### FAQ schema with concise Q&A pairs
|
|
43
|
+
|
|
44
|
+
`FAQPage` is the highest-precision signal. Each `acceptedAnswer` is extracted as a direct response to its `name` question. Pair with matching visible HTML.
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"@context": "https://schema.org",
|
|
49
|
+
"@type": "FAQPage",
|
|
50
|
+
"mainEntity": [{
|
|
51
|
+
"@type": "Question",
|
|
52
|
+
"name": "How do I add a canonical tag in Next.js?",
|
|
53
|
+
"acceptedAnswer": {
|
|
54
|
+
"@type": "Answer",
|
|
55
|
+
"text": "In App Router: export const metadata = { alternates: { canonical: 'https://example.com/page' } }. In Pages Router: use <Head><link rel=\"canonical\" href=\"...\" /></Head>."
|
|
56
|
+
}
|
|
57
|
+
}]
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Rules: question text = natural-language query; answer = self-contained under 300 chars for voice engines.
|
|
62
|
+
|
|
63
|
+
### HowTo schema with numbered steps
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"@context": "https://schema.org",
|
|
68
|
+
"@type": "HowTo",
|
|
69
|
+
"name": "How to configure robots.txt for a Next.js site",
|
|
70
|
+
"step": [
|
|
71
|
+
{ "@type": "HowToStep", "position": 1, "name": "Create the file", "text": "Add robots.txt to public/." },
|
|
72
|
+
{ "@type": "HowToStep", "position": 2, "name": "Add directives", "text": "Set User-agent, Disallow, Allow, and Sitemap URL." }
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Match with a visible `<ol>` — never schema without HTML steps.
|
|
78
|
+
|
|
79
|
+
### QAPage schema for single-question articles
|
|
80
|
+
|
|
81
|
+
Use `QAPage` for knowledge-base and support docs structured around one primary question. Include `author` + `answerCount` fields.
|
|
82
|
+
|
|
83
|
+
### Entity-first writing
|
|
84
|
+
|
|
85
|
+
Define the subject before explaining it. AI engines build knowledge-graph connections from entity definitions.
|
|
86
|
+
|
|
87
|
+
```html
|
|
88
|
+
<!-- Good: entity defined first -->
|
|
89
|
+
<h2>Core Web Vitals</h2>
|
|
90
|
+
<p>Core Web Vitals are three Google-defined UX metrics: LCP, INP, and CLS. They are used as ranking signals in Google Search since 2021.</p>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## 3. Technical Signals
|
|
96
|
+
|
|
97
|
+
### llms.txt / llms-full.txt
|
|
98
|
+
|
|
99
|
+
The `llms.txt` convention (analogous to `robots.txt` for AI agents) lets agents discover curated content. Serve at domain root.
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
# /llms.txt
|
|
103
|
+
> ExampleCo builds developer tooling for SEO automation.
|
|
104
|
+
|
|
105
|
+
## Docs
|
|
106
|
+
- [API Reference](https://example.com/docs/api/llms.txt): Full API docs
|
|
107
|
+
- [Getting Started](https://example.com/docs/start/llms.txt): Setup guide
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`llms-full.txt` = concatenated full-text for one-shot context loading.
|
|
111
|
+
|
|
112
|
+
### robots.txt AI bot directives
|
|
113
|
+
|
|
114
|
+
Known AI crawler `User-agent` strings:
|
|
115
|
+
|
|
116
|
+
| Bot | Owner | Purpose |
|
|
117
|
+
|-----|-------|---------|
|
|
118
|
+
| `GPTBot` | OpenAI | ChatGPT training + search |
|
|
119
|
+
| `OAI-SearchBot` | OpenAI | ChatGPT Search real-time retrieval |
|
|
120
|
+
| `ClaudeBot` | Anthropic | Claude training |
|
|
121
|
+
| `anthropic-ai` | Anthropic | Claude real-time retrieval |
|
|
122
|
+
| `PerplexityBot` | Perplexity | Real-time answer grounding |
|
|
123
|
+
| `Google-Extended` | Google | Gemini training (NOT AI Overviews) |
|
|
124
|
+
| `Googlebot` | Google | Organic + AI Overviews |
|
|
125
|
+
|
|
126
|
+
Blocking `Googlebot` blocks AI Overviews. Block `Google-Extended` to opt out of Gemini training only.
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
User-agent: GPTBot
|
|
130
|
+
Allow: /docs/
|
|
131
|
+
Disallow: /
|
|
132
|
+
|
|
133
|
+
User-agent: Google-Extended
|
|
134
|
+
Disallow: /
|
|
135
|
+
|
|
136
|
+
Sitemap: https://example.com/sitemap.xml
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Sitemap with lastmod
|
|
140
|
+
|
|
141
|
+
Include `<lastmod>` reflecting actual content change dates — not today's build date. AI engines prefer recently-updated sources.
|
|
142
|
+
|
|
143
|
+
### Clean HTML semantics
|
|
144
|
+
|
|
145
|
+
AI parsers rely on heading hierarchy. Rules:
|
|
146
|
+
- One `<h1>` per page — becomes the answer title in AI responses.
|
|
147
|
+
- `<h2>` maps to extractable sub-questions.
|
|
148
|
+
- Never skip heading levels.
|
|
149
|
+
- Use `<article>`, `<section>`, `<main>` over `<div>` wrappers.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## 4. Content Quality Signals
|
|
154
|
+
|
|
155
|
+
### E-E-A-T
|
|
156
|
+
|
|
157
|
+
| Signal | Implementation |
|
|
158
|
+
|--------|---------------|
|
|
159
|
+
| Experience | First-person case studies, original screenshots, tested procedures |
|
|
160
|
+
| Expertise | Author byline + `Person` schema with `sameAs` links (LinkedIn, GitHub) |
|
|
161
|
+
| Authoritativeness | `Organization` schema with `sameAs` to Wikidata/Crunchbase |
|
|
162
|
+
| Trustworthiness | HTTPS, `dateModified` on articles, correction notices |
|
|
163
|
+
|
|
164
|
+
```json
|
|
165
|
+
{
|
|
166
|
+
"@context": "https://schema.org",
|
|
167
|
+
"@type": "Article",
|
|
168
|
+
"headline": "How to Optimize robots.txt for AI Crawlers",
|
|
169
|
+
"author": {
|
|
170
|
+
"@type": "Person",
|
|
171
|
+
"name": "Jane Doe",
|
|
172
|
+
"sameAs": ["https://github.com/janedoe", "https://linkedin.com/in/janedoe"]
|
|
173
|
+
},
|
|
174
|
+
"datePublished": "2026-03-01",
|
|
175
|
+
"dateModified": "2026-04-10"
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Citation-worthy content
|
|
180
|
+
|
|
181
|
+
- Statistics with sources: "76% of developers use React (State of JS 2024)" — not "most developers."
|
|
182
|
+
- Dated claims: include year on every statistic.
|
|
183
|
+
- Expert quotes in `<blockquote cite="...">` with named speaker.
|
|
184
|
+
- Outbound links to primary sources (w3.org, ietf.org, schema.org, developers.google.com, developer.mozilla.org, arxiv.org) — AI engines treat authoritative outbound links as a credibility signal.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## 5. Grep Patterns for Automated Detection
|
|
189
|
+
|
|
190
|
+
All patterns below yield `INFO` + `heuristic` findings.
|
|
191
|
+
|
|
192
|
+
### Missing FAQ schema on FAQ-like content
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
Detect: (?i)(frequently asked questions|faq|common questions) in *.html,*.tsx,*.jsx,*.vue,*.svelte,*.astro
|
|
196
|
+
Then check: "FAQPage" in same file or its layout
|
|
197
|
+
Flag if: FAQ heading found, no FAQPage schema present
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Missing llms.txt
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
Glob: llms.txt in public/, static/, site root
|
|
204
|
+
Flag if: not found
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### robots.txt missing AI bot directives
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
File: robots.txt
|
|
211
|
+
Pattern: GPTBot|ClaudeBot|PerplexityBot|Google-Extended|OAI-SearchBot|anthropic-ai
|
|
212
|
+
Flag if: robots.txt exists but no AI-specific User-agent rules found
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### How-to headings without HowTo schema
|
|
216
|
+
|
|
217
|
+
```
|
|
218
|
+
Detect: (?i)<h[2-3][^>]*>how (to|do|can|should)\s in *.html,*.tsx,*.jsx,*.vue,*.svelte,*.astro
|
|
219
|
+
Then check: "HowTo" in same file or layout
|
|
220
|
+
Flag if: how-to heading found, no HowTo schema present
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Missing author/date metadata on articles
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
Detect: (?i)<article|"@type"\s*:\s*"Article" in *.html,*.tsx,*.jsx,*.vue,*.svelte,*.astro
|
|
227
|
+
Then check: datePublished|rel="author"|<time datetime
|
|
228
|
+
Flag if: article detected, neither datePublished in JSON-LD nor <time datetime> in HTML
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## Checklist (Category 6 AEO additions)
|
|
234
|
+
|
|
235
|
+
- [ ] `llms.txt` present at domain root.
|
|
236
|
+
- [ ] `robots.txt` has explicit AI bot directives.
|
|
237
|
+
- [ ] Sitemap `<lastmod>` reflects actual content change dates.
|
|
238
|
+
- [ ] FAQ content uses `FAQPage` schema with self-contained answers.
|
|
239
|
+
- [ ] How-to content uses `HowTo` schema with `step` array + visible `<ol>`.
|
|
240
|
+
- [ ] Single-question pages use `QAPage` schema.
|
|
241
|
+
- [ ] Articles have `datePublished` + `dateModified` in JSON-LD.
|
|
242
|
+
- [ ] Author bylines use `Person` schema with `sameAs` links.
|
|
243
|
+
- [ ] Statistics include inline source citations with year.
|
|
244
|
+
- [ ] `<h2>` headings phrased as natural-language questions where applicable.
|
|
245
|
+
- [ ] Entity terms defined on first use.
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## References
|
|
250
|
+
|
|
251
|
+
- llmstxt.org spec: https://llmstxt.org
|
|
252
|
+
- OpenAI GPTBot docs: https://platform.openai.com/docs/gptbot
|
|
253
|
+
- Google-Extended opt-out: https://developers.google.com/search/docs/crawling-indexing/overview-google-crawlers
|
|
254
|
+
- Google E-E-A-T guidance: https://developers.google.com/search/docs/fundamentals/creating-helpful-content
|
|
255
|
+
- Schema.org FAQPage: https://schema.org/FAQPage
|
|
256
|
+
- Schema.org HowTo: https://schema.org/HowTo
|
|
257
|
+
- Schema.org QAPage: https://schema.org/QAPage
|
|
258
|
+
- Schema.org Article: https://schema.org/Article
|
|
259
|
+
- Princeton/Georgia Tech GEO paper (Aggarwal et al., 2023): https://arxiv.org/abs/2311.09735
|