feat(landing): public marketing landing page with prerender, motion and code split (spec 0003)#53
Merged
Merged
Conversation
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
enabled auto-merge
July 14, 2026 21:13
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
prefers-reduced-motion.renderToString(no new package) renders the same tree the client hydrates, emitted as a separateindex.prerender.htmlwith title, meta description, Open Graph, Twitter, canonical and product JSON-LD./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.Linked spec / use case
docs/specs/0003-landing-page/(index.md, rationale.md, verify.md). Status: In Progress.How it was verified
npm run lint,tsc --noEmit,npm run buildall clean.GET /returns the landing markup plus SEO tags;GET /registerandGET /profilereturn the neutral bootstrap with zero landing content; static assets still serve./registerloads only the main entry chunk: no landing chunk, no framer-motion, no persona images.<details>, so every answer is in the prerendered HTML for crawlers even with JavaScript off.Definition of done (from CLAUDE.md)
context/progress-log.mdentry added (mandatory for real work)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)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.