Sanity.io SSR previews without changing your front-end code
The simplest approach to previews I've found so far
Backstory: how I got to this method
Back in September 2018, I published a guide on how to do live previews to react websites with Sanity which relied on listening to data changes from the client-side.
This works okay, but requires you to change your components and front-end code. Previews should be table-stakes for editorial experience, but when projects are running late they often get de-prioritized. That's why I started using the method below.
The easiest way to provide web previews for Sanity.io content: fetching the draft version of documents if the requested URL includes a ?preview=true
query parameter.
This only works in server-side rendering (SSR) contexts. If you're using static site generators like GatsbyJS, Eleventy or Bridgetown, you'll need to approach previews differently. If your site is built with a reactive framework like React, Svelte, or Vue, my guide on client-side Sanity.io previews may help 😊
Here's an example with SvelteKit for a blog post route handler:
import { sanityClient } from '$lib/client'; // If requests include a ?preview=true query parameter, we're in preview mode export function isPreview(request) { return Boolean(request.url.searchParams?.get('preview')); } export const get = async (event) => { const { slug } = event.params; const data = await sanityClient.fetch(/* groq */`{ "post": *[ _type == "post" && slug.current == $slug // If not in preview, only show published documents ${isPreview(event) ? '' : '&& !(_id in path("drafts.**"))'} ] ${ isPreview(event) ? // In preview, get draft if existent - it'll have the freshest _updatedAt '| order(_updatedAt desc)' : '' } [0]{ ..., // your query here }, }`, { slug } ); if (!data.post?._id) { return { status: 404 } } return { status: 200, body: data, }; };
The three main parts of the code above:
- We define if we're in preview mode by checking the "preview" query parameter and converting it to a boolean
- If we're not in preview, exclude draft documents (
!(_id in path("drafts.**"))
in the query's filter) - If we are in preview, give preference to drafts
- We could query only drafts, but if the document was published and didn't have a draft, the page would return a 404.
- Instead, we order all documents that match the given
slug
by their_updatedAt
value. As drafts are the freshest, they'll always show up first. - However, if no draft is present, the published version will show up.
If the GROQ query above is unclear, my tiny course for the language may help you 😊
Accessing previews from the Sanity studio
To replicate the UI above, you can use Simeon Griggs' great sanity-plugin-iframe-pane and add it as a custom view to previewable documents.
If you don't have a custom structure builder, start by following the official docs on custom document views.
// /deskStructure.js import S from '@sanity/desk-tool/structure-builder'; import { EarthGlobeIcon, EditIcon } from '@sanity/icons'; import Iframe from 'sanity-plugin-iframe-pane'; // Replace with your own approach to getting the URL for each document function resolveProductionUrl(document) { return slugToAbsUrl( getDocumentPath({ _type: document._type, slug: document.slug?.current }) ); } // Example where only the homepage, case studies & posts are previewable const previewableTypes = ['home', 'caseStudy', 'post']; /** * Defines views/tabs for each content type. */ export const getDefaultDocumentNode = (props) => { if (!previewableTypes.includes(props.schemaType)) { return S.document().views(S.view.form()); } return S.document().views([ S.view.form().icon(EditIcon), S.view .component(Iframe) .options({ defaultSize: 'desktop', url: (doc) => resolveProductionUrl(doc) + '?preview=true', reload: { button: true } }) .icon(EarthGlobeIcon) .title('Preview') ]); };
Want to have live previews that change as users edit? In my experience, this is more of a wasteful distraction than a useful feature - I want to focus on writing most of the time and only check how it'll visually look every once in a while.
But, if that's your jam, you can pass the document's _rev
version id as a separate query parameter in the URL to constantly refresh the iFrame as users edit the content.
S.view .component(Iframe) .options({ defaultSize: 'desktop', url: (doc) => resolveProductionUrl(doc) + `?preview=true&_rev=${doc._rev}`, reload: { button: true } }) .icon(EarthGlobeIcon) .title('Preview')
---
And that's it! You'll need to change your GROQ queries slightly, but compared to changing components and creating dedicated preview routes, this method is much simpler than my previous iteration 😊