Building petermarc.com: design, code, and AI in one repo
A full rebuild of my portfolio and services site: not a template swap, but a typed content layer, token-driven CSS, scroll-linked sections, and a component library shaped around how I actually pitch founder work, built iteratively in Cursor with the same stack I recommend to clients.
- Product design
- Front-end
- Meta

Why rebuild
The old site did the job, but it couldn't show how I work now. I needed a place that reads like a lead product designer who ships, case studies with real metrics, a services page that filters founders quickly, interaction experiments that prove craft, and copy that doesn't need a call to explain what I do. The constraint was time: one person, no agency pipeline, no CMS admin. Everything had to live in the repo, deploy in minutes, and stay editable during client work.
The site is the case study.
Every pattern on petermarc.com, scroll reveals, portfolio filters, short-form work, is something I would defend in a client review.
Stack
App Router first. Server Components where possible, client components only when scroll, motion, or browser APIs require it. No database, portfolio data is TypeScript modules compiled at build time.
Core dependencies:
- Next.js 16.2 (Turbopack dev + static export paths)
- React 19, concurrent rendering, modern refs
- TypeScript 5, strict types for portfolio entities
- Tailwind CSS 4, @import tailwindcss + @theme inline tokens
- Framer Motion 12, menu, reveals, scroll-linked opacity
- Lucide, icon set for nav and UI chrome
- Three.js / OGL, optional WebGL backgrounds (hero experiments)
- @calcom/embed-react, intro booking embed
Architecture
The repo splits into three layers: routes under `src/app`, presentation under `src/components`, and content under `src/lib`. Portfolio case studies and experiments are data, not MDX, that keeps layouts consistent and lets TypeScript enforce block types (pull quotes, flow compares, component grids) inside long-form narratives.
- `src/app`, App Router pages, layouts, API routes (`/api/contact`, game leaderboard), sitemap and robots
- `src/components/ui`, primitives: Button, TextLink, Pill, PageGrid, SectionHeader, Reveal
- `src/components/portfolio`, CaseStudyTemplate, ShortPortfolio, FilterChips
- `src/components/effects`, pixel backgrounds, cursor dot, decrypted text, gradient blinds
- `src/lib/content.ts`, marketing copy for homepage, services, about (single source for nav labels)
- `src/lib/portfolio/`, case-studies.ts, explorations.ts, short-portfolio.ts, portfolio-order.ts
- `src/lib/feature-flags.ts`, build-time toggles (logo loader, button icons, layout experiments)
- Content-in-code over CMS, faster iteration, full git history, no auth surface
- Shared CaseStudySectionBlock for case studies and experiments, one narrative renderer
- Optional `demo` on experiments, live stage for holo-cards / icon-burst; cover hero for write-ups
- Portfolio filters as client-side route replace, no full page reload when switching chips
Design system
Tokens live in `src/lib/tokens.ts` and mirror into CSS custom properties in `globals.css`. Dark is the default page shell; white and warm bands alternate for long scroll pages. Section wrappers (`WarmSection`, `InvertedSection`) set `data-nav-surface` so the fixed nav inverts logo/menu contrast automatically.
- Typography scale: `--text-hero` through `--text-small`, clamp-based editorial sizes, Inter Display for headings
- Radius tokens: `--radius-chip`, `--radius-panel`, `--radius-card`, consistent softness site-wide
- Spacing: `--page-gutter`, `--page-gap`, `--page-max`, PageGrid and Container align all sections
- Button variants: primary / secondary / ghost / onDark, CSS variables per surface so secondary on dark is solid, not ghost
- Pills & tags: shared `Pill` component; filter chips reuse tag styling with active ink fill
UI components
Primitives are intentionally boring, predictable API, no prop explosion. Composition happens at section level.
Key primitives and behaviour:
- Button, pill CTA, optional trailing ArrowLinkIcon via `icon` prop, TextReveal hover on label
- TextReveal, dual-line hover swap (menu links, masonry labels)
- SectionHeader, eyebrow + display title + lead; optional DecryptedHeading
- Reveal, Framer Motion fade/slide on scroll into view
- PixelFrame, stepped 12-column SVG outline; content-height layout for stats and pricing cards
- SeeAllWorkLink, underlined ↳ label + portfolio count, no pill chrome
- PageGrid, 12-column editorial grid with col-span helpers
- MediaCard, writing section cards with cover + tag + footer link
Portfolio system
Portfolio is the most complex surface: three presentation modes (card grid, full case study, vertical short portfolio) fed from one typed data layer.
- ShortPortfolio, full-viewport vertical stack, scroll-synced index, fixed prev/next nav portaled to body, hash deep links (`/portfolio/short#klarna`)
- ShortPortfolioStack, same slide cards at full width on the main portfolio index (extras not already in case study grid)
- WorkMasonryGrid, homepage-only layout: featured 16:9, pair row, stagger with tall slot; mobile uniform 4:3
- FilterChips, All / Short / Case studies / discipline / Experiments with live counts from `portfolioFilterCounts`
- CaseStudyTemplate, inverted hero + pixel background, centered rail layout, sticky TOC, outcomes metrics, prev/next cards
- CaseStudyTemplate, shared layout for case studies and experiments; live demo embed for interaction experiments
Order is data, not JSX.
`portfolio-order.ts` defines flagship slugs, card order, and short-portfolio slide order, reordering the homepage or index is a one-line array change, not a layout rewrite.
Motion & effects
Motion is functional first, orientation and feedback, not decoration. Every animated component checks `prefers-reduced-motion` and degrades to static state.
- Nav, fullscreen clip-path menu, staggered link reveal, scroll lock while open
- SprintSteps, scroll-driven line between [01]–[03] indices; text opacity tied to scroll progress via JS measurement
- ServicesPricingTimeline (/startups), vertical 2px line centred in the section, card opacity and PixelFrame stroke emphasis synced to scroll
- LogoLoader, first-visit-only intro (gated by sessionStorage + `NEXT_PUBLIC_LOGO_LOADER` on production)
- CaseStudyPixelBackground / DisciplinesPixelBackground, canvas pixel grids tied to section tone
- CursorDot, custom cursor on interactive `[data-cursor]` targets
- DecryptedText / DecryptedHeading, scramble reveal for hero moments
- 404 games, four mini-games with optional leaderboard API route
Content model
Marketing copy and portfolio entities are separate. `content.ts` holds site-wide strings (nav, hero, services page, about testimonials). Portfolio entities extend a shared shape with `kind: 'case-study' | 'experiment'`.
Case study section blocks (composable in data):
- pullQuote, editorial callout
- flowCompare, before/after step lists
- exploration, design variant comparison table
- componentGrid, bullet grid for systems / stacks
- decisions, numbered decision list
- changelog, grouped release notes per version
- beforeAfter, side-by-side narrative
- researchMethods, method / answered pairs
- image / video, inline media figures
Version control & deploy
GitHub repo with a staging branch for iterative work and main for production promotion. Vercel connects to the repo, preview deployments on every push, production on main merge.
- Branch flow: feature work on `staging` → verify preview URL → merge to `main` when ready
- Environment: `VERCEL_ENV=production` drives logo loader default via `next.config.ts`
- Feature flags in `feature-flags.ts`, toggle button icons, case study layout, services sections without branch divergence
- Static generation: 46 routes at build (`generateStaticParams` for case studies, explorations, services slugs)
- API routes: contact form POST handler, game leaderboard, serverless on Vercel
- Assets in `/public`, AVIF heroes, short-portfolio MP4 loops, exploration demo sprites
- No CMS webhooks, content PRs are the publish pipeline
- Staging branch avoids half-finished portfolio data hitting production
- Typecheck + `next build` as the pre-ship gate (no separate CI yaml in repo yet)
Building with Cursor
Most files were written or refactored pair-programming with Cursor agents, not generated in one shot. The workflow that worked: narrow task, point at existing component conventions, run build, fix types, ship to staging.
- AGENTS.md points agents at Next.js 16 docs in node_modules, avoids stale App Router assumptions
- Small diffs, one section, one component, one content block per pass
- Reuse before invent, Button, SectionHeader, CaseStudySectionBlock extended instead of forked
- Browser preview CSS edits fed back into globals.css (short portfolio nav contrast, button padding symmetry)
- Agent transcripts for long sessions, context for why a pattern exists

