Skip to content

Portfolio starter

A personal site where availability sits in the header, work comes before biography, and five project routes are generated from one segment.

Portfoliostarterportfoliopersonalfreelancewriting

Live preview

Open the full page

Live preview — Built with Foundry

Open the starter

10 routes6 route definitionsThe frame below is the real starter, navigable inside the preview.

Route inventory

10 concrete routes from 6 definitions

editorial palette
  • Path
    /
    Label
    Home
    Routes
    single
    Composed from
    3 blocks
  • Path
    /projects
    Label
    Projects
    Routes
    single
    Composed from
    2 blocks
  • Path
    /projects/:slug
    Label
    Project
    Routes
    5 generated
    Composed from
    page: project-detail
  • Path
    /about
    Label
    About
    Routes
    single
    Composed from
    2 blocks
  • Path
    /writing
    Label
    Writing
    Routes
    single
    Composed from
    2 blocks
  • Path
    /contact
    Label
    Contact
    Routes
    single
    Composed from
    2 blocks

Component inventory

Primitives this starter leans on

Section inventory

Blocks composed into its routes

Implementation notes

  • Availability is a Status component, so it carries an icon and a word rather than a coloured dot.
  • The project routes share the case-study detail page with the Agency starter.

Architecture

Where the starter actually lives

src/
├── app/
│   └── (preview)/starters/[slug]/preview/[[...path]]/   one route for all ten
├── lib/
│   └── starters/index.ts                                the route tables
├── components/
│   ├── starter-kit/
│   │   ├── shell.tsx        header, footer and drawer, generated from routes
│   │   ├── render.tsx       maps a route to catalogue blocks or a shared page
│   │   ├── page-header.tsx  shared page header with breadcrumbs
│   │   └── pages.tsx        cart, orders, tables, articles — shared by all
│   ├── blocks/              every section, form, navbar and footer
│   └── ui/                  the primitives underneath them
└── content/
    └── demo.ts              all copy, products, posts and people

Dependencies

Runtime packages this starter needs

  • next
  • react
  • react-dom
  • lucide-react
  • tailwindcss

No state library, no styling runtime, no component library. Tailwind is a build-time dependency only.

Source

Every starter is a record in this array. There is no per-starter component tree.

import type { Starter } from './types'

export * from './types'

/**
 * The ten starter products.
 *
 * Each is a route table plus a list of catalogue blocks per route. There is no
 * per-starter component tree, which is the point: adding an eleventh starter
 * means adding a record to this array.
 */
