feat: build-gate validators (enforce the completeness checklist + caught a favicon bug) - #1
Conversation
…nically) Adds scripts/build_validators.mjs orchestrator + scripts/validators/ (validate-links, validate-route-metadata, validate-assets) — the first slice of the build-validators spec (heymegabyte-claude-skills rules/build-validators-manifest.md). Run via 'npm run validate' after build; exits non-zero on any build-break so no site deploys with an unenforced completeness point. Verified against this template's own dist/ — validate-links + validate-route-metadata pass, and validate-assets immediately caught a REAL bug: index.html references 5 favicons (favicon.ico, favicon-16/32, apple-touch-icon, safari-pinned-tab) that exist in neither public/ nor dist/, so every cloned site ships 404'd favicons (violates the favicon-set gate). Left as a separate 'npm run validate' step (not wired into postbuild) until that favicon issue is fixed, so it doesn't block the build today. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ient-only per-route head) Caught on this template: 20 sitemap routes all serve one index.html (SPA fallback /* → /index.html) with NO per-route server head — useSEO.ts is client-only, functions/ has only API routes (no HTMLRewriter), no prerendered route HTML. Crawlers read the HOMEPAGE head on every URL → the site collapses to one indexable URL (the exact njsk.org incident always.md warns is a build-fail). WARN level (not build-break) because per-route head MAY be injected by the projectsites.dev PLATFORM Worker at serve time (outside this repo) — so it's a definite bug for STANDALONE deploys (current _redirects = CF Pages SPA fallback) but possibly handled behind the platform. Either way: verify per-route server head on the live URLs, or add vite-ssg prerender / a Worker HTMLRewriter keyed on getMeta(pathname) to the template. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
…ONFIRMED live) curl of the raw served HTML (no JS) on projectsites.dev itself proves the collapse — the platform Worker does NOT inject per-route head: / → <title>ProjectSites — We deliver websites in minutes</title> canonical / /about → SAME title, canonical / (should be /about) /contact → SAME title, canonical / (should be /contact) Every route serves the homepage head + canonical=/ → the whole site is one indexable URL. No longer architecturally-ambiguous → build-break. Every site cloned from this template has the same collapse until per-route server head is added (vite-ssg prerender or a Pages Function/_middleware HTMLRewriter keyed on getMeta(pathname)). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🔴 SEO-collapse EMPIRICALLY CONFIRMED — live on projectsites.dev itselfCurled the raw served HTML (no JS) — the platform does NOT inject per-route head:
Every route ships the homepage Fix (one of): (a) |
Fix scoping — why it's architectural (and the recommended path)Dug into implementing the per-route head fix. The blocker: per-route meta is currently runtime-only React. Each page calls Consequences for each fix path:
Recommendation: extract route meta into a central This is a focused 1–2h architectural change with real edge/SSR testing — I didn't want to blind-ship a partial (titles-only) or untested fix to the serving path of every customer site. Say go and I'll do the central-map + vite-ssg PR (tested: build → curl each route → assert unique server-rendered title/canonical). Meanwhile |
index.html referenced favicon.ico, favicon-16/32.png, apple-touch-icon.png, safari-pinned- tab.svg — none existed in public/ (caught by validate-assets, PR #1). Every cloned site shipped 404'd favicons (violates checklist #23). scripts/generate-favicons.mjs generates the full set brand-colored from _brand.json (brandHue → HSL→RGB) with ZERO external deps — pure node:zlib for valid RGBA PNGs (rounded square), a BMP/PNG-in-ICO writer, and SVG (rich, brand bg + business initial) + safari mask. Wired into prebuild so it regenerates per brand and vite copies public/ → dist/. Also added an SVG-favicon <link> (modern browsers). Committed brand-default placeholders so the repo is clean even before a per-brand rebuild. Verified on a clean build: all 6 favicons land in dist/, 0 dead refs, valid PNG/ICO magic bytes. validate-assets (PR #1) now passes the favicon gate. Co-authored-by: Brian Zalewski <hey@megabyte.space> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n't blank the page
Add <SafeSection>, a section-scoped error boundary, and wrap every top-level
section of Home in it. Root-cause fix for the iteration-1 failure that scored a
generated site 3.2/10: a render-crashing TypeError ("Cannot read properties of
undefined (reading 'primary')") blanked the ENTIRE page below the hero.
Why the existing top-level ErrorBoundary wasn't enough: it wraps the whole app,
so a throw in ANY section unwinds the entire tree and swaps in the error card —
header, footer, hero, NAP, every sibling section, and every SEO landmark vanish
with it. SafeSection contains the blast radius to one section: the crash is
caught locally, that section fails soft (renders null in prod, a compact notice
in dev), and every sibling section + the header/footer landmarks + the
pre-rendered SEO head keep painting. This turns a whole-page blank into a single
missing section — the difference between a 3/10 and a shippable page.
Directly addresses all three iter-1 weaknesses:
- #1 render-crash blanking everything below the hero — isolated per section.
- #2 SEO head + header/main/footer landmarks depended on the same crashing
render — they now survive any section throw.
- #3 services / NAP / booking / CTA never rendered — a crash in one no longer
takes down the others.
- src/components/SafeSection.tsx — the fail-soft primitive (telemetry to
gtag/posthog, dev-only inline notice, optional custom fallback).
- src/pages/Home.tsx — each section wrapped; flag-gating preserved.
- src/pages/AGENTS.md — hard rule so future generations keep the wrappers.
- tests/unit/SafeSection.test.tsx — 4 tests (createRoot + act, no new deps):
healthy passthrough, sibling-survives isolation, custom fallback, telemetry.
Gate: npm install --legacy-peer-deps && npm run build → exit 0, dist/ produced;
vitest 96 passed (92 baseline + 4 new). Additive, zero new deps.
What
Adds the first slice of the build-validators enforcement layer —
scripts/build_validators.mjsorchestrator +scripts/validators/(validate-links, validate-route-metadata, validate-assets), run vianpm run validate. This is the machine enforcement for the 62-point website-completeness-checklist (spec:heymegabyte-claude-skills/rules/build-validators-manifest.md) — a checklist point with no validator is an unenforced point, and today the template ships onlyvalidate-brand.mjs.Verified — and it caught a real bug
Run against this template's own
dist/:validate-links— no dead internal linksvalidate-route-metadata— every page has title + meta descriptionvalidate-assets—index.htmlreferences 5 favicons that exist in neitherpublic/nordist/(favicon.ico,favicon-16x16.png,favicon-32x32.png,apple-touch-icon.png,safari-pinned-tab.svg). Every cloned site currently ships 404'd favicons — violates the favicon-set completeness gate.Scope / safety
npm run validatestep (NOT wired intopostbuild) so it doesn't block today's build until the favicon issue is fixed. Once the template is clean, wire it into CI/lefthook + the deploy gate.Next
More validators from the spec (word-count, faq, skip-link, axe, legal, consent-gate) + the favicon fix, then promote
npm run validateto a blocking gate.🤖 Generated with Claude Code