M1: initial scaffold — pipeline, specs, source, Dockerfile

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Paul O'Reilly
2026-04-17 23:52:40 +12:00
commit 8584def154
22 changed files with 1413 additions and 0 deletions

127
spec/dom-analysis.md Normal file
View File

@@ -0,0 +1,127 @@
# DOM Analysis Spec (WE-D)
## Overview
Extracts structured metadata from a loaded page using Playwright's `page.evaluate()`.
Provides the page structure data used in the report's "Page Structure" section.
Runs in the same browser session as the screenshots to avoid a second page load.
## Responsibilities
- Extract title, meta tags, canonical URL, Open Graph tags
- Count and validate heading hierarchy (H1H6)
- Audit images for missing alt text
- Audit links for external targets missing `rel="noopener"`
- Detect JSON-LD structured data
- Return a plain object (serialisable to JSON)
Delegates to: Playwright `page.evaluate()`
## Dependencies
Read [pipeline.md](pipeline.md) for output file layout.
Read [screenshot.md](screenshot.md) — DOM extraction reuses the same Playwright page.
## Data Model
```js
{
title: 'Example Domain',
titleLength: 14,
meta: {
description: null, // string or null
descriptionLength: null, // number or null
robots: 'index, follow', // string or null
viewport: 'width=device-width, initial-scale=1', // string or null
},
canonical: null, // string URL or null
openGraph: {
title: null,
description: null,
image: null,
type: null,
},
headings: {
h1: ['Example Domain'], // array of text content
h2: [],
h3: [],
h4: [],
h5: [],
h6: [],
skips: [], // e.g. ['H1→H3'] if H2 is absent between H1 and H3
},
images: {
total: 12,
missingAlt: 3, // count with no alt attribute or alt=""
missingAltSrcs: ['...'], // up to 5 example src values
},
links: {
total: 24,
external: 8,
externalNewTab: 4, // target="_blank"
externalNewTabNoOpener: 3, // target="_blank" without rel containing noopener
},
structuredData: [], // array of parsed JSON-LD objects (empty if none)
}
```
## Requirements
**WE-D-1:** Extract metadata in a single `page.evaluate()` call to minimise
round-trips. All DOM access happens inside the browser context.
**WE-D-2:** `meta.description` is `null` if the `<meta name="description">` tag
is absent or has an empty `content` attribute.
**WE-D-3:** Heading hierarchy skips are detected by comparing sequential heading
levels. If the level jumps by more than 1 (e.g., H1 → H3), record the skip as
`"H1→H3"` in `headings.skips`.
Why: Heading skips confuse screen readers and weaken SEO.
**WE-D-4:** An image is counted as `missingAlt` if it lacks the `alt` attribute
entirely OR if `alt` is an empty string `""`.
Why: Both mean the image is inaccessible to screen readers.
**WE-D-5:** A link is counted as `externalNewTabNoOpener` if it has `target="_blank"`
AND its `rel` attribute does not contain `noopener`.
Why: Without `noopener`, the opened page can access `window.opener` — a security risk.
**WE-D-6:** Structured data is extracted from all `<script type="application/ld+json">`
tags. Parse each; if parsing fails, include the raw text string instead of the object.
**WE-D-7:** The `missingAltSrcs` array contains at most 5 example `src` values
(truncated to 80 chars each) to keep the report concise.
**WE-D-8:** The result must be JSON-serialisable (no DOM nodes, functions, or circular refs).
**WE-D-9:** Run on the desktop viewport page (the same page used for the desktop screenshot).
Do not navigate again.
## Scenarios
### Scenario: Well-structured page
**Given:** Page with one H1, sequential headings, all images have alt, no blank-target links
**When:** `analyzeDom(page)` is called
**Then:** `headings.skips` is empty, `images.missingAlt === 0`, `links.externalNewTabNoOpener === 0`
### Scenario: Missing meta description
**Given:** Page with no `<meta name="description">` tag
**When:** DOM is analysed
**Then:** `meta.description === null`, `meta.descriptionLength === null`
### Scenario: Heading skip detected
**Given:** Page with H1 then H3 (no H2)
**When:** DOM is analysed
**Then:** `headings.skips` includes `"H1→H3"`
### Scenario: JSON-LD present
**Given:** Page with `<script type="application/ld+json">{"@type":"WebSite"}</script>`
**When:** DOM is analysed
**Then:** `structuredData` contains `[{ "@type": "WebSite" }]`