---
name: ship-ready-frontend
description: "Build landing pages, marketing sites and public web pages that work beyond the screenshot: static or server-rendered HTML that crawlers, AI bots and link previews can read; a complete head (title, description, canonical, OG, Twitter, favicons, lang); robots.txt, sitemap.xml and a real 404; Core Web Vitals; accessibility; no horizontal scroll or cropping at any Tailwind breakpoint. Use whenever writing or editing a public-facing page, landing, marketing site, section or layout in any stack (Next.js, Astro, Nuxt, Vite/React, Lovable/Bolt exports, Laravel Blade, plain HTML, Shopify/Liquid), e.g. 'make me a landing', 'build this design', 'add a section', 'the preview has no image'. Load together with frontend-design / dinkbit-frontend-developer (look and feel); this skill covers whether it works. Not for screens behind login beyond responsive/a11y. To audit or fix an existing site, use ai-site-review; for a DCLogic bundle export, use new-landing-page."
---

# Ship-ready frontend

AI-built pages tend to look finished and fail where nobody looks: bots get an empty shell, the share
preview has no image, the form fakes success, the hero overflows at 375. This skill applies the rules
**while building** and proves them with checks before saying "done".

It owns no rules of its own. Rules, causes and fixes live in the **ai-site-review** skill's
`references/failure-catalog.md` (read it before the first component), and the checks are that skill's
scripts. One catalog, so the builder and the reviewer can't drift. Install both skills side by side.

