Skip to content

feat(landing): public marketing landing page with prerender, motion and code split (spec 0003)#53

Merged
Chineme123 merged 9 commits into
mainfrom
feat/landing-page
Jul 14, 2026
Merged

feat(landing): public marketing landing page with prerender, motion and code split (spec 0003)#53
Chineme123 merged 9 commits into
mainfrom
feat/landing-page

Conversation

@Chineme123

Copy link
Copy Markdown
Owner

Summary

Builds the public marketing landing page at / (spec 0003): the front door Quiztin did not have. Composed entirely from the locked design system, it speaks to both audiences through one hero with a "For students / For teachers" toggle, and it is prerendered to static HTML so search engines and social cards work on a client rendered SPA.

Highlights:

  • Content: toggle hero, how it works, the two audience value blocks, an honest AI feedback vignette, FAQ, the free for classrooms line, and a footer. Five AI generated persona photos, disclosed in visible microcopy as illustrative.
  • Motion (framer-motion): scroll reveals, ambient bubble drift, hero crossfade, highlighter draw on, smooth anchor scrolling, a scroll progress bar, hero parallax, and an animated FAQ accordion. Every one of them yields to prefers-reduced-motion.
  • Prerender + SEO: renderToString (no new package) renders the same tree the client hydrates, emitted as a separate index.prerender.html with title, meta description, Open Graph, Twitter, canonical and product JSON-LD.
  • Serving fix: the gateway serves the prerendered markup only at exact / via an explicit endpoint; every other route still gets the neutral SPA bootstrap. A cross check caught that a naive prerender would have served landing markup for every deep link.
  • Code split: the landing and framer-motion are their own chunk, so signing into the app never downloads them (main entry dropped ~188KB to ~141KB gzip).

Linked spec / use case

docs/specs/0003-landing-page/ (index.md, rationale.md, verify.md). Status: In Progress.

How it was verified

  • 66 frontend tests green (27 new for the landing), npm run lint, tsc --noEmit, npm run build all clean.
  • Verified over a real gateway run against the built output: GET / returns the landing markup plus SEO tags; GET /register and GET /profile return the neutral bootstrap with zero landing content; static assets still serve.
  • The prerendered page hydrates with no console errors and is fully interactive.
  • Confirmed via the Performance API that /register loads only the main entry chunk: no landing chunk, no framer-motion, no persona images.
  • The FAQ stays a native <details>, so every answer is in the prerendered HTML for crawlers even with JavaScript off.

Definition of done (from CLAUDE.md)

  • context/progress-log.md entry added (mandatory for real work)
  • If a decision changed a context file, that file is updated too (the drift rule) — ui-rules.md §5 (landing hero as an allowed gradient surface), library-docs.md (framer-motion + the prerender approach), foundation.md §7 feat(frontend): foundation — scaffold, primitives, API client, auth shell #25 (react-router v7 to v8 drift reconciled)
  • Build + tests green (frontend build/lint/tsc/test; gateway builds; backend services untouched)
  • No secrets committed

Note

The landing FAQ currently answers "Is the AI feedback real?" with "Yes" — the AI layer is still stubbed, so that copy needs softening or the AI needs shipping before this is promoted publicly. Flagged, not fixed here.

Chineme123 and others added 9 commits July 13, 2026 15:40
Adds docs/specs/0003-landing-page (index, rationale, verify) for the public marketing landing page, accepted after an independent-model cross check that caught a gateway-fallback blocker. Reconciles ui-rules.md §5 to allow the landing hero as a second blueberry-gradient surface.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…0003)

Public LandingPage at / for everyone (removes the authed index redirect); in-repo SVG decoration kit; the For students/For teachers toggle hero (ARIA tabs); how-it-works, value split, AI-feedback vignette, FAQ, CTA band, footer; five on-brand persona assets; RegisterPage reads the ?role= hint. Content build only: motion, prerender/SEO, code-split, and tests are still pending.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…h (spec 0003)

Motion for the marketing landing page (AC-10), all landing only.

New features/landing/motion/ kit:
- useMotionReady.ts combines framer-motion useReducedMotion with a mount
  gate, so the resting state is always the fully visible one; the prerender
  and reduced motion paths render plain, visible content.
- motion.tsx: Reveal (scroll reveal, fades and lifts on first in view) and
  AmbientFloat (slow drift on the decorative bubbles).

Wired in:
- Hero: crossfade of copy and persona on toggle, plus a polite aria-live
  line announcing the content change (completes AC-2).
- Highlight: the coral fill now sweeps in (the highlighter draw on).
- HowItWorks, ValueSplit, AISpotlight, Faq, CtaBand: Reveal on section
  content, staggered on the step and FAQ lists.

Also moved the hero toggle keydown onto the tabs (roving focus) so the
tablist is not interactive (lint clean), and recorded framer-motion in
context/library-docs.md.

framer-motion is still in the main bundle; the next slice code splits the
landing route (AC-13). Reduced motion is asserted in the coming test suite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… exact / (spec 0003)

Static HTML for the marketing landing page (AC-11) plus the gateway
serving split the cross check flagged (AC-14).

Frontend:
- Split the route table into routes.tsx (no createBrowserRouter, so the
  prerender can import it in Node) and a shared AppRoot.tsx (one app tree,
  used by both the client and the prerender).
- prerender.tsx renders that tree with a memory router pinned to "/" via
  renderToString (react-dom/server, no new package). seo.ts builds the
  head (title, description, Open Graph, Twitter, canonical, product
  JSON-LD). scripts/prerender.mjs injects both into the built index.html
  and writes a separate index.prerender.html.
