---
name: ai-site-review
description: "Review a site or landing page generated with AI (Claude, v0, Lovable, Bolt, DCLogic/Strapi bundles, Framer AI, Cursor, etc.) before it goes live, so it doesn't just look good: crawlability (empty JS shell), indexability (meta, OG/Twitter, canonical, robots.txt, sitemap.xml), PageSpeed/Core Web Vitals, accessibility, Lighthouse 'Agentic Browsing' / 'Navegación con agentes', analytics (GTM/GA4), favicons, forms that really submit, Tailwind breakpoints (320–1536) + cross-browser, exposed secrets, and content verification (hallucinated data). Produces a checklist report with severity, evidence, proposed fixes and a verdict. Use whenever the user asks to review, audit, QA or 'check' an AI-built site/landing, asks if a generated page is ready to publish or indexable, or says 'revisa este sitio/landing hecho con IA'. For a pure content QA handoff to a client use revision-qa-sitios; for a git diff use code-review-qa."
---

# AI Site Review

AI builders are good at the part you can see in a screenshot and bad at everything else. The typical
generated site looks finished but:

- ships **an empty HTML shell** (`<div id="root"></div>`) that only becomes a page after JS runs, so
  Google renders it late or partially, while AI crawlers (GPTBot, ClaudeBot, Perplexity), link previews
  (WhatsApp, Slack, LinkedIn) and Bing usually get **nothing**;
- has meta/OG tags injected by JS (react-helmet, `<helmet>`), so they don't exist for the same bots;
- has a form that shows "¡Gracias!" without sending anything, or sends to a placeholder endpoint;
- invents content: stats, testimonials, client logos, certifications, addresses, phone numbers;
- has no favicon set, no sitemap, no robots.txt, no analytics, `noindex` left over from the preview;
- loads 4 MB of PNGs, unpinned CDN scripts, and fonts from three places;
- uses `<div onClick>` instead of buttons, which breaks screen readers and AI agents alike;
- leaks a key in the bundle or serves `/.env` or `/.git/` from the docroot.

The job is to answer **"can this go live and do its job (be found, load fast, convert, be trusted)?"**
and to give the team a checklist with the fix for each "no".

Measure, don't eyeball. Every finding carries a number, a URL or `file:line`, and a proposed fix.
"Sin verificar" is a valid result. A guess is not.

`references/failure-catalog.md` is the source of truth for each failure's cause, rule and fix, and for
the house standards (breakpoints, 80% empty-shell gate, head set). The `ship-ready-frontend` skill reads
the same file while building, so keep rules there, not duplicated here.

## Paths, tools and agents (Claude Code and OpenCode)

- `$SKILL_DIR` = the folder containing this SKILL.md. Claude Code shows it as "Base directory for this
  skill"; OpenCode lists it as the skill's location. If you don't have it:
  `SKILL_DIR=$(dirname "$(find ~/.claude/skills ~/.config/opencode/skills ~/.agents/skills .claude/skills .opencode/skills -path '*ai-site-review/SKILL.md' 2>/dev/null | head -1)")`
- `$OUT` = a temp folder for reports and screenshots: your agent's scratchpad if it has one, otherwise
  `OUT=$(mktemp -d)`. Never write output into the project being reviewed.
- Browser tools are named as the chrome-devtools MCP names them (`take_snapshot`, `lighthouse_audit`,
  `resize_page`…). Use the Playwright MCP equivalent if that's the one available, or the scripts alone.
- To ask the user: the question tool if your agent has one (AskUserQuestion / question), else a message.

## Setup (once per machine)

- Python 3 and Node 18+ on the PATH, Google Chrome installed.
- `npm i -g playwright lighthouse`. `render_check.mjs` finds the global Playwright and uses the
  installed Chrome, so no browser download is needed; `lighthouse` is the CLI for Performance (the
  chrome-devtools MCP `lighthouse_audit` excludes Performance). Only WebKit/Firefox checks need `npx playwright install webkit firefox`.
- Your own Playwright scripts: if the bundled browser isn't installed, launch with `chromium.launch({ channel: 'chrome' })` (the scripts here already fall back to it).
- **What opens a browser:** `render_check.mjs`, `form_check.mjs` and `lighthouse` run a *headless* Chrome, so nothing
  appears on screen, and subagents run in the background. Add `--headed` to the two scripts to watch them. The
  chrome-devtools MCP tools (`take_snapshot`, `lighthouse_audit`, `performance_start_trace`, `list_console_messages`)
  drive a visible Chrome: prefer them for the interactive checks when they are available, and say in the report which
  path ran. MCP tools only accept `filePath`/`outputDirPath` inside the workspace: omit them to get the result inline.
