Everything *[NYC] 2026: see what we announced.

Sanity+Next

A Sanity.io + Next.js template for building a complete CMS-powered website.

Loading

README

SanityNext

A monorepo combining a Next.js 15 frontend with a Sanity Studio v3 backend for building content-driven websites.

  • nextsite/ — Next.js 15 App Router frontend (React 19, Tailwind CSS 3)
  • sanitydev/ — Sanity Studio v3 (content authoring and schema management)

Content flows: Sanity Studio → GROQ API → Next.js frontend (PortableText + custom renderers).

Prerequisites

  • Node.js v18 LTS or later (v22 recommended)
  • npm 9+
  • Sanity CLI: npm i -g @sanity/cli

Quick Start

Open two terminals:

Next.js frontend:

cd nextsite
npm install
cp .env.example .env.local   # then fill in your values
npm run dev

Runs at http://localhost:3000

Sanity Studio:

cd sanitydev
npm install
cp .env.example .env         # then fill in your Sanity project ID
sanity dev

Runs at http://localhost:3333

Environment Variables

nextsite/.env.local

Copy nextsite/.env.example to nextsite/.env.local and fill in your values:

NEXT_PUBLIC_SANITY_PROJECT_ID=your_sanity_project_id
NEXT_PUBLIC_SANITY_DATASET=production
NEXT_PUBLIC_URL=http://localhost:3000
JWT_SECRET=your_strong_random_secret
SMTP_HOST=smtp.office365.com
SMTP_PORT=587
SMTP_USER=your@email.com
SMTP_PASS=your_password
FROM_EMAIL=your@email.com
FROM_NAME=Your Site Name
SANITY_TOKEN=your_sanity_write_token

sanitydev/.env

Copy sanitydev/.env.example to sanitydev/.env and fill in your values:

SANITY_STUDIO_PROJECT_ID=your_sanity_project_id
SANITY_STUDIO_DATASET=production

Never commit .env or .env.local files. Both are gitignored. Only .env.example files are committed.

Project Structure

SanityNext/
└─ Structure/
   ├─ nextsite/                        # Next.js 15 App Router frontend
   │  ├─ app/
   │  │  ├─ [slug]/page.tsx            # Dynamic page renderer
   │  │  ├─ blog/                      # Blog listing and post pages
   │  │  ├─ api/                       # API routes (contact, auth, account)
   │  │  ├─ globals.css                # Global styles (Tailwind base + overrides)
   │  │  └─ layout.tsx                 # Root layout (fonts, providers)
   │  ├─ components/
   │  │  ├─ PageRenderer.tsx           # Maps Sanity section types to React components
   │  │  ├─ pte.tsx                    # PortableText custom renderers
   │  │  ├─ SliderBlock.tsx            # Image/content slider
   │  │  ├─ ContactForm.tsx            # Dynamic contact form
   │  │  ├─ header.tsx                 # Navigation / menus
   │  │  ├─ footer.tsx                 # Footer menus and logo
   │  │  ├─ CookieBanner.tsx           # GDPR cookie consent
   │  │  └─ BlogPostContent.tsx        # Blog post renderer
   │  ├─ sanity/
   │  │  └─ queries.ts                 # All GROQ queries
   │  ├─ lib/
   │  │  ├─ sanity.ts                  # Sanity client + image URL builder
   │  │  ├─ auth-server.ts             # JWT auth helpers (server-side)
   │  │  └─ seo.ts                     # SEO metadata helpers
   │  ├─ contexts/
   │  │  ├─ CookieConsentContext.tsx   # Cookie consent state
   │  │  └─ PostHogContext.tsx         # Analytics context
   │  ├─ tailwind.config.js
   │  ├─ next.config.ts
   │  └─ tsconfig.json
   └─ sanitydev/                       # Sanity Studio v3
      ├─ schemas/                      # All document/object schemas
      │  ├─ page.ts                    # Page document
      │  ├─ sections.ts                # Full-width section
      │  ├─ columnSections.ts          # Column-based section
      │  ├─ slider.ts                  # Slider/carousel
      │  ├─ form.ts / formStyle.ts     # Contact form + styling
      │  ├─ blogPost.ts                # Blog post document
      │  ├─ menu.ts / menuItem.ts      # Navigation menus
      │  ├─ headerSettings.ts          # Header configuration
      │  ├─ footerSettings.ts          # Footer configuration
      │  └─ ...                        # accordion, button, icon, video, etc.
      ├─ sanity.config.ts              # Studio config (projectId, plugins)
      ├─ structure.ts                  # Custom desk structure
      └─ tsconfig.json

Scripts

nextsite:

CommandDescription
npm run devStart dev server (port 3000)
npm run buildProduction build
npm run startStart production server
npm run lintESLint check

sanitydev:

CommandDescription
sanity devStart Studio locally (port 3333)
sanity buildBuild Studio
sanity deployDeploy Studio to Sanity's CDN

Key Patterns

GROQ — Dereference sections

*[_type == "page" && slug.current == $slug][0]{
  title,
  sections[]{
    ...,
    _type == "sections" => @->,
    columnContent[]{
      ...,
      content[]{ ..., _type == "sections" => @-> }
    }
  }
}

Color fields

Schemas use type: 'string' for CSS color values (hex, rgba, CSS variable). Pass directly as inline styles — no conversion needed.

Images

Always use the urlFor() helper from nextsite/lib/sanity.ts:

import { urlFor } from '@/lib/sanity'
urlFor(image).width(800).auto('format').url()

GROQ must dereference the slug: internalLink->{ slug }.
In pte.tsx, internal links resolve to /${slug.current}.

Auth

JWT-based auth: login, register, verify, forgot-password, reset-password API routes live in nextsite/app/api/. Token stored in an httpOnly cookie.

API Routes

RouteMethodDescription
/api/contactPOSTSend email via Nodemailer (SMTP)
/api/loginPOSTAuthenticate user, set cookie
/api/registerPOSTCreate new user account
/api/verifyGETVerify email address
/api/forgot-passwordPOSTSend password reset email
/api/reset-passwordPOSTReset password with token
/api/meGETGet current authenticated user
/api/logoutPOSTClear auth cookie
/api/accountGET/PUTFetch/update account details

Troubleshooting

Internal links resolve to #
Ensure GROQ includes internalLink->{ slug } for all relevant content arrays including nested sections.

Images broken
Always use urlFor() — never use asset.url directly.

SMTP 535 auth error
Enable "Authenticated SMTP" for the mailbox in Microsoft 365 Exchange Admin Center (https://aka.ms/smtp_auth_disabled).

Sanity "attribute count exceeds 2000" error
Reduce type: 'color' fields (use type: 'string' instead) or delete unused documents. See session history for migration scripts.

Sanity Studio not reflecting schema changes
Restart sanity dev after editing any file in sanitydev/schemas/.

Deploying to Vercel

  1. Import the repo into Vercel (choose the Structure/nextsite directory as the root).
  2. In Project Settings → Environment Variables, add all variables from nextsite/.env.example with your real values.
  3. Deploy — no secrets should ever be in the codebase.

The vercel.json env block uses @variable_name references that resolve to Vercel-managed secrets/env vars. Set the matching variables in the Vercel dashboard.

Contributing

  1. Edit schemas in sanitydev/schemas/ — restart Studio after changes.
  2. Update GROQ queries in nextsite/sanity/queries.ts to fetch new fields.
  3. Render new content types in nextsite/components/pte.tsx or PageRenderer.tsx.
  4. Commit and push: git push origin main.

Repository

GitHub: https://github.com/fernandofas/SanityNext