Joint session with Vercel: How to build intelligent storefronts (May 15th)

Sanity + Next.js + Cloudflare + i18n Starter

A scalable starter template combining Sanity CMS, Next.js, Cloudflare Workers and i18n for building high-performance, content-first web applications with internationalization support.

By Pedro Duque


README

Next.js + Sanity + Cloudflare + i18n Starter

A production-ready website starter combining Next.js 16, Sanity CMS with Visual Editing and built-in internationalization support, and deployment to Cloudflare Workers via OpenNext.

Note: After clicking the deploy button, you still need to configure environment variables in your Cloudflare dashboard (see Environment Variables).

Demo

https://sanity-next-cloudflare-i18n-starter.pedroduque8.workers.dev/

Features

  • Next.js 16 App Router — Static site generation with incremental revalidation
  • i18n out of the box — Three locales (en/pt/pl) with next-intl for UI and @tinloof/sanity-document-i18n for translatable blog posts. Locale-prefixed URLs (/en, /pt, /pl), per-locale slugs, <link rel="alternate" hreflang> everywhere
  • Sanity Visual Editing — Live preview with the Pages navigator and real-time Live Content API
  • Cloudflare Workers — Edge deployment powered by OpenNext for Cloudflare
  • Page Builder — Drag-and-drop page sections with Visual Editing support
  • Pathname-based routing — Clean URL structure for pages and posts
  • AI-powered alt text — Auto-generate image descriptions with Sanity AI Assist
  • Unsplash integration — Seamless media management
  • pnpm workspaces — Monorepo with frontend/ and studio/ packages

Stack

LayerTechnology
FrontendNext.js 16, React 19, Tailwind CSS v4
CMSSanity v5, @tinloof/sanity-studio
DeploymentCloudflare Workers via OpenNext
Package managerpnpm

Quick Start

1. Clone and install

npx degit pedroduke/sanity-nextjs-cloudflare-i18n-starter my-site
cd my-site
pnpm install

2. Configure environment variables

Copy the example files and fill in your values:

cp frontend/.env.example frontend/.env.local
cp studio/.env.example studio/.env.local

See Environment Variables below for what each variable does.

3. Run locally

pnpm dev

This starts both servers in parallel:

4. Create content

In the Studio, click + New document and create a Post or Page. Publish it and it will appear on the frontend immediately.

To import sample data:

pnpm run import-sample-data

Deployment

Deploy Sanity Studio

Before deploying the frontend, set SANITY_STUDIO_PREVIEW_URL in Sanity Manage to your Cloudflare Workers URL (e.g. https://your-project.workers.dev), then deploy the Studio:

cd studio && pnpm run deploy
# or
cd studio && npx sanity deploy

Deploy Frontend to Cloudflare Workers

From the repo root:

cd frontend && pnpm run deploy

This runs opennextjs-cloudflare build && opennextjs-cloudflare deploy and publishes your Next.js app as a Cloudflare Worker.

To preview locally using the Cloudflare runtime before deploying:

pnpm --filter frontend preview

Invite collaborators

Open Sanity Manage, select your project, and click Invite project members.

Environment Variables

Frontend (frontend/.env.local)

VariableRequiredDescription
NEXT_PUBLIC_SANITY_PROJECT_IDYour Sanity project ID (from sanity.io/manage)
NEXT_PUBLIC_SANITY_DATASETDataset name, usually production
SANITY_API_READ_TOKENRead token for draft/live content (create in Sanity Manage → API → Tokens)
NEXT_PUBLIC_SANITY_API_VERSIONAPI version date, defaults to latest
NEXT_PUBLIC_SANITY_STUDIO_URLDeployed Studio URL for Visual Editing, defaults to http://localhost:3333

Studio (studio/.env.local)

VariableRequiredDescription
SANITY_STUDIO_PROJECT_IDYour Sanity project ID
SANITY_STUDIO_DATASETDataset name, usually production
SANITY_STUDIO_PREVIEW_URLYour deployed frontend URL for live preview, defaults to http://localhost:3000
SANITY_STUDIO_STUDIO_HOSTCustom hostname for the deployed Studio

Cloudflare Workers (production)

Set the same frontend variables as Secrets or Environment Variables in your Cloudflare dashboard or via wrangler secret put:

cd frontend
wrangler secret put SANITY_API_READ_TOKEN

Internationalization

This template ships with three locales: English (en, default), Portuguese (pt), Polish (pl). All public URLs are locale-prefixed (/en/..., /pt/..., /pl/...); a request to / redirects to /en.

What gets translated where

LayerToolLives in
Developer-authored UI strings (Header, Footer, Hero, Features, About copy, etc.)next-intlfrontend/messages/{en,pt,pl}.json
Blog posts@tinloof/sanity-document-i18nSanity Studio (one document per language, linked via translation.metadata)
Sanity page documents (/[...path])Single-locale by designSame English body served under any locale prefix
settings singleton (site title, OG image)Single-locale by designSanity (title falls back to Metadata.siteTitle from messages)

Adding or removing a locale

  1. Edit frontend/i18n/routing.ts — add the locale code to locales.
  2. Add frontend/messages/{newLocale}.json (copy en.json and translate).
  3. Add a LanguageSwitcher.<code> translation key in every messages file (the locale's native name).
  4. In Studio, add the locale to documentI18n({ locales: [...] }) in studio/sanity.config.ts.
  5. Re-run pnpm typegen from the repo root.

Translating sample posts

The bundled studio/sample-data.tar.gz ships English-only posts. To make the demo trilingual:

  1. Run pnpm --filter studio dev, open Studio.
  2. Pick a post → click the Português / Polski badge in the language menu → fill title, pathname (use a localised slug like /posts/ola-mundo), excerpt, content → publish.
  3. Repeat for 2–3 sample posts so the listing has content per locale.
  4. Optional: regenerate the bundled tarball so a fresh clone gets the translated content:
cd studio
pnpm sanity dataset export production --overwrite
mv production.tar.gz sample-data.tar.gz

How the language switcher behaves

Clicking a locale in the header dropdown:

  • On /[locale], /[locale]/about, /[locale]/posts, /[locale]/[...path] — swaps the locale prefix on the same pathname.
  • On /[locale]/posts/[slug] — looks up the translated document's pathname and navigates there. If no translation exists, falls back to /[locale]/posts.

Listing pages (/[locale]/posts) only show posts that have a translation for that locale. Missing-translation posts are never silently fallen back to English.

Project Structure

.
├── frontend/               # Next.js app
│   ├── app/                # App Router routes and components
│   ├── sanity/             # Sanity client, queries, and live config
│   ├── wrangler.jsonc      # Cloudflare Workers config
│   └── open-next.config.ts # OpenNext config
└── studio/                 # Sanity Studio
    └── src/schemaTypes/    # Document and object schemas

Resources

Related contributions