- Optional: the chrome-devtools or Playwright MCP for interactive checks. Without them, use the scripts
  and `npx lighthouse` (below).

Check it with `node $SKILL_DIR/scripts/render_check.mjs https://example.com --widths 375 --out $OUT/rc`.
If it says "playwright not found", run the install above.

Scripts (write output to `$OUT`):
- `scripts/crawl_check.py <url>`: no-JS bot view, head/meta, robots, sitemap, redirects, exposed files.
  Stdlib only.
- `scripts/render_check.mjs <url> --out <dir> [--browser webkit] [--widths …] [--no-axe]`: no-JS vs
  rendered word ratio, text invisible without JS, overflow offenders + full-page screenshots per
  breakpoint, horizontal scroll sampled while scrolling down (reveal start states), **content parity**
  (blocks visible at the widest width but missing at a narrower one fail), text spilling out of its card,
  axe-core serious/critical violations at 375 and 1280. Needs
  the `playwright` package (project, global, or `NODE_PATH`). Uses the installed Chrome if Playwright's
  chromium isn't downloaded. If Playwright isn't available, use snippets §5 through the MCP browser.

## Phase 0 — Inputs (ask once, in one message)

Ask once, with the question tool if available, and ask only what you can't find yourself:

1. **URL(s)**: production, preview, or local. Is the preview supposed to be indexable? (Usually no:
   then `noindex` there is correct and must be removed at launch.) **Ask whether a local URL already
   serves the project** (Valet/Herd `https://<project>.test`, MAMP, Docker) before starting a server in
   the scratchpad. Use it for rendering, e2e and Lighthouse; Valet/Herd run nginx and ignore `.htaccess`
   (and return 404s/soft-404s of their own), so server rules are checked on Apache, the real host or a
   plain static server (failure-catalog §7, cPanel).
2. **Repo path** if there is one, and the stack (detect from manifests if you have the path).
3. **Sources of truth for content**: current site, brochure, approved copy doc, the client's Google
   Business profile. Without sources you can only flag what is *unverifiable*, not what is wrong.