export const starters: Starter[] = [
  {
    slug: 'saas',
    name: 'SaaS',
    brand: 'Cinder',
    tagline: 'Deployment infrastructure for teams that ship daily.',
    category: 'saas',
    palette: 'editorial',
    description:
      'A twelve-route subscription product: marketing site, authentication, an in-product dashboard and account management, all from the same component set.',
    tags: ['saas', 'subscription', 'dashboard', 'authentication'],
    difficulty: 'advanced',
    featured: true,
    routes: [
      {
        path: '',
        label: 'Home',
        title: 'Ship faster, break less',
        description: 'The marketing home page.',
        blocks: [
          'sections-hero-stats-band',
          'sections-logos-wordmarks',
          'sections-features-alternating',
          'sections-testimonials-three-column',
          'sections-cta-inverted-panel',
        ],
      },
      {
        path: 'features',
        label: 'Features',
        title: 'Features',
        description: 'What the product actually does.',
        blocks: [
          'sections-features-icon-grid',
          'sections-features-bento',
          'sections-features-two-column-list',
          'sections-cta-centered',
        ],
      },
      {
        path: 'pricing',
        label: 'Pricing',
        title: 'Pricing',
        description: 'Three plans and the detail behind them.',
        blocks: [
          'sections-pricing-three-tier',
          'sections-pricing-feature-matrix',
          'sections-faq-accordion',
        ],
      },
      {
        path: 'customers',
        label: 'Customers',
        title: 'Customers',
        description: 'Who uses it, and what changed.',
        blocks: [
          'sections-case-studies-featured',
          'sections-case-studies-grid',
          'sections-testimonials-masonry',
        ],
      },
      {
        path: 'about',
        label: 'About',
        title: 'About',
        description: 'The team and how the product got here.',
        blocks: [
          'sections-timeline-company-history',
          'sections-team-grid',
          'sections-stats-divided-band',
        ],
      },
      {
        path: 'contact',
        label: 'Contact',
        title: 'Contact',
        description: 'Four routes to a human.',
        blocks: ['sections-contact-routed-cards', 'sections-contact-offices'],
      },
      {
        path: 'login',
        label: 'Sign in',
        title: 'Sign in',
        description: 'Authentication with a server-style rejection.',
        inNav: false,
        blocks: ['forms-login'],
      },
      {
        path: 'register',
        label: 'Create account',
        title: 'Create your account',
        description: 'Registration with a live password meter.',
        inNav: false,
        blocks: ['forms-register'],
      },
      {
        path: 'dashboard',
        label: 'Dashboard',
        title: 'Overview',
        description: 'The in-product overview.',
        inNav: false,
        blocks: [
          'sections-dashboard-metric-row',
          'sections-dashboard-chart-panel',
          'sections-dashboard-activity-feed',
          'sections-dashboard-quick-actions',
        ],
      },
      {
        path: 'settings',
        label: 'Settings',
        title: 'Settings',
        description: 'Workspace and personal preferences.',
        inNav: false,
        page: 'settings',
        blocks: ['forms-preferences'],
      },
      {
        path: 'billing',
        label: 'Billing',
        title: 'Billing',
        description: 'Invoicing details and the plan matrix.',
        inNav: false,
        blocks: ['forms-billing-details', 'sections-pricing-feature-matrix'],
      },
      {
        path: 'team',
        label: 'Team',
        title: 'Team',
        description: 'Members, roles and invitations.',
        inNav: false,
        page: 'users-table',
        blocks: ['sections-dashboard-onboarding-checklist'],
      },
    ],
    usesComponents: [
      'component/button',
      'component/table',
      'component/metric',
      'component/field',
      'component/dropdown-menu',
    ],
    usesSections: [
      'section/hero-stats-band',
      'section/pricing-three-tier',
      'section/dashboard-metric-row',
    ],
    implementationNotes: [
      'Marketing and product routes share one component set at different densities — the marketing pages read well at Relaxed, the dashboard at Compact.',
      'Authentication routes reuse the catalogue form flows rather than re-implementing them.',
      'The dashboard is four application sections stacked; nothing about it is bespoke.',
    ],
  },
  {
    slug: 'agency',
    name: 'Agency',
    brand: 'Quarry Studio',
    tagline: 'We build the systems other teams build products on.',
    category: 'agency',
    palette: 'minimal',
    description:
      'A studio site whose work index drives five dynamic case-study routes, with a process section that names deliverables rather than activities.',
    tags: ['agency', 'studio', 'portfolio', 'case-studies'],
    difficulty: 'intermediate',
    featured: true,
    routes: [
      {
        path: '',
        label: 'Home',
        title: 'Quarry Studio',
        description: 'Practice statement and selected work.',
        blocks: [
          'sections-hero-editorial-split',
          'sections-case-studies-featured',
          'sections-case-studies-grid',
          'sections-logos-with-quote',
        ],
      },
      {
        path: 'work',
        label: 'Work',
        title: 'Selected work',
        description: 'The full archive.',
        blocks: ['sections-case-studies-index-list', 'sections-case-studies-metrics-strip'],
      },
      {
        path: 'work/:slug',
        label: 'Case study',
        title: 'Case study',
        description: 'One engagement in detail.',
        inNav: false,
        page: 'project-detail',
        params: [
          { slug: 'northwind-console', title: 'Northwind — one console for eleven tools' },
          { slug: 'halcyon-storefront', title: 'Halcyon — a storefront that loads on a train' },
          { slug: 'meridian-docs', title: 'Meridian — documentation that cannot go stale' },
          { slug: 'kestrel-onboarding', title: 'Kestrel — onboarding in four steps' },
          { slug: 'lumen-rebrand', title: 'Lumen Works — a rebrand in one pull request' },
        ],
      },
      {
        path: 'services',
        label: 'Services',
        title: 'Services',
        description: 'How an engagement runs.',
        blocks: [
          'sections-process-split-detail',
          'sections-process-connected-flow',
          'sections-comparison-side-by-side',
        ],
      },
      {
        path: 'about',
        label: 'About',
        title: 'About the studio',
        description: 'The people and the history.',
        blocks: [
          'sections-team-leadership',
          'sections-team-list-rows',
          'sections-timeline-company-history',
        ],
      },
      {
        path: 'journal',
        label: 'Journal',
        title: 'Journal',
        description: 'Writing from the studio.',
        blocks: ['sections-blog-editorial-index'],
      },
      {
        path: 'contact',
        label: 'Contact',
        title: 'Start a conversation',
        description: 'Enquiries and offices.',
        blocks: ['sections-contact-split-form', 'sections-contact-offices'],
      },
    ],
    usesComponents: [
      'component/card',
      'component/badge',
      'component/breadcrumb',
      'component/avatar',
    ],
    usesSections: [
      'section/case-studies-featured',
      'section/process-split-detail',
      'section/team-leadership',
    ],
    implementationNotes: [
      'The Minimal palette squares every corner and removes elevation, which is why this starter looks nothing like the SaaS one despite sharing every component.',
      'Case study routes are generated from one dynamic segment with five parameter values.',
      'The contact section embeds the catalogue contact form directly.',
    ],
  },
  {
    slug: 'ecommerce',
    name: 'Ecommerce',
    brand: 'Quarry',
    tagline: 'Clothing made to be worn until it wears out.',
    category: 'ecommerce',
    palette: 'warm',
    description:
      'A storefront with ten product routes, five category routes, a working cart with quantity controls and the multi-step checkout from the form catalogue.',
    tags: ['ecommerce', 'storefront', 'cart', 'checkout'],
    difficulty: 'advanced',
    featured: true,
    routes: [
      {
        path: '',
        label: 'Home',
        title: 'Quarry',
        description: 'Seasonal statement and the product grid.',
        blocks: [
          'sections-product-collection-banner',
          'sections-product-feature-grid',
          'sections-testimonials-with-rating',
        ],
      },
      {
        path: 'shop',
        label: 'Shop',
        title: 'Everything',
        description: 'The full catalogue.',
        blocks: ['sections-product-feature-grid', 'sections-product-comparison-row'],
      },
      {
        path: 'shop/:slug',
        label: 'Product',
        title: 'Product',
        description: 'One product in detail.',
        inNav: false,
        page: 'product-detail',
        params: [
          { slug: 'field-shell-jacket', title: 'Field Shell Jacket' },
          { slug: 'quarry-overshirt', title: 'Quarry Overshirt' },
          { slug: 'meridian-knit', title: 'Meridian Merino Knit' },
          { slug: 'harbour-cardigan', title: 'Harbour Cardigan' },
          { slug: 'atlas-chino', title: 'Atlas Chino' },
          { slug: 'ironwood-trouser', title: 'Ironwood Wool Trouser' },
          { slug: 'lumen-tote', title: 'Lumen Tote' },
          { slug: 'kestrel-cap', title: 'Kestrel Six-Panel Cap' },
          { slug: 'verdant-tee', title: 'Verdant Heavy Tee' },
          { slug: 'halcyon-socks', title: 'Halcyon Ribbed Socks' },
        ],
      },
      {
        path: 'category/:slug',
        label: 'Category',
        title: 'Category',
        description: 'Products in one category.',
        inNav: false,
        blocks: ['sections-product-feature-grid', 'sections-product-collection-banner'],
        params: [
          { slug: 'outerwear', title: 'Outerwear' },
          { slug: 'knitwear', title: 'Knitwear' },
          { slug: 'trousers', title: 'Trousers' },
          { slug: 'accessories', title: 'Accessories' },
          { slug: 'basics', title: 'Basics' },
        ],
      },
      {
        path: 'cart',
        label: 'Bag',
        title: 'Your bag',
        description: 'Quantity controls and a live total.',
        page: 'cart',
      },
      {
        path: 'checkout',
        label: 'Checkout',
        title: 'Checkout',
        description: 'The four-step checkout flow.',
        inNav: false,
        blocks: ['forms-checkout'],
      },
      {
        path: 'account',
        label: 'Account',
        title: 'Your account',
        description: 'Details, addresses and preferences.',
        inNav: false,
        page: 'account',
      },
      {
        path: 'orders',
        label: 'Orders',
        title: 'Order history',
        description: 'Past orders and their status.',
        inNav: false,
        page: 'orders',
      },
    ],
    usesComponents: ['component/table', 'component/badge', 'component/button', 'component/select'],
    usesSections: [
      'section/product-feature-grid',
      'section/product-detail-split',
      'section/product-collection-banner',
    ],
    implementationNotes: [
      'Ten product routes and five category routes come from two dynamic segments, not fifteen files.',
      'The cart is one shared starter-kit page with real quantity state and an empty state.',
      'Checkout is the catalogue checkout wizard, unchanged.',
    ],
  },
  {
    slug: 'portfolio',
    name: 'Portfolio',
    brand: 'Amara Osei',
    tagline: 'Frontend architecture, design systems, accessibility.',
    category: 'portfolio',
    palette: 'editorial',
    description:
      'A personal site where availability sits in the header, work comes before biography, and five project routes are generated from one segment.',
    tags: ['portfolio', 'personal', 'freelance', 'writing'],
    difficulty: 'starter',
    featured: false,
    routes: [
      {
        path: '',
        label: 'Home',
        title: 'Amara Osei',
        description: 'Availability, practice and selected work.',
        blocks: [
          'sections-hero-editorial-split',
          'sections-case-studies-index-list',
          'sections-case-studies-quote-outcome',
        ],
      },
      {
        path: 'projects',
        label: 'Projects',
        title: 'Projects',
        description: 'Everything worth showing.',
        blocks: ['sections-case-studies-grid', 'sections-case-studies-metrics-strip'],
      },
      {
        path: 'projects/:slug',
        label: 'Project',
        title: 'Project',
        description: 'One project in detail.',
        inNav: false,
        page: 'project-detail',
        params: [
          { slug: 'northwind-console', title: 'Northwind Console' },
          { slug: 'halcyon-storefront', title: 'Halcyon Storefront' },
          { slug: 'meridian-docs', title: 'Meridian Docs' },
          { slug: 'kestrel-onboarding', title: 'Kestrel Onboarding' },
          { slug: 'lumen-rebrand', title: 'Lumen Rebrand' },
        ],
      },
      {
        path: 'about',
        label: 'About',
        title: 'About',
        description: 'How I got here.',
        blocks: ['sections-timeline-company-history', 'sections-team-leadership'],
      },
      {
        path: 'writing',
        label: 'Writing',
        title: 'Writing',
        description: 'Articles and notes.',
        blocks: ['sections-blog-editorial-index', 'sections-blog-category-columns'],
      },
      {
        path: 'contact',
        label: 'Contact',
        title: 'Get in touch',
        description: 'Availability and a booking route.',
        blocks: ['sections-contact-booking-cta', 'sections-contact-split-form'],
      },
    ],
    usesComponents: ['component/status', 'component/badge', 'component/breadcrumb'],
    usesSections: [
      'section/case-studies-index-list',
      'section/blog-editorial-index',
      'section/contact-booking-cta',
    ],
    implementationNotes: [
      'Availability is a Status component, so it carries an icon and a word rather than a coloured dot.',
      'The project routes share the case-study detail page with the Agency starter.',
    ],
  },
  {
    slug: 'documentation',
    name: 'Documentation',
    brand: 'Foundry Docs',
    tagline: 'Reference, guides and an API surface.',
    category: 'documentation',
    palette: 'corporate',
    description:
      'A developer documentation site with six article routes, a guide index, an API reference and a working search page.',
    tags: ['docs', 'developer', 'reference', 'api'],
    difficulty: 'intermediate',
    featured: true,
    routes: [
      {
        path: '',
        label: 'Home',
        title: 'Foundry Docs',
        description: 'Where to start.',
        blocks: [
          'sections-hero-code-sample',
          'sections-features-numbered-list',
          'sections-faq-categorised',
        ],
      },
      {
        path: 'docs',
        label: 'Docs',
        title: 'Documentation',
        description: 'Every reference page.',
        blocks: ['sections-features-two-column-list', 'sections-integrations-api-first'],
      },
      {
        path: 'docs/:slug',
        label: 'Article',
        title: 'Article',
        description: 'One documentation page.',
        inNav: false,
        page: 'docs-article',
        params: [
          { slug: 'getting-started', title: 'Getting started' },
          { slug: 'installation', title: 'Installation' },
          { slug: 'theming', title: 'Theming' },
          { slug: 'tokens', title: 'Design tokens' },
          { slug: 'composition', title: 'Composition' },
          { slug: 'accessibility', title: 'Accessibility' },
        ],
      },
      {
        path: 'guides',
        label: 'Guides',
        title: 'Guides',
        description: 'Task-shaped walkthroughs.',
        page: 'guides',
      },
      {
        path: 'api',
        label: 'API',
        title: 'API reference',
        description: 'Endpoints and authentication.',
        page: 'api-reference',
      },
      {
        path: 'changelog',
        label: 'Changelog',
        title: 'Changelog',
        description: 'What shipped, and when.',
        blocks: ['sections-timeline-release-log'],
      },
      {
        path: 'search',
        label: 'Search',
        title: 'Search',
        description: 'Search across guides and the API.',
        page: 'search',
      },
    ],
    usesComponents: [
      'component/tabs',
      'component/search-field',
      'component/badge',
      'component/breadcrumb',
    ],
    usesSections: [
      'section/hero-code-sample',
      'section/integrations-api-first',
      'section/timeline-release-log',
    ],
    implementationNotes: [
      'Six article routes come from one dynamic segment and one shared prose page.',
      'The search page degrades to an empty state with a real suggestion rather than a dead control.',
      'The Corporate palette keeps the type grotesque throughout, which suits a reference site.',
    ],
  },
  {
    slug: 'admin',
    name: 'Admin',
    brand: 'Console',
    tagline: 'Internal tools at compact density.',
    category: 'admin',
    palette: 'corporate',
    description:
      'An internal console: overview, a filterable member table, orders, analytics and settings — all at compact density on the same components as the marketing starters.',
    tags: ['admin', 'dashboard', 'internal', 'tables'],
    difficulty: 'advanced',
    featured: true,
    routes: [
      {
        path: '',
        label: 'Overview',
        title: 'Overview',
        description: 'Metrics, activity and quick actions.',
        blocks: [
          'sections-dashboard-metric-row',
          'sections-dashboard-quick-actions',
          'sections-dashboard-activity-feed',
        ],
      },
      {
        path: 'dashboard',
        label: 'Dashboard',
        title: 'Dashboard',
        description: 'Charts and service health.',
        blocks: [
          'sections-dashboard-chart-panel',
          'sections-dashboard-status-overview',
          'sections-dashboard-onboarding-checklist',
        ],
      },
      {
        path: 'users',
        label: 'Users',
        title: 'Members',
        description: 'A filterable member table.',
        page: 'users-table',
      },
      {
        path: 'orders',
        label: 'Orders',
        title: 'Orders',
        description: 'Recent orders and their status.',
        blocks: ['sections-dashboard-data-table', 'sections-empty-filtered-out'],
      },
      {
        path: 'analytics',
        label: 'Analytics',
        title: 'Analytics',
        description: 'Traffic, conversion and top pages.',
        page: 'analytics',
      },
      {
        path: 'settings',
        label: 'Settings',
        title: 'Settings',
        description: 'Workspace configuration.',
        page: 'settings',
        blocks: ['forms-notifications'],
      },
    ],
    usesComponents: [
      'component/table',
      'component/metric',
      'component/status',
      'component/search-field',
    ],
    usesSections: [
      'section/dashboard-data-table',
      'section/dashboard-status-overview',
      'section/empty-filtered-out',
    ],
    implementationNotes: [
      'Every screen uses the same primitives as the marketing starters; only the density differs.',
      'The member table has a working filter with a live result count and a real empty state.',
    ],
  },
  {
    slug: 'restaurant',
    name: 'Restaurant',
    brand: 'Ember',
    tagline: 'A small dining room and a nine-seat counter.',
    category: 'restaurant',
    palette: 'warm',
    description:
      'A hospitality site whose menu is real, searchable text with dietary tags — never a PDF — with booking above everything else.',
    tags: ['restaurant', 'hospitality', 'menu', 'booking'],
    difficulty: 'starter',
    featured: false,
    routes: [
      {
        path: '',
        label: 'Home',
        title: 'Ember',
        description: 'The room, the hours and the menu.',
        page: 'reservations',
        blocks: ['sections-testimonials-carousel'],
      },
      {
        path: 'menu',
        label: 'Menu',
        title: 'This week',
        description: 'Three courses, changed with the produce.',
        blocks: ['sections-contact-offices'],
      },
      {
        path: 'reservations',
        label: 'Reservations',
        title: 'Reservations',
        description: 'Booking and the house policy.',
        page: 'reservations',
      },
      {
        path: 'about',
        label: 'About',
        title: 'About Ember',
        description: 'The people and the history.',
        blocks: ['sections-team-leadership', 'sections-timeline-company-history'],
      },
      {
        path: 'private-dining',
        label: 'Private dining',
        title: 'Private dining',
        description: 'Three spaces and how to enquire.',
        page: 'private-dining',
      },
      {
        path: 'contact',
        label: 'Contact',
        title: 'Contact',
        description: 'Where we are and how to reach us.',
        blocks: ['sections-contact-offices', 'sections-contact-split-form'],
      },
    ],
    usesComponents: ['component/badge', 'component/button', 'component/card'],
    usesSections: [
      'section/contact-offices',
      'section/testimonials-carousel',
      'section/team-leadership',
    ],
    implementationNotes: [
      'The full menu is a description list with dietary tags, so it is readable and searchable on a phone.',
      'The Warm palette carries a serif display face and cream surfaces, which no other starter uses.',
    ],
  },
  {
    slug: 'startup',
    name: 'Startup',
    brand: 'Kestrel',
    tagline: 'Stop rebuilding the same twenty components.',
    category: 'startup',
    palette: 'modern',
    description:
      'An early-stage company site that is explicit about scope, with a single pricing plan and a solutions page rather than a feature wall.',
    tags: ['startup', 'early-stage', 'scope', 'pricing'],
    difficulty: 'starter',
    featured: false,
    routes: [
      {
        path: '',
        label: 'Home',
        title: 'Kestrel',
        description: 'The claim, the proof and the limits.',
        blocks: [
          'sections-hero-two-audience',
          'sections-stats-inline-proof',
          'sections-features-checklist-split',
          'sections-testimonials-with-rating',
        ],
      },
      {
        path: 'product',
        label: 'Product',
        title: 'Product',
        description: 'What it does, in detail.',
        blocks: [
          'sections-features-bento',
          'sections-features-spotlight',
          'sections-features-interactive-preview',
        ],
      },
      {
        path: 'solutions',
        label: 'Solutions',
        title: 'Solutions',
        description: 'Who it is for.',
        page: 'solutions',
      },
      {
        path: 'pricing',
        label: 'Pricing',
        title: 'Pricing',
        description: 'One plan, no tier to outgrow.',
        blocks: ['sections-pricing-single-plan', 'sections-faq-with-support-cta'],
      },
      {
        path: 'customers',
        label: 'Customers',
        title: 'Customers',
        description: 'Who is already using it.',
        blocks: ['sections-logos-grid-cells', 'sections-testimonials-logo-quote-pair'],
      },
      {
        path: 'blog',
        label: 'Blog',
        title: 'Blog',
        description: 'Writing from the team.',
        blocks: ['sections-blog-card-grid'],
      },
      {
        path: 'contact',
        label: 'Contact',
        title: 'Contact',
        description: 'Talk to a person.',
        blocks: ['sections-contact-split-form'],
      },
    ],
    usesComponents: ['component/card', 'component/badge', 'component/button'],
    usesSections: [
      'section/features-checklist-split',
      'section/pricing-single-plan',
      'section/hero-two-audience',
    ],
    implementationNotes: [
      'The Modern palette rounds corners generously and switches the accent to teal.',
      'The included-and-excluded checklist sits on the home page, where it filters out the wrong customers early.',
    ],
  },
  {
    slug: 'blog',
    name: 'Content',
    brand: 'The Quarterly',
    tagline: 'Four issues a year on design systems and craft.',
    category: 'blog',
    palette: 'editorial',
    description:
      'A magazine with six article routes, four category indexes and four author pages — twenty-one routes from six definitions.',
    tags: ['magazine', 'editorial', 'articles', 'authors'],
    difficulty: 'intermediate',
    featured: false,
    routes: [
      {
        path: '',
        label: 'Home',
        title: 'The Quarterly',
        description: 'Lead article and the indexes.',
        blocks: [
          'sections-blog-featured-plus-list',
          'sections-blog-category-columns',
          'sections-newsletter-with-archive',
        ],
      },
      {
        path: 'stories',
        label: 'Stories',
        title: 'All stories',
        description: 'The full archive.',
        blocks: ['sections-blog-editorial-index'],
      },
      {
        path: 'stories/:slug',
        label: 'Story',
        title: 'Story',
        description: 'One article.',
        inNav: false,
        page: 'article',
        params: [
          { slug: 'density-as-an-axis', title: 'Density belongs in your token system' },
          { slug: 'manual-tab-activation', title: 'Why your tabs should not auto-activate' },
          { slug: 'documentation-from-source', title: 'Generate documentation from the file' },
          { slug: 'server-components-marketing', title: 'Marketing pages do not need a runtime' },
          {
            slug: 'variants-that-earn-their-place',
            title: 'A variant should represent a decision',
          },
          { slug: 'focus-return', title: 'The half of focus management everyone forgets' },
        ],
      },
      {
        path: 'categories/:slug',
        label: 'Category',
        title: 'Category',
        description: 'Articles in one section.',
        inNav: false,
        page: 'category-index',
        params: [
          { slug: 'design-systems', title: 'Design systems' },
          { slug: 'accessibility', title: 'Accessibility' },
          { slug: 'tooling', title: 'Tooling' },
          { slug: 'performance', title: 'Performance' },
        ],
      },
      {
        path: 'authors/:slug',
        label: 'Author',
        title: 'Author',
        description: 'One contributor.',
        inNav: false,
        page: 'author',
        params: [
          { slug: 'priya-raman', title: 'Priya Raman' },
          { slug: 'tomas-lindqvist', title: 'Tomas Lindqvist' },
          { slug: 'amara-osei', title: 'Amara Osei' },
          { slug: 'sofia-delgado', title: 'Sofia Delgado' },
        ],
      },
      {
        path: 'about',
        label: 'About',
        title: 'About the publication',
        description: 'The masthead and the method.',
        blocks: ['sections-team-list-rows', 'sections-newsletter-panel'],
      },
    ],
    usesComponents: ['component/badge', 'component/avatar', 'component/pagination'],
    usesSections: [
      'section/blog-featured-plus-list',
      'section/blog-editorial-index',
      'section/newsletter-with-archive',
    ],
    implementationNotes: [
      'Twenty-one routes come from six definitions, because three of them carry parameter lists.',
      'Category pages fall back to recent articles when a category has no matches, rather than rendering an error.',
    ],
  },
  {
    slug: 'product',
    name: 'Product landing',
    brand: 'Vector',
    tagline: 'Typed event streaming with a schema registry.',
    category: 'product',
    palette: 'modern',
    description:
      'A single-product site built around one capability, with an integration surface, a resource index and an interactive preview.',
    tags: ['product', 'landing', 'integrations', 'single-product'],
    difficulty: 'starter',
    featured: false,
    routes: [
      {
        path: '',
        label: 'Home',
        title: 'Vector',
        description: 'One product, one promise.',
        blocks: [
          'sections-hero-product-mosaic',
          'sections-features-spotlight',
          'sections-stats-with-context',
          'sections-cta-testimonial-backed',
        ],
      },
      {
        path: 'product',
        label: 'Product',
        title: 'The product',
        description: 'How it works.',
        blocks: ['sections-features-alternating', 'sections-features-interactive-preview'],
      },
      {
        path: 'features',
        label: 'Features',
        title: 'Features',
        description: 'Everything it does.',
        blocks: ['sections-features-icon-grid', 'sections-features-two-column-list'],
      },
      {
        path: 'integrations',
        label: 'Integrations',
        title: 'Integrations',
        description: 'What it connects to.',
        blocks: [
          'sections-integrations-logo-grid',
          'sections-integrations-featured-pair',
          'sections-integrations-api-first',
        ],
      },
      {
        path: 'pricing',
        label: 'Pricing',
        title: 'Pricing',
        description: 'Usage-based, with a calculator.',
        blocks: [
          'sections-pricing-usage-calculator',
          'sections-pricing-toggle-cadence',
          'sections-faq-two-column',
        ],
      },
      {
        path: 'resources',
        label: 'Resources',
        title: 'Resources',
        description: 'Documentation and guides.',
        page: 'resources',
      },
      {
        path: 'contact',
        label: 'Contact',
        title: 'Contact',
        description: 'Enquiries and support.',
        blocks: ['sections-contact-routed-cards'],
      },
    ],
    usesComponents: ['component/slider', 'component/combobox', 'component/badge'],
    usesSections: [
      'section/integrations-logo-grid',
      'section/pricing-usage-calculator',
      'section/features-spotlight',
    ],
    implementationNotes: [
      'The pricing page pairs a usage calculator with a cadence toggle, which covers both self-serve and sales-assisted buyers.',
      'The integrations page is three catalogue sections in sequence — grid, featured pair, API.',
    ],
  },
]

