Loaders and Overlays
Make your front end Presentation-aware.
This guide article introduces you to the concepts of Loaders and Overlays and how to implement them in a front end. They are necessary parts to enable Visual Editing in Presentation for your content team.
Loaders provide a convenient, unified way of loading data from Content Lake: A single front end data fetching implementation across production, development and preview states, for both server and client-side rendering.
We built Loaders to make it easier to integrate full-fledged Visual Editing functionality in your front end with less maintenance and repeated code. Loaders handle the heavy lifting of implementing Visual Editing into your front end by:
- Supporting rendering Overlays in your application, using Content Source Maps (CSM) to map your content to the exact documents and fields it came from.
- Enabling seamless switching into preview mode, using Perspectives to display either published or draft content.
- Setting up near-instant, as-you-type updates in preview mode.
Crucially, loaders only run preview code paths in preview states, so your production site is kept fast and lean.
Loaders are available for several frameworks:
These framework-specific loaders are built on top of a foundational Core Loader that can be used in any JavaScript-based project.
Overlays can be added using a separate package that works in conjunction with Loaders. When Loaders are configured to return data with stega encoding (more on this below), the Overlays package creates interactive links from each individual piece of data on the page back to the Sanity Studio, highlighting the document and field it came from.
If using Vercel's visual editing, these overlays will appear automatically when the Vercel Toolbar is visible – and the page contains stega encoding – such as in preview builds.
You may still wish to configure Overlays, but only have them enabled when your front end is rendered inside an Iframe, so they work within Presentation.
Configuring Loaders and Overlays is slightly different depending on your front end. For complete templates, guides, and example code for Next.js and Remix, see Guides and examples on the Presentation page.
Those examples could be generalized by the following diagram, showing where on the server or client React Loader functions should be called, and under which circumstances should be imported.
The following is a brief overview of the functions contained in React Loader.
You can install the required packages with npm
, or your preferred package manager:
npm install @sanity/react-loader
npm install @sanity/client
npm install @sanity/overlays
The library exports three utilities:
loadQuery
: A loading function that returns a promise with query response and Content Source Map data. This is typically used to fetch content during server-side-rendering (SSR), which can then be used to initially hydrate thedata
returned by theuseQuery
hook.useQuery
: A React hook for loading and streaming content data on the client side. TheuseQuery
hook returns an object withdata
,error
, andloading
properties:data
contains any initial data passed into the hook – such as the loadQuery's server-side response – and whenliveMode
is active, and updates the client-side state with fresh data from the query results.error
contains any errors that may have occurred when executing the query.loading
indicates whether the query is still in progress.
useLiveMode
: a React hook that enables real-time updates for the queried data from the server to the client. It returnsliveMode
, a boolean value indicating whether the studio is in live preview mode. This is useful for scenarios where you want to adapt your UI based on whether the editor is in live preview. Any changes to the data in the Content Lake are reflected in the application without requiring a manual refresh. This enables applications to provide a seamless and responsive user experience with up-to-date data.
The useLiveMode
hook is typically only executed inside a component you'll create (often named VisualEditing
). Which should only be lazily imported into your application when preview mode is required.
The existence of this component signals to the rest of the application – and any useQuery
hooks – that live updates will be performed and that all content should be encoded with "Stega."
See the Visual Editing with Next.js App Router guide for an example VisualEditing
component.
You need to add one more piece to complete the puzzle: render elements that help users visually identify editable elements on a page so that they can click and edit them in the studio.
To make this happen, you need the Content Source Maps (CSM) to make it into your front end.
For simple strings, you can do this automatically with Stega. Stega is a method to encode metadata inside of text strings that we invented in collaboration with Vercel. It encodes the CSM steganographically into as invisible characters strings in your front end. Your visitors won't see these encodings, but the Overlays will see them and overlay the editing UI on hover.
Overlays render interactive visual indicators over your DOM elements. When they are clicked, we'll signal the Studio to display the right field, even deeply nested ones, we'll take the user right to it. Similarly, when a user clicks a field in a form - we'll scroll the DOM node into view if it exists on the page.
We recommend providing a HistoryAdapter
to keep the studio preview pane in sync with your application router or history state. HistoryAdapter
Implementations may differ depending on the framework you use. In any case, they must provide two methods:
subscribe
: receives anavigate
parameter, which is a function to execute when a newHistoryUpdate
is sent to the preview frame.update
: is called when the preview frame history changes. It receives aHistoryUpdate
parameter to push/pop/replace your application history.
While Stega works for simple string values, you might want to enable overlays for more complex elements (like Product Card or Images) or for the cases where Stega strings don't work.
Overlays also work for elements that have a data-sanity
attribute with the necessary information for where the field and studio are.
To generate this data object, you can use the encodeDataAttribute
from the useQuery
hook (in React front ends) and pass the field path for a given document type.
// Example from a Navigation component in a Next.js project
"use client";
import { type QueryResponseInitial } from "@sanity/react-loader/rsc";
import Link from "next/link";
import { NAV_QUERY } from "@/sanity/lib/queries";
import { resolveHref } from "@/sanity/lib/resolveHref";
import { useQuery } from "@/sanity/loader/useQuery";
import { NavigationData } from "@/types";
import { useCallback } from "react";
import { useEncodeDataAttribute } from "@sanity/react-loader";
import { STUDIO_ORIGIN } from "@/sanity/store";
type Props = {
initial: QueryResponseInitial<NavigationData>;
};
export function Navigation(props: Props) {
const { initial } = props;
const { data, sourceMap } = useQuery<NavigationData>(
NAV_QUERY,
{},
{ initial }
);
const encodeDataAttribute = useEncodeDataAttribute(
data,
sourceMap,
STUDIO_ORIGIN
);
return (
<nav>
{data.map((navItem) => {
const href = resolveHref(navItem._type, navItem.slug);
if (!href) {
return null;
}
return (
<Link
key={navItem._key}
href={href}
data-sanity={encodeDataAttribute?.([
"navigation",
navItem._key,
"slug",
])}
>
{navItem.title}
</Link>
);
})}
</nav>
);
}
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 install @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 {vercelStegaCleanAll} from "@sanity/client/stega"
function showDocument(document: SanityDocument, currentMarket: string) {
return vercelStegaCleanAll(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:
export function MyComponent({ text }: { text: string }) {
return <h1>{text}</h1>;
}
Rewrite using @vercel/stega
to avoid any styling issues:
import { vercelStegaSplit } from "@vercel/stega";
export function MyComponent({ text }: { text: string }) {
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
);
}
export function MyComponent({ text }: { text: string }) {
return (
<h1>
<Clean value={text} />
</h1>
);
}
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>