Type something to search...
How to Embed Sanity Studio in a Next.js App

How to Embed Sanity Studio in a Next.js App

To embed Sanity Studio in a Next.js app, add a sanity.config.ts file at the project root with 'use client' at the top and a basePath such as /studio, then create an optional catch-all route at app/studio/[[...tool]]/page.tsx that renders <NextStudio config={config} /> from next-sanity/studio. Re-export metadata and viewport from next-sanity/studio so the route isn't indexed and works on mobile, and add your site's origins to CORS with credentials. Running npx sanity@latest init inside the Next.js project can generate all of this for you.

An embedded Studio means one repository, one build and one deployment for both your website and your editing interface. Editors go to yoursite.com/studio, and developers change schemas and front-end code in the same pull request. This guide goes beyond the basic setup: it covers when embedding is the right call, both installation routes, how to keep site-only components out of the Studio with route groups, customising metadata and wrapping the Studio, multiple workspaces, the Presentation tool, upgrades and the errors you're most likely to hit. If you're still deciding where to host the Studio, how to deploy Sanity Studio compares all the options.

When to Embed the Studio

Embedding is convenient, but it isn't always the best structure. Here's how it compares with a standalone Studio:

Embedded in Next.jsStandalone Studio
URLyoursite.com/studioname.sanity.studio or your own host
Repositories and deploysOneUsually two, or a monorepo
Studio auto-updatesNot availableAvailable on Sanity hosting
Site build timeLonger, because the Studio is bundledUnaffected
Content modelTends to become website-shapedEasier to keep channel-agnostic
Good fitSolo developers, small and medium sitesLarger teams, several front ends

Embed when the Studio exists mainly to serve this one website and you value a single deployment. Prefer a standalone Studio when several apps read the same content or when content and front-end work are owned by different teams. Whichever you choose, the way your pages connect Sanity with Next.js stays the same.

Requirements

  • Next.js 16 with the App Router: NextStudio is designed for the App Router.
  • Node.js 22.12 or later: required by the current sanity package.
  • Packages: next-sanity, sanity and styled-components. With npm 7+ or pnpm 8+, installing next-sanity pulls in the other two as peer dependencies.

Option 1: Let the CLI Set It Up

Inside your Next.js project, run:

npx sanity@latest init

The CLI detects Next.js and then:

  1. Asks you to log in and choose or create a Sanity project and dataset.
  2. Asks whether to embed the Studio and at which route, defaulting to /studio.
  3. Writes sanity.config.ts, sanity.cli.ts, the Studio route file and a src/sanity/ folder with environment helpers and schema types.
  4. Adds http://localhost:3000 to the project's CORS origins if it's missing.

Start the dev server with npm run dev, go to http://localhost:3000/studio and log in. That's a working embedded Studio. The rest of this guide explains the generated files and how to extend them.

Option 2: Set It Up by Hand

Step 1: Install the Packages

npm install next-sanity sanity styled-components @sanity/vision

Step 2: Create sanity.config.ts

Put the Studio configuration at the root of the Next.js project:

// sanity.config.ts
'use client'

import { defineConfig } from 'sanity'
import { structureTool } from 'sanity/structure'
import { visionTool } from '@sanity/vision'
import { schemaTypes } from './src/sanity/schemaTypes'

export default defineConfig({
  name: 'default',
  title: 'My Site',
  basePath: '/studio',
  projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
  dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!,
  plugins: [structureTool(), visionTool()],
  schema: { types: schemaTypes },
})

Two lines here are specific to embedding:

  • 'use client': the Studio is a client-side React application. Without the directive, Next.js tries to evaluate the config as server code and the route fails.
  • basePath: must match the route the Studio is mounted on. If you mount it at /admin, set basePath: '/admin'.

Because the Studio runs in the browser, it reads only NEXT_PUBLIC_ environment variables. That's fine for the project ID and dataset, which aren't secrets. Never put tokens in the Studio config; editors authenticate with their own Sanity login. If the Studio's plugins and tools are new to you, the Sanity Studio walkthrough is a good primer.

Step 3: Add sanity.cli.ts

This file lets you run CLI commands such as npx sanity cors add or npx sanity typegen generate from the Next.js project:

// sanity.cli.ts
import { defineCliConfig } from 'sanity/cli'

export default defineCliConfig({
  api: {
    projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
    dataset: process.env.NEXT_PUBLIC_SANITY_DATASET,
  },
})

Step 4: Create the Catch-All Route

