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
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) withnext-intlfor UI and@tinloof/sanity-document-i18nfor 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/andstudio/packages
Stack
| Layer | Technology |
|---|---|
| Frontend | Next.js 16, React 19, Tailwind CSS v4 |
| CMS | Sanity v5, @tinloof/sanity-studio |
| Deployment | Cloudflare Workers via OpenNext |
| Package manager | pnpm |
Quick Start
1. Clone and install
npx degit pedroduke/sanity-nextjs-cloudflare-i18n-starter my-site
cd my-site
pnpm install2. 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.localSee Environment Variables below for what each variable does.
3. Run locally
pnpm devThis starts both servers in parallel:
- Frontend → http://localhost:3000
- Studio → http://localhost:3333
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-dataDeployment
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 deployDeploy Frontend to Cloudflare Workers
From the repo root:
cd frontend && pnpm run deployThis 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 previewInvite collaborators
Open Sanity Manage, select your project, and click Invite project members.
Environment Variables
Frontend (frontend/.env.local)
| Variable | Required | Description |
|---|---|---|
NEXT_PUBLIC_SANITY_PROJECT_ID | ✅ | Your Sanity project ID (from sanity.io/manage) |
NEXT_PUBLIC_SANITY_DATASET | ✅ | Dataset name, usually production |
SANITY_API_READ_TOKEN | ✅ | Read token for draft/live content (create in Sanity Manage → API → Tokens) |
NEXT_PUBLIC_SANITY_API_VERSION | — | API version date, defaults to latest |
NEXT_PUBLIC_SANITY_STUDIO_URL | — | Deployed Studio URL for Visual Editing, defaults to http://localhost:3333 |
Studio (studio/.env.local)
| Variable | Required | Description |
|---|---|---|
SANITY_STUDIO_PROJECT_ID | ✅ | Your Sanity project ID |
SANITY_STUDIO_DATASET | ✅ | Dataset name, usually production |
SANITY_STUDIO_PREVIEW_URL | — | Your deployed frontend URL for live preview, defaults to http://localhost:3000 |
SANITY_STUDIO_STUDIO_HOST | — | Custom 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_TOKENInternationalization
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
| Layer | Tool | Lives in |
|---|---|---|
| Developer-authored UI strings (Header, Footer, Hero, Features, About copy, etc.) | next-intl | frontend/messages/{en,pt,pl}.json |
| Blog posts | @tinloof/sanity-document-i18n | Sanity Studio (one document per language, linked via translation.metadata) |
Sanity page documents (/[...path]) | Single-locale by design | Same English body served under any locale prefix |
settings singleton (site title, OG image) | Single-locale by design | Sanity (title falls back to Metadata.siteTitle from messages) |
Adding or removing a locale
- Edit
frontend/i18n/routing.ts— add the locale code tolocales. - Add
frontend/messages/{newLocale}.json(copyen.jsonand translate). - Add a
LanguageSwitcher.<code>translation key in every messages file (the locale's native name). - In Studio, add the locale to
documentI18n({ locales: [...] })instudio/sanity.config.ts. - Re-run
pnpm typegenfrom the repo root.
Translating sample posts
The bundled studio/sample-data.tar.gz ships English-only posts. To make the demo trilingual:
- Run
pnpm --filter studio dev, open Studio. - 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. - Repeat for 2–3 sample posts so the listing has content per locale.
- 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.gzHow 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