export const starterBySlug = new Map(starters.map((starter) => [starter.slug, starter]))

/**
 * Absolute path for a route inside a starter preview. Kept here rather than in
 * the shell so Server Components can build links without importing a client
 * module.
 */
export function starterHref(starter: Starter, path: string): string {
  const base = `/starters/${starter.slug}/preview`
  return path ? `${base}/${path}` : base
}

/** Every concrete path a starter renders, with dynamic segments expanded. */
export function expandRoutes(starter: Starter): Array<{ path: string; title: string }> {
  const out: Array<{ path: string; title: string }> = []
  for (const route of starter.routes) {
    if (route.params) {
      for (const param of route.params) {
        out.push({ path: route.path.replace(':slug', param.slug), title: param.title })
      }
    } else {
      out.push({ path: route.path, title: route.title })
    }
  }
  return out
}

/** Total concrete routes across every starter. */
export const starterRouteCount = starters.reduce(
  (total, starter) => total + expandRoutes(starter).length,
  0,
)

export function matchRoute(starter: Starter, path: string) {
  const direct = starter.routes.find((route) => route.path === path && !route.params)
  if (direct) return { route: direct, param: undefined }

  for (const route of starter.routes) {
    if (!route.params) continue
    const prefix = route.path.replace(':slug', '')
    if (!path.startsWith(prefix)) continue
    const slug = path.slice(prefix.length)
    const param = route.params.find((entry) => entry.slug === slug)
    if (param) return { route, param }
  }

  return undefined
}

lib/starters/index.ts

import type { Starter } from './types'

export * from './types'

/**
 * The ten starter products.
 *
 * Each is a route table plus a list of catalogue blocks per route. There is no
 * per-starter component tree, which is the point: adding an eleventh starter
 * means adding a record to this array.
 */