`$REVIEW_DIR` = the ai-site-review folder. It's normally next to this skill's folder (`../ai-site-review`
from this SKILL.md, in `~/.claude/skills`, `~/.config/opencode/skills` or a project's `.claude/skills`).
If it isn't:
`REVIEW_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)")`.
Not installed at all → stop and say so; don't improvise the checks. `$OUT` = your agent's scratchpad or
`$(mktemp -d)`, never the project folder.

## Auditing an existing page (not building)

The same checks work as a retroactive pass on a page someone else built or that you patched (a client delivery, an
AI export). Differences: there is no build, so serve the folder as-is; compare against the previous live version
(git or `curl`) and report what disappears (ai-site-review → *Replacing a live page*); `robots.txt`/sitemap live at the
site root when the page is hosted in a subfolder, check the root copy separately; scores come from a plain static
server or the real host, never a dev server that soft-404s.

## Hard rules

1. Public content is in the HTML the server sends. If `curl` can't see it, it doesn't exist.
2. All SEO/share tags are in the server-sent `<head>`, per page. Never JS-only (`react-helmet` alone,
   `<helmet>` portals).
3. Never invent facts, contacts, testimonials, logos, metrics or reviews. Use a visible placeholder
   `[[CLIENT: …]]` and list it at hand-off.
4. Never claim done with a check unrun. "Not verified" is allowed; pretending isn't.
5. No horizontal scroll from 320 to 1536. Fix the element; never `body{overflow-x:hidden}`.
6. Links are `<a href>`, actions are `<button>`, fields have `<label>`.
7. The LCP element is in the HTML, not lazy-loaded, not faded in, and prioritized.
8. `SITE_URL` comes from env/config; no localhost, preview or copied-source domain in canonical, OG or
   sitemap.
9. No `noindex` in production; always `noindex` on preview, via env. No build step → do it at the host:
   an `X-Robots-Tag: noindex` header for the preview environment (Vercel/Netlify headers config,
   `.htaccess` on the staging vhost) or the host's preview password protection. Verify with crawl_check.
10. Forms show success only after a real 2xx. One exception, documented in the code: a filled honeypot field fakes success and sends nothing, so bots learn nothing.
11. Third parties load on first use, not in `<head>`: GTM after `load` + idle, captcha on first form focus, booking/chat embeds on click or submit.
12. A decorative background video must never be the LCP: poster `<img>` preloaded; start the video on first interaction or after ~10 s.
13. No text under 12px; every auto-moving element has a pause path (see failure-catalog §4).

## Workflow

**1. Inputs (ask once, default the rest).** Site URL, site name, language/locale (`es-MX` / `es_MX`),
per-page title + description, OG image (or OK to make a 1200×630 one), favicon source, stack, form
destination, GTM/GA4 ID, sources for facts. Anything missing → `[[CLIENT: …]]` or `[[TODO: …]]`,
never invented.

**2. Pick rendering before the first component.**

| Situation | Choice |
|---|---|
| New public site | Static: Astro, Next SSG/`output: export`, Nuxt generate, Blade |
| Per-request data | SSR + cache |
| Existing Vite/React SPA, no migration | Prerender every public route at build + static per-route head |
| One standalone landing | Plain HTML/CSS + minimal JS; copy `assets/head.html` and fill every `{{…}}` by hand (no build step) |
| DCLogic/Strapi bundle export | Hand off to `new-landing-page` |
| Behind login | CSR is fine; skip SEO checks, keep responsive + a11y |

Stack specifics (Next metadata API, Astro sitemap, Blade layout, Shopify): failure-catalog §7. Fetch
current framework docs with `context7` before writing stack code.

**3. Site config + one head source first.** Config with `url, name, locale, ogLocale, defaultOgImage,
twitterHandle, themeColor, gtmId`. One head component/partial used by every page, taking
`title, description, path, ogImage?, noindex?`, with canonical and og:url derived from `url + path`. It's
the one abstraction that always pays off, because it's what stops missing and mismatched tags.
No build step (plain HTML)? The head itself is the config: write the site URL once per file it appears
in (head, robots.txt, sitemap.xml, JSON-LD) and list those files at hand-off so a domain change is a
find-and-replace.
`assets/head.html` has the full house tag set to copy from.

**Copy you can't verify.** Figures already in the source ("18% de ahorro", "48 h") are kept but listed
as *sin verificar* at hand-off; don't silently keep them and don't delete them. Constants that drive
a calculator or estimator (rates, fees) become visible placeholders, and the feature is flagged "don't
publish until confirmed". A link with no known destination (aviso de privacidad, social profile) is a
visible `[[CLIENT: …]]` text, never `href="#"`.

**4. Build sections mobile-first.** Write the 320–375 layout first, then `sm:` 640, `md:` 768, `lg:`
1024, `xl:` 1280, `2xl:` 1536. Semantic HTML first, ARIA only for gaps. All tab/accordion panels are
rendered (toggle with `hidden`/`<details>`). Content is visible without JS; animations enhance, never
gate. Make a component when a block repeats or has behavior, not for one-offs. Look and feel come
from `frontend-design` / `dinkbit-frontend-developer` when installed; this skill doesn't depend on them.

**5. Ship kit, in the same change, not "later".** Head tags (step 3), favicon set + `site.webmanifest`,
`robots.txt` with an absolute `Sitemap:` line, a `sitemap.xml` generated from routes (or static for one
page) whose `<loc>` matches the canonical exactly, a real 404 (status 404, not 200), JSON-LD with
confirmed facts only, the OG image file, GTM in head + noscript, env-driven `noindex`. Static files
must win over any SPA catch-all rewrite. Apache / cPanel host → start from `assets/htaccess` (https that
skips `/.well-known/` for AutoSSL, dotfile/dev-file 404s, `ErrorDocument 404`, headers, cache, gzip).

**6. Checks before saying "done".** Run them against a local **production** build (`build` +
`preview`/`serve`), not the dev server. **First ask whether the site already has a local URL** (Valet/Herd
`https://<project>.test`, MAMP, a Docker port) and use it for G2, G3, G5, G7 and the e2e run; don't spin up
a scratchpad server when one exists. Those `.test` hosts run nginx and ignore `.htaccess`, and some
soft-404: server rules (redirects, 404s, blocked dotfiles, headers) and G1/G4 need Apache, the real host, or
a plain static server. No local URL and no build step → serve the folder as-is
(`python3 -m http.server <port>` in the background, stop it after). Write output to `$OUT`. Needs the one-time setup in
`ai-site-review` (Python 3, Node, Chrome, `npm i -g playwright`). If the build needs secrets you don't
have (CMS URL, SMTP), ask for them or for a preview URL. Never invent values; say which checks waited.

| # | Check | Pass |
|---|---|---|
| G1 | `python3 -I $REVIEW_DIR/scripts/crawl_check.py <url>` | 0 FAIL, except items that only exist after deploy (https redirect, HSTS), which you list |
| G2 | `node $REVIEW_DIR/scripts/render_check.mjs <url> --out $OUT/render` (add the project's own breakpoints, e.g. `--widths 320,375,640,760,768,900,1024,1280,1536`) | No-JS text ≥ 80% of rendered, nothing invisible without JS, 0 overflow offenders at every width **and mid-scroll**, no content block missing vs the widest width (parity), no text outside its card, axe 0 serious/critical. **Read every screenshot** (crop, overlap, unreachable menu). Scrubbed sections show their start state in full-page shots: judge them with the section centered (failure-catalog §3) |
| G3 | Same script, `--browser webkit --widths 390` | No iOS hero crop / `100vh` issue. Ask before `npx playwright install webkit`; if skipped, say "Safari sin probar" |
| G4 | Lighthouse **CLI** (`--form-factor=mobile`, the default), on a plain static server or the real host, 2 runs, report the worse: `lighthouse <url> --output=json --output-path=$OUT/lh.json --chrome-flags="--headless=new"`. The chrome-devtools MCP `lighthouse_audit` **excludes Performance**: fine for a11y/SEO/agentic, not for this gate | Perf ≥ 90, A11y ≥ 95, Best Practices ≥ 95, SEO 100, CLS ≤ 0.1, LCP ≤ 2.5 s |
| G5 | Accessibility tree: `take_snapshot` (chrome-devtools MCP), Playwright `page.locator('body').ariaSnapshot()`, or the Lighthouse `agent-accessibility-tree` audit | Every CTA, tab and field is a named role, not `generic` |
| G6 | Residue grep (failure-catalog §6) on source + build output | No lorem, template titles, `example.com`, `href="#"`, unreported `[[…]]` |
| G7 | Every submit path (forms, chat widget, booking, estimator that posts): mock a 500 and a 200 (snippets §6, or Playwright `page.route`). Endpoint not built yet → mock it and list `[[TODO: endpoint]]`. No submit path → N/A | Error shown on 500, success only on 200, button disabled while sending. Abort third parties in the test (captcha, GTM, booking) and fake the token (`window.<Captcha>.getToken`); use `ai-site-review/scripts/form_check.mjs`. Check the honeypot makes **0** API calls. One real test lead only with the user's OK, marked as test; afterwards keep the endpoint stubbed (`page.route`) in every rerun so the suite never sends real leads |

On a failure, fix the cause and rerun that check. Never weaken a check to pass it. After every fix round
rerun the whole set plus the e2e suite, not only the check that failed: a reviewer reading the diff missed
both regressions of one landing (a 6px mid-scroll overflow, a mobile menu hidden by a CSS cleanup) that the
e2e run caught.

Editing rules that prevent self-inflicted bugs:
- **Minified files**: never delete rules from minified CSS/JS with a regex. A cleanup that removed
  `.ph-toggle{…}` left `body.review-mode ` glued to the next `@media` block and hid the mobile menu. Edit
  exact strings, then diff the prettified before/after (`npx prettier --parser css`) and read every hunk.
- **Prove each patch applied**: assert the old string exists exactly once before replacing, and don't chain
  the patch behind another command with `&&` (a failed `grep` silently skipped one, and the untouched code
  then looked like a site bug).
- **Hand-off docs are claims, not facts**: verify what a README/PLACEHOLDERS file says the build does (one
  said a review toggle was hidden in production; it showed after scrolling).
- **Git**: leave staging and commits to the user; delete with plain `rm`, never `git rm`. Scroll-reveal
content only appears after scrolling, so take screenshots with render_check (it scrolls first), not
per-element captures, or reveals look like missing content. A check the user won't allow (e.g. no
WebKit install) is reported as not run, with the reason, never as passed.

**7. Hand-off.** Check results with numbers (one line each), the open `[[CLIENT: …]]` / `[[TODO: …]]`
list, and what wasn't tested and why. For anything going live, run `ai-site-review` on the deployed
URL: it covers what a local build can't (real redirects, headers, analytics firing, exposed files,
content verification against the client's sources).

## Out of scope unless asked

Keyword research and content strategy; `llms.txt`, WebMCP, AMP, PWA offline; dynamic rendering
(serving bots a different version); visual regression tooling.