The Studio has its own client-side router, with URLs like /studio/structure/post and /studio/vision. Every one of those paths must render the Studio, so use an optional catch-all segment:

// src/app/studio/[[...tool]]/page.tsx
import { NextStudio } from 'next-sanity/studio'
import config from '../../../../sanity.config'

export const dynamic = 'force-static'

export { metadata, viewport } from 'next-sanity/studio'

export default function StudioPage() {
  return <NextStudio config={config} />
}

What each part does:

  • [[...tool]]: the double brackets make the segment optional, so both /studio and /studio/anything/below match.
  • dynamic = 'force-static': the Studio shell is identical for every user, and content loads in the browser after login, so the route can be static.
  • metadata and viewport: sensible defaults, including robots: 'noindex' and a mobile-friendly viewport.
  • NextStudio: renders the Studio in a full-height container, with a fallback message for browsers without JavaScript.

Note that the page file itself stays a Server Component. Only the config, and the Studio it creates, run on the client.

Step 5: Allow Your Origins

The Studio talks to Sanity's APIs from the browser with the editor's credentials, so every origin it runs on needs a CORS entry with credentials allowed:

npx sanity cors add http://localhost:3000 --credentials
npx sanity cors add https://www.example.com --credentials

Add preview deployment domains too, if editors use them. Only allow origins you control. The guide to securing your Sanity project explains why credentialed CORS deserves care.

Keep Site Components Out of the Studio

The Studio inherits whatever your root layout renders. If that includes a site header, footer, analytics, <SanityLive /> or <VisualEditing />, they'll appear inside the Studio too, or try to connect where they shouldn't. The fix is route groups, which give each part of the app its own root layout without changing URLs:

src/app/
  (site)/
    layout.tsx            # <html>, header, SanityLive, VisualEditing
    page.tsx
    posts/[slug]/page.tsx
  (studio)/
    layout.tsx            # minimal <html> and <body> only
    studio/[[...tool]]/page.tsx

Move your existing app/layout.tsx into (site) rather than leaving it above both groups. The Studio's layout can be minimal:

// src/app/(studio)/layout.tsx
export default function StudioLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body style={{ margin: 0 }}>{children}</body>
    </html>
  )
}

The URL is still /studio, so basePath doesn't change. The Studio page now sits one folder deeper, so its import becomes ../../../../../sanity.config. To avoid counting dots, add a path alias in tsconfig.json, or import with the @/ alias if your config lives under src/.

Customise Metadata

To change the browser tab title or viewport behaviour, spread the defaults and override only what you need:

// src/app/(studio)/studio/[[...tool]]/page.tsx
import type { Metadata, Viewport } from 'next'
import {
  NextStudio,
  metadata as studioMetadata,
  viewport as studioViewport,
} from 'next-sanity/studio'
import config from '../../../../../sanity.config'

export const dynamic = 'force-static'

export const metadata: Metadata = {
  ...studioMetadata,
  title: 'My Site Studio',
}

export const viewport: Viewport = {
  ...studioViewport,
  interactiveWidget: 'resizes-content',
}

export default function StudioPage() {
  return <NextStudio config={config} />
}

Keep robots: 'noindex' from the defaults. The Studio requires a login to see content, but there's no reason for search engines to index its shell.

Wrap the Studio in Your Own Components

Occasionally you need something around the Studio, such as a custom navigation bar, an extra context provider or access to Studio hooks outside a plugin. Pass children to NextStudio, and it delegates rendering to them:

// src/app/(studio)/studio/[[...tool]]/CustomStudio.tsx
'use client'

import { NextStudio } from 'next-sanity/studio'
import { StudioLayout, StudioProvider } from 'sanity'
import config from '../../../../../sanity.config'

export function CustomStudio() {
  return (
    <NextStudio config={config}>
      <StudioProvider config={config}>
        {/* Components here can use the same hooks as Studio plugins */}
        <StudioLayout />
      </StudioProvider>
    </NextStudio>
  )
}

Render <CustomStudio /> from the page, which remains a Server Component so it can keep its metadata and viewport exports. Most projects never need this. For branding, theming Sanity Studio and the Studio's own config options are simpler.

Multiple Workspaces

A Studio can expose several workspaces, for example one per dataset. Export an array from defineConfig, and give each workspace a basePath under the Studio route:

// sanity.config.ts
'use client'

import { defineConfig } from 'sanity'
import { structureTool } from 'sanity/structure'
import { schemaTypes } from './src/sanity/schemaTypes'