export const starters: Starter[] = [
  {
    slug: 'saas',
    name: 'SaaS',
    brand: 'Cinder',
    tagline: 'Deployment infrastructure for teams that ship daily.',
    category: 'saas',
    palette: 'editorial',
    description:
      'A twelve-route subscription product: marketing site, authentication, an in-product dashboard and account management, all from the same component set.',
    tags: ['saas', 'subscription', 'dashboard', 'authentication'],
    difficulty: 'advanced',
    featured: true,
    routes: [
      {
        path: '',
        label: 'Home',
        title: 'Ship faster, break less',
        description: 'The marketing home page.',
        blocks: [
          'sections-hero-stats-band',
          'sections-logos-wordmarks',
          'sections-features-alternating',
          'sections-testimonials-three-column',
          'sections-cta-inverted-panel',
        ],
      },
      {
        path: 'features',
        label: 'Features',
        title: 'Features',
        description: 'What the product actually does.',
        blocks: [
          'sections-features-icon-grid',
          'sections-features-bento',
          'sections-features-two-column-list',
          'sections-cta-centered',
        ],
      },
      {
        path: 'pricing',
        label: 'Pricing',
        title: 'Pricing',
        description: 'Three plans and the detail behind them.',
        blocks: [
          'sections-pricing-three-tier',
          'sections-pricing-feature-matrix',
          'sections-faq-accordion',
        ],
      },
      {
        path: 'customers',
        label: 'Customers',
        title: 'Customers',
        description: 'Who uses it, and what changed.',
        blocks: [
          'sections-case-studies-featured',
          'sections-case-studies-grid',
          'sections-testimonials-masonry',
        ],
      },
      {
        path: 'about',
        label: 'About',
        title: 'About',
        description: 'The team and how the product got here.',
        blocks: [
          'sections-timeline-company-history',
          'sections-team-grid',
          'sections-stats-divided-band',
        ],
      },
      {
        path: 'contact',
        label: 'Contact',
        title: 'Contact',
        description: 'Four routes to a human.',
        blocks: ['sections-contact-routed-cards', 'sections-contact-offices'],
      },
      {
        path: 'login',
        label: 'Sign in',
        title: 'Sign in',
        description: 'Authentication with a server-style rejection.',
        inNav: false,
        blocks: ['forms-login'],
      },
      {
        path: 'register',
        label: 'Create account',
        title: 'Create your account',
        description: 'Registration with a live password meter.',
        inNav: false,
        blocks: ['forms-register'],
      },
      {
        path: 'dashboard',
        label: 'Dashboard',
        title: 'Overview',
        description: 'The in-product overview.',
        inNav: false,
        blocks: [
          'sections-dashboard-metric-row',
          'sections-dashboard-chart-panel',
          'sections-dashboard-activity-feed',
          'sections-dashboard-quick-actions',
        ],
      },
      {
        path: 'settings',
        label: 'Settings',
        title: 'Settings',
        description: 'Workspace and personal preferences.',
        inNav: false,
        page: 'settings',
        blocks: ['forms-preferences'],
      },
      {
        path: 'billing',
        label: 'Billing',
        title: 'Billing',
        description: 'Invoicing details and the plan matrix.',
        inNav: false,
        blocks: ['forms-billing-details', 'sections-pricing-feature-matrix'],
      },
      {
        path: 'team',
        label: 'Team',
        title: 'Team',
        description: 'Members, roles and invitations.',
        inNav: false,
        page: 'users-table',
        blocks: ['sections-dashboard-onboarding-checklist'],
      },
    ],
    usesComponents: [
      'component/button',
      'component/table',
      'component/metric',
      'component/field',
      'component/dropdown-menu',
    ],
    usesSections: [
      'section/hero-stats-band',
      'section/pricing-three-tier',
      'section/dashboard-metric-row',
    ],
    implementationNotes: [
      'Marketing and product routes share one component set at different densities — the marketing pages read well at Relaxed, the dashboard at Compact.',
      'Authentication routes reuse the catalogue form flows rather than re-implementing them.',
      'The dashboard is four application sections stacked; nothing about it is bespoke.',
    ],
  },
  {
    slug: 'agency',
    name: 'Agency',
    brand: 'Quarry Studio',
    tagline: 'We build the systems other teams build products on.',
    category: 'agency',
    palette: 'minimal',
    description:
      'A studio site whose work index drives five dynamic case-study routes, with a process section that names deliverables rather than activities.',
    tags: ['agency', 'studio', 'portfolio', 'case-studies'],
    difficulty: 'intermediate',
    featured: true,
    routes: [
      {
        path: '',
        label: 'Home',
        title: 'Quarry Studio',
        description: 'Practice statement and selected work.',
        blocks: [
          'sections-hero-editorial-split',
          'sections-case-studies-featured',
          'sections-case-studies-grid',
          'sections-logos-with-quote',
        ],
      },
      {
        path: 'work',
        label: 'Work',
        title: 'Selected work',
        description: 'The full archive.',
        blocks: ['sections-case-studies-index-list', 'sections-case-studies-metrics-strip'],
      },
      {
        path: 'work/:slug',
        label: 'Case study',
        title: 'Case study',
        description: 'One engagement in detail.',
        inNav: false,
        page: 'project-detail',
        params: [
          { slug: 'northwind-console', title: 'Northwind — one console for eleven tools' },
          { slug: 'halcyon-storefront', title: 'Halcyon — a storefront that loads on a train' },
          { slug: 'meridian-docs', title: 'Meridian — documentation that cannot go stale' },
          { slug: 'kestrel-onboarding', title: 'Kestrel — onboarding in four steps' },
          { slug: 'lumen-rebrand', title: 'Lumen Works — a rebrand in one pull request' },
        ],
      },
      {
        path: 'services',
        label: 'Services',
        title: 'Services',
        description: 'How an engagement runs.',
        blocks: [
          'sections-process-split-detail',
          'sections-process-connected-flow',
          'sections-comparison-side-by-side',
        ],
      },
      {
        path: 'about',
        label: 'About',
        title: 'About the studio',
        description: 'The people and the history.',
        blocks: [
          'sections-team-leadership',
          'sections-team-list-rows',
          'sections-timeline-company-history',
        ],
      },
      {
        path: 'journal',
        label: 'Journal',
        title: 'Journal',
        description: 'Writing from the studio.',
        blocks: ['sections-blog-editorial-index'],
      },
      {
        path: 'contact',
        label: 'Contact',
        title: 'Start a conversation',
        description: 'Enquiries and offices.',
        blocks: ['sections-contact-split-form', 'sections-contact-offices'],
      },
    ],
    usesComponents: [
      'component/card',
      'component/badge',
      'component/breadcrumb',
      'component/avatar',
    ],
    usesSections: [
      'section/case-studies-featured',
      'section/process-split-detail',
      'section/team-leadership',
    ],
    implementationNotes: [
      'The Minimal palette squares every corner and removes elevation, which is why this starter looks nothing like the SaaS one despite sharing every component.',
      'Case study routes are generated from one dynamic segment with five parameter values.',
      'The contact section embeds the catalogue contact form directly.',
    ],
  },
  {
    slug: 'ecommerce',
    name: 'Ecommerce',
    brand: 'Quarry',
    tagline: 'Clothing made to be worn until it wears out.',
    category: 'ecommerce',
    palette: 'warm',
    description:
      'A storefront with ten product routes, five category routes, a working cart with quantity controls and the multi-step checkout from the form catalogue.',
    tags: ['ecommerce', 'storefront', 'cart', 'checkout'],
    difficulty: 'advanced',
    featured: true,
    routes: [
      {
        path: '',
        label: 'Home',
        title: 'Quarry',
        description: 'Seasonal statement and the product grid.',
        blocks: [
          'sections-product-collection-banner',
          'sections-product-feature-grid',
          'sections-testimonials-with-rating',
        ],
      },
      {
        path: 'shop',
        label: 'Shop',
        title: 'Everything',
        description: 'The full catalogue.',
        blocks: ['sections-product-feature-grid', 'sections-product-comparison-row'],
      },
      {
        path: 'shop/:slug',
        label: 'Product',
        title: 'Product',
        description: 'One product in detail.',
        inNav: false,
        page: 'product-detail',
        params: [
          { slug: 'field-shell-jacket', title: 'Field Shell Jacket' },
          { slug: 'quarry-overshirt', title: 'Quarry Overshirt' },
          { slug: 'meridian-knit', title: 'Meridian Merino Knit' },
          { slug: 'harbour-cardigan', title: 'Harbour Cardigan' },
          { slug: 'atlas-chino', title: 'Atlas Chino' },
          { slug: 'ironwood-trouser', title: 'Ironwood Wool Trouser' },
          { slug: 'lumen-tote', title: 'Lumen Tote' },
          { slug: 'kestrel-cap', title: 'Kestrel Six-Panel Cap' },
          { slug: 'verdant-tee', title: 'Verdant Heavy Tee' },
          { slug: 'halcyon-socks', title: 'Halcyon Ribbed Socks' },
        ],
      },
      {
        path: 'category/:slug',
        label: 'Category',
        title: 'Category',
        description: 'Products in one category.',
        inNav: false,
        blocks: ['sections-product-feature-grid', 'sections-product-collection-banner'],
        params: [
          { slug: 'outerwear', title: 'Outerwear' },
          { slug: 'knitwear', title: 'Knitwear' },
          { slug: 'trousers', title: 'Trousers' },
          { slug: 'accessories', title: 'Accessories' },
          { slug: 'basics', title: 'Basics' },
        ],
      },
      {
        path: 'cart',
        label: 'Bag',
        title: 'Your bag',
        description: 'Quantity controls and a live total.',
        page: 'cart',
      },
      {
        path: 'checkout',
        label: 'Checkout',
        title: 'Checkout',
        description: 'The four-step checkout flow.',
        inNav: false,
        blocks: ['forms-checkout'],
      },
      {
        path: 'account',
        label: 'Account',
        title: 'Your account',
        description: 'Details, addresses and preferences.',
        inNav: false,
        page: 'account',
      },
      {
        path: 'orders',
        label: 'Orders',
        title: 'Order history',
        description: 'Past orders and their status.',
        inNav: false,
        page: 'orders',
      },
    ],
    usesComponents: ['component/table', 'component/badge', 'component/button', 'component/select'],
    usesSections: [
      'section/product-feature-grid',
      'section/product-detail-split',
      'section/product-collection-banner',
    ],
    implementationNotes: [
      'Ten product routes and five category routes come from two dynamic segments, not fifteen files.',
      'The cart is one shared starter-kit page with real quantity state and an empty state.',
      'Checkout is the catalogue checkout wizard, unchanged.',
    ],
  },
  {
    slug: 'portfolio',
    name: 'Portfolio',
    brand: 'Amara Osei',
    tagline: 'Frontend architecture, design systems, accessibility.',
    category: 'portfolio',
    palette: 'editorial',
    description:
      'A personal site where availability sits in the header, work comes before biography, and five project routes are generated from one segment.',
    tags: ['portfolio', 'personal', 'freelance', 'writing'],
    difficulty: 'starter',
    featured: false,
    routes: [
      {
        path: '',
        label: 'Home',
        title: 'Amara Osei',
        description: 'Availability, practice and selected work.',
        blocks: [
          'sections-hero-editorial-split',
          'sections-case-studies-index-list',
          'sections-case-studies-quote-outcome',
        ],
      },
      {
        path: 'projects',
        label: 'Projects',
        title: 'Projects',
        description: 'Everything worth showing.',
        blocks: ['sections-case-studies-grid', 'sections-case-studies-metrics-strip'],
      },
      {
        path: 'projects/:slug',
        label: 'Project',
        title: 'Project',
        description: 'One project in detail.',
        inNav: false,
        page: 'project-detail',
        params: [
          { slug: 'northwind-console', title: 'Northwind Console' },
          { slug: 'halcyon-storefront', title: 'Halcyon Storefront' },
          { slug: 'meridian-docs', title: 'Meridian Docs' },
          { slug: 'kestrel-onboarding', title: 'Kestrel Onboarding' },
          { slug: 'lumen-rebrand', title: 'Lumen Rebrand' },
        ],
      },
      {
        path: 'about',
        label: 'About',
        title: 'About',
        description: 'How I got here.',
        blocks: ['sections-timeline-company-history', 'sections-team-leadership'],
      },
      {
        path: 'writing',
        label: 'Writing',
        title: 'Writing',
        description: 'Articles and notes.',
        blocks: ['sections-blog-editorial-index', 'sections-blog-category-columns'],
      },
      {
        path: 'contact',
        label: 'Contact',
        title: 'Get in touch',
        description: 'Availability and a booking route.',
        blocks: ['sections-contact-booking-cta', 'sections-contact-split-form'],
      },
    ],
    usesComponents: ['component/status', 'component/badge', 'component/breadcrumb'],
    usesSections: [
      'section/case-studies-index-list',
      'section/blog-editorial-index',
      'section/contact-booking-cta',
    ],
    implementationNotes: [
      'Availability is a Status component, so it carries an icon and a word rather than a coloured dot.',
      'The project routes share the case-study detail page with the Agency starter.',
    ],
  },
  {
    slug: 'documentation',
    name: 'Documentation',
    brand: 'Foundry Docs',
    tagline: 'Reference, guides and an API surface.',
    category: 'documentation',
    palette: 'corporate',
    description:
      'A developer documentation site with six article routes, a guide index, an API reference and a working search page.',
    tags: ['docs', 'developer', 'reference', 'api'],
    difficulty: 'intermediate',
    featured: true,
    routes: [
      {
        path: '',
        label: 'Home',
        title: 'Foundry Docs',
        description: 'Where to start.',
        blocks: [
          'sections-hero-code-sample',
          'sections-features-numbered-list',
          'sections-faq-categorised',
        ],
      },
      {
        path: 'docs',
        label: 'Docs',
        title: 'Documentation',
        description: 'Every reference page.',
        blocks: ['sections-features-two-column-list', 'sections-integrations-api-first'],
      },
      {
        path: 'docs/:slug',
        label: 'Article',
        title: 'Article',
        description: 'One documentation page.',
        inNav: false,
        page: 'docs-article',
        params: [
          { slug: 'getting-started', title: 'Getting started' },
          { slug: 'installation', title: 'Installation' },
          { slug: 'theming', title: 'Theming' },
          { slug: 'tokens', title: 'Design tokens' },
          { slug: 'composition', title: 'Composition' },
          { slug: 'accessibility', title: 'Accessibility' },
        ],
      },
      {
        path: 'guides',
        label: 'Guides',
        title: 'Guides',
        description: 'Task-shaped walkthroughs.',
        page: 'guides',
      },
      {
        path: 'api',
        label: 'API',
        title: 'API reference',
        description: 'Endpoints and authentication.',
        page: 'api-reference',
      },
      {
        path: 'changelog',
        label: 'Changelog',
        title: 'Changelog',
        description: 'What shipped, and when.',
        blocks: ['sections-timeline-release-log'],
      },
      {
        path: 'search',
        label: 'Search',
        title: 'Search',
        description: 'Search across guides and the API.',
        page: 'search',
      },
    ],
    usesComponents: [
      'component/tabs',
      'component/search-field',
      'component/badge',
      'component/breadcrumb',
    ],
    usesSections: [
      'section/hero-code-sample',
      'section/integrations-api-first',
      'section/timeline-release-log',
    ],
    implementationNotes: [
      'Six article routes come from one dynamic segment and one shared prose page.',
      'The search page degrades to an empty state with a real suggestion rather than a dead control.',
      'The Corporate palette keeps the type grotesque throughout, which suits a reference site.',
    ],
  },
  {
    slug: 'admin',
    name: 'Admin',
    brand: 'Console',
    tagline: 'Internal tools at compact density.',
    category: 'admin',
    palette: 'corporate',
    description:
      'An internal console: overview, a filterable member table, orders, analytics and settings — all at compact density on the same components as the marketing starters.',
    tags: ['admin', 'dashboard', 'internal', 'tables'],
    difficulty: 'advanced',
    featured: true,
    routes: [
      {
        path: '',
        label: 'Overview',
        title: 'Overview',
        description: 'Metrics, activity and quick actions.',
        blocks: [
          'sections-dashboard-metric-row',
          'sections-dashboard-quick-actions',
          'sections-dashboard-activity-feed',
        ],
      },
      {
        path: 'dashboard',
        label: 'Dashboard',
        title: 'Dashboard',
        description: 'Charts and service health.',
        blocks: [
          'sections-dashboard-chart-panel',
          'sections-dashboard-status-overview',
          'sections-dashboard-onboarding-checklist',
        ],
      },
      {
        path: 'users',
        label: 'Users',
        title: 'Members',
        description: 'A filterable member table.',
        page: 'users-table',
      },
      {
        path: 'orders',
        label: 'Orders',
        title: 'Orders',
        description: 'Recent orders and their status.',
        blocks: ['sections-dashboard-data-table', 'sections-empty-filtered-out'],
      },
      {
        path: 'analytics',
        label: 'Analytics',
        title: 'Analytics',
        description: 'Traffic, conversion and top pages.',
        page: 'analytics',
      },
      {
        path: 'settings',
        label: 'Settings',
        title: 'Settings',
        description: 'Workspace configuration.',
        page: 'settings',
        blocks: ['forms-notifications'],
      },
    ],
    usesComponents: [
      'component/table',
      'component/metric',
      'component/status',
      'component/search-field',
    ],
    usesSections: [
      'section/dashboard-data-table',
      'section/dashboard-status-overview',
      'section/empty-filtered-out',
    ],
    implementationNotes: [
      'Every screen uses the same primitives as the marketing starters; only the density differs.',
      'The member table has a working filter with a live result count and a real empty state.',
    ],
  },
  {
    slug: 'restaurant',
    name: 'Restaurant',
    brand: 'Ember',
    tagline: 'A small dining room and a nine-seat counter.',
    category: 'restaurant',
    palette: 'warm',
    description:
      'A hospitality site whose menu is real, searchable text with dietary tags — never a PDF — with booking above everything else.',
    tags: ['restaurant', 'hospitality', 'menu', 'booking'],
    difficulty: 'starter',
    featured: false,
    routes: [
      {
        path: '',
        label: 'Home',
        title: 'Ember',
        description: 'The room, the hours and the menu.',
        page: 'reservations',
        blocks: ['sections-testimonials-carousel'],
      },
      {
        path: 'menu',
        label: 'Menu',
        title: 'This week',
        description: 'Three courses, changed with the produce.',
        blocks: ['sections-contact-offices'],
      },
      {
        path: 'reservations',
        label: 'Reservations',
        title: 'Reservations',
        description: 'Booking and the house policy.',
        page: 'reservations',
      },
      {
        path: 'about',
        label: 'About',
        title: 'About Ember',
        description: 'The people and the history.',
        blocks: ['sections-team-leadership', 'sections-timeline-company-history'],
      },
      {
        path: 'private-dining',
        label: 'Private dining',
        title: 'Private dining',
        description: 'Three spaces and how to enquire.',
        page: 'private-dining',
      },
      {
        path: 'contact',
        label: 'Contact',
        title: 'Contact',
        description: 'Where we are and how to reach us.',
        blocks: ['sections-contact-offices', 'sections-contact-split-form'],
      },
    ],
    usesComponents: ['component/badge', 'component/button', 'component/card'],
    usesSections: [
      'section/contact-offices',
      'section/testimonials-carousel',
      'section/team-leadership',
    ],
    implementationNotes: [
      'The full menu is a description list with dietary tags, so it is readable and searchable on a phone.',
      'The Warm palette carries a serif display face and cream surfaces, which no other starter uses.',
    ],
  },
  {
    slug: 'startup',
    name: 'Startup',
    brand: 'Kestrel',
    tagline: 'Stop rebuilding the same twenty components.',
    category: 'startup',
    palette: 'modern',
    description:
      'An early-stage company site that is explicit about scope, with a single pricing plan and a solutions page rather than a feature wall.',
    tags: ['startup', 'early-stage', 'scope', 'pricing'],
    difficulty: 'starter',
    featured: false,
    routes: [
      {
        path: '',
        label: 'Home',
        title: 'Kestrel',
        description: 'The claim, the proof and the limits.',
        blocks: [
          'sections-hero-two-audience',
          'sections-stats-inline-proof',
          'sections-features-checklist-split',
          'sections-testimonials-with-rating',
        ],
      },
      {
        path: 'product',
        label: 'Product',
        title: 'Product',
        description: 'What it does, in detail.',
        blocks: [
          'sections-features-bento',
          'sections-features-spotlight',
          'sections-features-interactive-preview',
        ],
      },
      {
        path: 'solutions',
        label: 'Solutions',
        title: 'Solutions',
        description: 'Who it is for.',
        page: 'solutions',
      },
      {
        path: 'pricing',
        label: 'Pricing',
        title: 'Pricing',
        description: 'One plan, no tier to outgrow.',
        blocks: ['sections-pricing-single-plan', 'sections-faq-with-support-cta'],
      },
      {
        path: 'customers',
        label: 'Customers',
        title: 'Customers',
        description: 'Who is already using it.',
        blocks: ['sections-logos-grid-cells', 'sections-testimonials-logo-quote-pair'],
      },
      {
        path: 'blog',
        label: 'Blog',
        title: 'Blog',
        description: 'Writing from the team.',
        blocks: ['sections-blog-card-grid'],
      },
      {
        path: 'contact',
        label: 'Contact',
        title: 'Contact',
        description: 'Talk to a person.',
        blocks: ['sections-contact-split-form'],
      },
    ],
    usesComponents: ['component/card', 'component/badge', 'component/button'],
    usesSections: [
      'section/features-checklist-split',
      'section/pricing-single-plan',
      'section/hero-two-audience',
    ],
    implementationNotes: [
      'The Modern palette rounds corners generously and switches the accent to teal.',
      'The included-and-excluded checklist sits on the home page, where it filters out the wrong customers early.',
    ],
  },
  {
    slug: 'blog',
    name: 'Content',
    brand: 'The Quarterly',
    tagline: 'Four issues a year on design systems and craft.',
    category: 'blog',
    palette: 'editorial',
    description:
      'A magazine with six article routes, four category indexes and four author pages — twenty-one routes from six definitions.',
    tags: ['magazine', 'editorial', 'articles', 'authors'],
    difficulty: 'intermediate',
    featured: false,
    routes: [
      {
        path: '',
        label: 'Home',
        title: 'The Quarterly',
        description: 'Lead article and the indexes.',
        blocks: [
          'sections-blog-featured-plus-list',
          'sections-blog-category-columns',
          'sections-newsletter-with-archive',
        ],
      },
      {
        path: 'stories',
        label: 'Stories',
        title: 'All stories',
        description: 'The full archive.',
        blocks: ['sections-blog-editorial-index'],
      },
      {
        path: 'stories/:slug',
        label: 'Story',
        title: 'Story',
        description: 'One article.',
        inNav: false,
        page: 'article',
        params: [
          { slug: 'density-as-an-axis', title: 'Density belongs in your token system' },
          { slug: 'manual-tab-activation', title: 'Why your tabs should not auto-activate' },
          { slug: 'documentation-from-source', title: 'Generate documentation from the file' },
          { slug: 'server-components-marketing', title: 'Marketing pages do not need a runtime' },
          {
            slug: 'variants-that-earn-their-place',
            title: 'A variant should represent a decision',
          },
          { slug: 'focus-return', title: 'The half of focus management everyone forgets' },
        ],
      },
      {
        path: 'categories/:slug',
        label: 'Category',
        title: 'Category',
        description: 'Articles in one section.',
        inNav: false,
        page: 'category-index',
        params: [
          { slug: 'design-systems', title: 'Design systems' },
          { slug: 'accessibility', title: 'Accessibility' },
          { slug: 'tooling', title: 'Tooling' },
          { slug: 'performance', title: 'Performance' },
        ],
      },
      {
        path: 'authors/:slug',
        label: 'Author',
        title: 'Author',
        description: 'One contributor.',
        inNav: false,
        page: 'author',
        params: [
          { slug: 'priya-raman', title: 'Priya Raman' },
          { slug: 'tomas-lindqvist', title: 'Tomas Lindqvist' },
          { slug: 'amara-osei', title: 'Amara Osei' },
          { slug: 'sofia-delgado', title: 'Sofia Delgado' },
        ],
      },
      {
        path: 'about',
        label: 'About',
        title: 'About the publication',
        description: 'The masthead and the method.',
        blocks: ['sections-team-list-rows', 'sections-newsletter-panel'],
      },
    ],
    usesComponents: ['component/badge', 'component/avatar', 'component/pagination'],
    usesSections: [
      'section/blog-featured-plus-list',
      'section/blog-editorial-index',
      'section/newsletter-with-archive',
    ],
    implementationNotes: [
      'Twenty-one routes come from six definitions, because three of them carry parameter lists.',
      'Category pages fall back to recent articles when a category has no matches, rather than rendering an error.',
    ],
  },
  {
    slug: 'product',
    name: 'Product landing',
    brand: 'Vector',
    tagline: 'Typed event streaming with a schema registry.',
    category: 'product',
    palette: 'modern',
    description:
      'A single-product site built around one capability, with an integration surface, a resource index and an interactive preview.',
    tags: ['product', 'landing', 'integrations', 'single-product'],
    difficulty: 'starter',
    featured: false,
    routes: [
      {
        path: '',
        label: 'Home',
        title: 'Vector',
        description: 'One product, one promise.',
        blocks: [
          'sections-hero-product-mosaic',
          'sections-features-spotlight',
          'sections-stats-with-context',
          'sections-cta-testimonial-backed',
        ],
      },
      {
        path: 'product',
        label: 'Product',
        title: 'The product',
        description: 'How it works.',
        blocks: ['sections-features-alternating', 'sections-features-interactive-preview'],
      },
      {
        path: 'features',
        label: 'Features',
        title: 'Features',
        description: 'Everything it does.',
        blocks: ['sections-features-icon-grid', 'sections-features-two-column-list'],
      },
      {
        path: 'integrations',
        label: 'Integrations',
        title: 'Integrations',
        description: 'What it connects to.',
        blocks: [
          'sections-integrations-logo-grid',
          'sections-integrations-featured-pair',
          'sections-integrations-api-first',
        ],
      },
      {
        path: 'pricing',
        label: 'Pricing',
        title: 'Pricing',
        description: 'Usage-based, with a calculator.',
        blocks: [
          'sections-pricing-usage-calculator',
          'sections-pricing-toggle-cadence',
          'sections-faq-two-column',
        ],
      },
      {
        path: 'resources',
        label: 'Resources',
        title: 'Resources',
        description: 'Documentation and guides.',
        page: 'resources',
      },
      {
        path: 'contact',
        label: 'Contact',
        title: 'Contact',
        description: 'Enquiries and support.',
        blocks: ['sections-contact-routed-cards'],
      },
    ],
    usesComponents: ['component/slider', 'component/combobox', 'component/badge'],
    usesSections: [
      'section/integrations-logo-grid',
      'section/pricing-usage-calculator',
      'section/features-spotlight',
    ],
    implementationNotes: [
      'The pricing page pairs a usage calculator with a cadence toggle, which covers both self-serve and sales-assisted buyers.',
      'The integrations page is three catalogue sections in sequence — grid, featured pair, API.',
    ],
  },
]