Quality bar
Ship criteria for a personal site should match client work: accessible defaults, performance-aware media, and honest metadata.
- Skip to content link, focus rings on interactive elements, aria labels on portfolio filters and short-portfolio nav
- Reduced motion, menu, reveals, ticker, decrypted text all respect OS preference
- Images, next/image with explicit dimensions; AVIF heroes; video posters for short portfolio loops
- SEO, metadata in layouts, JSON-LD in homepage, sitemap.xml generated from portfolio slugs
- AEO, `llms.txt` and structured copy so answer engines can cite case studies accurately
- Performance, PageSpeed pass on desktop after lazy media, narrowed fonts, and deferred WebGL
- Cal.com embed with theme tokens aligned to contact section


What shipped
A single repo that is portfolio, services pitch, contact funnel, and interaction lab. New case studies are a typed object and a hero image. New experiments can be a live demo or a long-form build log. The site demonstrates the same systems thinking I apply to client products, just with me as the only stakeholder.
Changelog
Numbered beta releases ship through the footer build credit.
v1.1.4
Nav menu, case study ToC, portfolio filters, and SEO polish.
- Site menu — hamburger morphs to X; link navigation unlocks scroll and lands at page top
- Case study ToC — fixed nav without scroll lock; hides over full-bleed media; no #section deep links
- Portfolio filters — switch short / case studies / disciplines without full page reload
- Startups pricing — engagement card content centered in PixelFrame
- SEO — llms-full.txt, FAQ schema on /startups, breadcrumbs, AI crawler access in robots.txt
v1.1.3
Mobile layout fixes for pricing timeline and work masonry.
- Pricing timeline cards — taller PixelFrame on mobile, stacked title and price
- Work masonry — wider vertical gaps between portfolio entries on small screens
v1.1.2
Case study layout polish, impact metrics, and exploration UX.
- Case study footer spacing and hash scroll offsets
- Recommendations grid with compact multi-card layout
- Impact uplift metrics on discount and mobile banking case studies
- Exploration pages link to live demo from last section
- Changelog CornerDownRight icons; flow diagram white badges
v1.1.1
Case study polish, client recommendations, and metadata hardening.
- Recommendations
- Links
- Case study anchors
- Open Graph
Beta v1.1.0
First bump after the initial public beta (v1.0.0).
- PixelFrame — stepped 12-column outline (Red Dot–style corner cuts), 2px stroke, content-driven height, measure-before-show so borders do not flash on load
- About stats — credentials in PixelFrame cards, scroll-triggered stat counters, padding tuned after layout measurement
- About — Selected awards row (Red Dot, iF, Awwwards, App Store Best of); designer icon on experience header
- Startups pricing — tiers in PixelFrame panels, icons tinted to frame stroke, scroll-driven vertical timeline (centred 2px line behind cards, content opacity + border emphasis on scroll)
- Work masonry — per-card hover modes (video, dim, swap); more space before the work grid on /startups
- Case studies — Sources footer with award badges; inline markdown for external links in body copy
- Analytics — Google Analytics 4 on production, including case study view events
ExperimentHolographic card carousel
Klarna card selector that feels like flipping through Pokémon trading cards, ghost-duplicate infinite scroll, pointer tilt, accelerometer-driven holo layers, and iOS haptic steps..
Icon burst
Native Apple-style app icons bursting outward in every direction on tap.
Have a product flow that feels harder to explain than it should?
Send me your product, MVP, deck, or prototype. I'll tell you where I'd start.