A clean, ultra-minimal, vertically scrolling marketing site translated from a
4-page horizontal presentation mockup. Built as a custom Next.js front-end with
a typed mock-content layer designed to be swapped for a headless CMS (Sanity
recommended) without component changes.
A single-page editorial website composed of four scroll sections — Index,
Practice, Work, Contact — anchored from a sticky nav. Premium serif
display type (Instrument Serif) paired with Geist Sans for body and Geist Mono
for eyebrows. Generous white space, asymmetric editorial grid for the work
section, and a sticky blurred header.
Imagery is sourced from Unsplash for now via next/image, with a reusable
Placeholder component that falls back to a labeled grey box (with the
reserved aspect ratio baked into the corner) for any media item without a
src — so the layout stays stable while final assets are being finalized.
| Layer | Choice |
|---|---|
| Framework | Next.js 16 (App Router, Turbopack) |
| Runtime | React 19 + React Compiler (auto memo) |
| Styling | Tailwind CSS v4 (CSS-based @theme) |
| Typography | Geist Sans / Geist Mono / Instrument Serif |
| Images | next/image with images.unsplash.com allowed |
| CMS (planned) | Sanity (embedded Studio at /studio) |
| Language | TypeScript (strict) |
npm install
npm run dev
Open http://localhost:3000.
| Command | Purpose |
|---|---|
npm run dev |
Start the Turbopack dev server |
npm run build |
Production build |
npm run start |
Run the production build |
npm run lint |
ESLint (eslint-config-next) |
src/
├── app/
│ ├── layout.tsx # Fonts, metadata, html shell
│ ├── page.tsx # Composes the 4 sections
│ ├── globals.css # Tailwind v4 tokens + base styles
│ └── _components/ # Non-routable UI (Next.js private folder)
│ ├── Nav.tsx
│ ├── Section.tsx # Shell + <Eyebrow> primitive
│ ├── Hero.tsx # Section 1 — Index
│ ├── Statement.tsx # Section 2 — Practice
│ ├── Works.tsx # Section 3 — Work (editorial grid)
│ ├── Contact.tsx # Section 4 — Contact
│ ├── Placeholder.tsx # next/image w/ grey-box fallback
│ └── Footer.tsx
└── lib/
└── content.ts # Typed mock content — single source of truth
src/lib/content.ts is the single source of truth for every piece of copy
and structure on the page. It follows the principles in
.agents/skills/content-modeling-best-practices/:
eyebrow, headline, pillars, media), never presentation blobs.site.name, site.tagline, nav, and sociallayout.tsx, Nav, and Footer from one place.MediaPlaceholder { label, ratio, src?, alt?, caption? }. When src is absent the layout still reserves theBecause components only read from typed exports in content.ts, the CMS
integration is mechanical:
sanity + next-sanity.Hero, Statement, Work,WorksSection, Contact, and Site types.export const hero = await getHero(), etc.) inside Server Components.app/studio/[[...tool]]/page.tsx.No component file needs to change.
| Breakpoint | Behavior |
|---|---|
< sm (mobile) |
Single-column stack; nav links hidden, Contact shortcut shown |
sm–md |
Nav links re-appear, tighter type, single-column content |
md (≥768px) |
12-column editorial grid kicks in for Statement, Works, Contact |
lg (≥1024px) |
Larger display sizes, increased vertical rhythm (py-40) |
Smooth-scroll between sections is handled by native
html { scroll-behavior: smooth } and in-page # anchors — no client JS,
no hamburger needed at the current section count.
Tokens live in src/app/globals.css and are exposed as Tailwind utilities via
@theme inline:
bg-background / text-foreground — paper + inktext-muted — secondary copyborder-rule — hairline section dividersbg-placeholder / text-placeholder-fg — grey-box fallbackfont-sans / font-serif / font-mono — Geist / Instrument Serif / Geist MonoThis repo follows Next.js 16-specific guidance — see
AGENTS.md and node_modules/next/dist/docs/ before writing code, as APIs
differ from older training data. Key choices in use:
"use client" anywhere yet).next.config.ts → no manual useMemo /useCallback._components/ is the Next.js convention for non-routable UI colocatedapp/.@theme inline — there is notailwind.config.ts./studiocontent.ts/work/[slug]