export const starterBySlug = new Map(starters.map((starter) => [starter.slug, starter]))

/**
 * Absolute path for a route inside a starter preview. Kept here rather than in
 * the shell so Server Components can build links without importing a client
 * module.
 */
export function starterHref(starter: Starter, path: string): string {
  const base = `/starters/${starter.slug}/preview`
  return path ? `${base}/${path}` : base
}

/** Every concrete path a starter renders, with dynamic segments expanded. */
export function expandRoutes(starter: Starter): Array<{ path: string; title: string }> {
  const out: Array<{ path: string; title: string }> = []
  for (const route of starter.routes) {
    if (route.params) {
      for (const param of route.params) {
        out.push({ path: route.path.replace(':slug', param.slug), title: param.title })
      }
    } else {
      out.push({ path: route.path, title: route.title })
    }
  }
  return out
}

/** Total concrete routes across every starter. */
export const starterRouteCount = starters.reduce(
  (total, starter) => total + expandRoutes(starter).length,
  0,
)

export function matchRoute(starter: Starter, path: string) {
  const direct = starter.routes.find((route) => route.path === path && !route.params)
  if (direct) return { route: direct, param: undefined }

  for (const route of starter.routes) {
    if (!route.params) continue
    const prefix = route.path.replace(':slug', '')
    if (!path.startsWith(prefix)) continue
    const slug = path.slice(prefix.length)
    const param = route.params.find((entry) => entry.slug === slug)
    if (param) return { route, param }
  }

  return undefined
}

components/starter-kit/render.tsx

import { previews } from '@/registry/previews.generated'
import { Container } from '@/components/ui/layout'
import { Alert } from '@/components/ui/alert'
import { StarterPageHeader } from './page-header'
import { starterHref } from '@/lib/starters'
import {
  AccountPage,
  AnalyticsPage,
  ApiReferencePage,
  ArticlePage,
  AuthorPage,
  CartPage,
  CategoryIndexPage,
  DocsArticlePage,
  GuidesPage,
  OrdersPage,
  PrivateDiningPage,
  ProductDetailPage,
  ProjectDetailPage,
  ReservationsPage,
  ResourcesPage,
  SettingsPage,
  SolutionsPage,
  StarterSearchPage,
  UsersTablePage,
} from './pages'
import type { Starter, StarterPageKind, StarterRoute } from '@/lib/starters/types'

/**
 * Starter route renderer
 *
 * One component renders every page of every starter: it looks up the catalogue
 * blocks the route declares, renders them in order, and mounts a shared
 * starter-kit page when the route needs content no block covers.
 *
 * This is the mechanism behind "ten products, no duplicated code" — there is
 * exactly one place where a starter page can be assembled.
 */
const pageComponents: Record<
  StarterPageKind,
  (props: { slug: string; title: string }) => React.ReactNode
> = {
  cart: () => <CartPage />,
  orders: () => <OrdersPage />,
  account: () => <AccountPage />,
  'users-table': () => <UsersTablePage />,
  analytics: () => <AnalyticsPage />,
  'docs-article': (props) => <DocsArticlePage {...props} />,
  'api-reference': () => <ApiReferencePage />,
  guides: () => <GuidesPage />,
  search: () => <StarterSearchPage />,
  'project-detail': (props) => <ProjectDetailPage {...props} />,
  article: (props) => <ArticlePage {...props} />,
  'product-detail': (props) => <ProductDetailPage {...props} />,
  settings: () => <SettingsPage />,
  reservations: () => <ReservationsPage />,
  'private-dining': () => <PrivateDiningPage />,
  solutions: () => <SolutionsPage />,
  resources: () => <ResourcesPage />,
  author: (props) => <AuthorPage {...props} />,
  'category-index': (props) => <CategoryIndexPage {...props} />,
}

export function StarterRouteView({
  starter,
  route,
  param,
}: {
  starter: Starter
  route: StarterRoute
  param?: { slug: string; title: string }
}) {
  const isIndex = route.path === ''
  const title = param?.title ?? route.title
  const slug = param?.slug ?? route.path

  const crumbs = isIndex
    ? undefined
    : [
        { label: starter.brand, href: starterHref(starter, '') },
        ...(param
          ? [
              { label: route.label, href: starterHref(starter, route.path.replace('/:slug', '')) },
              { label: title },
            ]
          : [{ label: route.label }]),
      ]

  const Page = route.page ? pageComponents[route.page] : undefined
  const blocks = route.blocks ?? []
  const missing = blocks.filter((id) => !previews[id])

  return (
    <>
      {isIndex ? (
        // Index routes are composed entirely of blocks, which all lead with an
        // h2. The starter still needs exactly one h1, so it is provided here and
        // visually hidden.
        <h1 className="sr-only">
          {starter.brand} — {starter.tagline}
        </h1>
      ) : (
        <StarterPageHeader
          eyebrow={param ? route.label : undefined}
          title={title}
          description={route.description}
          crumbs={crumbs}
        />
      )}

      {Page ? Page({ slug, title }) : null}

      {blocks.map((id, index) => {
        const Block = previews[id]
        if (!Block) return null
        return <Block key={`${id}-${index}`} />
      })}

      {missing.length > 0 ? (
        <Container className="py-8">
          <Alert tone="warning" title="Missing block">
            {missing.join(', ')}
          </Alert>
        </Container>
      ) : null}
    </>
  )
}

components/starter-kit/shell.tsx

'use client'

import { useState } from 'react'
import Link from 'next/link'
import { Menu } from 'lucide-react'
import { cn } from '@/lib/cn'
import { BrandMark } from '@/components/library/brand'
import { Drawer } from '@/components/ui/drawer'
import { starterHref } from '@/lib/starters'
import type { Starter, StarterRoute } from '@/lib/starters/types'

/**
 * StarterShell
 *
 * The navigation chrome every starter shares. The header and footer link lists
 * are generated from the starter's own route tree, which is what guarantees
 * that no starter can link to a route it does not have — or to another
 * starter's routes by accident.
 *
 * Desktop navigation and the mobile drawer render from the same array, so they
 * can never drift apart.
 */
export function StarterShell({
  starter,
  currentPath,
  children,
}: {
  starter: Starter
  currentPath: string
  children: React.ReactNode
}) {
  // Open only for the route it was opened on, so navigating closes the drawer
  // without an effect.
  const [openedOn, setOpenedOn] = useState<string | null>(null)
  const open = openedOn === currentPath
  const setOpen = (next: boolean) => setOpenedOn(next ? currentPath : null)

  const navRoutes = starter.routes.filter(
    (route) => route.inNav !== false && !route.path.includes(':'),
  )
  const footerRoutes = starter.routes.filter((route) => !route.path.includes(':'))

  const isActive = (route: StarterRoute) => route.path === currentPath

  return (
    <div data-palette={starter.palette} className="flex min-h-full flex-col bg-canvas text-ink">
      <header className="sticky top-0 z-40 border-b border-line bg-canvas/90 backdrop-blur-sm">
        <div className="mx-auto flex h-14 w-full max-w-6xl items-center gap-4 px-4 sm:px-6">
          <Link href={starterHref(starter, '')} className="flex items-center gap-2 text-ink-strong">
            <BrandMark className="size-5 text-accent" />
            <span className="display-type text-md font-semibold tracking-tight">
              {starter.brand}
            </span>
          </Link>

          <nav aria-label={`${starter.brand} primary`} className="ml-4 hidden lg:block">
            <ul className="flex items-center gap-0.5">
              {navRoutes.map((route) => (
                <li key={route.path || 'index'}>
                  <Link
                    href={starterHref(starter, route.path)}
                    aria-current={isActive(route) ? 'page' : undefined}
                    className={cn(
                      'flex h-8 items-center rounded-md px-2.5 text-sm font-medium transition-colors',
                      isActive(route)
                        ? 'bg-surface-sunken text-ink-strong'
                        : 'text-ink-muted hover:bg-surface-sunken hover:text-ink',
                    )}
                  >
                    {route.label}
                  </Link>
                </li>
              ))}
            </ul>
          </nav>

          <button
            type="button"
            onClick={() => setOpen(true)}
            aria-expanded={open}
            className="ml-auto flex size-9 items-center justify-center rounded-md text-ink-muted hover:bg-surface-sunken lg:hidden"
          >
            <Menu className="size-5" aria-hidden="true" />
            <span className="sr-only">Open {starter.brand} navigation</span>
          </button>
        </div>
      </header>

      <main id="starter-main" className="flex-1">
        {children}
      </main>

      <footer className="mt-auto border-t border-line bg-surface-sunken">
        <div className="mx-auto w-full max-w-6xl px-4 py-10 sm:px-6">
          <div className="flex flex-wrap items-start justify-between gap-8">
            <div className="max-w-xs">
              <div className="flex items-center gap-2 text-ink-strong">
                <BrandMark className="size-4 text-accent" />
                <span className="display-type text-sm font-semibold">{starter.brand}</span>
              </div>
              <p className="mt-2 text-sm text-ink-muted">{starter.tagline}</p>
            </div>

            <nav aria-label={`${starter.brand} footer`}>
              <ul className="grid gap-x-10 gap-y-2 sm:grid-cols-2">
                {footerRoutes.map((route) => (
                  <li key={route.path || 'index'}>
                    <Link
                      href={starterHref(starter, route.path)}
                      className="text-sm text-ink-muted transition-colors hover:text-accent"
                    >
                      {route.label}
                    </Link>
                  </li>
                ))}
              </ul>
            </nav>
          </div>

          <p className="mt-8 border-t border-line pt-5 text-xs text-ink-subtle">
            Built with Foundry — {starter.name} starter. Fictional content; nothing here submits or
            purchases anything.
          </p>
        </div>
      </footer>

      <Drawer
        open={open}
        onClose={() => setOpen(false)}
        side="left"
        size="sm"
        title={`${starter.brand} navigation`}
      >
        <nav aria-label={`${starter.brand} mobile`} className="p-3">
          <ul className="flex flex-col gap-0.5">
            {footerRoutes.map((route) => (
              <li key={route.path || 'index'}>
                <Link
                  href={starterHref(starter, route.path)}
                  aria-current={isActive(route) ? 'page' : undefined}
                  className={cn(
                    'flex min-h-11 items-center rounded-md px-3 text-sm transition-colors',
                    isActive(route)
                      ? 'bg-accent-soft font-medium text-accent-soft-ink'
                      : 'text-ink hover:bg-surface-sunken',
                  )}
                >
                  {route.label}
                </Link>
              </li>
            ))}
          </ul>
        </nav>
      </Drawer>
    </div>
  )
}

