@eightmedia/flightcheck 0.0.0-stage → 1.0.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 (127) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/LICENSE +691 -0
  3. package/README.md +152 -2
  4. package/dist/analyze/fixManifest.d.ts +65 -0
  5. package/dist/analyze/fixManifest.d.ts.map +1 -0
  6. package/dist/analyze/fixManifest.js +588 -0
  7. package/dist/analyze/report.d.ts +283 -0
  8. package/dist/analyze/report.d.ts.map +1 -0
  9. package/dist/analyze/report.js +726 -0
  10. package/dist/analyze/reportTier1.d.ts +46 -0
  11. package/dist/analyze/reportTier1.d.ts.map +1 -0
  12. package/dist/analyze/reportTier1.js +251 -0
  13. package/dist/analyze/stack.d.ts +33 -0
  14. package/dist/analyze/stack.d.ts.map +1 -0
  15. package/dist/analyze/stack.js +188 -0
  16. package/dist/analyze/structuredReport.d.ts +62 -0
  17. package/dist/analyze/structuredReport.d.ts.map +1 -0
  18. package/dist/analyze/structuredReport.js +524 -0
  19. package/dist/analyze.d.ts +3 -0
  20. package/dist/analyze.d.ts.map +1 -0
  21. package/dist/analyze.js +40 -0
  22. package/dist/checks/agent-ready.d.ts +63 -0
  23. package/dist/checks/agent-ready.d.ts.map +1 -0
  24. package/dist/checks/agent-ready.js +219 -0
  25. package/dist/checks/analyze.d.ts +15 -0
  26. package/dist/checks/analyze.d.ts.map +1 -0
  27. package/dist/checks/analyze.js +63 -0
  28. package/dist/checks/crawl.d.ts +28 -0
  29. package/dist/checks/crawl.d.ts.map +1 -0
  30. package/dist/checks/crawl.js +86 -0
  31. package/dist/checks/dns.d.ts +25 -0
  32. package/dist/checks/dns.d.ts.map +1 -0
  33. package/dist/checks/dns.js +80 -0
  34. package/dist/checks/index.d.ts +24 -0
  35. package/dist/checks/index.d.ts.map +1 -0
  36. package/dist/checks/index.js +12 -0
  37. package/dist/checks/issues.d.ts +8 -0
  38. package/dist/checks/issues.d.ts.map +1 -0
  39. package/dist/checks/issues.js +3 -0
  40. package/dist/checks/redirect.d.ts +38 -0
  41. package/dist/checks/redirect.d.ts.map +1 -0
  42. package/dist/checks/redirect.js +209 -0
  43. package/dist/checks/registry.d.ts +8 -0
  44. package/dist/checks/registry.d.ts.map +1 -0
  45. package/dist/checks/registry.js +275 -0
  46. package/dist/checks/robots-txt.d.ts +16 -0
  47. package/dist/checks/robots-txt.d.ts.map +1 -0
  48. package/dist/checks/robots-txt.js +99 -0
  49. package/dist/checks/runner.d.ts +9 -0
  50. package/dist/checks/runner.d.ts.map +1 -0
  51. package/dist/checks/runner.js +110 -0
  52. package/dist/checks/security-headers.d.ts +12 -0
  53. package/dist/checks/security-headers.d.ts.map +1 -0
  54. package/dist/checks/security-headers.js +53 -0
  55. package/dist/checks/sitemap.d.ts +36 -0
  56. package/dist/checks/sitemap.d.ts.map +1 -0
  57. package/dist/checks/sitemap.js +140 -0
  58. package/dist/checks/types.d.ts +72 -0
  59. package/dist/checks/types.d.ts.map +1 -0
  60. package/dist/checks/types.js +20 -0
  61. package/dist/cli.d.ts +3 -0
  62. package/dist/cli.d.ts.map +1 -0
  63. package/dist/cli.js +125 -0
  64. package/dist/crawler/checkGroups.d.ts +7 -0
  65. package/dist/crawler/checkGroups.d.ts.map +1 -0
  66. package/dist/crawler/checkGroups.js +29 -0
  67. package/dist/crawler/crawlState.d.ts +7 -0
  68. package/dist/crawler/crawlState.d.ts.map +1 -0
  69. package/dist/crawler/crawlState.js +6 -0
  70. package/dist/crawler/defaults.d.ts +11 -0
  71. package/dist/crawler/defaults.d.ts.map +1 -0
  72. package/dist/crawler/defaults.js +10 -0
  73. package/dist/crawler/extractors.d.ts +5 -0
  74. package/dist/crawler/extractors.d.ts.map +1 -0
  75. package/dist/crawler/extractors.js +169 -0
  76. package/dist/crawler/jsAssets.d.ts +2 -0
  77. package/dist/crawler/jsAssets.d.ts.map +1 -0
  78. package/dist/crawler/jsAssets.js +22 -0
  79. package/dist/crawler/outputPaths.d.ts +14 -0
  80. package/dist/crawler/outputPaths.d.ts.map +1 -0
  81. package/dist/crawler/outputPaths.js +51 -0
  82. package/dist/crawler/reporter.d.ts +3 -0
  83. package/dist/crawler/reporter.d.ts.map +1 -0
  84. package/dist/crawler/reporter.js +12 -0
  85. package/dist/crawler/robots.d.ts +5 -0
  86. package/dist/crawler/robots.d.ts.map +1 -0
  87. package/dist/crawler/robots.js +29 -0
  88. package/dist/crawler/runCrawler.d.ts +22 -0
  89. package/dist/crawler/runCrawler.d.ts.map +1 -0
  90. package/dist/crawler/runCrawler.js +172 -0
  91. package/dist/crawler/schema.d.ts +87 -0
  92. package/dist/crawler/schema.d.ts.map +1 -0
  93. package/dist/crawler/schema.js +44 -0
  94. package/dist/crawler/secrets.d.ts +4 -0
  95. package/dist/crawler/secrets.d.ts.map +1 -0
  96. package/dist/crawler/secrets.js +70 -0
  97. package/dist/crawler/sitemap.d.ts +8 -0
  98. package/dist/crawler/sitemap.d.ts.map +1 -0
  99. package/dist/crawler/sitemap.js +105 -0
  100. package/dist/crawler/stack.d.ts +3 -0
  101. package/dist/crawler/stack.d.ts.map +1 -0
  102. package/dist/crawler/stack.js +1 -0
  103. package/dist/crawler/stackDetect.d.ts +24 -0
  104. package/dist/crawler/stackDetect.d.ts.map +1 -0
  105. package/dist/crawler/stackDetect.js +604 -0
  106. package/dist/crawler/tier1Extractors.d.ts +5 -0
  107. package/dist/crawler/tier1Extractors.d.ts.map +1 -0
  108. package/dist/crawler/tier1Extractors.js +255 -0
  109. package/dist/crawler/tracking.d.ts +8 -0
  110. package/dist/crawler/tracking.d.ts.map +1 -0
  111. package/dist/crawler/tracking.js +50 -0
  112. package/dist/crawler/urlUtils.d.ts +12 -0
  113. package/dist/crawler/urlUtils.d.ts.map +1 -0
  114. package/dist/crawler/urlUtils.js +53 -0
  115. package/dist/index.d.ts +14 -0
  116. package/dist/index.d.ts.map +1 -0
  117. package/dist/index.js +7 -0
  118. package/docs/agent-fixes.md +17 -0
  119. package/docs/agent-ready.md +44 -0
  120. package/docs/analyze.md +103 -0
  121. package/docs/check-api.md +142 -0
  122. package/docs/checks.md +116 -0
  123. package/docs/crawler.md +158 -0
  124. package/docs/index.md +24 -0
  125. package/docs/qc-checks-plan.md +796 -0
  126. package/docs/stack.md +99 -0
  127. package/package.json +86 -4