const shared = {
  projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
  plugins: [structureTool()],
  schema: { types: schemaTypes },
}

export default defineConfig([
  { ...shared, name: 'production', title: 'Production', dataset: 'production', basePath: '/studio/production' },
  { ...shared, name: 'staging', title: 'Staging', dataset: 'staging', basePath: '/studio/staging' },
])

The same catch-all route serves both, and editors switch between them from the workspace menu. See Sanity datasets and environments for when separate datasets make sense.

Add the Presentation Tool

An embedded Studio pairs well with Visual Editing, because the Studio and the site share an origin. When you add presentationTool() to the config, previewUrl.initial can be omitted and the Studio previews its own site:

import { presentationTool } from 'sanity/presentation'

plugins: [
  structureTool(),
  presentationTool({
    previewUrl: {
      previewMode: { enable: '/api/draft-mode/enable' },
    },
  }),
],

Set the front-end client's stega.studioUrl to '/studio' to match. The complete setup, including Draft Mode routes and overlays, is in setting up Sanity Visual Editing in Next.js.

Upgrading an Embedded Studio

Studios hosted on Sanity can receive automatic updates. An embedded Studio can't, because it's bundled into your app. You upgrade it like any other dependency:

npm install sanity@latest next-sanity@latest @sanity/vision@latest
npm run build

Upgrade sanity and @sanity/vision together, check the release notes for breaking changes and test the Studio locally before deploying. If your schema lives in the same repo, deploying it with npx sanity schemas deploy keeps features that rely on the deployed schema, such as TypeGen against a remote schema and some Sanity tooling, in sync.

Troubleshooting

  • "Export default doesn't exist in target module": sanity.config.ts is missing 'use client' at the top.
  • Studio loads, but navigation breaks or shows a blank screen: basePath doesn't match the route folder, or the route isn't a catch-all.
  • Endless login loop or CORS error: the exact origin, including protocol and port, isn't in CORS with credentials enabled.
  • Site header or live preview appears inside the Studio: split the app into (site) and (studio) route groups.
  • Missing peer dependency warnings: install sanity and styled-components explicitly, especially with Yarn 1.
  • Build gets slower: the Studio adds a large client bundle. That's expected; if it becomes a problem, move the Studio to a standalone deployment.

FAQ: Embedding Sanity Studio in Next.js

Yes. The Studio shell is public, but every request for content uses the editor's Sanity login and the project's roles. Keep tokens out of sanity.config.ts, restrict CORS to origins you control and keep the default noindex metadata.

Yes. Rename the route folder, for example to app/admin/[[...tool]], and set basePath to /admin in sanity.config.ts. The two must always match.

Not for visitors. The Studio's JavaScript only loads on the Studio route, so other pages aren't affected. It does make builds a little longer, because Next.js has to bundle the Studio.

Mount the Studio on an App Router route even if the rest of your site uses the Pages Router. Next.js supports both routers in one app, and NextStudio is designed for the App Router.

No, your Next.js deployment serves the Studio. You may still want sanity schemas deploy so that tools relying on the deployed schema stay current, but there's no separate Studio hosting step.

Because it inherits the root layout. Split your app into route groups, each with its own root layout, so the Studio gets a minimal layout without site components.


Conclusion

Embedding Sanity Studio in Next.js comes down to a client-side sanity.config.ts with the right basePath, an optional catch-all route that renders NextStudio, and CORS entries for every origin the Studio runs on. Route groups keep your site's layout, live updates and overlays out of the editing interface, and the metadata defaults keep the Studio out of search results.

The payoff is a single codebase where schema changes, queries and components ship together, and editors work at a URL on your own domain. Just remember the trade-off: you're responsible for upgrading the Studio. If that's acceptable, embedding is the simplest way to run Sanity alongside a Next.js blog or any other App Router site.

Tags :
Share :

Related Posts

Building a Static Site With Astro and Sanity

Building a Static Site With Astro and Sanity

To build a static site with Astro and Sanity, keep Astro's default output: 'static', configure @sanity/astro with useCdn: false, and fetch

Dive Deeper
Common Sanity Mistakes and How to Fix Them

Common Sanity Mistakes and How to Fix Them

The most common Sanity mistakes are using the CDN where you need fresh data, fetching whole documents with ..., leaving slugs unvalidated, ship

Dive Deeper
How to Create a Custom Sanity Plugin

How to Create a Custom Sanity Plugin

To create a custom Sanity plugin, wrap a piece of Studio configuration in definePlugin from the sanity package and add it to the plugins ar

Dive Deeper