components/starter-kit/pages.tsx

'use client'

import { useState } from 'react'
import Link from 'next/link'
import {
  ArrowRight,
  BookOpen,
  Calendar,
  Filter,
  Minus,
  Plus,
  Search as SearchIcon,
  Trash2,
  Users as UsersIcon,
} from 'lucide-react'
import { cn } from '@/lib/cn'
import { Avatar } from '@/components/ui/avatar'
import { Badge } from '@/components/ui/badge'
import { Button, ButtonLink } from '@/components/ui/button'
import { Card, CardDescription, CardTitle, Panel } from '@/components/ui/card'
import { Container, Divider } from '@/components/ui/layout'
import { EmptyState } from '@/components/ui/empty-state'
import { Metric } from '@/components/ui/metric'
import { Progress } from '@/components/ui/progress'
import { SearchField } from '@/components/ui/search-field'
import { Status } from '@/components/ui/status'
import { Table } from '@/components/ui/table'
import { Tabs } from '@/components/ui/tabs'
import { formatCurrency, formatDate } from '@/lib/format'
import { caseStudies, integrations, posts, products, team } from '@/content/demo'

/**
 * Starter pages
 *
 * The handful of screens no marketing section covers: a cart, an order list,
 * an admin table, an article body. Everything else in a starter is composed
 * from catalogue blocks.
 *
 * Each is written once and shared by every starter that needs it, which is why
 * ten products do not mean ten carts.
 */

/* -------------------------------------------------------------- commerce */

const cartLines = [
  { product: products[0], quantity: 1, size: 'M' },
  { product: products[6], quantity: 2, size: 'One size' },
  { product: products[8], quantity: 3, size: 'L' },
].filter((line) => line.product)

export function CartPage() {
  const [lines, setLines] = useState(cartLines.map((line) => ({ ...line })))

  const subtotal = lines.reduce(
    (total, line) => total + (line.product?.priceCents ?? 0) * line.quantity,
    0,
  )
  const shipping = subtotal > 15000 ? 0 : 800
  const tax = Math.round((subtotal + shipping) * 0.2)

  const update = (slug: string, delta: number) => {
    setLines((current) =>
      current
        .map((line) =>
          line.product?.slug === slug
            ? { ...line, quantity: Math.max(0, line.quantity + delta) }
            : line,
        )
        .filter((line) => line.quantity > 0),
    )
  }

  if (lines.length === 0) {
    return (
      <Container className="py-section">
        <EmptyState
          size="lg"
          icon={<SearchIcon className="size-5" />}
          title="Your bag is empty"
          description="Nothing here yet. Everything you add is kept for seven days."
          action={
            <ButtonLink href="/starters/ecommerce/preview/shop">Continue shopping</ButtonLink>
          }
        />
      </Container>
    )
  }

  return (
    <Container className="py-section">
      <div className="grid gap-8 lg:grid-cols-[1.5fr_1fr] lg:gap-12">
        <div>
          <h2 className="sr-only">Items in your bag</h2>
          <ul className="divide-y divide-[var(--color-border-subtle)] border-y border-line">
            {lines.map((line) => (
              <li key={line.product?.slug} className="flex gap-4 py-5">
                <div
                  className="grid-paper size-20 shrink-0 rounded-md border border-line bg-surface-sunken sm:size-24"
                  aria-hidden="true"
                />
                <div className="min-w-0 flex-1">
                  <div className="flex flex-wrap items-start justify-between gap-2">
                    <div className="min-w-0">
                      <Link
                        href={`/starters/ecommerce/preview/shop/${line.product?.slug}`}
                        className="text-sm font-medium text-ink-strong hover:text-accent"
                      >
                        {line.product?.name}
                      </Link>
                      <p className="mt-0.5 text-xs text-ink-muted">Size {line.size}</p>
                    </div>
                    <p className="font-mono text-sm text-ink-strong tabular-nums">
                      {formatCurrency((line.product?.priceCents ?? 0) * line.quantity)}
                    </p>
                  </div>

                  <div className="mt-3 flex items-center gap-3">
                    <div className="flex items-center rounded-md border border-line">
                      <Button
                        variant="ghost"
                        size="icon-sm"
                        onClick={() => update(line.product?.slug ?? '', -1)}
                        aria-label={`Decrease quantity of ${line.product?.name}`}
                      >
                        <Minus className="size-3.5" />
                      </Button>
                      <span
                        className="w-8 text-center font-mono text-sm tabular-nums"
                        aria-live="polite"
                      >
                        {line.quantity}
                      </span>
                      <Button
                        variant="ghost"
                        size="icon-sm"
                        onClick={() => update(line.product?.slug ?? '', 1)}
                        aria-label={`Increase quantity of ${line.product?.name}`}
                      >
                        <Plus className="size-3.5" />
                      </Button>
                    </div>
                    <Button
                      variant="ghost"
                      size="sm"
                      onClick={() => update(line.product?.slug ?? '', -line.quantity)}
                      leadingIcon={<Trash2 className="size-3.5" />}
                    >
                      Remove
                    </Button>
                  </div>
                </div>
              </li>
            ))}
          </ul>
        </div>

        <aside aria-label="Order summary">
          <div className="rounded-xl border border-line bg-surface p-5">
            <h2 className="text-md font-semibold text-ink-strong">Summary</h2>
            <dl className="mt-4 flex flex-col gap-2 text-sm">
              <div className="flex justify-between gap-3">
                <dt className="text-ink-muted">Subtotal</dt>
                <dd className="font-mono text-ink tabular-nums">{formatCurrency(subtotal)}</dd>
              </div>
              <div className="flex justify-between gap-3">
                <dt className="text-ink-muted">Shipping</dt>
                <dd className="font-mono text-ink tabular-nums">
                  {shipping === 0 ? 'Free' : formatCurrency(shipping)}
                </dd>
              </div>
              <div className="flex justify-between gap-3">
                <dt className="text-ink-muted">VAT (20%)</dt>
                <dd className="font-mono text-ink tabular-nums">{formatCurrency(tax)}</dd>
              </div>
            </dl>
            <Divider className="my-4" weight="subtle" />
            <div className="flex items-baseline justify-between">
              <span className="text-sm font-semibold text-ink-strong">Total</span>
              <span className="font-mono text-lg font-semibold text-ink-strong tabular-nums">
                {formatCurrency(subtotal + shipping + tax)}
              </span>
            </div>
            <ButtonLink
              href="/starters/ecommerce/preview/checkout"
              block
              size="lg"
              className="mt-5"
            >
              Checkout
            </ButtonLink>
            <p className="mt-3 text-center text-xs text-ink-subtle">
              Demo only — no payment is taken.
            </p>
          </div>
        </aside>
      </div>
    </Container>
  )
}

/* ---------------------------------------------------------------- orders */

interface OrderRecord {
  id: string
  reference: string
  placed: string
  status: 'operational' | 'pending' | 'degraded'
  statusLabel: string
  items: number
  totalCents: number
}

const orderRecords: OrderRecord[] = [
  {
    id: 'o1',
    reference: 'QRY-4821',
    placed: '2026-03-14',
    status: 'pending',
    statusLabel: 'Preparing',
    items: 3,
    totalCents: 51600,
  },
  {
    id: 'o2',
    reference: 'QRY-4702',
    placed: '2026-02-28',
    status: 'operational',
    statusLabel: 'Delivered',
    items: 1,
    totalCents: 18500,
  },
  {
    id: 'o3',
    reference: 'QRY-4611',
    placed: '2026-02-02',
    status: 'operational',
    statusLabel: 'Delivered',
    items: 2,
    totalCents: 14900,
  },
  {
    id: 'o4',
    reference: 'QRY-4498',
    placed: '2026-01-11',
    status: 'degraded',
    statusLabel: 'Returned',
    items: 1,
    totalCents: 9800,
  },
]

export function OrdersPage() {
  return (
    <Container className="py-section">
      <Panel title="Order history" description="Four fictional orders" headingLevel="h2" flush>
        <Table
          caption="Order history with reference, date, status, item count and total"
          rows={orderRecords}
          rowKey={(row) => row.id}
          responsive="stack"
          columns={[
            {
              key: 'reference',
              header: 'Order',
              cell: (row) => (
                <code className="font-mono text-xs text-ink-strong">{row.reference}</code>
              ),
            },
            { key: 'placed', header: 'Placed', cell: (row) => formatDate(row.placed) },
            {
              key: 'status',
              header: 'Status',
              cell: (row) => <Status appearance="dot" kind={row.status} label={row.statusLabel} />,
            },
            { key: 'items', header: 'Items', align: 'end', cell: (row) => row.items },
            {
              key: 'total',
              header: 'Total',
              align: 'end',
              cell: (row) => (
                <span className="font-mono tabular-nums">{formatCurrency(row.totalCents)}</span>
              ),
            },
          ]}
        />
      </Panel>
    </Container>
  )
}

/* --------------------------------------------------------------- account */

export function AccountPage() {
  return (
    <Container className="py-section">
      <div className="grid gap-6 lg:grid-cols-[1fr_1.6fr]">
        <Card>
          <div className="flex items-center gap-3.5">
            <Avatar name="Priya Raman" size="xl" decorative />
            <div className="min-w-0">
              <CardTitle>Priya Raman</CardTitle>
              <CardDescription className="mt-0.5 break-token">
                priya@northwind.example
              </CardDescription>
              <Badge tone="accent" size="sm" className="mt-2">
                Member since 2024
              </Badge>
            </div>
          </div>
          <Divider className="my-5" weight="subtle" />
          <dl className="flex flex-col gap-3 text-sm">
            <div>
              <dt className="label-caps text-ink-subtle">Default address</dt>
              <dd className="mt-1 text-ink">18 Ashfield Row, London EC2A 4NE</dd>
            </div>
            <div>
              <dt className="label-caps text-ink-subtle">Payment</dt>
              <dd className="mt-1 font-mono text-ink">•••• 4242</dd>
            </div>
          </dl>
        </Card>

        <div className="flex flex-col gap-4">
          <Panel title="Recent activity" description="Last 30 days" headingLevel="h2">
            <ul className="flex flex-col gap-3 text-sm">
              <li className="flex items-center justify-between gap-3">
                <span className="text-ink">Order QRY-4821 placed</span>
                <time dateTime="2026-03-14" className="text-xs text-ink-subtle">
                  14 Mar
                </time>
              </li>
              <li className="flex items-center justify-between gap-3">
                <span className="text-ink">Address updated</span>
                <time dateTime="2026-03-02" className="text-xs text-ink-subtle">
                  02 Mar
                </time>
              </li>
              <li className="flex items-center justify-between gap-3">
                <span className="text-ink">Order QRY-4702 delivered</span>
                <time dateTime="2026-02-28" className="text-xs text-ink-subtle">
                  28 Feb
                </time>
              </li>
            </ul>
          </Panel>

          <Panel title="Preferences" description="Applied to this account" headingLevel="h2">
            <div className="flex flex-wrap gap-2">
              <Badge tone="success" dot>
                Order updates by email
              </Badge>
              <Badge>Marketing off</Badge>
              <Badge>Metric sizes</Badge>
            </div>
          </Panel>
        </div>
      </div>
    </Container>
  )
}

/* ------------------------------------------------------------ admin data */

interface UserRecord {
  id: string
  name: string
  email: string
  role: string
  status: 'operational' | 'pending' | 'idle'
  statusLabel: string
  lastActive: string
}

const userRecords: UserRecord[] = team.slice(0, 6).map((member, index) => ({
  id: `u${index}`,
  name: member.name,
  email: `${member.name.split(' ')[0]?.toLowerCase()}@foundry.example`,
  role: index === 0 ? 'Owner' : index < 3 ? 'Admin' : 'Member',
  status: index === 5 ? 'pending' : index === 4 ? 'idle' : 'operational',
  statusLabel: index === 5 ? 'Invited' : index === 4 ? 'Inactive' : 'Active',
  lastActive: index === 5 ? '—' : `${14 - index} Mar`,
}))

export function UsersTablePage() {
  const [query, setQuery] = useState('')
  const filtered = userRecords.filter((user) =>
    `${user.name} ${user.email} ${user.role}`.toLowerCase().includes(query.trim().toLowerCase()),
  )

  return (
    <Container className="py-8">
      <Panel
        title="Members"
        description={`${filtered.length} of ${userRecords.length}`}
        headingLevel="h2"
        flush
        action={
          <Button size="sm" leadingIcon={<UsersIcon className="size-3.5" />}>
            Invite
          </Button>
        }
      >
        <div className="border-b border-line-subtle p-3">
          <SearchField
            fieldSize="sm"
            value={query}
            onValueChange={setQuery}
            placeholder="Filter members"
            aria-label="Filter members"
          />
        </div>

        <p aria-live="polite" className="sr-only">
          {query.trim() ? `${filtered.length} members match ${query.trim()}` : ''}
        </p>

        <Table
          caption="Workspace members with role, status and last activity"
          rows={filtered}
          rowKey={(row) => row.id}
          responsive="stack"
          density="compact"
          empty={
            <div className="p-4">
              <EmptyState
                appearance="bare"
                size="sm"
                icon={<Filter className="size-5" />}
                title={`No members match “${query.trim()}”`}
                description="Check the spelling, or clear the filter."
              />
            </div>
          }
          columns={[
            {
              key: 'name',
              header: 'Member',
              cell: (row) => (
                <span className="flex items-center gap-2.5">
                  <Avatar name={row.name} size="xs" decorative />
                  <span className="min-w-0">
                    <span className="block truncate font-medium text-ink-strong">{row.name}</span>
                    <span className="block truncate text-xs text-ink-muted">{row.email}</span>
                  </span>
                </span>
              ),
            },
            { key: 'role', header: 'Role', cell: (row) => <Badge size="sm">{row.role}</Badge> },
            {
              key: 'status',
              header: 'Status',
              cell: (row) => <Status appearance="dot" kind={row.status} label={row.statusLabel} />,
            },
            { key: 'active', header: 'Last active', align: 'end', cell: (row) => row.lastActive },
          ]}
        />
      </Panel>
    </Container>
  )
}

