A New Perspective: Sanity Perspectives and Live Previews

July 7, 2023

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.

Screenshot of CMS authoring interface next to a live preview of the CMS content shown in a webpage

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!

Related Posts

Powering Our Website's Evolution: Next.js App Router and Sanity CMS in Action

June 15, 2023
What we've learned building web apps using Sanity and Next.js App Router that is great for content authors and great for end users while rearchitecting our website.
Carlos Kelly
Grant Sander

How we reduced image bandwidth by 86% migrating our media library to Cloudinary

February 8, 2023
Merely migrating to Cloudinary is saving us significant image bandwidth, which is great for our end-users and great for the world.

Third-party Packages in Sanity Studio V2

November 15, 2022
To get around our "modern language features" issue, we can tweak the Sanity Webpack configuration so that it uses babel to transpile the library files for our third-party libraries that are causing us problems.