- main.tsx hydrates when #root has prerendered markup, else mounts fresh.
- build runs vite build then npm run prerender (vite build --ssr + script).

Gateway (src/Gateway/Program.cs):
- Serve index.prerender.html only at exact "/" via an explicit endpoint
  that outranks the SPA fallback; keep the neutral index.html as the catch
  all; drop UseDefaultFiles so "/" is not rewritten to the neutral doc.

Verified over a real gateway run on the built output: / returns the
landing markup and SEO tags; /register and /profile return the neutral
bootstrap with zero landing content; the page hydrates cleanly and is
interactive. Also switched the hero toggle off AnimatePresence mode="wait"
(which felt stuck for a second) to a snappy keyed fade.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…c 0003)

The landing page and framer-motion are now their own chunk, so signing
straight into the app never downloads them (AC-13).

- routes.tsx holds every route EXCEPT the landing, and no longer imports it.
- main.tsx builds the router: on the prerendered "/" it dynamically imports
  the landing chunk then hydrates with an eager element (tree matches the
  prerender, hydration stays clean); on any other route the landing is a
  lazy route, so its chunk loads only on navigation to "/".
- prerender.tsx keeps the landing eager (Node). Removed the unused router.tsx.
- The landing CSS is now a separate chunk, so scripts/prerender.mjs reads the
  Vite build manifest and links the landing CSS (and modulepreloads its JS)
  in the prerendered document, so "/" still paints fully styled (no FOUC).
- Enabled build.manifest in vite.config.ts for that lookup.

Verified over a real gateway run: main entry dropped ~188KB to ~141KB gzip;
on "/" the landing chunk loads and the page is styled with no flash; on
"/register" the Performance API shows only the main entry, no landing chunk,
no framer-motion, no persona images.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
27 tests across the landing feature (Vitest + Testing Library + vitest-axe),
frontend suite now 57.

- Hero toggle: default audience, content swap, arrow keys, the aria-live
  content-change announcement, tab/tabpanel roles, no auto-rotate, and the
  CTA role hints (AC-2, AC-4).
- Auth-adaptive nav: signed out vs signed in (AC-5).
- Reduced-motion path: Reveal and AmbientFloat render fully visible with no
  hidden or shifted start state (AC-10).
- SEO builder: title, description, Open Graph, Twitter, canonical, valid
  product JSON-LD with escaping (AC-11).
- Prerender output has the hero copy and every section plus the head tags;
  the neutral bootstrap stays landing-free (AC-11, AC-14).
- Whole page: section order (AC-3), the free line, the persona disclosure
  (AC-15), and a clean axe pass (AC-12).

Added jsdom stubs for matchMedia and IntersectionObserver to the shared test
setup, plus a setReducedMotion helper. Tests run the deterministic reduced
motion path; the animated path and colour contrast are verified in-browser.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…03 to In Progress

- foundation.md §7 #25 and library-docs.md now say React Router v8 (the
  installed 8.2.x), noting the memory router used for the spec 0003 prerender.
  Closes the drift the spec cross check flagged.
- ui-rules.md §5 already names the landing hero as an allowed gradient
  surface (confirmed).
- Spec 0003 status Proposed -> In Progress. It advances to Accepted after an
  independent /check verify pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…point, code split)

Update the spec files to match what shipped:
- index.md: add a Build status section (files as built, the deviations from
  the plan, verification), correct the prerender to renderToString, note the
  explicit gateway endpoint and the dynamic-import code split with manifest
  CSS injection, mark the react-router and framer-motion follow-ups done.
- rationale.md: the prerender reuses react-dom/server renderToString (no new
  tool); the react-router drift is reconciled to 8.2.x.
- verify.md: point AC-11/AC-13/AC-14 at the real artifacts (index.prerender.html,
  the LandingPage chunk, the gateway MapGet endpoint) and the test files;
  correct the regression note (the gateway Program.cs changed, 57 tests).

Status stays In Progress; advances to Accepted after /check verify.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…0003, AC-10)

Scroll:
- Smooth in-page scrolling for the nav anchors, scoped to the landing with
  :root:has(.qz-landing) so it never changes how the rest of the app scrolls,
  plus scroll-margin-top so a section lands below the sticky nav.
- A scroll progress bar across the top of the viewport (ScrollProgress).
- Parallax drift on the hero's two background blobs (Parallax), moving in
  opposite directions for depth.

FAQ:
- The accordion glides open and closed with a rotating chevron. It stays a
  native <details>, so with no JavaScript the browser still opens it and every
  answer remains in the prerendered markup (AC-11). When motion is allowed
  React drives the open state so the panel can animate, holding the open
  attribute through the collapse so the browser does not hide it mid-animation.

Under reduced motion: anchors jump, the bar is not rendered, the blobs sit
still, and the FAQ is the plain native element.

Notes: framer-motion's window-level useScroll() sits at zero here because it
measures the scrollable range once and the landing arrives as its own lazy
chunk while the page is still growing, so the bar tracks scroll itself via a
MotionValue fed by a scroll listener (re-measured on resize + ResizeObserver).
framer-motion's useReducedMotion caches its answer once per module, so the
motion-allowed tests live in their own files. 9 new tests, suite now 66.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Chineme123
Chineme123 enabled auto-merge July 14, 2026 21:13
@Chineme123
Chineme123 merged commit c94c301 into main Jul 14, 2026
6 checks passed
@Chineme123
Chineme123 deleted the feat/landing-page branch July 14, 2026 21:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant