When using a headless CMS to drive website content, giving content authors “live previews” of their changes goes a long way in fostering authoring confidence – since content authors will have a chance to see what their changes will look like on the production website before it goes live.
Sanity, the self-described “Composable Content Cloud”, is a modern headless CMS platform that provides developers extreme flexibility in crafting content consumption and authoring workflows. In this post I wrote a little bit about live previews in a Sanity (and Next.js)-driven website, but forewent any technical details about the content queries we needed to write to make live previews happen.
In this short post I’ll walk through Perspectives – a hot-off-the-press feature from Sanity – and how they've drastically simplified our live preview workflows.
The “Before Times”
Our production website setup looks a little something like the following in terms of data fetching:
- For end-users, we want to serve Next.js-cached query results from Sanity’s Content Lake, and we want to never include draft documents in these results (we don’t want our marketing team crafting up some novel content and have that leaked to the world before we’re ready).
- In live preview mode, we want to never cache query results from the Content Lake and give priority to draft documents so that our content authors can view their draft changes as if they were live.
These two things seem like reasonable features of a content authoring workflow, but take a little bit of crafting to get right. By default, Sanity will serve draft documents alongside the published version of said documents – and it’s up to the developer to manually filter out draft documents as necessary.
For example, running the following GROQ query:
*[_type == "employee" && slug.current == "grant-sander"]{ _id, name }
might end up with a result that looks something like this:
[ { _id: "drafts.employee.grant-sander", name: "Grant Sander" }, { _id: "employee.grant-sander", name: "Grant Sander" } ]
To support live previews, you then need to add logic to try to grab a draft document (if one exists!) in live preview mode, and fallback to the published document otherwise.
The way we were handling this was adding GROQ logic with score
and order
method calls like the following:
const isDraftMode = true; // Dynamic value based on preview mode const query = ` *[_type == "employee" && slug.current == "grant-sander"] | score(_id in path('drafts.**')) | order(_score ${isDraftMode ? "desc" : "asc"}) [0]{_id, name } `; const result = executeQuery(query);
In other scenarios, we were adding filter conditions like !(_id in path("drafts.*"))
to our queries to ensure we weren’t returning draft documents in our query results.
The end result: supporting live previews was “polluting” nearly every single one of our queries with additional logic.
Gaining Perspective
Sanity’s new “perspectives” functionality has significantly reduced our “query pollution”. These so-called “perspectives” are effectively context filters that you can run your Content Lake/GROQ queries against – almost like additional query filters/logic that happens at the Content Lake level instead of the consuming-code level. Sanity will be adding more pre-baked perspectives to the Content Lake in the future, but their initial set of perspectives is built around the exact problem we’re looking at in this post: how to handle draft and published documents in a reasonable manner.
To make use of these new perspectives, we simply add a perspective: published
or perspective: previewDrafts
configuration flag to our query execution request as we see fit, and the Sanity backend handles the rest of the magic. The TL;DR on these two perspectives:
published
will ensure that no draft documents are returned from the query;previewDrafts
will "ignore" a published document if a draft version exists and always return the draft version, so that you can retrieve draft documents without having to worry about the published version getting in the way.
With these two perspectives, we can remove nearly all of our added GROQ query logic to handle previews.
Our example from above then simplifies down to something like the following:
const isDraftMode = true; // Dynamic value based on preview mode // 💡 Remove the score/order from our query const query = `*[_type == "employee" && slug.current == "grant-sander"][0]{_id, name }`; // 💡 Add the fetch-level, merely set the perspective accordingly! // (your fetch implementation may vary) const result = executeQuery( query, { perspective: isDraftMode ? "previewDrafts" : "published" } );
For “reasons”, we have a custom fetch
wrapper that takes in a isDraftMode
flag and will set the perspective query parameter accordingly, but the @sanity/client
library has options for setting the perspective
option at a per-client and per-query level!
At this point, our simple query to fetch some employee information:
*[_type == "employee" && slug.current == "grant-sander"]{ _id, name }
can remain simple. If we want the draft version to be prioritized for preview mode, we merely execute this query with perspective: previewDrafts
. Otherwise, we execute with perspective: published
. In either case, we should only return a single document, and don’t have to jump through additional hoops to massage it for our live preview use-case!