Visual Editing with SvelteKit
Get started with Sanity Visual Editing in a new or existing SvelteKit application.
Following this guide will enable you to:
- Render overlays in your application, allowing content editors to jump directly from Sanity content to its source in Sanity Studio.
- Edit your content and see changes reflected in an embedded preview of your application in Sanity’s Presentation tool.
- Optional: Provide instant updates and seamless switching between draft and published content.
- A Sanity project with a hosted or embedded Studio. Read more about hosting here.
- A SvelteKit application using Svelte 5 with SSR. Follow this guide to set one up.
The following steps should be performed in your SvelteKit application.
Install the dependencies that will provide your application with data fetching and Visual Editing capabilities.
npm install @sanity/client @sanity/visual-editing
Create a .env
file in your application’s root directory to provide Sanity specific configuration.
You can use Manage to find your project ID and dataset, and to create a token with Viewer permissions which will be used to fetch preview content.
The URL of your Sanity Studio will depend on where it is hosted or embedded.
# .env
# Public
PUBLIC_SANITY_PROJECT_ID="YOUR_PROJECT_ID"
PUBLIC_SANITY_DATASET="YOUR_DATASET"
PUBLIC_SANITY_STUDIO_URL="https://YOUR_PROJECT.sanity.studio"
# Private
SANITY_VIEWER_TOKEN="YOUR_VIEWER_TOKEN"
Create a Sanity client instance to handle fetching data from Content Lake.
Configuring the stega
option enables automatic overlays for basic data types when preview mode is enabled. You can read more about how stega works here.
// src/lib/sanity.ts
import {createClient} from '@sanity/client'
import {
PUBLIC_SANITY_DATASET,
PUBLIC_SANITY_PROJECT_ID,
PUBLIC_SANITY_STUDIO_URL,
} from "$env/static/public";
export const client = createClient({
projectId: PUBLIC_SANITY_PROJECT_ID,
dataset: PUBLIC_SANITY_DATASET,
useCdn: true,
stega: {
enabled: true,
studioUrl: PUBLIC_SANITY_STUDIO_URL,
},
})
Create a server-only Sanity client instance using the Viewer token and client created above. This will be used to fetch draft content when in preview mode.
// src/lib/server/sanity.ts
import {SANITY_VIEWER_TOKEN} from '$env/static/private'
import {client} from '$lib/sanity'
export const serverClient = client.withConfig({
token: SANITY_VIEWER_TOKEN,
})
Preview mode allows authorized content editors to view and interact with draft content.
In the server hooks file, call and export the handlePreview
handle function, which adds preview mode to your application.
// src/hooks.server.ts
import {handlePreview} from '@sanity/visual-editing/svelte';
import {serverClient} from '$lib/server/sanity'
export const handle = handlePreview({client: serverClient})
Protip
If your server hooks file already exports a handle
function, use SvelteKit's sequence function.
First, define the queries you will use to fetch data from Content Lake.
// src/lib/queries.ts
export type PageResult = { title: string }
export const pageQuery = /* groq */`*[_type == "page"][0]{title}`
Next, define a load function that uses your query to fetch and return data.
When fetching content using the Sanity client in an application that implements visual editing using stega, make sure to set stega
to false
when preview mode is disabled.
// src/routes/+page.server.ts
import {pageQuery as query, type PageResult} from '$lib/queries'
import type {PageServerLoad} from './$types'
export const load: PageServerLoad = async ({locals: {client, preview}}) => {
const options = {stega: preview ? true : false}
const page = await client.fetch<PageResult>(query, {}, options)
return {page}
}
The load function’s return value will be available in the corresponding .svelte
file via the data
prop.
// src/routes/+page.svelte
<script lang="ts">
let {data}: Props = $props();
let {page} = $derived(data)
</script>
<h1>{page.title}</h1>
The handle function implemented above adds a preview
property to the locals
object, exposing the status of preview mode on the server. The server layout file lets you expose this value to client for use in a Svelte layout file.
// src/routes/+layout.server.ts
import type {LayoutServerLoad} from './$types'
export const load: LayoutServerLoad = ({locals: {preview}}) => {
return {preview}
}
The <VisualEditing>
component handles rendering overlays, enabling click to edit, and refreshing pages in your application when content changes.
By exposing the preview
value and conditionally rendering this component, you can ensure only content editors will have access to these features.
// src/routes/+layout.svelte
<script lang="ts">
import {VisualEditing} from '@sanity/visual-editing/svelte'
let {children, data} = $props();
</script>
{@render children()}
{#if data.preview}
<VisualEditing />
{/if}
To set up Presentation tool in your Sanity Studio, import the tool from sanity/presentation
, add it to your plugins
array, and configure previewUrl
, optionally passing an origin, path, and endpoints to enable and disable preview mode.
We similarly recommend using environment variables loaded via a .env
file to support development and production environments.
// sanity.config.ts
import {defineConfig} from 'sanity'
import {presentationTool} from 'sanity/presentation'
export default defineConfig({
// ... project configuration
plugins: [
presentationTool({
previewUrl: {
origin: process.env.SANITY_STUDIO_PREVIEW_ORIGIN,
preview: '/',
previewMode: {
enable: '/preview/enable',
disable: '/preview/disable',
},
}
}),
// ... other plugins
],
})
Instant updates and perspective switching require opting into using loaders to fetch data.
Add the Svelte specific loader to your application dependencies:
npm install @sanity/svelte-loader
Update your server hooks file to call setServerClient
and replace the handlePreview
handle function added during the initial setup with createRequestHandler
.
This function also handles adding Preview mode, but crucially also sets up the loadQuery
helper function which will be used for fetching content on the server.
// src/hooks.server.ts
import {createRequestHandler, setServerClient} from '@sanity/svelte-loader'
import {serverClient} from '$lib/server/sanity'
setServerClient(serverClient)
export const handle = createRequestHandler()
Create a server load
function for your page. Using locals.loadQuery
ensures fetching from Content Lake is performed using the correct perspective: draft content can be fetched if preview mode is enabled, otherwise published content is always returned.
// src/routes/+page.server.ts
import {pageQuery as query, type PageResult} from '$lib/queries'
import type {PageServerLoad} from './$types'
export const load: PageServerLoad = async ({locals: {client, preview}}) => {
const options = {stega: preview ? true : false}
const page = await client.fetch<PageResult>(query, {}, options)
return {page}
}
// src/routes/+page.server.ts
import {pageQuery as query, type PageResult} from '$lib/queries'
import type {PageServerLoad} from './$types'
export const load: PageServerLoad = async ({locals: {loadQuery}}) => {
const initial = await loadQuery<PageResult>(query)
return {query, options: {initial}}
}
Structuring the load function return value in this way ensures we can pass the data
value directly to the useQuery
function.
When your application is viewed in Presentation tool, useQuery
provides instant content updates and seamless switching between draft and published content.
// src/routes/+page.svelte
<script lang="ts">
import {useQuery} from '@sanity/svelte-loader'
import type {PageData} from './$types'
export let data: PageData
const query = useQuery(data)
$: ({ data: page } = $query)
</script>
<h1>{page.title}</h1>
Finally, in the root layout component, render the LiveMode
component with your client instance to enable instant updates when in preview mode.
// src/routes/+layout.svelte
<script lang="ts">
import {VisualEditing} from '@sanity/visual-editing/svelte'
import {LiveMode} from '@sanity/svelte-loader'
import {client} from '$lib/sanity'
let {children, data} = $props();
</script>
{@render children()}
{#if data.preview}
<VisualEditing />
<LiveMode {client} />
{/if}
The handler functions referenced in this guide add properties to SvelteKit’s event.locals
object. If your application is written in TypeScript, extend the App.Locals
interface to ensure type safety.
If using handlePreview
:
// app.d.ts
import type {VisualEditingLocals} from '@sanity/visual-editing/svelte'
declare global {
namespace App {
interface Locals extends VisualEditingLocals {}
}
}
export {}
If using createRequestHandler
:
// app.d.ts
import type {LoaderLocals} from '@sanity/svelte-loader'
declare global {
namespace App {
interface Locals extends LoaderLocals {}
}
}
export {}
useQuery
also returns an encodeDataAttribute
helper method for generating data-sanity
attributes. These attributes give you direct control over rendering overlays in your application, and are especially useful if not using stega encoding.
// src/routes/+page.svelte
<script lang="ts">
import {useQuery} from '@sanity/svelte-loader'
import type {PageData} from './$types'
export let data: PageData
const query = useQuery(data)
$: ({data: page, encodeDataAttribute} = $query)
</script>
<h1 data-sanity={encodeDataAttribute(['title'])}>
{page.title}
</h1>
Your application might need to conditionally render elements if preview mode is enabled, for example to notify Content Editors that they are viewing draft content, or to provide a mechanism for disabling preview mode.
Using the preview
value that the server layout exposed in the initial setup, call setPreviewing
to hydrate the value of the isPreviewing
helper.
// src/routes/+layout.ts
import {setPreviewing} from '@sanity/visual-editing/svelte'
import type {LayoutLoad} from './$types'
export const load: LayoutLoad = ({data}) => {
setPreviewing(data.preview)
return data
}
Use the isPreviewing
helper in any .svelte
file in your application.
// src/components/DisplayPreview.svelte
<script lang="ts">
import {isPreviewing} from '@sanity/visual-editing/svelte'
</script>
{#if $isPreviewing}
<div>Preview Mode is Enabled!</div>
{/if}