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.
| # | Item | Satisfied by (file/feature) | How to verify |
|---|---|---|---|
| 1 | Custom 404 page | site/app/not-found.tsx — full page with heading, copy, and links back to Home/Features/Contact/Sign in; robots: noindex, nofollow set via buildMetadata | curl -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 |
| 2 | Meta title per page | Every 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 |
| 3 | Meta description per page | Same buildMetadata() call takes a description per page, rendered as <meta name="description"> and reused for OG/Twitter descriptions | grep -n "description:" site/app/**/page.tsx — each page's description string is unique to that page's content |
| 4 | CTA above the fold | site/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" |
| 5 | Favicon set | site/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.ts | ls 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) |
| 6 | Robots.txt file | site/app/robots.ts — allows /, disallows /api/ and /contact/thank-you, points sitemap/host at siteConfig.url | curl http://localhost:3102/robots.txt → 200 with Sitemap:/Allow:/Disallow: lines (confirmed in this session) |
| 7 | Sitemap.xml | site/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) |
| 8 | Open graph image | site/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 |
| 9 | Alt text on every image | N/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 canvas | grep -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 |
| 10 | Mobile breakpoints | Tailwind 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 |
| 11 | Sticky mobile CTA | site/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 restyled | Read 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) |
| 12 | Loading states | site/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 |
| 13 | Form error states | site/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 disagree | Read 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 |
| 14 | Thank you page | site/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 fallback | curl 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 |
| 15 | Privacy policy page | site/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 banner | curl 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 |
| 16 | Terms and conditions | site/app/legal/terms/page.tsx — governing law, liability cap, jurisdiction pulled from site/lib/legal-placeholders.ts; linked from the footer | curl http://localhost:3102/legal/terms → 200 (confirmed in this session); grep -n "GOVERNING_LAW|LIABILITY_CAP" site/app/legal/terms/page.tsx |
| 17 | Cookie banner | site/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 footer | Read 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 |
| 18 | Analytics installed | site/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 submit | Read site/app/layout.tsx (gaId gate) and site/components/analytics/GoogleAnalytics.tsx (consent !== 'granted' early return); grep -n "trackEvent" site/components/contact/ContactForm.tsx |
| 19 | Real contact address | site/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 launch | grep -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 |
| 20 | Compressed images | No 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.md | ls -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 againstweb/siterunning vianext starton ports 3101/3102 respectively (no nginx in front), matching the routes each project serves on its own — seedocs/marketing-site.mdfor 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'ssiteCtas()makes "Start free" →/app/signupthe primary CTA only whenNEXT_PUBLIC_SIGNUP_ENABLED=true, and leaves "Talk to us" →/contactin place otherwise (decision D25). The rest of this table is a/sitechecklist item and was left as it stands; the one drift found is row 7's/pricingomission. - 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 insite/lib/site-config.tsrendered throughnext/linkinSiteHeader(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/pricingomission was fixed in the same pass. Row 4 (CTA above the fold) is untouched: the hero still rendersPrimaryCtafirst, andsiteCtas()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-emptyalt, 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 beingaria-hiddendecoration with a static fallback rather than by carrying alt text (a<canvas>has noaltattribute), and satisfy item 20's "don't ship weight you don't need" spirit by never downloading their ~130KB+ chunk under reduced motion. Seedocs/site-design-system.mdfor the full rationale.