@@ -0,0 +1,796 @@
1
+ # QC Checks Implementation Plan
2
+
3
+ Plan to extend Flightcheck into a full quality-control tool for delivered and maintained
4
+ sites. Covers hreflang, robots.txt, stack detection, sitemap validation, legal pages,
5
+ image alt text, broken links, accessibility, security headers, DNS, and optional
6
+ performance auditing.
7
+
8
+ **Status:** in progress — Phase 1 shipped, Phase 2 mostly shipped
9
+ **Last updated:** 2026-09-28
10
+
11
+ ---
12
+
13
+ ## Goal
14
+
15
+ Add the checks below as modular, exportable functions (see `docs/check-api.md`) so they can:
16
+
17
+ - Run from the CLI (`pnpm run flightcheck`)
18
+ - Run independently via API endpoints in a consuming project
19
+ - Execute in parallel where dependencies allow
20
+
21
+ Each check produces structured JSON output and feeds into the Markdown TODO report via
22
+ `analyze`.
23
+
24
+ ---
25
+
26
+ ## Tiered audit strategy
27
+
28
+ Lighthouse is powerful but heavy (~30s+ per URL, requires Chrome). Most of its **SEO**
29
+ and **best-practices** signal is static HTML analysis — which Flightcheck already does
30
+ during crawl. Running Lighthouse on every page (or even as the primary a11y engine) is
31
+ the wrong default.
32
+
33
+ Instead, use four tiers:
34
+
35
+ | Tier | Engine | Chrome? | Covers |
36
+ | --- | --- | --- | --- |
37
+ | **0** | Crawl + analyze (existing) | No | Meta, OG, tracking, secrets, main landmark |
38
+ | **1** | Fetch + cheerio (implemented) | No | SEO depth, headers, DNS, links, robots, sitemap, legal, stack, static a11y hints |
39
+ | **2** | axe-core + Playwright (planned) | Yes, 3–5 URLs | Real WCAG a11y (contrast, focus, ARIA in rendered DOM) |
40
+ | **3** | Lighthouse or PageSpeed API (optional) | Yes / remote | Performance scores + Core Web Vitals only |
41
+
42
+ **Design principle:** SEO and best practices default to **Tier 0 + 1**. Accessibility
43
+ defaults to **Tier 1 static rules + Tier 2 sampled axe**. Lighthouse is **opt-in** for
44
+ client-facing performance reports.
45
+
46
+ ### What each tier replaces from Lighthouse
47
+
48
+ | Lighthouse category | Flightcheck approach |
49
+ | --- | --- |
50
+ | **SEO** | Tier 0/1 — crawl meta groups, hreflang, robots, sitemap, structured data (future) |
51
+ | **Best practices** | Tier 1 — security headers, HTTPS, mixed content, image alt, deprecated patterns |
52
+ | **Accessibility** | Tier 1 static + Tier 2 `@axe-core/playwright` on sampled URLs |
53
+ | **Performance** | Tier 3 only — Lighthouse or PageSpeed Insights API on sampled URLs |
54
+
55
+ ### Weight comparison
56
+
57
+ | Approach | Speed | Chrome? | Best for |
58
+ | --- | --- | --- | --- |
59
+ | Crawl + custom rules | Fast | No | SEO, metadata, links, infra |
60
+ | axe on crawled HTML (jsdom) | Fast | No | Structure-only a11y (partial) |
61
+ | `@axe-core/playwright` | ~5–15s × N URLs | Yes | Full a11y on samples |
62
+ | Lighthouse | ~30s+ × N URLs | Yes | CWV + performance score |
63
+ | PageSpeed Insights API | ~10–20s × N URLs | Remote | CWV without local Chrome |
64
+
65
+ ---
66
+
67
+ ## Current baseline
68
+
69
+ | Area | Today |
70
+ | --- | --- |
71
+ | Metadata / SEO | Crawl `meta`, `social`, `icons` groups + analyze report |
72
+ | Tracking | GTM, GA4, Matomo, etc. detection |
73
+ | Security | Secret scan in HTML + same-origin JS |
74
+ | Landmarks | `<main>` count |
75
+ | Infra | `robots-txt`, `security-headers`, `dns` checks implemented and reported |
76
+ | Sitemap | `sitemap` check implemented — validates and diffs against crawled URLs |
77
+ | Standalone checks | `security-headers`, `dns`, `robots-txt`, `sitemap`, `redirect` implemented; `lighthouse` registered but unsupported |
78
+
79
+ ---
80
+
81
+ ## Architecture
82
+
83
+ ### Two integration patterns
84
+
85
+ **Pattern A — Crawl extensions** (per-page data during crawl)
86
+
87
+ Best when the check inspects each HTML page:
88
+
89
+ - hreflang
90
+ - image alt (+ static a11y hints: headings, multiple h1, form labels, lang, positive tabindex)
91
+ - broken links (internal + optional external)
92
+ - stack fingerprints (per-page signals aggregated site-wide)
93
+
94
+ Add new check groups in `checkGroups.ts`:
95
+
96
+ ```text
97
+ meta, icons, social, tracking, security (existing)
98
+ links, images, hreflang, stack, a11y-static (new crawl groups)
99
+ ```
100
+
101
+ **Pattern B — Standalone checks** (origin-level or sampled URLs)
102
+
103
+ Best when the check hits one URL, DNS, or a small URL sample:
104
+
105
+ - `robots-txt`
106
+ - `sitemap`
107
+ - `legal-pages`
108
+ - `security-headers`
109
+ - `dns`
110
+ - `a11y-axe` (sampled URLs, Tier 2)
111
+ - `performance` (sampled URLs, Tier 3 — optional)
112
+
113
+ Each gets `src/checks/<id>.ts` + registry entry + analyze report section.
114
+
115
+ ### Execution waves (updated)
116
+
117
+ ```text
118
+ Wave 1 (parallel, no browser):
119
+ crawl, robots-txt, security-headers, dns, legal-pages
120
+
121
+ Wave 2 (after crawl, parallel where possible):
122
+ sitemap (compare with crawledUrls)
123
+ a11y-axe (3–5 sampled URLs via Playwright + axe-core)
124
+
125
+ Wave 3 (optional, after crawl):
126
+ performance (Lighthouse or PageSpeed API on same URL sample)
127
+
128
+ Wave 4:
129
+ analyze (merges all check outputs into TODO report)
130
+ ```
131
+
132
+ Wave 2 and 3 share a URL sampler (`src/checks/sampleUrls.ts`):
133
+
134
+ 1. Homepage (`startUrl`)
135
+ 2. One URL per unique `<meta name="template">` value
136
+ 3. Max 5 URLs (configurable)
137
+
138
+ ### Shared context
139
+
140
+ Extend `CheckOutputs` in `src/checks/types.ts`:
141
+
142
+ ```typescript
143
+ type CheckOutputs = {
144
+ crawl?: CrawlCheckResult;
145
+ analyze?: AnalyzeCheckResult;
146
+ robotsTxt?: RobotsTxtCheckResult;
147
+ sitemap?: SitemapCheckResult;
148
+ legalPages?: LegalPagesCheckResult;
149
+ a11yAxe?: A11yAxeCheckResult;
150
+ performance?: PerformanceCheckResult;
151
+ securityHeaders?: SecurityHeadersCheckResult;
152
+ dns?: DnsCheckResult;
153
+ };
154
+ ```
155
+
156
+ Crawl result gains optional site-level aggregates:
157
+
158
+ ```typescript
159
+ type CrawlCheckResult = {
160
+ records: VerificationRecord[];
161
+ crawledUrls: string[];
162
+ stack?: StackDetectionResult;
163
+ // ...
164
+ };
165
+ ```
166
+
167
+ ---
168
+
169
+ ## Check specifications
170
+
171
+ ### 1. Hreflang (`hreflang` — crawl group)
172
+
173
+ **Purpose:** Validate multilingual SEO linking.
174
+
175
+ **Per page, extract:**
176
+
177
+ - `<link rel="alternate" hreflang="…" href="…">`
178
+ - `<html lang="…">` as fallback context
179
+
180
+ **Rules:**
181
+
182
+ | Rule | Severity |
183
+ | --- | --- |
184
+ | Page has hreflang tags but no self-referencing entry | warning |
185
+ | `hreflang` value invalid (not BCP 47-ish) | warning |
186
+ | Target URL returns non-200 (optional fetch, rate-limited) | error |
187
+ | Missing return link (A→B but B→A missing) | error |
188
+ | No `x-default` on multilingual sites (≥2 languages detected site-wide) | warning |
189
+ | Duplicate `hreflang` for same page | error |
190
+
191
+ **Output:** `record.hreflang: { tags: HreflangTag[]; issues: HreflangIssue[] }`
192
+
193
+ **Analyze todos:**
194
+
195
+ - `Fix hreflang self-reference on page`
196
+ - `Add x-default hreflang in site layout`
197
+ - `Add return hreflang link: {lang} ↔ {url}`
198
+
199
+ ---
200
+
201
+ ### 2. Robots.txt (`robots-txt` — standalone check) — IMPLEMENTED
202
+
203
+ **Status:** Implemented in `src/checks/robots-txt.ts` (`runRobotsTxtCheck()`).
204
+
205
+ **Purpose:** Report robots.txt presence and common misconfigurations.
206
+
207
+ **Note:** The robots.txt report logic lives in the standalone `src/checks/robots-txt.ts`.
208
+ `src/crawler/robots.ts` is separate and only provides `isAllowed()` for `--respect-robots-txt`
209
+ crawl behaviour — it is **not** extended by this check.
210
+
211
+ **Fetch:** `{origin}/robots.txt`
212
+
213
+ **Rules:**
214
+
215
+ | Rule | Severity |
216
+ | --- | --- |
217
+ | robots.txt missing (404) | warning |
218
+ | robots.txt not parseable | error |
219
+ | `Disallow: /` for `User-agent: *` on production | critical |
220
+ | Sitemap directive missing | warning |
221
+ | Sitemap URL not reachable | error |
222
+ | Crawl-delay extremely high | review |
223
+
224
+ **Output:**
225
+
226
+ ```typescript
227
+ type RobotsTxtCheckResult = {
228
+ url: string;
229
+ status: number | null;
230
+ body?: string;
231
+ sitemapUrls: string[];
232
+ issues: Issue[];
233
+ };
234
+ ```
235
+
236
+ **Note:** Keep `--respect-robots-txt` as crawl behaviour; this check is **report-only**
237
+ by default.
238
+
239
+ ---
240
+
241
+ ### 3. Stack detection (`stack` — crawl group + aggregation) — IMPLEMENTED
242
+
243
+ **Status:** Implemented — per-page fingerprinting in `src/crawler/stackDetect.ts`,
244
+ site-wide aggregation in `src/analyze/stack.ts`.
245
+
246
+ **Purpose:** Report CMS, framework, and hosting signals for handover docs and debugging.
247
+
248
+ **Approach:** Heuristic fingerprinting (no Wappalyzer dependency initially). Aggregate
249
+ signals across all crawled pages; confidence score per detection.
250
+
251
+ **Signal sources:**
252
+
253
+ | Source | Examples |
254
+ | --- | --- |
255
+ | `<meta name="generator">` | WordPress, Sanity Studio |
256
+ | HTML comments / data attributes | `__NEXT_DATA__`, `data-sanity`, `data-nuxt` |
257
+ | Script/link paths | `/wp-content/`, `/_next/`, `/@vite/` |
258
+ | Response headers (once per origin) | `x-powered-by`, `server`, `x-vercel-id` |
259
+ | Cookie names | `__prerender_bypass`, `wp-settings` |
260
+ | Existing `<meta name="template">` | Already in crawl |
261
+
262
+ **Output:**
263
+
264
+ ```typescript
265
+ type StackDetectionResult = {
266
+ detections: Array<{
267
+ name: string;
268
+ category: "cms" | "framework" | "hosting" | "analytics" | "other";
269
+ confidence: "high" | "medium" | "low";
270
+ evidence: string[];
271
+ }>;
272
+ };
273
+ ```
274
+
275
+ **Analyze:** New `## Stack` section (informational, not necessarily todos unless
276
+ conflicting signals).
277
+
278
+ **Future:** Optional `@wappalyzer/wappalyzer-core` if heuristics are insufficient.
279
+
280
+ ---
281
+
282
+ ### 4. Sitemap (`sitemap` — standalone check) — IMPLEMENTED
283
+
284
+ **Status:** Implemented in `src/checks/sitemap.ts` (`runSitemapCheck()`).
285
+
286
+ **Purpose:** Verify sitemap exists and matches crawled site.
287
+
288
+ **Reuse:** `src/crawler/sitemap.ts` → `collectUrlsFromSitemap()`
289
+
290
+ **Inputs:**
291
+
292
+ - `startUrl` / origin
293
+ - `crawledUrls` from crawl output (normalized, no query/hash)
294
+ - Optional `robotsTxt.sitemapUrls` from robots check
295
+
296
+ **Rules:**
297
+
298
+ | Rule | Severity |
299
+ | --- | --- |
300
+ | No sitemap found (robots + `/sitemap.xml` both fail) | error |
301
+ | Sitemap URLs not reachable (4xx/5xx) | error |
302
+ | In sitemap but not crawled (and not excluded) | warning |
303
+ | Crawled but not in sitemap | warning |
304
+ | Over N% mismatch | error (configurable threshold, default 10%) |
305
+
306
+ **Dependency:** Runs in wave 2 after crawl when comparing lists.
307
+
308
+ ---
309
+
310
+ ### 5. Legal pages (`legal-pages` — standalone check)
311
+
312
+ **Purpose:** Confirm required legal pages exist and are linked.
313
+
314
+ **Configurable paths (defaults):**
315
+
316
+ ```typescript
317
+ const DEFAULT_LEGAL_PATHS = [
318
+ "/privacy", "/privacy-policy", "/privacybeleid",
319
+ "/cookies", "/cookie-policy", "/cookiebeleid",
320
+ "/terms", "/algemene-voorwaarden",
321
+ "/disclaimer",
322
+ ];
323
+ ```
324
+
325
+ **Rules:**
326
+
327
+ | Rule | Severity |
328
+ | --- | --- |
329
+ | No privacy page reachable | error |
330
+ | No cookie policy (EU/NL sites) | error |
331
+ | Linked from footer but 404 | critical |
332
+ | Page exists but not linked from any crawled page | warning |
333
+
334
+ ---
335
+
336
+ ### 6. Image alt + static a11y (`images`, `a11y-static` — crawl groups) — IMPLEMENTED
337
+
338
+ **Status:** Implemented in `src/crawler/tier1Extractors.ts`; output fields `images` and
339
+ `a11yStatic` are defined on `VerificationRecord` in `src/crawler/schema.ts`.
340
+
341
+ **Purpose:** Tier 1 accessibility and SEO — no browser required.
342
+
343
+ **Image alt (`images` group):**
344
+
345
+ - `<img>` without `alt` attribute
346
+ - `<img alt="">` on non-decorative images
347
+ - Generic alt text (`image`, `photo`, filename)
348
+
349
+ Issue messages include a clickable markdown link `[filename](url)` when the image URL can be
350
+ resolved from `src`, `srcset`, `<picture>` / `<source>` elements, or nested `/_next/image?url=…`
351
+ proxy URLs.
352
+
353
+ **Static a11y hints (`a11y-static` group):** implemented rule IDs:
354
+
355
+ - `heading-skip` — heading hierarchy skips a rank (h1 → h3)
356
+ - `multiple-h1` — multiple h1 elements
357
+ - `unlabeled-input` — form inputs without associated labels
358
+ - `missing-lang` — missing `lang` on `<html>`
359
+ - `positive-tabindex` — positive tabindex values
360
+
361
+ **Decorative image exclusions:**
362
+
363
+ - `aria-hidden="true"`, `role="presentation"`, ≤2px dimensions
364
+
365
+ **Output:** `record.images`, `record.a11yStatic: { issues[] }`
366
+
367
+ **Limitation:** Cannot detect color contrast, focus visibility, or JS-rendered widgets.
368
+ Those require Tier 2 axe.
369
+
370
+ ---
371
+
372
+ ### 7. Broken links (`links` — crawl group)
373
+
374
+ **Purpose:** Find internal broken links and optionally external failures.
375
+
376
+ **Fetch strategy:**
377
+
378
+ - Internal: check against crawl status map first; fetch uncached with concurrency limit
379
+ - External: optional (`--check-external-links`), HEAD request, timeout 10s, max N per site
380
+
381
+ **Rules:**
382
+
383
+ | Rule | Severity |
384
+ | --- | --- |
385
+ | Internal link → 404/500 | error |
386
+ | Internal link → redirect loop | error |
387
+ | External link → 4xx/5xx | warning |
388
+
389
+ ---
390
+
391
+ ### 8. Accessibility — axe (`a11y-axe` — standalone check, Tier 2)
392
+
393
+ **Purpose:** Real WCAG accessibility audit on a small URL sample.
394
+
395
+ **Why not Lighthouse for a11y:** Lighthouse embeds axe-core but adds performance overhead
396
+ and aggregates away detail. Running axe directly via Playwright is faster and gives
397
+ better violation output.
398
+
399
+ **Implementation:** `@axe-core/playwright`
400
+
401
+ ```typescript
402
+ import { AxeBuilder } from "@axe-core/playwright";
403
+
404
+ const results = await new AxeBuilder({ page })
405
+ .withTags(["wcag2a", "wcag2aa", "best-practice"])
406
+ .analyze();
407
+ ```
408
+
409
+ **Scope:** Same URL sampler as performance (max 5 URLs).
410
+
411
+ **Output per URL:**
412
+
413
+ ```typescript
414
+ type A11yAxePageResult = {
415
+ url: string;
416
+ violations: Array<{
417
+ id: string;
418
+ impact: "critical" | "serious" | "moderate" | "minor";
419
+ description: string;
420
+ helpUrl: string;
421
+ nodes: Array<{ html: string; target: string[]; failureSummary?: string }>;
422
+ }>;
423
+ passes: number;
424
+ incomplete: number;
425
+ };
426
+ ```
427
+
428
+ **Thresholds (configurable):**
429
+
430
+ - Any `critical` violation → error
431
+ - Any `serious` violation → warning
432
+
433
+ **Analyze:** `## Accessibility (axe)` section — group violations by rule ID across
434
+ sampled pages; todos reference WCAG rule and affected selector.
435
+
436
+ **Optional Tier 1 supplement:** Run axe-core against crawled HTML strings via jsdom for
437
+ extra coverage on all pages. Disable rules that need computed styles (`color-contrast`,
438
+ `link-in-text-block`, `target-size`, etc.). Document as `--a11y-static-axe` experiment;
439
+ not required for v1.
440
+
441
+ ---
442
+
443
+ ### 9. Performance (`performance` — standalone check, Tier 3, optional)
444
+
445
+ **Purpose:** Core Web Vitals and performance scores for client reports. **Not required
446
+ for delivery QC** — opt-in via `--checks performance` or `--profile delivery-full`.
447
+
448
+ **Scope:** Same URL sampler (max 5 URLs).
449
+
450
+ **Implementation options:**
451
+
452
+ | Option | Pros | Cons |
453
+ | --- | --- | --- |
454
+ | `lighthouse` + `chrome-launcher` | Full control, offline | Heavy, needs Chrome |
455
+ | PageSpeed Insights API | No local Chrome | API key, rate limits, sends URL to Google |
456
+ | `web-vitals` + Playwright | Lightweight CWV only | No full audit categories |
457
+
458
+ **Recommended default:** PageSpeed Insights API for CI/cloud; local Lighthouse for
459
+ offline/dev when Chrome is available.
460
+
461
+ **Output per URL:**
462
+
463
+ ```typescript
464
+ type PerformancePageResult = {
465
+ url: string;
466
+ source: "lighthouse" | "pagespeed-insights";
467
+ scores: { performance: number | null };
468
+ metrics: {
469
+ lcp?: string;
470
+ cls?: string;
471
+ inp?: string;
472
+ ttfb?: string;
473
+ fcp?: string;
474
+ };
475
+ opportunities?: Array<{ id: string; title: string; displayValue?: string }>;
476
+ };
477
+ ```
478
+
479
+ **Thresholds (configurable):**
480
+
481
+ - Performance score < 80 → warning
482
+ - LCP > 2.5s → warning
483
+ - CLS > 0.1 → warning
484
+
485
+ **Analyze:** `## Performance` section — scores table + top opportunities as todos.
486
+
487
+ **Note:** Do not duplicate SEO or a11y scores from Lighthouse here; those categories
488
+ are covered by Tier 0/1 and `a11y-axe`.
489
+
490
+ ---
491
+
492
+ ### 10. Security headers (`security-headers` — standalone check) — IMPLEMENTED
493
+
494
+ **Status:** Implemented in `src/checks/security-headers.ts` (`runSecurityHeadersCheck()`).
495
+
496
+ **Purpose:** Tier 1 best practices — verify HTTP security headers.
497
+
498
+ **Headers to check:**
499
+
500
+ | Header | Severity if missing |
501
+ | --- | --- |
502
+ | `Strict-Transport-Security` | error (if HTTPS) |
503
+ | `Content-Security-Policy` | warning |
504
+ | `X-Frame-Options` or CSP `frame-ancestors` | warning |
505
+ | `X-Content-Type-Options: nosniff` | warning |
506
+ | `Referrer-Policy` | review |
507
+ | `Permissions-Policy` | review |
508
+
509
+ Pure fetch — no browser needed.
510
+
511
+ ---
512
+
513
+ ### 11. DNS (`dns` — standalone check) — IMPLEMENTED
514
+
515
+ **Status:** Implemented in `src/checks/dns.ts` (`runDnsCheck()`).
516
+
517
+ **Purpose:** Record locations, ownership signals, and email security.
518
+
519
+ **Queries (Node `dns.promises` + optional `whoiser`):**
520
+
521
+ | Query | Data |
522
+ | --- | --- |
523
+ | A / AAAA | IP addresses |
524
+ | CNAME | CDN / hosting chain |
525
+ | MX / TXT / NS | Mail + SPF/DMARC |
526
+ | WHOIS (optional) | Registrar, expiry |
527
+
528
+ **Geo/locations:** Resolve A records → optional IP geolocation (informational).
529
+
530
+ **Note:** WHOIS opt-in (`dns: { whois: true }`).
531
+
532
+ ---
533
+
534
+ ### 12. Redirect (`redirect` — standalone check) — IMPLEMENTED
535
+
536
+ **Status:** Implemented in `src/checks/redirect.ts`. Probes the start URL (and www/non-www
537
+ alias), and when the origin is a pure redirect it drives **redirect-only mode** — short-circuiting
538
+ the pipeline to a reduced check set instead of a full crawl/analyze. Output feeds the
539
+ "Domain redirect" analyze section.
540
+
541
+ ---
542
+
543
+ ## Analyze report integration
544
+
545
+ Extend `src/analyze/report.ts` to consume a unified `FlightcheckReport` shape:
546
+
547
+ ```typescript
548
+ type FlightcheckReport = {
549
+ crawl: VerificationRecord[];
550
+ robotsTxt?: RobotsTxtCheckResult;
551
+ sitemap?: SitemapCheckResult;
552
+ legalPages?: LegalPagesCheckResult;
553
+ a11yAxe?: A11yAxeCheckResult;
554
+ performance?: PerformanceCheckResult;
555
+ securityHeaders?: SecurityHeadersCheckResult;
556
+ dns?: DnsCheckResult;
557
+ stack?: StackDetectionResult;
558
+ };
559
+ ```
560
+
561
+ The **Issues summary** and Tier 1 infra report sections (redirect, DNS, security headers,
562
+ robots.txt, sitemap, agent readiness) are implemented in `src/analyze/reportTier1.ts` and wired into the main
563
+ report via `src/analyze/report.ts`.
564
+
565
+ Report sections (top to bottom):
566
+
567
+ 1. Summary (pages analyzed, fix scopes, **Issues summary** by category: infra, SEO, a11y, best practices, performance)
568
+ 2. Security (secrets — existing)
569
+ 3. DNS (infra overview)
570
+ 4. Security headers
571
+ 5. Stack (informational)
572
+ 6. Robots.txt + sitemap
573
+ 7. Agent readiness
574
+ 8. Legal pages
575
+ 9. Accessibility (axe) — Tier 2
576
+ 10. Performance — Tier 3, if run
577
+ 11. Fix site-wide
578
+ 12. Fix in templates
579
+ 13. Fix per page (hreflang, images, static a11y, broken links)
580
+ 14. Issue coverage
581
+
582
+ ---
583
+
584
+ ## Implementation phases
585
+
586
+ ### Phase 1 — Tier 1 quick wins (1–2 weeks) — SHIPPED
587
+
588
+ No browser required. Covers most SEO + best practices.
589
+
590
+ | Task | Type | Files | Status |
591
+ | --- | --- | --- | --- |
592
+ | Security headers check | standalone | `src/checks/security-headers.ts` | done |
593
+ | Robots.txt check | standalone | `src/checks/robots-txt.ts` | done |
594
+ | DNS check | standalone | `src/checks/dns.ts` | done |
595
+ | Image alt + static a11y crawl groups | crawl | `src/crawler/tier1Extractors.ts`, `schema.ts` | done |
596
+ | Register checks + wave plan | infra | `types.ts`, `registry.ts`, `runner.ts` | done |
597
+ | Analyze sections | report | `src/analyze/report.ts`, `src/analyze/reportTier1.ts` | done |
598
+ | Tests + docs | | `*.test.ts`, `docs/check-api.md` | done |
599
+
600
+ **Deliverable:**
601
+
602
+ ```bash
603
+ pnpm run flightcheck -- https://example.com --checks crawl,security-headers,dns,robots-txt --parallel
604
+ ```
605
+
606
+ ---
607
+
608
+ ### Phase 2 — Crawl depth + infra (2–3 weeks) — MOSTLY SHIPPED
609
+
610
+ | Task | Type | Files | Status |
611
+ | --- | --- | --- | --- |
612
+ | Stack detection | crawl + aggregate | `src/crawler/stackDetect.ts`, `src/analyze/stack.ts` | done |
613
+ | Sitemap compare | standalone | `src/checks/sitemap.ts` | done |
614
+ | Agent readiness | standalone | `src/checks/agent-ready.ts` | done |
615
+ | Image alt + static a11y | crawl | `src/crawler/tier1Extractors.ts` | done |
616
+ | Broken links group | crawl | `src/crawler/links.ts` | not implemented |
617
+ | Hreflang group | crawl | `src/crawler/hreflang.ts` | not implemented |
618
+ | Legal pages | standalone | `src/checks/legal-pages.ts` | not implemented |
619
+ | URL sampler (shared) | util | `src/checks/sampleUrls.ts` | not implemented |
620
+ | Analyze integration | report | `src/analyze/report.ts` | done |
621
+
622
+ **Deliverable:** Full metadata + link + infra report without any browser.
623
+
624
+ ---
625
+
626
+ ### Phase 3 — Tier 2 accessibility (1 week)
627
+
628
+ | Task | Notes |
629
+ | --- | --- |
630
+ | Add `@axe-core/playwright`, `playwright` | Document browser install (`pnpm exec playwright install chromium`) |
631
+ | `runA11yAxeCheck()` | Uses shared URL sampler |
632
+ | Analyze a11y section | Group violations by rule; WCAG-linked todos |
633
+ | Smoke test | Mark `@slow`, skip in default CI |
634
+
635
+ **Deliverable:**
636
+
637
+ ```bash
638
+ pnpm run flightcheck -- https://example.com --checks crawl,a11y-axe --parallel
639
+ ```
640
+
641
+ ---
642
+
643
+ ### Phase 4 — Tier 3 performance, optional (1 week)
644
+
645
+ | Task | Notes |
646
+ | --- | --- |
647
+ | `runPerformanceCheck()` | Lighthouse or PageSpeed Insights adapter pattern |
648
+ | `--performance-provider lighthouse\|pagespeed` | Default `pagespeed` in CI, `lighthouse` locally |
649
+ | Analyze performance section | CWV metrics + opportunities only |
650
+ | Deprecate standalone `lighthouse` check ID | Rename registry entry to `performance` (not yet done — `lighthouse` is still the registered id with `implemented: false` in `src/checks/registry.ts`) |
651
+
652
+ **Deliverable:**
653
+
654
+ ```bash
655
+ pnpm run flightcheck -- https://example.com --profile delivery-full
656
+ # runs crawl + tier 1 + a11y-axe + performance on sampled URLs
657
+ ```
658
+
659
+ ---
660
+
661
+ ### Phase 5 — Polish & maintenance (ongoing)
662
+
663
+ - Baseline diff (compare to last approved JSON)
664
+ - External link checking toggle
665
+ - Config profiles: `delivery`, `delivery-full`, `maintenance`
666
+ - Per-client `.flightcheck.json` (legal paths, thresholds)
667
+ - API endpoint wiring guide for consuming project
668
+
669
+ ---
670
+
671
+ ## Profiles (CLI)
672
+
673
+ | Profile | Checks | Browser? |
674
+ | --- | --- | --- |
675
+ | `default` | crawl + security-headers + dns + robots-txt + sitemap + analyze (`DEFAULT_CHECK_IDS` in `src/checks/types.ts`) | No |
676
+ | `delivery` | crawl + all Tier 1 + sitemap + legal + a11y-axe + analyze | Yes, 3–5 URLs |
677
+ | `delivery-full` | `delivery` + performance | Yes, 3–5 URLs |
678
+ | `maintenance` | crawl + analyze + baseline-diff (future) | No |
679
+
680
+ ```bash
681
+ pnpm run flightcheck -- https://example.com --profile delivery
682
+ ```
683
+
684
+ ---
685
+
686
+ ## New check IDs (registry)
687
+
688
+ Currently registered in `src/checks/registry.ts`:
689
+
690
+ ```text
691
+ crawl (implemented)
692
+ analyze (implemented)
693
+ security-headers (implemented)
694
+ dns (implemented)
695
+ robots-txt (implemented)
696
+ sitemap (implemented)
697
+ lighthouse (registered, implemented: false — NOT yet renamed to performance)
698
+ ```
699
+
700
+ Still to add / rename:
701
+
702
+ ```text
703
+ legal-pages
704
+ a11y-axe
705
+ performance (optional Tier 3; planned to replace lighthouse — rename not done yet)
706
+ ```
707
+
708
+ Crawl groups:
709
+
710
+ ```text
711
+ links, images, hreflang, stack, a11y-static
712
+ ```
713
+
714
+ Updated wave plan:
715
+
716
+ ```typescript
717
+ // Wave 1
718
+ ["crawl", "robots-txt", "security-headers", "dns", "legal-pages"]
719
+
720
+ // Wave 2 (after crawl)
721
+ ["sitemap", "a11y-axe"]
722
+
723
+ // Wave 3 (optional)
724
+ ["performance"]
725
+
726
+ // Wave 4
727
+ ["analyze"]
728
+ ```
729
+
730
+ Still to do in `src/checks/types.ts` — rename `lighthouse` → `performance` (not yet done),
731
+ add `a11y-axe` and `legal-pages`. (`robots-txt` and `sitemap` are already present.)
732
+
733
+ ---
734
+
735
+ ## Dependencies (estimated)
736
+
737
+ | Package | Used by | Tier | Notes |
738
+ | --- | --- | --- | --- |
739
+ | None extra | security-headers, robots, dns, links, hreflang | 1 | fetch + cheerio |
740
+ | `@axe-core/playwright` | a11y-axe | 2 | Real a11y on sampled URLs |
741
+ | `playwright` | a11y-axe, performance (optional) | 2–3 | `playwright install chromium` |
742
+ | `lighthouse` + `chrome-launcher` | performance (local adapter) | 3 | Optional, heavy |
743
+ | PageSpeed Insights API | performance (remote adapter) | 3 | `GOOGLE_PAGESPEED_API_KEY` |
744
+ | `whoiser` | dns (optional) | 1 | WHOIS opt-in |
745
+
746
+ **Not recommended as dependencies:**
747
+
748
+ - Full Lighthouse for SEO/a11y — overlap with Tier 0/1/2
749
+ - `@capgo/seo-checker` — useful reference for rule ideas; crawl already covers live sites
750
+ - Wappalyzer — defer; heuristics sufficient initially
751
+
752
+ ---
753
+
754
+ ## Testing strategy
755
+
756
+ | Layer | Approach |
757
+ | --- | --- |
758
+ | Pure extractors | vitest + HTML fixtures |
759
+ | Standalone checks (Tier 1) | vitest + mocked `fetch` / DNS |
760
+ | Sitemap compare | Fixture XML + crawl URL sets |
761
+ | a11y-axe (Tier 2) | Smoke test on local HTML fixture server, `@slow` |
762
+ | performance (Tier 3) | Mock PageSpeed API response; Lighthouse smoke `@slow` |
763
+ | Integration | Golden-file report snapshot for eight.nl fixture |
764
+
765
+ ---
766
+
767
+ ## Open decisions
768
+
769
+ 1. **External links** — opt-in (recommended).
770
+ 2. **Performance provider** — PageSpeed API default in CI; Lighthouse for local dev (recommended).
771
+ 3. **WHOIS** — opt-in (recommended).
772
+ 4. **Legal page paths** — per-client `.flightcheck.json` (recommended).
773
+ 5. **axe on all crawled HTML via jsdom** — experiment later; Tier 2 sample is sufficient for v1.
774
+ 6. **Stack detection** — heuristics first; Wappalyzer as optional plugin later.
775
+
776
+ ---
777
+
778
+ ## Success criteria
779
+
780
+ A site like `https://eight.nl` run with `--profile delivery` should report:
781
+
782
+ - Missing Twitter/OG tags (Tier 0 — existing)
783
+ - robots.txt and sitemap status (Tier 1)
784
+ - Sitemap vs crawl diff (Tier 1)
785
+ - Privacy/cookie pages present and linked (Tier 1)
786
+ - Image alt gaps + static a11y hints on all pages (Tier 1)
787
+ - Broken internal links (Tier 1)
788
+ - Detected stack — Next.js + Sanity + Vercel (Tier 1)
789
+ - Security header gaps (Tier 1)
790
+ - DNS / SPF / DMARC overview (Tier 1)
791
+ - axe WCAG violations on homepage + template samples (Tier 2)
792
+ - Core Web Vitals on homepage + template samples (Tier 3, `--profile delivery-full` only)
793
+ - Single Markdown report ready for an AI agent or client handover doc
794
+
795
+ Without `--profile delivery-full`, the run completes **without Chrome** and still covers
796
+ the majority of delivery QC.