# contao-pagespeed 0.1.0

Page speed measurement (local Lighthouse, Google PageSpeed Insights, CrUX real-user data), a remote browser (REST, Playwright WebSocket, MCP), image/video optimisation with shareable preview links, and the playbooks (skills) to act on the results, for NCM / 2d.systems hotel websites.

## Auth

Every endpoint except the public ones (/health, /info, /info.json, /llms.txt, /openapi.json and media preview links) needs `Authorization: Bearer <token>` (or `?token=<token>` for clients that cannot set headers). Ask the operator for a token, then load `/info?token=<token>` to get ready-to-run snippets.

## Connect from Claude Code

```bash
claude mcp add --transport http pagespeed https://pagespeed.dev.2d.systems/mcp --header "Authorization: Bearer <token>"
claude mcp add --transport http pagespeed-browser https://pagespeed.dev.2d.systems/mcp/browser --header "Authorization: Bearer <token>"
```

Or install the plugin (skills + both MCP servers):

```
export PAGESPEED_TOKEN=<token>
/plugin marketplace add /home/twod/projects/contao-pagespeed
/plugin install contao-pagespeed@contao-pagespeed
```

## Which engine

- **local**: Lighthouse 13 in Chrome on our server. Repeatable, no quota, works on any reachable URL incl. basic-auth stages that let the "Chrome-Lighthouse" user agent through. Use it for before/after comparisons, with runs: 3 to smooth out noise.
- **psi**: Google PageSpeed Insights: Lighthouse on Google's infrastructure, the same number a client sees on pagespeed.web.dev. Also returns field data. Uses Google API quota and takes 15-40 s.
- **field**: Chrome UX Report (CrUX) real-user p75 values (LCP, INP, CLS, FCP, TTFB) and the Core Web Vitals assessment: what Google ranks on. Comes with every psi audit (field.url / field.origin); GET /field-history gives ~25 weekly points. Small sites often have none (reason: "no_crux_data"); field data only exists for live domains.

`POST /audits` (MCP `page_speed_audit`) defaults to `engine: "both"`, mobile, all four categories. Typical loop: audit, read the top opportunities and the LCP element, read the matching skill, fix, re-audit with `engine: "local", runs: 3`, compare with `GET /history`.

## Audit result shape

`{ id, status, finalUrl, lab: { local, psi }, field: { url, origin, reason }, reports }`. Each `lab.*` holds `scores` (0-1: performance, accessibility, bestPractices, seo), `metrics` (lcpMs, fcpMs, tbtMs, cls, siMs, ttfbMs), the top 8 `opportunities` with estimated `savingsMs`/`savingsBytes`, the `lcpElement` CSS selector and the per-run scores. A failed engine shows `{ error: { code, message } }` in its slot; status 422 means every requested engine failed.

## MCP

- `https://pagespeed.dev.2d.systems/mcp`: tools `page_speed_audit`, `get_audit`, `audit_history`, `field_data`, `field_history`, `screenshot`, `list_skills`, `get_skill`, `record_learning`, `list_learnings`, `resolve_learning`, `optimize_image`, `process_video`, `get_media_job`; every skill is also a resource `skill://<name>/SKILL.md`.
- `https://pagespeed.dev.2d.systems/mcp/browser`: Microsoft @playwright/mcp: browser_navigate, browser_snapshot, browser_click, browser_type, browser_evaluate, browser_network_requests, browser_take_screenshot, ... (one isolated browser per MCP session, closed when idle).

  - `page_speed_audit`: Run a page speed audit and wait for the result (up to 5 min). engine "both" (default) = local Lighthouse + Google PageSpeed Insights + CrUX real-user field data. Use engine "local" with runs: 3 for before/after comparisons. Returns scores, Core Web Vitals, top opportunities with savings, the LCP element and report links.
  - `get_audit`: Fetch an audit by id: status, scores, metrics, opportunities, field data, report links.
  - `audit_history`: Stored metric history for a URL, oldest first. source: local | psi | field (default all).
  - `field_data`: Current real-user CrUX field data (p75 LCP, INP, CLS, FCP, TTFB and the Core Web Vitals assessment) for a URL and its origin, via Google PSI.
  - `field_history`: Weekly CrUX p75 history (~25 weeks) for a URL or an origin.
  - `screenshot`: Screenshot a page, optionally emulating a Playwright device such as "iPhone 15" or "Pixel 7".
  - `list_skills`: List the playbooks (skills) hosted by this service. Read the matching one with get_skill before starting a task.
  - `get_skill`: Read a skill (SKILL.md) by name, including unreviewed field notes from recent projects.
  - `record_learning`: Record something a project taught you (gotcha, fix that worked or failed, measured effect) for a skill. It appears immediately as a field note in get_skill and is later curated into the playbook. Record learnings at the end of every page speed job.
  - `list_learnings`: List recorded learnings (default: pending) for curation, optionally for one skill.
  - `resolve_learning`: Curation: mark a learning merged (after committing it into the skill files) or rejected, with a note.
  - `optimize_image`: Optimise an image by URL: by default a responsive AVIF/WebP/JPEG set (480-2560w, never upscaled) with ready <picture> and preload snippets, or exact outputs. Returns public preview URLs, savings and a compare page to review before deploying. Use it on the LCP element or "Improve image delivery" findings of an audit.
  - `process_video`: Transcode a video by URL to MP4/WebM (quality high|balanced|small, maxWidth, trim, audio keep|drop) plus a poster frame. Waits up to 60 s, then returns the job; poll get_media_job. Returns public preview URLs and a <video> snippet.
  - `get_media_job`: Status and result of an image/video job (variants, savings, public URLs, snippets, compareUrl).