export function AnalyticsPage() {
  const channels = [
    { name: 'Direct', share: 42 },
    { name: 'Search', share: 28 },
    { name: 'Referral', share: 18 },
    { name: 'Social', share: 12 },
  ]

  return (
    <Container className="flex flex-col gap-4 py-8">
      <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
        <Metric
          label="Sessions"
          value="128,402"
          delta={8.2}
          sparkline={[20, 24, 22, 28, 31, 30, 35, 38]}
        />
        <Metric
          label="Conversion"
          value="3.8%"
          delta={0.6}
          sparkline={[10, 11, 10, 12, 13, 13, 14, 15]}
        />
        <Metric
          label="Average order"
          value="$142"
          delta={-2.1}
          sparkline={[30, 29, 31, 28, 27, 26, 26, 25]}
        />
        <Metric
          label="Refund rate"
          value="1.2%"
          delta={-0.4}
          invertTrend
          sparkline={[9, 8, 8, 7, 7, 6, 6, 5]}
        />
      </div>

      <div className="grid gap-4 lg:grid-cols-2">
        <Panel title="Traffic by channel" description="Last 30 days" headingLevel="h2">
          <ul className="flex flex-col gap-4">
            {channels.map((channel) => (
              <li key={channel.name}>
                <Progress value={channel.share} label={channel.name} showValue />
              </li>
            ))}
          </ul>
        </Panel>

        <Panel title="Top pages" description="By sessions" headingLevel="h2" flush>
          <Table
            caption="Top pages by session count"
            rows={[
              { id: 'p1', path: '/', sessions: '41,208', rate: '4.1%' },
              { id: 'p2', path: '/shop', sessions: '28,904', rate: '5.6%' },
              { id: 'p3', path: '/shop/field-shell-jacket', sessions: '14,882', rate: '9.2%' },
              { id: 'p4', path: '/category/outerwear', sessions: '11,405', rate: '6.8%' },
            ]}
            rowKey={(row) => row.id}
            density="compact"
            columns={[
              {
                key: 'path',
                header: 'Path',
                cell: (row) => <code className="font-mono text-xs">{row.path}</code>,
              },
              {
                key: 'sessions',
                header: 'Sessions',
                align: 'end',
                cell: (row) => <span className="tabular-nums">{row.sessions}</span>,
              },
              {
                key: 'rate',
                header: 'Conversion',
                align: 'end',
                cell: (row) => <span className="tabular-nums">{row.rate}</span>,
              },
            ]}
          />
        </Panel>
      </div>
    </Container>
  )
}

export function SettingsPage() {
  return (
    <Container className="flex flex-col gap-4 py-8">
      <Panel title="Workspace" description="Visible to every member" headingLevel="h2">
        <dl className="grid gap-4 sm:grid-cols-2">
          <div>
            <dt className="label-caps text-ink-subtle">Name</dt>
            <dd className="mt-1 text-sm text-ink">Acme Platform</dd>
          </div>
          <div>
            <dt className="label-caps text-ink-subtle">Slug</dt>
            <dd className="mt-1 font-mono text-sm text-ink">acme-platform</dd>
          </div>
          <div>
            <dt className="label-caps text-ink-subtle">Region</dt>
            <dd className="mt-1 text-sm text-ink">EU West (Ireland)</dd>
          </div>
          <div>
            <dt className="label-caps text-ink-subtle">Plan</dt>
            <dd className="mt-1 text-sm text-ink">Business</dd>
          </div>
        </dl>
      </Panel>

      <Panel title="Danger zone" description="These actions cannot be undone" headingLevel="h2">
        <div className="flex flex-wrap items-center justify-between gap-3">
          <p className="text-sm text-ink-muted">
            Deleting a workspace removes every project, deployment and log.
          </p>
          <Button variant="destructive" size="sm">
            Delete workspace
          </Button>
        </div>
      </Panel>
    </Container>
  )
}

/* ------------------------------------------------------------ docs pages */

export function DocsArticlePage({ slug }: { slug: string; title: string }) {
  return (
    <Container size="prose" className="py-section">
      <article className="prose-foundry">
        <p className="label-caps text-accent">Guide</p>
        <p className="text-md text-ink-muted">
          A fictional documentation article, rendered from the starter route table. The slug for
          this page is <code>{slug}</code>.
        </p>
        <h2>Why this exists</h2>
        <p>
          Every documentation page in this starter is generated from one route definition and one
          page component. Adding a page means adding a record, not a file — which is exactly how the
          Foundry catalogue itself works.
        </p>
        <h2>What it demonstrates</h2>
        <ul>
          <li>A prose layout constrained to the reading measure</li>
          <li>Heading hierarchy that starts at h1 and never skips a level</li>
          <li>Inline code, lists and blockquotes styled from tokens</li>
        </ul>
        <blockquote>
          Documentation that is generated from the thing it documents cannot drift from it. That is
          the whole argument.
        </blockquote>
        <h2>Next steps</h2>
        <p>
          Continue to the <Link href="/starters/documentation/preview/api">API reference</Link>, or
          read the <Link href="/docs/composition">composition guide</Link> in the main Foundry
          documentation.
        </p>
      </article>
    </Container>
  )
}

export function ApiReferencePage() {
  const endpoints = [
    {
      method: 'GET',
      path: '/v1/components',
      description: 'List catalogue items with optional category and tag filters.',
    },
    {
      method: 'GET',
      path: '/v1/components/{slug}',
      description: 'Fetch one item with its source files and resolved relations.',
    },
    {
      method: 'POST',
      path: '/v1/exports',
      description: 'Generate a token export for a named palette and density.',
    },
    {
      method: 'GET',
      path: '/v1/events',
      description: 'Subscribe to catalogue changes as a server-sent stream.',
    },
    { method: 'DELETE', path: '/v1/exports/{id}', description: 'Remove a generated export.' },
  ]

  return (
    <Container className="py-section">
      <div className="grid gap-8 lg:grid-cols-[1fr_1.4fr] lg:gap-12">
        <div>
          <h2 className="display-type text-xl font-semibold text-ink-strong">Authentication</h2>
          <p className="mt-3 text-sm leading-relaxed text-ink-muted">
            Every request carries a bearer token. Tokens are scoped to one workspace and one
            environment, and are shown once at creation.
          </p>
          <pre className="thin-scrollbar mt-4 overflow-x-auto rounded-lg border border-line bg-code-surface p-4 font-mono text-xs text-code-ink">
            <code>{`Authorization: Bearer fnd_live_9f3a…`}</code>
          </pre>
          <p className="mt-4 text-xs text-ink-subtle">
            This API is fictional. No host answers these requests.
          </p>
        </div>

        <div>
          <h2 className="display-type text-xl font-semibold text-ink-strong">Endpoints</h2>
          <ul className="mt-4 divide-y divide-[var(--color-border-subtle)] rounded-xl border border-line bg-surface">
            {endpoints.map((endpoint) => (
              <li
                key={`${endpoint.method}-${endpoint.path}`}
                className="flex flex-wrap items-baseline gap-x-3 gap-y-1 p-4"
              >
                <Badge
                  size="sm"
                  appearance="solid"
                  tone={
                    endpoint.method === 'GET'
                      ? 'info'
                      : endpoint.method === 'DELETE'
                        ? 'danger'
                        : 'success'
                  }
                  className="font-mono"
                >
                  {endpoint.method}
                </Badge>
                <code className="min-w-0 font-mono text-xs break-token text-ink-strong">
                  {endpoint.path}
                </code>
                <span className="w-full text-xs text-ink-muted">{endpoint.description}</span>
              </li>
            ))}
          </ul>
        </div>
      </div>
    </Container>
  )
}

export function GuidesPage() {
  const guides = [
    { title: 'Install the token file', minutes: '5 min', level: 'Starter' },
    { title: 'Copy your first component', minutes: '10 min', level: 'Starter' },
    { title: 'Compose a landing page from sections', minutes: '25 min', level: 'Intermediate' },
    {
      title: 'Add a palette without touching a component',
      minutes: '15 min',
      level: 'Intermediate',
    },
    { title: 'Adapt a starter into an existing codebase', minutes: '45 min', level: 'Advanced' },
  ]

  return (
    <Container className="py-section">
      <ul className="grid gap-4 sm:grid-cols-2">
        {guides.map((guide) => (
          <li key={guide.title}>
            <Link
              href="/starters/documentation/preview/docs/getting-started"
              className="group flex h-full flex-col rounded-xl border border-line bg-surface p-5 transition-colors hover:border-accent"
            >
              <BookOpen className="size-5 text-accent" aria-hidden="true" />
              <h2 className="mt-4 flex-1 text-md font-semibold text-ink-strong group-hover:text-accent">
                {guide.title}
              </h2>
              <div className="mt-4 flex items-center gap-2">
                <Badge size="sm">{guide.level}</Badge>
                <span className="text-xs text-ink-subtle">{guide.minutes}</span>
              </div>
            </Link>
          </li>
        ))}
      </ul>
    </Container>
  )
}

export function StarterSearchPage() {
  const [query, setQuery] = useState('')
  const pool = [
    ...integrations.map((item) => ({
      label: item.name,
      group: item.category,
      href: '/starters/documentation/preview/api',
    })),
    ...posts.map((post) => ({
      label: post.title,
      group: post.category,
      href: '/starters/documentation/preview/guides',
    })),
  ]
  const results = query.trim()
    ? pool.filter((entry) => entry.label.toLowerCase().includes(query.trim().toLowerCase()))
    : []

  return (
    <Container size="narrow" className="py-section">
      <SearchField
        fieldSize="lg"
        value={query}
        onValueChange={setQuery}
        placeholder="Search the documentation"
        aria-label="Search the documentation"
      />

      <p aria-live="polite" className="mt-3 text-sm text-ink-muted">
        {query.trim() ? `${results.length} results for “${query.trim()}”` : 'Type to search.'}
      </p>

      {query.trim() && results.length === 0 ? (
        <EmptyState
          className="mt-8"
          icon={<SearchIcon className="size-5" />}
          title={`No results for “${query.trim()}”`}
          description="Try a shorter term, or browse the guides instead."
          action={
            <ButtonLink href="/starters/documentation/preview/guides">Browse guides</ButtonLink>
          }
        />
      ) : null}

      {results.length > 0 ? (
        <ul className="mt-6 divide-y divide-[var(--color-border-subtle)] rounded-xl border border-line bg-surface">
          {results.map((entry) => (
            <li key={`${entry.group}-${entry.label}`}>
              <Link
                href={entry.href}
                className="flex items-center justify-between gap-3 px-4 py-3 hover:bg-surface-sunken"
              >
                <span className="text-sm text-ink">{entry.label}</span>
                <span className="label-caps text-ink-subtle">{entry.group}</span>
              </Link>
            </li>
          ))}
        </ul>
      ) : null}
    </Container>
  )
}

/* ------------------------------------------------------------- editorial */

export function ProjectDetailPage({ slug }: { slug: string; title: string }) {
  const study = caseStudies.find((entry) => entry.slug === slug) ?? caseStudies[0]
  if (!study) return null

  return (
    <Container size="narrow" className="py-section">
      <div className="flex flex-wrap items-center gap-2">
        <Badge tone="accent">{study.client}</Badge>
        <Badge>{study.sector}</Badge>
        <Badge>{study.year}</Badge>
      </div>
      <p className="mt-5 text-md leading-relaxed text-ink-muted">{study.summary}</p>

      <dl className="mt-8 grid grid-cols-3 gap-4 border-y border-line py-6">
        {study.metrics.map((metric) => (
          <div key={metric.label}>
            <dd className="display-type text-2xl font-semibold text-ink-strong tabular-nums">
              {metric.value}
            </dd>
            <dt className="label-caps mt-1 text-ink-subtle">{metric.label}</dt>
          </div>
        ))}
      </dl>

      <div className="prose-foundry mt-8">
        <h2>The problem</h2>
        <p>
          {study.client} arrived with several partially finished component sets and no shared
          vocabulary between them. The visible symptom was inconsistency; the underlying one was
          that nobody could say what &ldquo;medium&rdquo; meant.
        </p>
        <h2>What we did</h2>
        <p>
          We inventoried every surface, extracted the real decisions into a token set, and rebuilt
          the twenty components that accounted for most screens. Then we migrated one production
          route to prove the system held before writing any migration guide.
        </p>
        <h2>What changed</h2>
        <p>
          The figures above are the honest summary. The change nobody measured was that design
          review stopped being about spacing.
        </p>
      </div>

      <div className="mt-10 flex flex-wrap gap-3">
        <ButtonLink href="/starters/agency/preview/work" variant="outline">
          All work
        </ButtonLink>
        <ButtonLink
          href="/starters/agency/preview/contact"
          trailingIcon={<ArrowRight className="size-4" />}
        >
          Start a conversation
        </ButtonLink>
      </div>
    </Container>
  )
}

