Skip to main content

Checklist Traceability

A 20-item checklist (from a social-media post enumerating things a "vibe coded" website is commonly missing) mapped to what actually satisfies each item in /site, the public marketing site. Every row below was verified against the current repo state, not written from memory of the plan — the "How to verify" column reproduces the check.

#ItemSatisfied by (file/feature)How to verify
1Custom 404 pagesite/app/not-found.tsx — full page with heading, copy, and links back to Home/Features/Contact/Sign in; robots: noindex, nofollow set via buildMetadatacurl -s http://localhost:3102/does-not-exist returns HTTP 404 with the custom page body (confirmed in this session), not a generic Next error page
2Meta title per pageEvery route calls buildMetadata({ title, ... }) from site/lib/seo.ts, which sets title.absolute to "<Title> | ReelBolt"grep -n "buildMetadata(" site/app/**/page.tsx — each of /, /features, /about, /contact, /contact/thank-you, /legal/privacy, /legal/terms passes a distinct title
3Meta description per pageSame buildMetadata() call takes a description per page, rendered as <meta name="description"> and reused for OG/Twitter descriptionsgrep -n "description:" site/app/**/page.tsx — each page's description string is unique to that page's content
4CTA above the foldsite/components/home/Hero.tsx (rendered first by site/app/page.tsx) — the <h1> is immediately followed, in the same hero <section>, by PrimaryCta (site/components/ui/PrimaryCta.tsx), before any scroll-triggered content. It renders siteConfig.primaryCta: "Talk to us" → /contact by default, and "Start free" → /app/signup once NEXT_PUBLIC_SIGNUP_ENABLED=true (decision D25)Read site/components/home/Hero.tsx and site/components/ui/PrimaryCta.tsx; npm run build:pages then confirm the href="/contact" anchor appears in out/index.html before the id="how-it-works" section (confirmed this session on the default build: /contact anchor at byte offset 5888, how-it-works at 23838); re-run with NEXT_PUBLIC_SIGNUP_ENABLED=true and the anchor becomes href="/app/signup"
5Favicon setsite/public/favicon.ico, favicon-16.png, favicon-32.png, favicon-192.png, favicon-512.png, apple-touch-icon.png, plus site/app/icon.svg; all generated from site/assets/logo.svg by site/scripts/generate-icons.mjs (sharp + png-to-ico) and linked via manifest.tsls site/public/*.png site/public/favicon.ico; curl -o /dev/null -w '%{http_code}\n' http://localhost:3102/favicon.ico → 200 (confirmed in this session)
6Robots.txt filesite/app/robots.ts — allows /, disallows /api/ and /contact/thank-you, points sitemap/host at siteConfig.urlcurl http://localhost:3102/robots.txt → 200 with Sitemap:/Allow:/Disallow: lines (confirmed in this session)
7Sitemap.xmlsite/app/sitemap.ts — lists only the routes that are actually live (/, /features, /pricing, /about, /contact, /legal/privacy, /legal/terms); each URL built from siteConfig.url. /runner exists as a route but is deliberately not listed yet: the desktop runner is not built, so site/lib/runner-downloads.ts exports an empty artifact list, the page ships its "not available yet" state with noindex, and the sitemap entry and the indexing turn on together from that same list the day a signed installer is published — pre-listing a placeholder is the negative signal this row's rule exists to avoid (H6)curl http://localhost:3115/sitemap.xml → 200 with <urlset> XML listing those seven routes and no /runner (run at H6 against next start, alongside curl /runner → 200 with noindex, nofollow and curl /llms.txt → the same page list plus the pricing line)
8Open graph imagesite/app/opengraph-image.tsx (1200×630, next/og ImageResponse, dynamically rendered PNG) plus site/app/twitter-image.tsx re-exporting the same image for Twitter cards; wired into every page's metadata by buildMetadata()curl -o /dev/null -w '%{http_code}\n' http://localhost:3102/opengraph-image → 200; grep -n "og:image" <page HTML> shows the generated URL
9Alt text on every imageN/A by construction rather than an oversight: the site has zero <img>/next/image content elements — the only graphic (the logo) is inline decorative <svg aria-hidden="true"> in SiteHeader, which correctly takes no alt text since it's decorative, and the wordmark text stands in as the accessible label; opengraph-image/twitter-image each export a required alt string per Next's convention. The homepage redesign added two decorative <canvas>-bearing WebGL scenes (site/components/three/HeroScene.tsx, ViewfinderScene.tsx), reached only through HeroSceneMount.tsx/ViewfinderMount.tsx; both mounts wrap the canvas in a <div aria-hidden="true"> and both render a non-canvas static fallback (HeroFallback, a pure-CSS gradient box; ViewfinderStatic, a static aria-hidden SVG) whenever prefers-reduced-motion: reduce is set or the WebGL scene fails to load (SceneBoundary) — so no assistive-tech user or reduced-motion visitor is ever served an unlabeled interactive-looking canvasgrep -rn "<img|next/image|<Image" site/app site/components → no matches; grep -n "aria-hidden" site/components/layout/SiteHeader.tsx shows the logo SVG is marked decorative; grep -n "^export const alt" site/app/opengraph-image.tsx site/app/twitter-image.tsx; grep -n "aria-hidden" site/components/three/HeroSceneMount.tsx site/components/three/ViewfinderMount.tsx shows both canvas wrappers hidden; read site/components/three/HeroFallback.tsx and the ViewfinderStatic function in ViewfinderMount.tsx to confirm the static fallback markup
10Mobile breakpointsTailwind v4 responsive utilities (sm:/md:/lg:) throughout every page and component — e.g. 6 matching lines in site/app/page.tsx alone, plus the hero (site/components/home/Hero.tsx) and header (site/components/layout/SiteHeader.tsx, now h-20) each with their own set; grid layouts collapse to 1 column below sm, nav collapses to a hamburger below md (md:hidden/md:flex toggle in SiteHeader.tsx)grep -c "sm:|md:|lg:" site/app/page.tsx site/components/home/Hero.tsx site/components/layout/SiteHeader.tsx (line count; use grep -o for a per-occurrence count); load http://localhost:3102/ at a 400px viewport and confirm the header collapses to the hamburger menu
11Sticky mobile CTAsite/components/layout/StickyMobileCta.tsx — fixed inset-x-0 bottom-0 ... md:hidden bar holding the same PrimaryCta the rest of the site uses, hidden on /contact//contact/thank-you (HIDDEN_PATHS) and suppressed while cookie consent is still undecided (consent === null, from useCookieConsent()) so the two fixed bars never stack; unchanged in shape by the redesign, only restyledRead site/components/layout/StickyMobileCta.tsx; at a mobile viewport on any page other than /contact*, after accepting/rejecting the cookie banner, the bar is pinned to the bottom and its button reads siteConfig.primaryCta ("Talk to us" by default, "Start free" with NEXT_PUBLIC_SIGNUP_ENABLED=true)
12Loading statessite/app/loading.tsx (Next route-level Suspense fallback, skeleton-shaped) + site/components/ui/Skeleton.tsx (pulsing placeholder blocks) + site/components/ui/Spinner.tsx (used inside ContactForm's submit button while submitting is true)Read site/app/loading.tsx and site/components/ui/Skeleton.tsx; grep -n "Spinner" site/components/contact/ContactForm.tsx shows it rendered when submitting
13Form error statessite/components/contact/ContactForm.tsx — per-field aria-invalid/aria-describedby + red-bordered inputs + inline error <p role="alert"> on blur and on submit, plus a page-level role="alert" aria-live="assertive" banner for network/server/validation failures; site/lib/validation.ts is the single validator shared by client and the POST /api/contact server route so client and server never disagreeRead site/components/contact/ContactForm.tsx (see fieldErrors, bannerError); grep -n "validateContact" site/components/contact/ContactForm.tsx site/app/api/contact/route.ts shows both import the same function
14Thank you pagesite/app/contact/thank-you/page.tsx — dedicated route, noIndex: true metadata (excluded from the sitemap and disallowed in robots.ts), links back to Home/Features, and repeats contact details as a fallbackcurl http://localhost:3102/contact/thank-you → 200 (confirmed in this session); grep -n "noIndex" site/app/contact/thank-you/page.tsx; grep -n "thank-you" site/app/robots.ts
15Privacy policy pagesite/app/legal/privacy/page.tsx — full policy referencing COMPANY_LEGAL_NAME, retention periods, sub-processors, supervisory authority, DPO contact, etc. from site/lib/legal-placeholders.ts; linked from the footer and the cookie bannercurl http://localhost:3102/legal/privacy → 200 (confirmed in this session); grep -n "COMPANY_LEGAL_NAME|DPO_CONTACT|SUPERVISORY_AUTHORITY" site/app/legal/privacy/page.tsx
16Terms and conditionssite/app/legal/terms/page.tsx — governing law, liability cap, jurisdiction pulled from site/lib/legal-placeholders.ts; linked from the footercurl http://localhost:3102/legal/terms → 200 (confirmed in this session); grep -n "GOVERNING_LAW|LIABILITY_CAP" site/app/legal/terms/page.tsx
17Cookie bannersite/components/consent/CookieBanner.tsx (fixed bottom role="dialog" bar, Accept/Reject buttons, link to the privacy policy) driven by site/components/consent/CookieConsentProvider.tsx (localStorage-backed, defaults to null/no-choice on server and first client render to avoid a hydration mismatch); CookiePreferencesLink lets a visitor reopen it later from the footerRead site/components/consent/CookieBanner.tsx; in a browser with localStorage cleared, the banner appears on first visit and disappears after Accept/Reject; the footer's "Cookie preferences" control reopens it
18Analytics installedsite/components/analytics/GoogleAnalytics.tsx injects GA4 (gtag.js) only when NEXT_PUBLIC_GA_MEASUREMENT_ID is set and cookie consent is 'granted'; site/components/analytics/PageViewTracker.tsx fires a page_view on client-side route changes (App Router doesn't reload on navigation, so this is required for SPA pageviews); site/lib/gtag.ts's trackEvent fires the generate_lead event on a successful contact-form submitRead site/app/layout.tsx (gaId gate) and site/components/analytics/GoogleAnalytics.tsx (consent !== 'granted' early return); grep -n "trackEvent" site/components/contact/ContactForm.tsx
19Real contact addresssite/lib/legal-placeholders.ts centralizes every business fact (COMPANY_LEGAL_NAME, REGISTERED_ADDRESS, SUPPORT_EMAIL, PHONE_NUMBER, etc.) as one bracketed placeholder each, consumed by site/components/contact/ContactAddress.tsx (footer + thank-you page) and the contact/legal pages — deliberately not invented, since nobody but the site owner has these facts; see docs/marketing-site.md's launch checklist for the full token list to fill in before launchgrep -n "\[.*\]" site/lib/legal-placeholders.ts shows every constant is an unfilled bracketed token, not a fabricated value; grep -rn "COMPANY_LEGAL_NAME|REGISTERED_ADDRESS" site/components/contact/ContactAddress.tsx
20Compressed imagesNo large raster content images exist to compress: the only graphic is inline SVG (near-zero bytes, no encoding needed), the OG/Twitter card image is generated on demand as a PNG via next/og's ImageResponse (not a checked-in asset), and the favicon PNGs are small, sharp-rasterized outputs (16×16 through 512×512, largest is 12.6 KB) rather than hand-exported bloated files; next.config.ts also enables images: { formats: ['image/avif', 'image/webp'] } for any future next/image usage. The redesign applies the same "don't ship weight you don't need" discipline to code, not just images: the three.js/@react-three/fiber hero (~130KB+ gzipped) is isolated to site/components/three/HeroScene.tsx/ViewfinderScene.tsx, reached only via next/dynamic(..., { ssr: false }) from the two mount components, so it ships as its own lazy chunk requested only on the homepage and never at all under prefers-reduced-motion: reduce — every non-homepage route's First Load JS stays within ~1KB of the pre-redesign baseline. See docs/site-design-system.mdls -la site/public/*.png — largest (favicon-512.png) is 12,690 bytes (~12.6 KB, re-confirmed this session — unchanged since Phase 3's favicon regeneration); grep -n "formats" site/next.config.ts; grep -rln "from 'three'|from '@react-three/fiber'" site/app site/components site/lib → only HeroScene.tsx/ViewfinderScene.tsx; npm run build route table shows the homepage at 112 KB First Load JS and every other route at 103–107 KB (re-confirmed this session)

Notes​

  • All curl-verified rows above were checked in this session against web/site running via next start on ports 3101/3102 respectively (no nginx in front), matching the routes each project serves on its own — see docs/marketing-site.md for how nginx joins them at / and /app/* in the real deployment.
  • Re-verified for the final CLAUDE.md pass (H11). Rows 4 and 11 still describe the CTA correctly: site/lib/site-config.ts's siteCtas() makes "Start free" → /app/signup the primary CTA only when NEXT_PUBLIC_SIGNUP_ENABLED=true, and leaves "Talk to us" → /contact in place otherwise (decision D25). The rest of this table is a /site checklist item and was left as it stands; the one drift found is row 7's /pricing omission.
  • Re-verified for the H8 copy refresh (saas-launch). H8 added a sixth nav entry, "Teams", pointing at /features#teams — the nav is one array in site/lib/site-config.ts rendered through next/link in SiteHeader (desktop and mobile alike), so the header layout is unchanged (a short sixth label in the same flex row; if the nav is ever tightened again, re-run the H6 width sweep). Row 7's /pricing omission was fixed in the same pass. Row 4 (CTA above the fold) is untouched: the hero still renders PrimaryCta first, and siteCtas() was not modified.
  • Items 9 and 20 are satisfied mainly by an absence of raster content images rather than by an alt-text or compression policy being exercised on real photos — if a future page adds a photographic hero image or screenshot, it should go through next/image (already configured for AVIF/WebP output) with a real, non-empty alt, not a plain <img>. The homepage's two WebGL <canvas> elements (added by the visual redesign) are the one piece of genuinely interactive- looking graphics on the site; they satisfy item 9 by being aria-hidden decoration with a static fallback rather than by carrying alt text (a <canvas> has no alt attribute), and satisfy item 20's "don't ship weight you don't need" spirit by never downloading their ~130KB+ chunk under reduced motion. See docs/site-design-system.md for the full rationale.