## Playwright over WebSocket

```js
import { chromium } from 'playwright'; // playwright@1.63.0 exactly
const browser = await chromium.connect('wss://pagespeed.dev.2d.systems/ws', { headers: { Authorization: 'Bearer <token>' } });
const page = await browser.newPage();
await page.goto('https://www.hintersee.at/de/');
```

## Media (images and video)

Optimise images (AVIF/WebP/JPEG, responsive srcset sets with ready `<picture>` and preload snippets) and videos (MP4/WebM, poster frames). Every job gets a public, unguessable compare page and file URLs (no token needed) that expire after the retention period, so you can drop them into a stage page or send them to a client. See `POST /media/images` and `POST /media/videos` below.

## Learnings: help the playbooks evolve

At the end of a job, record what the project taught you (MCP `record_learning` or `POST /learnings`): a gotcha, a fix that worked or failed, a measured effect, with the site and the numbers. It shows up immediately as a "field note" under the skill, and gets curated into the reviewed playbook later (skill `curate-skills`).

## Endpoints

### GET /health (public)

Liveness: child processes (Playwright run-server, Playwright MCP), queues and sessions. 503 when a child is down.

```bash
curl -s "https://pagespeed.dev.2d.systems/health"
```

### POST /audits

Run a page speed audit: local Lighthouse, Google PageSpeed Insights + CrUX field data, or both (default). Waits up to 5 min and answers 200 (done), 422 (every engine failed) or 202 (still running). Pass "wait": false to get 202 + id immediately and poll GET /audits/:id.

```bash
curl -s -X POST -H "Authorization: Bearer <token>" -H "content-type: application/json" "https://pagespeed.dev.2d.systems/audits" -d '{"url":"https://www.hintersee.at/de/","formFactor":"mobile","engine":"both","runs":1}'
```

### GET /audits

Recent audits, newest first. Filters: url, status (queued|running|done|failed), limit (default 20).

```bash
curl -s -H "Authorization: Bearer <token>" "https://pagespeed.dev.2d.systems/audits?url=https://www.hintersee.at/de/&limit=5"
```

### GET /audits/:id

One audit: status, scores, metrics, top opportunities, LCP element, field data, report links.

```bash
curl -s -H "Authorization: Bearer <token>" "https://pagespeed.dev.2d.systems/audits/<id>"
```

### GET /audits/:id/report.html

Full Lighthouse HTML report. ?engine=local (default) or psi.

```bash
curl -s -H "Authorization: Bearer <token>" "https://pagespeed.dev.2d.systems/audits/<id>/report.html?engine=local"
```

### GET /audits/:id/report.json

Raw Lighthouse result (local) or raw PSI response incl. field data (psi). ?engine=local|psi

```bash
curl -s -H "Authorization: Bearer <token>" "https://pagespeed.dev.2d.systems/audits/<id>/report.json?engine=psi"
```

### GET /history

Stored metric history for a URL, oldest first: scores, LCP/FCP/TBT/CLS/SI/TTFB, INP and CWV pass for field rows. source=local|psi|field (default all).

```bash
curl -s -H "Authorization: Bearer <token>" "https://pagespeed.dev.2d.systems/history?url=https://www.hintersee.at/de/&formFactor=mobile&source=local&limit=20"
```

### GET /field-history

Weekly real-user CrUX p75 history (~25 weeks) for a url or an origin, straight from Google (cached 12 h).

```bash
curl -s -H "Authorization: Bearer <token>" "https://pagespeed.dev.2d.systems/field-history?origin=https://www.hintersee.at&formFactor=mobile"
```

### POST /screenshot

Screenshot a page (PNG/JPEG). Optional device emulation ("iPhone 15", "Pixel 7"), viewport, fullPage.

```bash
curl -s -X POST -H "Authorization: Bearer <token>" -H "content-type: application/json" "https://pagespeed.dev.2d.systems/screenshot" -d '{"url":"https://www.hintersee.at/de/","device":"iPhone 15","fullPage":false}'
```

