01 — Monitor
Every deploy re-fires a CrUX scan.
The GitHub push webhook on /api/deploys records the deploy, then immediately re-runs fetchAndPersistCwvfor the property’s pages — the same rescan trigger that ships in the deploy handler. INP, LCP, and CLS against the property’s last-known-good baseline, into a fresh CWV row tagged with the deploy SHA. No Friday-morning drift: a regression that lands in the release lands in the same window as the release.
In the product: operators see the deploy SHA as a commit badge on /properties/:id.
02 — Detect
Each scan is classified.
Every CrUX-triggered scan and every crawl run ends in a classified finding — a Finding.kind tag that decides whether the worker auto-drafts a fix or just files the regression.
In the product: operators see the classified findings in the list on /properties/:id, grouped on the latest crawl run.
- LOOP
A cycle in the redirect graph.
The crawler’s cycle-detection emits a Finding.kind of "redirect_loop" the moment it sees a hop repeat — /shop → /products → /shop. The fan-out downstream already keys on that kind.
- CANONICAL
A canonical the page is missing.
Pages without a self-referential <link rel="canonical"> land on the “canonical” finding kind — the complement to the redirect loop, surfaced through the same classification.
03 — Fix
Open redirect loops become a PR.
The deploy handler fan-out calls runRedirectPrForFinding on every open redirect_loopfinding: clone the property’s GitHub repo, write a fix/seo-* branch with the rewrite that breaks the cycle into a single direct → canonical hop, open the PR, and back-write Finding.htmlUrl so the open PR shows up in-product. The existing /api/pr/redirect handler is the manual one-off equivalent, gated by requireAuth() + per-property scoping, with idempotency on the same htmlUrl write-back so a second fan-out won’t double-open the PR.
In the product: operators see the PR link on the corresponding finding card on /properties/:id once a draft lands.
fix(redirects): break /shop → /products → /shop cycle on /shop-sale
- next.config.js+2 · −0
1 async redirects() {2+ { source: "/shop-sale", destination: "/shop", permanent: true },3 { source: "/shop", destination: "/products", permanent: true },
Wire it in
See how pricing fits a Friday release.
The plan picker maps straight onto the loop above: Solo runs the scan+draft fan-out on one property; Team pulls the cadence up; Agency unlocks cross-property dashboards. No scraped-contract quotes, no payment on file until you wire in your first property.