4. **Forms**: where should leads land (the client's own API or CRM, a webhook, HubSpot, SMTP/email)?
   Read the handler before asking; often the code answers it. **Can we submit one real test lead?**
   Default: no; mock only.
5. **Standards**: the house set in failure-catalog applies by default (full OG/Twitter, `keywords`,
   `X-UA-Compatible`, Tailwind breakpoints, Mexico: `es-MX`/`es_MX`, LFPDPPP privacy notice). Anything
   out of scope? Another country or language?
6. **Local build**: if the review is of a repo and the build needs secrets (`.env`, a CMS/Strapi URL,
   SMTP), ask for the preview URL or the env file. Never invent values to make it build. A repo that
   doesn't build as committed is itself a finding.
   When reading source to explain a finding, read the branch that's actually deployed
   (`git fetch` + `git show origin/<branch>:<file>`, read-only). A local checkout can be behind it.

If nobody answers, or you run non-interactively (subagent, CI), don't wait: assume reasonable defaults, write
them in the report's scope section as *supuestos*, and go on.

Reviewing a copy that will replace a live page (a redesign, a client or AI re-delivery)? Also run
[Replacing a live page](#replacing-a-live-page-parity), which is where the worst regressions hide.

## Phase 1 — Crawler view (no JS): the empty-shell test

This is the first thing to run, because it decides how serious everything else is.

```bash
python3 -I $SKILL_DIR/scripts/crawl_check.py <url>
```

Word counts differ by design: `crawl_check` counts words in the raw HTML text, `render_check` counts `innerText`; only
`render_check`'s ratio is gated. Don't compare the two numbers.

It fetches the raw HTML as a browser, Googlebot, GPTBot and ClaudeBot and checks: visible word count
(<50 = empty shell), H1, title, lang, every required meta/OG/Twitter tag, canonical == og:url == served
URL, favicons reachable, og:image reachable and absolute, GTM/GA4 present, JSON-LD parses, http→https
and trailing-slash redirects, real 404s (not soft-404 SPAs), robots.txt (not blocking `*`, declares the
sitemap, which AI bots it blocks), sitemap.xml (canonical listed with an *exact* match), `/llms.txt`,
publicly readable `/.env` `/.git/*` `/package.json`, and security headers. Run it once per page template. Checking a local or preview copy of a site that will live elsewhere? Add
`--prod-url https://prod.example.com/path/`: canonical is compared with that URL instead of the served one,
and a not-yet-deployed og:image is read from the served copy. Checks that can only be proven after deploy
(`noindex` on a preview, redirects, headers, HSTS) come out as INFO/WARN: list them under
*Tras deploy*, they are not failures of the copy.

Then `node $SKILL_DIR/scripts/render_check.mjs <url> --out $OUT/render`
for the ratio: **no-JS text must be ≥ 80% of rendered text**, and no text may be invisible without JS.
A ratio **above 100%** is fine: raw HTML holds text that isn't visible when rendered (hidden tab panels,
templates). It means "nothing missing without JS", not a measurement error.
Below 80% is 🔴 if the page's purpose depends on search/sharing (any public landing), with the
missing sections named. Fix options, best first:

1. **SSG/SSR**: Next.js/Astro/Nuxt static export, `vike`, or prerender at build (`react-snap`,
   `prerender-spa-plugin`, Astro islands). Right answer for any new build.
2. **Static shell duplicating the full page** (the `new-landing-page` skill pattern for DCLogic bundles):
   real HTML for every section, swapped out once the JS app mounts. Must be the **whole page**, not just
   the hero: if the app fails to mount (blocked CDN, JS error, no trailing slash), whatever isn't in the
   shell doesn't exist.
3. **Move SEO tags out of JS** into the real `<head>`, at minimum, even if the body stays client-rendered.

Also ask: what happens if the main JS fails? Block the bundle and look. Unpinned CDN dependencies
(`unpkg.com/react`, `@latest`) are a 🟠: the page can break on someone else's release.

## Phase 2 — Head, meta and sharing

Verify both **raw** (Phase 1) and **rendered** (`references/browser-snippets.md` §1). A tag that exists
only when rendered is a finding.

| Item | Standard |
|---|---|
| `<html lang>` | Set to the page language (`es-MX`, `es`, `en`). |
| `<meta charset="utf-8">` | First thing in `<head>`. |
| `viewport` | `width=device-width, initial-scale=1`. No `maximum-scale=1` / `user-scalable=no` (a11y fail). |
| `X-UA-Compatible` | `IE=edge`. House rule. Harmless legacy, report missing as 🟡. |
| `<title>` | Unique per page, ~30–60 chars, brand at the end, no template leftovers ("Vite + React", "Bundled Page", "My App"). |
| `description` | Unique, ~70–160 chars, actually describes this page. |
| `keywords` | House rule. Google ignores it; report missing as 🟡, never as a ranking issue. |
| `canonical` | Absolute, final URL, same trailing-slash convention as `og:url` and sitemap. Not pointing at the preview domain or at the source the AI copied from. |
| `robots` | No `noindex` on production. Check the `X-Robots-Tag` header too. |
| `og:locale` `og:title` `og:description` `og:url` `og:site_name` `og:type` `og:image` `og:image:width` `og:image:height` | All present. og:image absolute HTTPS, 1200×630, <~300 KB, self-hosted or on a host that will outlive the project. og:url == canonical. `og:locale` in `language_TERRITORY` format: `es_MX`, not `mx` or `es-MX`. |
| `twitter:card` `twitter:title` `twitter:description` `twitter:image` (+ `twitter:site`) | `summary_large_image`. Title/description fall back to OG, but the team checks them explicitly. |
| Favicons | `favicon.ico` at the root (browsers and Google request it blindly), an SVG or 32px PNG `rel=icon`, `apple-touch-icon` 180×180, optionally `manifest.webmanifest` with 192/512 icons. Google Search shows the favicon only if it's ≥48px and crawlable. Not the framework default (Vite logo, Next triangle, Lovable heart). |
| `theme-color` | Brand color. 🟡 |
| JSON-LD | `Organization` / `LocalBusiness` / `Service` / `FAQPage` matching visible content, with real `url`, `logo`, `sameAs`, address and phone. Validate with Rich Results Test. Fake reviews/ratings in schema are a 🔴 (manual-action risk). |
| `hreflang` | Only if multilingual. Must be reciprocal. |
| Share preview | Paste the URL in the Facebook Sharing Debugger / LinkedIn Post Inspector, or at least confirm the og:image URL returns `image/*` with 200. |

## Phase 3 — Indexability and discoverability

- `robots.txt` at the domain root: 200, `text/plain`, doesn't `Disallow: /` for `*`, declares `Sitemap:`
  with an absolute URL. Blocking AI crawlers (GPTBot, ClaudeBot, Google-Extended) is a business
  decision: ask, don't assume.
- `sitemap.xml`: 200, valid XML, every canonical URL listed **exactly** (protocol, host, trailing slash),
  no redirected/404/noindex URLs, `<lastmod>` real. Static file or generated (`next-sitemap`,
  `@astrojs/sitemap`, `spatie/laravel-sitemap`): if dynamic, confirm new pages actually appear.
- Status codes: real 404 for unknown URLs (SPAs often return 200 + "Not found" = soft-404), one 301 hop
  for http→https, www↔apex, and the trailing-slash variant (and it must not downgrade to http). No
  redirect chains, no 302 where 301 is meant.
- Internal links are real `<a href>`, not `onClick={navigate}`. Crawlers don't click.
- Preview/staging domains are `noindex` or password-protected, and production isn't. Search
  `site:<preview-domain>` if in doubt.
- After launch: Search Console property, submit sitemap, URL Inspection → "View crawled page" to see
  exactly what Google rendered. Put this in the post-launch checklist.

## Phase 4 — Analytics and tags

- GTM container (or GA4 directly, not both unless intended) in `<head>` + `<noscript>` iframe right
  after `<body>`. The correct ID for this property, which AI tools often paste as `GTM-XXXX` or reuse
  from another project.
- **It actually fires**: `/g/collect` requests with `en=page_view` (snippets §2). Installed but silent
  usually means an unpublished container or consent blocking.
- Conversions: a form submit / WhatsApp click / booking pushes a `dataLayer` event the container listens
  for. Generate one (mocked submit is fine) and watch `dataLayer`.
- Cookie/consent banner if the client needs one. Third-party pixels the client didn't ask for get removed.

## Phase 5 — Performance and Core Web Vitals

Measure Performance with the Lighthouse CLI (the chrome-devtools MCP `lighthouse_audit` excludes
Performance; use it only for the other categories): `lighthouse <url> --output=json --output-path=$OUT/lh.json --chrome-flags="--headless=new"`,
plus `--preset=desktop` for desktop, **and** PageSpeed
Insights on the live URL. PSI also shows CrUX field data, which is the real CWV once the site has
traffic. Targets: LCP ≤ 2.5 s, CLS ≤ 0.1, INP ≤ 200 ms (field), Performance ≥ 90 mobile as a goal.
Take Lighthouse scores from a **plain static server or the real host**, not from a dev server that answers unknown
paths with HTML (Laravel Valet, some SPA fallbacks): `/llms.txt` and the manifest then come back as malformed files and
Agentic Browsing drops to 50, and an http host fails `is-on-https` (Best Practices ~56). Run **at least two passes and
read the LCP element** (`lcp-breakdown-insight`): if it is a decorative background `<video>` the score swings by 15+
points between runs (see failure-catalog §4). A PSI link someone sends you may be stale: check its timestamp and rerun. On a preview host, Lighthouse SEO
drops (~69) because `is-crawlable` fails on the intentional `noindex`: report the other SEO audits, not
the score.

Then find the *cause* with `references/browser-snippets.md` §3 and a trace
(`performance_start_trace` → `performance_analyze_insight`):

- **LCP**: the element must be in the raw HTML, not lazy-loaded, `fetchpriority="high"`, preloaded if
  it's a CSS background, served at display size in WebP/AVIF. JS-rendered LCP traces back to Phase 1.
- **CLS**: `width`/`height` or `aspect-ratio` on images, iframes and embeds; reserved space for
  reCAPTCHA, Cal.com and cookie bars; `font-display: swap` + size-adjusted fallback; the static-shell →
  app swap shifting at ~1 s.
- **Weight**: images >2× their rendered size (resize to 2× max; `magick in -resize WxH -strip out`),
  PNG photos, total transfer, unused JS/CSS (Lighthouse "Reduce unused…"), whole icon libraries
  imported for 5 icons, Babel-standalone or dev builds in production.
- **Fonts**: self-host woff2, subset, ≤2 families, `preconnect` only to origins really used. AI
  scaffolds leave stale `fonts.googleapis.com` preconnects next to self-hosted fonts.
- **Third parties**: chat widgets, maps, video embeds → facade/lazy-load below the fold.
- **Caching/compression**: `Cache-Control: immutable` on hashed assets, `no-cache` on HTML, gzip/brotli
  on (check the `content-encoding` header).

## Phase 6 — Accessibility

Lighthouse a11y ≥ 95 is the floor, not the test. Lighthouse catches ~30–40% of issues. Add the manual
pass (snippets §4):

- One H1, no skipped heading levels, landmarks (`header`, `nav`, `main`, `footer`).
- Every image that communicates has `alt` with content; decorative ones `alt=""`.
- Every control is a real `<button>`/`<a href>` with an accessible name. Icon-only buttons get `aria-label`.
- Form fields have `<label>` (placeholder isn't a label), errors are announced (`role="alert"` /
  `aria-describedby`), required fields marked.
- Contrast ≥ 4.5:1 for body text, including text over hero images/gradients and the AI-favorite
  light-gray-on-white secondary text.
- Keyboard: visible focus, logical order, modals trap focus and close with Esc, no keyboard traps.
- Tap targets ≥ 24px (44px recommended), zoom not disabled, `prefers-reduced-motion` respected for
  heavy animation.

`render_check.mjs` runs axe-core (serious/critical) at 375 and 1280. For a deeper audit, load the
`ecc:accessibility` / `ecc:frontend-a11y` skills if they're installed.

## Phase 7 — Agentic browsing ("Navegación con agentes")

Lighthouse 13.3+ has an **Agentic Browsing** category (still marked "under development"). PSI shows it
as a pass ratio; the CLI gives a 0–1 score. Verified on Lighthouse 13.5, its audits are:
`agent-accessibility-tree`, `cumulative-layout-shift`, `webmcp-form-coverage`,
`webmcp-registered-tools`, `webmcp-schema-validity`, `llms-txt`, `ard-schema`. The WebMCP, `llms-txt`
and `ard-schema` audits come back *not applicable* when the site doesn't use them, so a missing
`llms.txt` doesn't lower the score. In practice the score is the a11y tree + CLS. PSI may run an older
Lighthouse; if the category is missing there, use the CLI:

```bash
npx -y lighthouse <url> --only-categories=agentic-browsing --output=json --output-path=$OUT/agentic.json --chrome-flags="--headless=new"
```

Read `categories["agentic-browsing"].score` and each audit's `score` / `scoreDisplayMode` from the JSON.
Don't describe an audit you haven't seen in the output (e.g. `ard-schema`); name it and quote its
title/description from the JSON.

Practical fixes:
- **A11y tree** = Phase 6 controls/labels. Check `take_snapshot`: every CTA, tab, accordion and form
  field must appear as a named `button`/`link`/`textbox`, not `generic`.
- **Shift after render** = Phase 5 CLS, especially the hydration swap. In the older PSI scorecard, `1/2`
  with a clean a11y tree usually means CLS just over 0.1.
- **`/llms.txt`** at the root: short markdown, with a title, a one-paragraph summary and links to the
  key pages. 🟡 unless the client cares about AI-search visibility.
- **WebMCP**: only flag if the site declares tools. Don't recommend adding it to a landing page.
- Beyond Lighthouse: an agent must be able to *complete the conversion*. Drive Playwright as an agent
  would ("book a call / send the contact form") and see if it gets stuck on unlabeled fields, custom
  selects, or a CTA that's a `div`.

## Phase 8 — Forms and conversion paths

Forms are where generated sites fail silently. For each form and CTA (snippets §6):

1. **Where does it go?** Read the handler (repo or Sources panel). `action="#"`, `console.log`, fake
   `setTimeout` success, `YOUR_FORM_ID`, a mailto action, or an API key that shouldn't be public is a 🔴.
2. **Validation**: empty, bad email, short phone, very long text, emoji/accents. Errors inline and visible.
3. **Truthful UI**: mock a 500 → it must show an error, not success. Mock a 200 → success + reset.
   Button disabled while sending, so no double leads.
4. **Spam**: reCAPTCHA/Turnstile or a honeypot. The domain is registered in the captcha console (the
   widget shows "invalid domain" otherwise). The backend's CORS allows the production origin.
5. **Legal (MX)**: link to the aviso de privacidad + consent in every form that collects personal data
   (LFPDPPP).
6. **End-to-end** (only with the user's OK): one real test lead, marked as test, confirmed in the
   destination (CRM/Zapier history/inbox) with the auto-reply. Otherwise write "envío real sin probar —
   responsable: X" in the report.
7. **A lead adapter/stub that is intentionally unconfigured** (`configured:false`, `INTEGRATION_NOT_CONFIGURED`,
   preview-only adapters) is correct *behaviour* (the UI must not say "enviado") but the page still delivers
   no leads: report it as 🔴 **(decisión pendiente)**, with the questions that unblock it (destination,
   captcha, who owns it). Check the stub's contract too: a captcha module the page calls but nothing defines
   (`window.XCaptcha` undefined) means it can never send even once configured.
   If the form doesn't use `fetch` (adapter object, SDK), mock the endpoint with Playwright `page.route` or
   replace the adapter before submitting; the `window.fetch` snippet only covers fetch-based forms.
8. **Zapier / webhook straight from the browser**: form-urlencoded, not JSON (no preflight); Zapier
   returns `Access-Control-Allow-Origin: *` (verified 2026-10), so the 2xx is readable. Honeypot field
   name must not be one autofill fills (`fax`). Rows in failure-catalog §6.
9. **Forms that open a booking widget (Cal.com, Calendly) after submit**: check the widget really loads (iframe from the
   booking host, availability grid visible, no 5xx), who it is prefilled with (name/email/phone sent to a third party
   before booking: privacy notice must say so), every entry point, and that opening it doesn't steal focus or trap scroll.
   Run `scripts/form_check.mjs` for the submit path itself.
10. Other conversion paths: `wa.me` link with the right country code and a prefilled message, `tel:`
   matching the visible number digit by digit, Cal.com/Calendly opening the right event, every CTA
   going somewhere real (no `href="#"`).

## Phase 9 — Responsive and cross-browser

Breakpoints are Tailwind's: **320, 375 (base mobile), 640, 768, 1024, 1280, 1536** (use the project's
own screens/`@media` values if it overrides them). `render_check.mjs` sweeps them all: overflow
offenders (elements sticking out on the right, not inside an intentional scroll container), body/html
`overflow-x:hidden` masking a bug, text cut off by an `overflow:hidden` container, inputs <16px on mobile, text <12px, JS errors, and a full-page
screenshot per width. **Read the screenshots.** Sections that look blank are usually scroll-reveal or lazy media that hadn't
played yet (video without `poster`, `loading=lazy` images, `opacity:0` reveals): confirm with the report's
invisible-text list and by scrolling the real page before calling it missing content. The script can't see a cut-off hero, overlapping
floating buttons, an unreachable menu, or stretched images. Without Playwright, do the same through
the MCP browser with snippets §5.

Two things the screenshots hide, both measured by the script now: **content parity** (a column or block
`display:none` on mobile, e.g. 3 of 8 speakers on one landing; the original build had it and nobody saw
it until a phone) and **mid-scroll overflow** (a `gsap.from({x:30})` reveal overflowing until its trigger).
Scroll-driven layouts (fans, spreads, pinned scenes) are judged with the section centered, at the end of
the scrub, at breakpoints and ~900/1200px: at scroll 0 they show their start state.

Cross-browser: run the same script with `--browser webkit --widths 390,768` (Safari/iOS) and
`--browser firefox --widths 375,1280`. Those engines need `npx playwright install webkit firefox` the
first time, so ask before installing.
Safari-specific usual suspects: `100vh` on iOS (use `dvh`), `backdrop-filter`, date inputs,
smooth-scroll, autoplay video without `playsinline muted`. If Playwright isn't an option, say
"cross-browser sin probar" and recommend BrowserStack or a real iPhone.

## Phase 10 — Security and secrets

- **Never print a secret while hunting for it.** Count (`grep -c`), print the file and line only, or cut the value
  (`grep -o 'api-key[^,;]*' file | cut -c1-14`). A key-length-based redaction regex misses short keys.
- **A deliberately public key** (an API key in client JS that the backend protects with a server-side captcha check and
  domain registration) is an *accepted risk*: list it with its mitigation and the next step (rate limiting) instead of
  leaving the verdict at NO PUBLICAR forever. Only the owner can accept it, and it must be written in the report.
- **Front end**: snippets §8 scans served HTML + same-origin JS for key patterns and exposed source
  maps. `crawl_check.py` covers `/.env`, `/.git/*`, manifests. A readable `/.git/index` means the whole
  repo and history can be downloaded: 🔴. Fix it at the server (`RedirectMatch 404 /\.git` in Apache;
  `location ~ /\.(?!well-known) { deny all; }` in nginx) and preferably keep the repo out of the
  docroot. Note: `<FilesMatch "^\.">` only matches dot-*files*, so it does **not** protect `.git/HEAD`.
- **Repo** (if available): delegate to the `ecc:security-reviewer` agent, or run
  `gitleaks detect --source <repo> --redact` across **history**, not just HEAD. Look at `.env`
  committed, `.env.example` with real values, keys hardcoded in config. Never print a secret's value:
  report type + `file:line` + "rotar".
- Headers: HSTS, `X-Content-Type-Options: nosniff`, `Referrer-Policy`, `X-Frame-Options`/CSP
  `frame-ancestors`. A basic CSP is 🟡 for a landing.
- External links `target="_blank"` with `rel="noopener"`. No mixed content.
- `npm audit --omit=dev` if there's a lockfile. Pinned CDN versions with SRI where possible.

## Phase 11 — Content verification (hallucination check)

Same method as `revision-qa-sitios` (load it if the deliverable is a client-facing QA doc). Take every
hard fact and check it against the Phase 0 sources: **coincide / no coincide (both values + source) /
sin verificar → question for the client**.

AI-specific things to hunt:
- Invented numbers ("+500 clientes", "98% satisfacción", "15 años"), testimonials with names/photos,
  client logos and certifications the company doesn't have, awards, team members.
- Contact data: phone, WhatsApp, email domain, address, hours, social profile URLs (AI invents
  plausible handles. Open each one).
- Placeholder residue: Lorem ipsum, "Your Company", "John Doe", `example.com`, `#` links, "Feature 1",
  © year wrong or hardcoded, English strings in a Spanish site (button labels, form errors, `alt`,
  `aria-label`, 404 page), mixed tú/usted.
- Legal pages: aviso de privacidad exists, belongs to *this* company (AI copies others'), and is linked.
- Stock or generated images with visible artifacts (hands, text, logos), or ones that misrepresent the
  real office/product/team.
- Claims that create legal exposure: guarantees, "#1 in Mexico", medical/financial promises.

## Phase 12 — Generated-code smells (if there's a repo)

From `code-review-qa`: don't review the whole repo line by line, but check what generated code
typically gets wrong. Look for dead routes/components, unused dependencies, console errors and hydration
warnings, TODO/FIXME left by the generator, duplicated components, API calls with no error handling,
env vars referenced but not defined in deploy, build that only works in dev (`npm run build` must pass),
no 404 page, and unpinned CDN scripts. For a full pre-merge review of a branch, hand off to
`code-review-qa`.

## Replacing a live page (parity) {#replacing-a-live-page-parity}

A redesign, a client re-delivery or an AI regeneration can look better and still lose what the old page
did. Before a copy replaces a live URL, diff it against the live version (the repo's committed page, or
`curl`) and list what **disappears or changes**:

1. **Lead capture**: endpoint, payload fields, captcha, honeypot, success/error handling, the WhatsApp/booking
   paths. Does the new form still reach the same place?
2. **Tracking**: GTM/GA4 container, `dataLayer` events (form_submit, whatsapp_click, agendar_click). An
   event the new JS dispatches that nothing listens to is zero analytics.
3. **Indexing**: robots meta (a delivery often ships `noindex` from its preview), canonical, og:url/image,
   sitemap entry, redirects, any URL that changes.
4. **Trust and compliance**: badges/awards/logos/testimonials that were added or removed, privacy-notice
   link, claims and numbers that changed (two figures for the same thing on one page).
5. **Behaviour the team chose deliberately**: git log for the live page (e.g. a commit that hid WhatsApp).
6. **Deploy surface**: per-folder `.htaccess`/headers/cache rules (an `immutable` one-year cache on
   unversioned `app.js`/`styles.css` means returning visitors keep the old files: version the filenames
   or `?v=`), trailing-slash behaviour, file names that must not move.
7. **Third parties the new page adds**: origins loaded on every visit (load on first use instead), and
   personal data sent to them before consent (e.g. prefilling a booking widget with the form's fields).

Report it as a table (live → new → verdict), then fix 1–3 first.

## Delegation (subagents)

Run independent work in parallel, but **only one agent drives the browser at a time**: the MCP
browser session is shared, and two agents resizing/navigating it corrupt each other's measurements.
Keep the browser phases (2, 4–9) in the main thread and delegate the rest:

| Agent / skill | Give it | When |
|---|---|---|
| `ecc:security-reviewer` (agent) | Repo path, "secrets in repo + history, exposed config, client-side keys; report type + file:line, never values" | Repo available |
| `Explore` (agent) | "Find form submit handlers, API endpoints, env var usage, meta/head setup, sitemap/robots generation" | Large or unfamiliar repo |
| `ecc:performance-optimizer` (agent) | The Lighthouse/trace findings + repo | After the report, if the user wants the perf fixes applied |
| `ecc:e2e-runner` (agent) | Form + CTA flows, cross-browser via Playwright projects | User wants repeatable tests, not a one-off check |
| `ecc:seo`, `ecc:accessibility`, `ecc:frontend-a11y` (skills) | — | Deeper reference in one area |
| `revision-qa-sitios` (skill) | — | Content-heavy site going to a client: produce its client QA document too |
| `code-review-qa` (skill) | — | The site arrives as a branch/PR |

The `ecc:*` agents and skills come from a plugin that may not be installed. If an agent type isn't
listed as available, do that part in the main thread (`gitleaks` / grep for secrets, the scripts for the
rest). Never stall waiting for a missing agent. Treat subagent output like any other evidence: verify
it before it goes in the report.

## Report

Default language: the user's (Spanish for dinkbit team deliverables). In chat for a single landing; a
shared document (Claude Docs / an artifact / a Markdown file in `$OUT`, whatever the agent supports) if the user asks for it or it'll be shared.

**Severity**
- 🔴 **Bloqueante**: the site doesn't do its job or creates risk. Empty shell with no fallback,
  `noindex`/robots blocking production, form not delivering leads or lying about success, exposed
  secrets/`.git`, invented facts or fake reviews, broken at a main breakpoint, LCP > 4 s mobile.
- 🟠 **Importante**: hurts ranking, conversion or trust. Missing/injected-only OG or canonical, no
  sitemap, analytics not firing, CLS > 0.1, a11y blockers (unnamed controls, unlabeled fields),
  unverified hard data, Safari bug.
- 🟡 **Mejora**: house-rule tags (`keywords`, `X-UA-Compatible`), `llms.txt`, JSON-LD, CSP, image
  polish, lastmod.

**Verdict (deterministic)**: any 🔴 → **NO PUBLICAR**. Mark each 🔴 as *técnico* (fix it in code) or
*decisión pendiente* (needs the owner: lead destination, which claims are true, whether to de-index). Say in
the Resumen when the only blockers are decisions, so a technically clean site isn't read as broken. Only 🟠/🟡 → **PUBLICAR CON PENDIENTES** (list
the 🟠 with an owner). Only 🟡 or nothing → **LISTO**.

```markdown
## Resumen
Veredicto: **…** · URL(s) · fecha · herramientas (Lighthouse vX, PSI, navegadores) · supuestos de alcance

## Puntajes
| | Móvil | Escritorio |
| Performance / Accesibilidad / Buenas prácticas / SEO | … | … |
| LCP / CLS / INP (campo si hay CrUX) | … | … |
| Navegación con agentes | x/y | |
| Palabras en HTML crudo vs renderizado | n / m | |

## Checklist
- [x] / [ ] per item, grouped: Rastreo · Meta y compartir · Indexación · Analítica · Rendimiento ·
  Accesibilidad · Agentes · Formularios · Responsive y navegadores · Seguridad · Contenido

## Hallazgos
### 🔴 Bloqueantes
- **Dónde** (URL / file:line / breakpoint) — Qué pasa, con el número.
  - Evidencia: …
  - Arreglo: concrete change (snippet, config, command).
### 🟠 Importantes
### 🟡 Mejoras
### 🟢 Bien hecho (only specific, decision-relevant things)

## Sin verificar / preguntas
Para el cliente · internas · lo que no se probó y por qué (e.g. envío real, Safari).

## Post-lanzamiento
- [ ] Search Console: propiedad, sitemap enviado, Inspección de URL → página rastreada
- [ ] PSI de nuevo a las 2–4 semanas (datos de campo)
- [ ] Debugger de Facebook/LinkedIn con la URL final
- [ ] Dominio de producción en reCAPTCHA y CORS
- [ ] Contenedor de GTM publicado; primera conversión real visible en GA4
```

After the report, **offer** to apply the fixes. Don't apply them during the review.

**Fixing (when the user says yes, or asks "fix SEO on this export" directly).** Leave read-only mode
and fix in dependency order, because earlier layers change what later checks measure:

1. **Exposure**: secrets, `/.git`, `/.env`. Rotate first, then block.
2. **Rendering**: get content and head into the served HTML (failure-catalog §1, §7 for the stack).
3. **Head**: one head source per page with the full house set; canonical/og:url from the site URL.
4. **Ship kit**: robots.txt, sitemap.xml, favicons + manifest, real 404, JSON-LD with confirmed facts
   only, analytics.
5. **Forms and conversion paths**: real endpoint, truthful success/error states.
6. **Responsive**: fix offenders at the element (never `body{overflow-x:hidden}`).
7. **Performance and a11y**: LCP, CLS, weight, then the a11y list.
8. **Content**: placeholders and unverified facts go back to the client as questions. Never invent
   replacements.

After each layer rerun the check that covers it, and the e2e suite after every round (a static review
misses runtime regressions). Follow the editing rules in `ship-ready-frontend` (step 6): exact-string
edits on minified files plus a prettified diff, assert each patch applied, verify hand-off docs, never
stage or commit. At the end rerun `crawl_check.py`, `render_check.mjs`
and Lighthouse and show before/after numbers. Fix the cause, never the check: no weakening a threshold,
deleting a test, or hiding overflow to make a gate pass.

## Rules

- Read-only by default: no edits to the repo, CMS, DNS or server config until the user approves fixes.
- Never submit a real form without permission. Never print a secret's value.
- Fresh tab + uncached reload before every measurement. Separate errors you caused from the site's own.
- Lab numbers vary ±10%. Run Lighthouse twice and report the median or the worse one, and say which.
- Don't report a house-rule tag as an SEO ranking problem, and don't report an intentional business
  decision (blocking AI bots, a US WhatsApp number) as a bug: ask.
- Same issue on every page = one finding listing the template, not N findings.