### POST /pdf

Print a page to PDF.

```bash
curl -s -X POST -H "Authorization: Bearer <token>" -H "content-type: application/json" "https://pagespeed.dev.2d.systems/pdf" -d '{"url":"https://www.hintersee.at/de/","format":"A4"}'
```

### POST /content

Rendered page after JavaScript: final URL, HTTP status, title, HTML, visible text.

```bash
curl -s -X POST -H "Authorization: Bearer <token>" -H "content-type: application/json" "https://pagespeed.dev.2d.systems/content" -d '{"url":"https://www.hintersee.at/de/"}'
```

### POST /run

Run a Playwright script in a fresh browser context: body of async ({ page, context, browser }) => {...}. Returns the JSON result and console output.

```bash
curl -s -X POST -H "Authorization: Bearer <token>" -H "content-type: application/json" "https://pagespeed.dev.2d.systems/run" -d '{"url":"https://www.hintersee.at/de/","script":"return await page.$$eval('img', (imgs) => imgs.map((i) => ({ src: i.currentSrc, loading: i.loading })));"}'
```

### GET /skills

Skills (playbooks) hosted here: name, description, supporting files, number of pending field notes.

```bash
curl -s -H "Authorization: Bearer <token>" "https://pagespeed.dev.2d.systems/skills"
```

### GET /skills/:name

A skill's SKILL.md (Markdown) plus its unreviewed field notes. Read it before working on the matching task. ?notes=false for the reviewed text only.

```bash
curl -s -H "Authorization: Bearer <token>" "https://pagespeed.dev.2d.systems/skills/<name>?notes=true"
```

### GET /skills/:name/files/*

A supporting file of a skill (scripts etc.), path as listed in GET /skills.

```bash
curl -s -H "Authorization: Bearer <token>" "https://pagespeed.dev.2d.systems/skills/<name>/files/<file>"
```

### POST /learnings

Record a learning from a project (a gotcha, a fix that worked or did not, a number). It shows up immediately as a "field note" in the skill and waits for curation into the playbook.

```bash
curl -s -X POST -H "Authorization: Bearer <token>" -H "content-type: application/json" "https://pagespeed.dev.2d.systems/learnings" -d '{"skill":"wordpress-pagespeed","title":"Lapsed RocketCDN keeps rewriting asset URLs","body":"When the RocketCDN subscription ends, WP Rocket still rewrites assets to rocketcdn.me, which 301s back to origin. Check one asset URL and switch CDN off.","site":"montafonerhof.com","cms":"wordpress","category":"caching","evidence":"every asset +1 redirect for months"}'
```

### GET /learnings

Recorded learnings, oldest first. Filters: skill, status (pending|merged|rejected|all, default pending), limit.

```bash
curl -s -H "Authorization: Bearer <token>" "https://pagespeed.dev.2d.systems/learnings?skill=pagespeed-playbook&status=pending"
```

### PATCH /learnings/:id

Curation: mark a learning merged (after committing it into the skill) or rejected, with a note (e.g. the commit).

```bash
curl -s -X PATCH -H "Authorization: Bearer <token>" -H "content-type: application/json" "https://pagespeed.dev.2d.systems/learnings/<id>" -d '{"status":"merged","note":"merged into pagespeed-playbook §1 in commit abc123"}'
```

### POST /media/images

Optimise an image from a URL (JSON) or an upload (multipart: file, options JSON). Default: responsive set 480-2560w (never upscaled) in AVIF, WebP and JPEG plus ready <picture> and preload snippets. Or pass options.outputs for exact variants ({format, quality, width, height, fit}). Returns public preview URLs and a compare page. Upload: curl -F file=@hero.jpg -F 'options={"responsive":{"widths":[768,1440]}}' ...

```bash
curl -s -X POST -H "Authorization: Bearer <token>" -H "content-type: application/json" "https://pagespeed.dev.2d.systems/media/images" -d '{"source":{"url":"https://www.hintersee.at/files/header/hero.jpg"},"options":{"responsive":{"widths":[768,1440,1920]},"sizes":"100vw"}}'
```

### POST /media/videos

Transcode a video (URL or multipart upload) to MP4 (H.264) and/or WebM (VP9) with quality high|balanced|small, optional maxWidth, trim {start, duration}, audio keep|drop, and a poster frame (JPEG + WebP). Asynchronous by default: returns 202 + job; poll GET /media/jobs/:id or open the compareUrl.

```bash
curl -s -X POST -H "Authorization: Bearer <token>" -H "content-type: application/json" "https://pagespeed.dev.2d.systems/media/videos" -d '{"source":{"url":"https://www.salzkammergut-aktiv.at/files/video/header.mp4"},"options":{"formats":["mp4","webm"],"quality":"balanced","maxWidth":1920,"audio":"drop"}}'
```

