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.
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.
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.
To enable Visual Editing on Vercel or any other hosting, follow these steps:
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.
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
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'
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.
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.
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.
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' ], ]
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
})
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.
With the Sanity GraphQL API v2023-08-01 update, Visual Editing is now also available for GraphQL. Read about it in the GraphQL docs.
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} />
See the documentation for more options when presenting images.
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.
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
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.
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
}
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 negativeletter-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>
);
}
Sometimes, you can experience type errors when trying to format dates.
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}`;
}
If the wrong element is highlighted when hovering them, it can be resolved by adding an attribute to the correct element.
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
, adddata-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>