A pnpm monorepo for the Anew website.
apps/
web/ Astro 5 website (deployed to Vercel)
studio/ Sanity Studio (deployed separately to *.sanity.studio)
packages/
ui/ Design system: tokens, primitives, section components
Requires Node 20+ and pnpm 11. Enable pnpm via corepack enable && corepack prepare pnpm@latest --activate if not installed.
pnpm install
cp apps/web/.env.example apps/web/.env # fill in SANITY_PROJECT_ID + dataset
pnpm dev # → http://localhost:4321
pnpm build # SSR bundle → apps/web/.vercel/output
pnpm previewcp apps/studio/.env.example apps/studio/.env # set SANITY_STUDIO_PROJECT_ID etc.
pnpm dev:studio # → http://localhost:3333
pnpm build:studio
pnpm deploy:studio # publishes to https://<host>.sanity.studiopnpm dev:all runs the web app and the studio in parallel.
The seed script (apps/studio/scripts/seed.ts) creates the four system pages (pageHome, pageContact, pagePrivacy, pageCookies), the siteMetadata and siteNavigation singletons, plus a few placeholder pages for the main nav. It also mirrors the legacy hard-coded homepage onto pageHome so the site looks identical right after the swap.
# 1. Make sure SANITY_API_WRITE_TOKEN is set in apps/studio/.env
# 2. Run:
pnpm seed:studioThe script is idempotent — assets are looked up by filename and reused; documents are upserted with createOrReplace. Run it again any time the seed data changes.
Deploy only apps/web (the Astro site). Sanity Studio (apps/studio) is not hosted on Vercel — use pnpm deploy:studio → https://<host>.sanity.studio.
When importing the repo, Vercel may suggest the Sanity preset because it finds apps/studio/sanity.config.ts. Decline that and configure manually:
| Setting | Value |
|---|---|
| Root Directory | apps/web |
| Framework Preset | Astro (not Sanity) |
| Build Command | astro build (leave default) |
| Install Command | Enable Override, then: pnpm install --frozen-lockfile --ignore-scripts && pnpm rebuild esbuild sharp |
apps/web/vercel.json only defines redirects and cache headers — not framework/install/build — so Project Settings and deployments stay in sync (no “configuration differs” warning).
If that warning still appears after a deploy: open the latest Production deployment → use Update Project Settings (or copy the install command above into Settings → Build & Deployment with Override on) → redeploy once.
Do not add a second Vercel project for apps/studio, and skip “install Sanity in apps/studio” during setup — that targets the CMS app, not the public website.
Set SANITY_PROJECT_ID, SANITY_DATASET, and Brevo/mail env vars on the web Vercel project (runtime reads from Sanity API; Studio env stays in apps/studio/.env).
Legacy: if the Vercel project root is still the repo root, the root vercel.json copies build output — prefer Root Directory apps/web for a new project instead.
The design system in packages/ui exposes:
Section(primitive wrapper) withtheme,mode,behavior,bg,layerpropsSectionHome,SectionBreaker,SectionQuote,SectionContent- Primitives:
Container,Button,Logo,LogoMark,Icon,Label,ContentBlock,ColumnCard
See docs/SECTIONS.md for the full section + component inventory (selectors, data-attributes, Sanity _type mapping). The Sanity schema for each section lives in docs/CONTENT-MODEL.md.
SectionContent is the generic content section. Combine align: "left" | "right" | "centered" with subLayout: "none" | "cards-row" | "cards-row-cta-below" | "cards-side" | "image-side" to produce the Figma variants.
/sections renders every section variant in both dark and light themes. All 22+ Figma variants are covered there — useful for visual diffing while iterating on tokens, primitives, or sub-layouts. The page is not Sanity-driven — it uses inline fixture data so it stays usable even before any content is published.
Every section accepts a bg prop:
- Viewport sections (
mode: "viewport"):{ type: "color" }(default),{ type: "image", src, align: "top" | "bottom" }, or{ type: "video", src, poster? }. Video bg makes the section transparent and renders into a single shared fixed<video>element behind the page. - Hug sections (
mode: "hug"):{ type: "color" }(default) or{ type: "image", src }(covers the section).
Theming is per-section via theme: "dark" | "light" — flip a single prop to swap colors on a section.
The site runs on a .nl domain so Dutch is the canonical default; English mirrors the same content under /en/. URL shape:
/→ 301 redirect to/nl/(every visitor lands on Dutch first)/nl/and/en/→ home page (pageHomefrom Sanity)/nl/<slug>and/en/<slug>→ any otherpagedocument, resolving the slug fromslugNl.current/slugEn.currentrespectively- Unknown locale tokens (
/fr/...) and missing slugs fall back to a redirect to/nl/
The language-switcher in the nav is computed at the page level — it points at the other locale's slug for the same page, falling back to that locale's home when no slug is set.