### GET /media/jobs/:id

Media job status and result: original, variants (bytes, dimensions, savings %, public URLs), posters, snippets, compareUrl, expiry.

```bash
curl -s -H "Authorization: Bearer <token>" "https://pagespeed.dev.2d.systems/media/jobs/<id>"
```

### GET /media/:id/ (public)

Public compare page of a media job (no token): original next to every variant with size and savings, slider comparison, snippets. Link is unguessable and expires.

```bash
curl -s "https://pagespeed.dev.2d.systems/media/<id>/"
```

### GET /media/:id/:file (public)

Public file of a media job (no token, expires): original, variants and posters. Supports Range requests for video.

```bash
curl -s "https://pagespeed.dev.2d.systems/media/<id>/<file>"
```

### POST /mcp

MCP server (streamable HTTP, stateless): page speed, field data, screenshots, media and skills tools. Add with: claude mcp add --transport http pagespeed <base>/mcp --header "Authorization: Bearer <token>"

### GET /info (public)

Start here: everything an agent needs to connect and work (Markdown). Add ?token=<token> to get snippets with your token filled in.

### GET /llms.txt (public)

Same as /info.

### GET /info.json (public)

The /info content as JSON.

```bash
curl -s "https://pagespeed.dev.2d.systems/info.json"
```

### GET /openapi.json (public)

OpenAPI 3 description generated from the route schemas.

```bash
curl -s "https://pagespeed.dev.2d.systems/openapi.json"
```

### GET /ws

Raw Playwright browser over WebSocket (one Chromium per connection, closed after 10 min). Client must use playwright@1.63.0 exactly: chromium.connect('<base-ws>/ws', { headers: { Authorization: 'Bearer <token>' } })

## Skills

Playbooks for this kind of work. Read the relevant one before changing a site.

- **contao-pagespeed** (https://pagespeed.dev.2d.systems/skills/contao-pagespeed): PageSpeed, accessibility and safe-deploy playbook for NCM Contao 4.9 sites, worked out on the DAS Hintersee site (www.hintersee.at, stage dashintersee.contao9.ncm.at) and other NCM Contao sites built on the same stack (extassets ATF CSS, ncmSeasonSwitch header slider, co_block cookie blocker, GtmHelper). Use when asked to improve PageSpeed/Lighthouse scores, LCP, render-blocking CSS/JS, images/WebP/AVIF, fonts, accessibility audits, or to deploy changes from stage to live.
- **curate-skills** (https://pagespeed.dev.2d.systems/skills/curate-skills): How to merge agent-recorded learnings (field notes) into the reviewed page speed skills in ~/projects/contao-pagespeed/skills - dedupe, resolve contradictions, keep SKILL.md concise, put site detail into case files, commit, and mark learnings merged. Use when asked to curate, update or clean up the skills/playbooks, or when list_skills shows many pending learnings.
- **page-speed-service** (https://pagespeed.dev.2d.systems/skills/page-speed-service): How to measure and improve page speed with the NCM PageSpeed service (pagespeed.dev.2d.systems) - local Lighthouse, Google PageSpeed Insights, CrUX real-user data, history, screenshots, a remote browser, and image/video optimisation with preview links. Use whenever you need PageSpeed/Lighthouse/Core Web Vitals numbers, before/after comparisons, a browser, or optimised images/videos for a site.
- **pagespeed-playbook** (https://pagespeed.dev.2d.systems/skills/pagespeed-playbook): CMS-independent method and fix catalogue for improving PageSpeed / Lighthouse / Core Web Vitals on hotel websites (Contao, WordPress, others). Ordered by impact, every item backed by measured results from real NCM projects. Use at the start of any page speed job, then switch to the CMS skill (contao-pagespeed, wordpress-pagespeed) for stack specifics.
- **wordpress-pagespeed** (https://pagespeed.dev.2d.systems/skills/wordpress-pagespeed): WordPress-specific PageSpeed work for NCM hotel sites, especially WP Rocket (cache exclusions, mobile cache, RocketCDN, RUCSS, auto LCP preload), theme asset dequeuing, reCAPTCHA scoping with ACF forms, Seekda widget deferral, and safe stage→live porting. Use with pagespeed-playbook when the site runs WordPress.

## Limits

- localAuditsInParallel: 2
- psiCallsInParallel: 4
- maxQueuedJobsPerQueue: 20
- browserSessions: 5
- sessionMaxMinutes: 10
- auditWaitSeconds: 300
- lighthouseRunsPerAudit: 1-5
- reportRetentionDays: 30
- mediaRetentionDays: 14
- Google API key configured: yes

Machine-readable: `/info.json`, OpenAPI: `/openapi.json`.
