Visual Editing

Visual Editing enables content teams to enjoy more intuitive content editing directly from your website.

The feature works using a new technology invented by Sanity called Content Source Maps, which is also available as an API for you to use to build your own implementation.

Getting started with Visual Editing requires minimal changes to your website’s front-end code, only some configuration in an enhanced Sanity Client.

Visual Editing can be used on any hosting with the installation and configuration of the @sanity/overlays package.

Vercel Visual Editing offers a similar experience without the additional package – as Vercel automatically adds the clickable buttons in preview builds – and benefits from the Vercel toolbar's other features like comments and preview.

Enterprise Feature

Visual Editing requires Content Source Maps, a feature available on a Sanity Enterprise plan.

To use Vercel Visual from Vercel's toolbar, you'll also need a Vercel Pro or Enterprise plan.

If you are an existing enterprise customer, contact our sales team to have Content Source Maps enabled on your project. Learn more about Sanity for Enterprise organizations here.

How does Visual Editing work?

This video walkthrough demonstrates Vercel Visual Editing. Read more on their documentation.

Visual Editing overlays contain a clickable Edit in Sanity Studio button onto each editable element on your website. Clicking the button opens your Studio in a new tab with the editor pane focused on the field corresponding to the front-end element.

Visual Editing can be activated on any page where Content Source Maps metadata is present on the page and the "Edit" button appears on the Vercel toolbar

Content Source Maps embeds invisible metadata into your query results, containing information about the document and field in your Content Lake that the data originated from.

Visual Editing uses this metadata to link each element in your front end to its corresponding document and field in your Sanity Studio, allowing for easy access and editing, particularly for teammates who are less familiar with your content model.

How to enable Visual Editing

To enable Visual Editing on Vercel or any other hosting, follow these steps:

1. Enable the feature for your project

Content Source Maps is only available for Enterprise customers and is not enabled by default. Contact sales to turn on the feature for your project.

Contact sales

2. Install the enhanced Sanity client

To include metadata for Visual Editing in your query results, you need the enhanced Sanity client included with either next-sanity (for Next.js projects) or @sanity/preview-kit (framework-agnostic). Using the vanilla JavaScript client @sanity/client will not work!

If you are already importing createClient from either next-sanity or @sanity/preview-kit/client, skip to step 4. Otherwise, start by installing the appropriate npm package.

# for Next.js applications
npm i next-sanity

# framework-agnostic version
npm i @sanity/preview-kit

3. Change your import statements

The client included in next-sanity and @sanity/preview-kit is a drop-in replacement for the vanilla JavaScript client with some extra features. Therefore, after installing either of these clients, everything should still work the same when you change your import statements.

// Depending on which package you installed, replace this:
import {createClient} from '@sanity/client'

// ... with one of these:
import {createClient} from 'next-sanity'
import {createClient} from '@sanity/preview-kit/client'

4. Configure the client

Finally, add the following lines to your client configuration:

const client = createClient({
  projectId: '<projectId>',
  dataset: 'production',
  apiVersion: '2022-05-03',
  useCdn: true,
studioUrl: '/studio', // Or: 'https://my-cool-project.sanity.studio'
encodeSourceMap: true, // Optional. Default to: process.env.VERCEL_ENV === 'preview',
})

The studioUrl is necessary to allow Visual Editing to construct complete URLs to content in your Sanity Studio. It can be a relative path for embedded studios – e.g., “/studio” – or a fully qualified URL for studios hosted elsewhere.

The encodeSourceMap property lets you conditionally enable or disable the generation of content source maps that are needed for Visual Editing. It is recommended that you only enable it for non-production builds.

With these steps complete, your front end should now include the metadata required for Visual Editing.

Using Vercel Visual Editing? You're done!

If you have access to Vercel Visual Editing, deploy a preview build of your front-end and look for the Edit icon on the Vercel toolbar. You should see clickable elements highlighted to open and edit content in your Sanity Studio.

For any hosting other than Vercel, continue to step 5

Gotcha

The first time you view a preview deployment that has Visual Editing enabled, you will be prompted to add the preview URL to the list of allowed CORS origins for your project. This is necessary to enable the feature. Read more about CORS here.

5. Install @sanity/overlays

If you do not have access to Vercel Visual Editing, a similar experience is available with the @sanity/overlays package.

npm i @sanity/overlays

This package is vanilla JavaScript and can be configured in any framework. Most commonly, you might import and run the enable function in a React application like this:

import { enableVisualEditing } from '@sanity/overlays'
export default function App() {
useEffect(enableVisualEditing, [])
// ... return <html> }

Ensure the function only runs once and at the root level. You might consider applying additional logic so that it is also only bundled and executed in non-production environments.

You should now – even in local development – be able to see highlighted elements that can be clicked to open their document and field in Sanity Studio.

Additional client configuration options for handling Content Source Maps

Log encoded paths to the console

The enhanced Sanity client accepts an optional logger parameter. Pass it the global console, to show debug information about which fields are being encoded and which (if any) are skipped.

const client = createClient({
  projectId: '<projectId>',
  dataset: 'production',
  apiVersion: '2022-05-03',
  useCdn: true,
  studioUrl: '/studio',
logger: console,
})

This should provide a nicely formatted report in your console, whether you are looking at your browser’s dev tools or your hosting's logs.

[@sanity/preview-kit]: Creating source map enabled client
[@sanity/preview-kit]: Stega encoding source map into result
  [@sanity/preview-kit]: Paths encoded: 3, skipped: 17
  [@sanity/preview-kit]: Table of encoded paths
  ┌─────────┬──────────────────────────────────┬───────────────────────────┬────────┐
  │ (index) │                     path         │           value           │ length │
  ├─────────┼──────────────────────────────────┼───────────────────────────┼────────┤
  │    0    │ ["footer",0,"children",0,"text"] │ '"The future is alrea...' │   67   │
  │    1    │ ["footer",1,"children",0,"text"] │     'Robin Williams'      │   14   │
  │    2    │             ["title"]            │     'Visual Editing'      │   14   │
  └─────────┴──────────────────────────────────┴───────────────────────────┴────────┘
  [@sanity/preview-kit]: List of skipped paths [
    [ 'footer', number, '_key' ],
    [ 'footer', number, 'children', number, '_key' ],
    [ 'footer', number, 'children', number, '_type' ],
    [ 'footer', number, '_type' ],
    [ 'footer', number, 'style' ],
    [ '_type' ],
    [ 'slug', 'current' ],
  ]

Customizing which paths to encode

The encodeSourceMapAtPath callback can be used to make a custom selection of which paths to encode and which to skip.

You may wish to skip encoding on a path if its value is used in your front end, but it is not useful for an author to be able to edit it.

const client = createClient({
  // ...rest of config omitted for brevity
  encodeSourceMapAtPath: props => {
	  if(props.path[0] === 'externalUrl') {
      // absolute urls in <a href> can't contain source maps, or they'll render as broken relative urls
      return false
    }
    return props.filterDefault(props)
  }
})

By default, all returned values will include metadata encoding unless they:

  • have keys starting with an underscore (e.g., _id and _type)
  • can be evaluated as a URL or ISO Date
  • are not returned as a string (such as numbers, see Encoding metadata on number fields below)
  • contain the path ["slug", "current"] (as the metadata encoding will break URLs and static build processes).

If you want keys like these to be included, you can skip invoking props.filterDefault(props) in your return statement.

const client = createClient({
  // ...rest of config omitted for brevity
  
  // returning `true` in every case to include all keys
  encodeSourceMapAtPath: () => true
})

Include the Content Source Map in your query result

To have the underlying Content Source Map returned as part of your query result, set the filterResponse option of the fetch call to false.

You may choose to include the Source Map for your own custom logic.

// The snippet below works with the standard client
import {createClient} from '@sanity/client'
// As well as the preview kit one, regardless of whether encoding to stega is on or off
import {createClient} from '@sanity/preview-kit/client'
// And next-sanity
import {createClient} from 'next-sanity'

const client = createClient({
  // ...rest of config omitted for brevity
  apiVersion: '2022-05-03',
  resultSourceMap: true // Tells Content Lake to include content source maps in the response
})

// const result = await client.fetch(query, params)
const {result, resultSourceMap} = await client.fetch(
  query,
  params,
  {filterResponse: false} // This option returns the entire API response instead of selecting just `result`
)

doSomethingAwesome(resultSourceMap)

Learn more about working with Content Source Maps, including how to develop your own implementation of Visual Editing.

GraphQL

With the Sanity GraphQL API v2023-08-01 update, Visual Editing is now also available for GraphQL. Read about it in the GraphQL docs.

Solutions

Creating editable images

Images can become clickable links if the alt attribute contains a value with encoded metadata.

The image schema type can contain additional fields, for example, an altText string field:

defineField({
  name: 'picture',
  type: 'image',
  fields: [defineField({name: 'altText', type: 'string'})],
})

Then in your front end, ensure this field value is used in the alt attribute. If it contains metadata encoding, Visual Editing will make the image a clickable element.

<img src={urlFor(image)} alt={image?.altText} />
Images can be Editable links if they contain encoded metadata in the alt attribute

See the documentation for more options when presenting images.

Encoding metadata on number fields

Only values returned as strings can be encoded with metadata. Number values will not contain encoding by default.

To make number values editable, cast them to strings using the string() GROQ function. This should add encoding to these values.

*[_type == "property"]{
  name, 
  description, 
  "beds": string(beds),
  "bathrooms": string(bathrooms)
}

You may, however, need to update your front end's logic to evaluate these values as strings instead of numbers.

Troubleshooting

A number of the solutions below rely on the vercelStegaSplit function from the @vercel/stega npm package. This works for any hosting provider, not just Vercel, as they both consume the same metadata.

Install it with:

npm i @vercel/stega

Comparing field values fails

Your production front end likely evaluates values returned from the Content Lake to perform specific logic. If these values contain encoded metadata from Content Source Maps, likely, they will no longer work.

How to fix

For example, imagine a function that determines that a Sanity document's market value is the same as the current market:

function showDocument(document: SanityDocument, currentMarket: string) {
  return document.market === currentMarket
}

Without Content Source Maps, this function works as expected. However, if document.market contains encoded metadata, this comparison will fail.

If document.market is never shown on the page and will not benefit from Visual Editing, it may be best to remove it from the encoded paths in encodeSourceMapAtPath.

Alternatively, "clean" the value before comparing it. Since you'll likely do this more than once, consider extracting to a helper function.

import {vercelStegaSplit} from '@vercel/stega'

function clean(value: string) {
  return vercelStegaSplit(value).cleaned
}

function showDocument(document: SanityDocument, currentMarket: string) {
  return clean(document.market) === currentMarket
}

The styling of the editable fields is incorrect

If the text on the page is breaking out of its container – or its container is much wider than normal – it can be resolved by splitting the encoded text out from the original text.

Note: This is not due to the encoded characters themselves. This problem should only present itself if the element also uses negative letter-spacing in its CSS or is inside of a <ReactWrapBalancer>.

Then identify where the problematic element is rendered in code, for example:

function MyComponent({ text }) {
	return (
		<h1>{text}</h1>
	);
}

Rewrite using @vercel/stega to avoid any styling issues:

import { vercelStegaSplit } from '@vercel/stega';

function MyComponent({ text }) {
	const { cleaned, encoded } = vercelStegaSplit(text);

	return (
		<h1>
			{cleaned}
			<span style={{ display: 'none' }}>{encoded}</span>
		</h1>
	);
}

If you find yourself doing this more than once, you might like to extract this logic to a reusable component:

import {vercelStegaSplit} from '@vercel/stega'

export default function Clean({value}: {value: string}) {
  const {cleaned, encoded} = vercelStegaSplit(value)

  return encoded ? (
    <>
      {cleaned}
      <span style={{display: 'none'}}>{encoded}</span>
    </>
  ) : (
    cleaned
  )
}

function MyComponent({ text }) {
	return (
		<h1><Clean value={text} /></h1>
	);
}

Formatting dates throws an error

Sometimes, you can experience type errors when trying to format dates.

How to fix

Identify where the date is formatted in code, for example:

function formatDate(datestring) {
	const date = new Date(datestring);
	return date.nicelyFormatted();
}

Rewrite using @vercel/stega to avoid any styling issues:

import { vercelStegaSplit } from '@vercel/stega';

function formatDate(datestring) {
	const { cleaned, encoded } = vercelStegaSplit(datestring);
	const date = new Date(cleaned);
	return `${date}${encoded}`;
}

The wrong element is being highlighted

If the wrong element is highlighted when hovering them, it can be resolved by adding an attribute to the correct element.

How to fix

For example, if this component highlights the <h1> and you want it to highlight the entire <section> element:

<section>
	<h1>{dynamicTitle}</h1>
	<div>Hardcoded Tagline</div>
</section>

Add a data attribute to highlight the correct item:

  • For Visual Editing with @sanity/overlays, add data-sanity-edit-target
  • For Vercel Visual Editing, add data-vercel-edit-target
<section data-sanity-edit-target>
	<h1>{dynamicTitle}</h1>
	<div>Hardcoded Tagline</div>
</section>

Was this article helpful?