Unlock seamless workflows and faster delivery with our latest releases – get the details

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.

Prerequisites

  • 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.

SvelteKit application setup

The following steps should be performed in your SvelteKit application.

Install dependencies

Install the dependencies that will provide your application with data fetching and Visual Editing capabilities.

npm install @sanity/client @sanity/visual-editing

Set environment variables

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"

Application setup

Sanity client

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

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.

Rendering pages

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>

Enable Visual Editing

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}

Studio setup

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
  ],
})

Optional Extras

Instant updates and perspective switching

Instant updates and perspective switching require opting into using loaders to fetch data.

Add the Svelte specific loader to your application dependencies:

Install dependencies

npm install @sanity/svelte-loader

Update server hooks

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()

Use loaders

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>

Enable live mode

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}

TypeScript: event.locals

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 {}

Adding data attributes

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>

Conditional rendering in preview mode with isPreviewing

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}

Was this article helpful?