Real-time previews
- From the Studio, editors can open front-end previews.
- They're open by default in the Pages Browser, reflecting the open document currently being edited.
@tinloof/sanity-toolkit
handles the UI & configuration part- Previews' architecture is meant to be seamless so you don't need to think about it - it should just work, unless you fetch data from external systems
Users need to be logged into the studio and enable cookies on the website to listen to preview changes. Otherwise, we won't be able to connect to Sanity to stream dataset mutations
How it works
Open the preview route with document's information in query parameters
The getDocumentPreviewUrl
function in app/routing/urls.ts
generates the URL, which looks like so:
const previewUrl = `/preview?id=${doc._id}&_type=${doc._type}&locale=${doc.locale}`
Adapt queries to run in preview mode
routeLoader
will identify when we're in preview mode and adapt the query and parameters accordingly. If previewing is turned on:
- Skip Remix's response caching
- Use
previewClient
instead of the regular Sanity client- Don't use Sanity's CDN to get only fresh data from their API
- Use Sanity's
perspective: "previewDrafts"
(opens in a new tab) to get the draft version of all documents, when available
- Send the
query
andparams
made to Sanity to the front-end in the_sanityRequests
property of the response
On the client-side, listen to changes in Sanity & re-fetch route-specific data
- We wrap the entire app in a
PreviewDataProvider
(added in/app/routes/preview.tsx
) - After mounted,
PreviewDataProvider
will instantiate theapp/components/PreviewLoader.tsx
component, which calls theusePreview
hook usePreview
will use thequery
andparams
used in the first load to instantiate@sanity/preview-kit
'sliveQuery
, keeping Sanity data fresh- Every time the data changes, it's ran against the
/app/routing/previewNeedsRefresh.ts
function to determine if we need to re-fetch the data in the server. See how to handle data from external systems - If so, we'll use Remix's
useFetcher
to re-fetch the preview route loader's data with an added&preview_refresh=true
query parameter routeLoader
will be re-run, but this time withpreviewState = PreviewState.BROWSER_REFRESH
, whichrunRouteQuery
will use to skip downloading global datahelper.queries.ts#prepareRouteQuerypreviewState === PreviewState.BROWSER_REFRESH ? // skip global data on refreshes routeSpecificQuery : queryWithGlobalData
- After doing the regular route loading, the browser will get the fetcher response data and set the
PreviewDataContext
value
Propagate preview data to the whole site through React Context
Directly or indirectly, every component in the website reads data with the useSanityData
hook:
export default function PreviewRoute() {
const data = useSanityData()
const Template = resolveRouteTemplate(data)
if (!Template) return null
return <Template {...(data as any)} />
}
// and hooks and components can also use it directly
export const useLocale = () => {
// The data for every route is fetched from queries built with `prepareRouteQuery`,
// which injects `"locale": $requestLocale`. We can use this to fetch the current page's locale.
const data = useSanityData()
return data?.locale || config.defaultLocale.value
}
useSanityData
merges the data from Remix's loader and PreviewDataContext
, making sure every component has the latest preview data. If not in preview, previewData
is undefined, meaning we use the published data coming from the initial route load.
// Simplified version:
export function useSanityData() {
const loaderData = useLoaderData()
const previewData = useContext(PreviewDataContext)?.data
return {
...(loaderData || {}),
// Previews only fetch route-specific data, so we need to merge it with the loader's to include global data
...(previewData || {}),
}
}
Handling data from external systems in previews
If you're using data from external systems in your website, you'll need to handle it in previews. For example, if you're using a CMS like Sanity to manage your content, but you're also using an e-commerce platform like Shopify to manage your products, you'll need to handle the data coming from Shopify.
To avoid having to write custom logic for fetching these data sources from previews, we rely on re-fetching the routeLoader
when this data needs to be refreshed. To determine when this need to happen, modify the app/routing/previewNeedsRefresh.ts
function. Here's the example of the Shopify template:
/**
* Wether or not a given preview route needs to re-fetch data from the server or if the
* sanity-only content from @sanity/preview-kit is enough.
*
* Re-fetching data from the server is useful only when loading data from external sources other than
* Sanity.
*/
export default function previewNeedsRefresh(
prevData: PreviewRouteData,
newData: PreviewRouteData,
): boolean {
const prevNodes = new Set(
extractShopifyDataToFetch(prevData).map((node) => node.shopifyGid),
)
const newNodes = new Set(
extractShopifyDataToFetch(newData).map((node) => node.shopifyGid),
)
// If there are new nodes that we don't have in the previous data, we need to re-fetch
if (prevNodes.size !== newNodes.size || !isSubset(newNodes, prevNodes)) {
return true // needs refresh
}
return false
}
If all your data is coming from Sanity, previewNeedsRefresh
should always
return false
. The in-browser querying through @sanity/preview-kit
is
enough to get all the data needed.