A bilingual photography portfolio built with Next.js App Router and Sanity. English URLs are unprefixed; French URLs use /fr. The Sanity Studio is embedded at /studio and supports authenticated draft-mode visual editing.
- Node.js 22 or newer
- npm
- A Sanity project and dataset
npm ci
cp .env.example .env.local
npm run devFill in every required value in .env.local, then open http://localhost:3000. Studio is at /studio.
| Variable | Visibility | Purpose |
|---|---|---|
NEXT_PUBLIC_SANITY_PROJECT_ID |
Public | Sanity project ID |
NEXT_PUBLIC_SANITY_DATASET |
Public | Sanity dataset name |
NEXT_PUBLIC_SANITY_API_VERSION |
Public | Pinned API version |
NEXT_PUBLIC_SANITY_USE_CDN |
Public | Use the Sanity CDN for published reads |
NEXT_PUBLIC_SITE_URL |
Public | One canonical site origin for metadata and sitemap URLs |
SANITY_API_READ_TOKEN |
Server only | Viewer token for draft mode and visual editing |
SANITY_WEBHOOK_SECRET |
Server only | Secret used to verify Sanity revalidation webhooks |
Never expose either server-only secret through a NEXT_PUBLIC_ variable. Rotate the read token after deploying the security cutover.
The Presentation tool opens the site through GET /api/preview. Sanity creates and validates the short-lived preview secret; the application additionally allowlists the destination before enabling draft mode. The read token stays on the server.
Presentation resolves English and French pages, posts, albums, and categories to their public URLs and shows additional “Used on” locations for index and collection pages. Draft mutations request a server-rendered refresh immediately and again after Sanity's consistency window; no browser-readable API token is used. When a preview URL is opened outside the Presentation iframe, use the floating Exit preview control to clear draft mode.
Add both the local frontend origin and every deployed preview/production origin to the Sanity project's CORS settings with credentials enabled. Presentation uses http://localhost:3000 during development and NEXT_PUBLIC_SITE_URL in production, so local Studio changes exercise the local frontend instead of the deployed bundle.
Site Settings owns editorial metadata only: site title, description, author, and social links. Navigation and page content are also authored in Sanity. Fonts and theme palettes are deployment configuration in src/config; changing them requires a code change and deployment. Existing legacy presentation fields in the dataset are intentionally left untouched but are ignored by this version.
Configure a Sanity webhook to send POST /api/revalidate with this projection:
{_id, _type}
Enable webhook signature authentication and use the same value as SANITY_WEBHOOK_SECRET. The endpoint rejects unsigned requests, unknown document types, malformed payloads, and bodies over 16 KiB. Publishing invalidates known cache tags; callers cannot submit arbitrary paths.
src/proxy.ts internally rewrites unprefixed routes to the en App Router segment. French remains publicly prefixed with /fr; /en/... redirects to the canonical unprefixed equivalent. Public gallery, album, category, blog, custom page, preview, Studio, and metadata URLs are preserved.
npm run format:check
npm run lint
npm run typecheck
npm run test:run
npm run test:e2e
npm run build
npm audit --omit=dev --audit-level=highGenerate schema-derived Sanity types with npm run sanity:schema && npm run sanity:typegen after schema changes.
Vercel is the production target. Configure all required variables for Preview and Production, deploy a Preview, run the Playwright suite against it with PLAYWRIGHT_BASE_URL, and verify the Sanity Presentation and webhook flows before promoting it. Keep the prior production deployment available as the rollback target.
See ARCHITECTURE.md for system boundaries and CONTRIBUTING.md for the change workflow.