export function ArticlePage({ slug }: { slug: string; title: string }) {
  const post = posts.find((entry) => entry.slug === slug) ?? posts[0]
  if (!post) return null

  return (
    <Container size="prose" className="py-section">
      <article>
        <div className="flex flex-wrap items-center gap-3">
          <Badge tone="accent">{post.category}</Badge>
          <time dateTime={post.date} className="text-sm text-ink-muted">
            {formatDate(post.date)}
          </time>
          <span className="text-sm text-ink-subtle">· {post.readingTime}</span>
        </div>

        <div className="mt-5 flex items-center gap-3">
          <Avatar name={post.author} size="md" decorative />
          <div>
            <p className="text-sm font-medium text-ink-strong">{post.author}</p>
            <p className="text-xs text-ink-muted">Foundry</p>
          </div>
        </div>

        <div className="prose-foundry mt-8">
          <p className="text-md">{post.excerpt}</p>
          <h2>The short version</h2>
          <p>
            Most component libraries fail at the second product rather than the first. The first one
            is built by the people who wrote the components; the second is built by people reading
            documentation.
          </p>
          <h2>What that means in practice</h2>
          <p>
            It means the documentation is part of the component, not an artefact produced
            afterwards. It means variants have to represent decisions rather than padding values.
            And it means the accessibility contract has to live in the primitive, because it will
            not survive being an item on a review checklist.
          </p>
          <ul>
            <li>Tokens before components</li>
            <li>States before variants</li>
            <li>One focus treatment, applied globally</li>
          </ul>
          <h2>Where this goes wrong</h2>
          <p>
            The usual failure is a library that is technically correct and practically unusable —
            forty props on a button, three ways to set a colour, and a theme provider that has to
            wrap everything. Every one of those is a decision that should have been made once, in
            the token layer.
          </p>
        </div>
      </article>
    </Container>
  )
}

export function AuthorPage({ slug, title }: { slug: string; title: string }) {
  const member =
    team.find((entry) => entry.name.toLowerCase().replace(/\s+/g, '-') === slug) ?? team[0]
  const written = posts.filter((post) => post.author === member?.name)

  return (
    <Container size="narrow" className="py-section">
      <div className="flex flex-wrap items-center gap-4">
        <Avatar name={member?.name ?? title} size="xl" decorative />
        <div>
          <p className="text-sm text-accent">{member?.role}</p>
          <p className="mt-2 max-w-md text-sm text-ink-muted">{member?.bio}</p>
        </div>
      </div>

      <h2 className="display-type mt-12 text-xl font-semibold text-ink-strong">
        {written.length > 0 ? 'Articles' : 'No articles yet'}
      </h2>

      {written.length > 0 ? (
        <ul className="mt-4 divide-y divide-[var(--color-border-subtle)] border-y border-line">
          {written.map((post) => (
            <li key={post.slug}>
              <Link
                href={`/starters/blog/preview/stories/${post.slug}`}
                className="group block py-5"
              >
                <p className="text-md font-semibold text-ink-strong group-hover:text-accent">
                  {post.title}
                </p>
                <p className="mt-1 text-sm text-ink-muted">{post.excerpt}</p>
                <time dateTime={post.date} className="mt-2 block text-xs text-ink-subtle">
                  {formatDate(post.date)}
                </time>
              </Link>
            </li>
          ))}
        </ul>
      ) : (
        <EmptyState
          className="mt-4"
          size="sm"
          icon={<BookOpen className="size-5" />}
          title="Nothing published yet"
          description="This author has not written anything in the demo dataset."
        />
      )}
    </Container>
  )
}

export function CategoryIndexPage({ slug, title }: { slug: string; title: string }) {
  const matching = posts.filter((post) => post.category.toLowerCase().replace(/\s+/g, '-') === slug)
  const shown = matching.length > 0 ? matching : posts.slice(0, 3)

  return (
    <Container className="py-section">
      <p className="text-sm text-ink-muted">
        {matching.length > 0
          ? `${matching.length} ${matching.length === 1 ? 'article' : 'articles'} in ${title}.`
          : `No articles filed under ${title} yet — showing recent articles instead.`}
      </p>

      <ul className="mt-6 grid gap-6 md:grid-cols-2 lg:grid-cols-3">
        {shown.map((post) => (
          <li key={post.slug}>
            <Link href={`/starters/blog/preview/stories/${post.slug}`} className="group block">
              <div
                className="grid-paper aspect-video rounded-lg border border-line bg-surface-sunken"
                aria-hidden="true"
              />
              <Badge tone="accent" size="sm" className="mt-4">
                {post.category}
              </Badge>
              <h2 className="mt-2 text-md leading-snug font-semibold text-ink-strong group-hover:text-accent">
                {post.title}
              </h2>
              <time dateTime={post.date} className="mt-2 block text-xs text-ink-subtle">
                {formatDate(post.date)}
              </time>
            </Link>
          </li>
        ))}
      </ul>
    </Container>
  )
}

/* ------------------------------------------------------------ commerce+ */

export function ProductDetailPage({ slug }: { slug: string; title: string }) {
  const product = products.find((entry) => entry.slug === slug) ?? products[0]
  if (!product) return null

  return (
    <Container className="py-section">
      <div className="grid gap-10 lg:grid-cols-2 lg:gap-16">
        <div>
          <div
            className="grid-paper aspect-square rounded-xl border border-line bg-surface-sunken"
            aria-hidden="true"
          />
          <div className="mt-3 grid grid-cols-4 gap-3">
            {[0, 1, 2, 3].map((index) => (
              <div
                key={index}
                className="grid-paper aspect-square rounded-md border border-line bg-surface-sunken"
                aria-hidden="true"
              />
            ))}
          </div>
        </div>

        <div>
          <div className="flex flex-wrap items-center gap-2">
            <Badge>{product.category}</Badge>
            {product.badge ? <Badge tone="accent">{product.badge}</Badge> : null}
            {!product.inStock ? <Badge tone="danger">Out of stock</Badge> : null}
          </div>
          <p className="mt-4 flex items-baseline gap-3">
            <span className="display-type text-2xl font-semibold text-ink-strong tabular-nums">
              {formatCurrency(product.priceCents)}
            </span>
            {product.compareAtCents ? (
              <span className="font-mono text-sm text-ink-subtle line-through tabular-nums">
                {formatCurrency(product.compareAtCents)}
              </span>
            ) : null}
          </p>
          <p className="mt-4 text-md leading-relaxed text-ink-muted">{product.description}</p>

          <Divider className="my-6" weight="subtle" />

          <Tabs
            label="Product information"
            items={[
              {
                id: 'details',
                label: 'Details',
                content: (
                  <ul className="flex flex-col gap-2 text-sm text-ink-muted">
                    <li>· {product.material}</li>
                    <li>
                      · Rated {product.rating} out of 5 from {product.reviews} reviews
                    </li>
                    <li>· Free delivery over $150</li>
                  </ul>
                ),
              },
              {
                id: 'delivery',
                label: 'Delivery',
                content: (
                  <p className="text-sm text-ink-muted">
                    Two to four working days as standard, next day for $12. Returns accepted for 60
                    days, unworn and with tags.
                  </p>
                ),
              },
            ]}
          />

          <div className="mt-6 flex flex-wrap gap-3">
            <ButtonLink
              href="/starters/ecommerce/preview/cart"
              size="lg"
              className={cn(!product.inStock && 'pointer-events-none opacity-50')}
            >
              {product.inStock ? 'Add to bag' : 'Out of stock'}
            </ButtonLink>
            <ButtonLink href="/starters/ecommerce/preview/shop" size="lg" variant="outline">
              Keep browsing
            </ButtonLink>
          </div>

          <p className="mt-5 text-xs text-ink-subtle">
            Fictional product. Nothing can be purchased in this demo.
          </p>
        </div>
      </div>
    </Container>
  )
}

/* ---------------------------------------------------------- hospitality */

export function ReservationsPage() {
  return (
    <Container size="narrow" className="py-section">
      <div className="rounded-xl border border-line bg-surface p-6 sm:p-8">
        <div className="flex items-center gap-3">
          <span className="flex size-10 items-center justify-center rounded-lg bg-accent-soft text-accent-soft-ink">
            <Calendar className="size-5" aria-hidden="true" />
          </span>
          <div>
            <h2 className="display-type text-lg font-semibold text-ink-strong">Reserve a table</h2>
            <p className="text-sm text-ink-muted">Wednesday to Saturday, 17:30 to 22:00.</p>
          </div>
        </div>

        <ul className="mt-6 flex flex-col gap-2 text-sm text-ink-muted">
          <li>· Tables are held for 15 minutes past the booking time.</li>
          <li>· Parties of seven or more are handled as private dining.</li>
          <li>· Dietary requirements can be noted when booking.</li>
        </ul>

        <ButtonLink href="/forms/booking" block size="lg" className="mt-6">
          Open the booking form
        </ButtonLink>
        <p className="mt-3 text-center text-xs text-ink-subtle">
          The booking form is a Foundry catalogue item. Nothing is reserved.
        </p>
      </div>
    </Container>
  )
}

export function PrivateDiningPage() {
  const spaces = [
    {
      name: 'The Counter',
      seats: '9 seated',
      detail: 'Chef’s counter with a set menu and paired drinks.',
    },
    {
      name: 'The Back Room',
      seats: '18 seated, 30 standing',
      detail: 'Private dining room with its own entrance.',
    },
    {
      name: 'Whole Restaurant',
      seats: '46 seated',
      detail: 'Exclusive hire, Wednesday to Saturday.',
    },
  ]

  return (
    <Container className="py-section">
      <ul className="grid gap-4 lg:grid-cols-3">
        {spaces.map((space) => (
          <li key={space.name}>
            <Card className="h-full">
              <div
                className="grid-paper mb-4 aspect-[4/3] rounded-md border border-line bg-surface-sunken"
                aria-hidden="true"
              />
              <CardTitle>{space.name}</CardTitle>
              <Badge size="sm" className="mt-2">
                {space.seats}
              </Badge>
              <CardDescription className="mt-3">{space.detail}</CardDescription>
            </Card>
          </li>
        ))}
      </ul>

      <div className="mt-8 rounded-xl border border-line bg-surface-sunken p-6">
        <h2 className="display-type text-lg font-semibold text-ink-strong">Enquire</h2>
        <p className="mt-2 max-w-xl text-sm text-ink-muted">
          Private dining is arranged directly with the team. Tell us the date, the number of guests
          and anything the kitchen should know.
        </p>
        <ButtonLink href="/forms/detailed-contact" className="mt-4">
          Send an enquiry
        </ButtonLink>
      </div>
    </Container>
  )
}

/* -------------------------------------------------------------- product */

export function SolutionsPage() {
  const solutions = [
    {
      name: 'For platform teams',
      body: 'Consolidate several internal libraries into one token set without a rewrite.',
    },
    {
      name: 'For agencies',
      body: 'Ship client work on a system you can re-theme in an afternoon.',
    },
    {
      name: 'For solo builders',
      body: 'Start from a whole product and delete what you do not need.',
    },
    {
      name: 'For design systems',
      body: 'Use the token manifest as the contract between design and code.',
    },
  ]

  return (
    <Container className="py-section">
      <ul className="grid gap-4 sm:grid-cols-2">
        {solutions.map((solution) => (
          <li key={solution.name}>
            <Card className="h-full">
              <CardTitle>{solution.name}</CardTitle>
              <CardDescription className="mt-2">{solution.body}</CardDescription>
              <Link
                href="/starters/startup/preview/pricing"
                className="mt-4 inline-flex items-center gap-1.5 text-sm font-semibold text-accent underline underline-offset-4"
              >
                See pricing
                <ArrowRight className="size-4" aria-hidden="true" />
              </Link>
            </Card>
          </li>
        ))}
      </ul>
    </Container>
  )
}

export function ResourcesPage() {
  const resources = [
    { title: 'Token reference', kind: 'Documentation', href: '/docs/design-tokens' },
    { title: 'Accessibility contracts', kind: 'Documentation', href: '/docs/accessibility' },
    { title: 'Composition guide', kind: 'Guide', href: '/docs/composition' },
    { title: 'Contributing', kind: 'Guide', href: '/docs/contributing' },
    { title: 'Changelog', kind: 'Release notes', href: '/changelog' },
    { title: 'Component catalogue', kind: 'Reference', href: '/components' },
  ]

  return (
    <Container className="py-section">
      <ul className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
        {resources.map((resource) => (
          <li key={resource.title}>
            <Link
              href={resource.href}
              className="group flex h-full flex-col rounded-xl border border-line bg-surface p-5 transition-colors hover:border-accent"
            >
              <Badge size="sm">{resource.kind}</Badge>
              <span className="mt-3 flex-1 text-md font-semibold text-ink-strong group-hover:text-accent">
                {resource.title}
              </span>
              <ArrowRight
                className="mt-4 size-4 text-ink-subtle transition-transform group-hover:translate-x-0.5"
                aria-hidden="true"
              />
            </Link>
          </li>
        ))}
      </ul>
    </Container>
  )
}

Demo source — adapt to your project. Foundry is not published as a package.

Usage

A personal site where availability is the first thing a visitor sees, because it is the first thing most of them came to check.

  • The project detail page is shared with the Agency starter.
  • Writing sits below work — it is what brings people back, not what gets them to hire you.

Variants and states

Every entry below is a genuine difference in behaviour or layout, and every one of them is visible in the preview above.

  • 10 concrete routes from 6 definitions
  • Editorial palette
  • Generated navigation with a mobile drawer
  • Its own 404 path for unknown routes

Accessibility

Availability
Status carries an icon and a word, so it survives greyscale and colour-vision deficiency.
Archive links
Every index row is one anchor with a descriptive accessible name.

Foundry implements published ARIA patterns and is tested against them. No WCAG certification is claimed — see the accessibility documentation for what is and is not covered.