Tue Nov 08 2022
Implementing Sanity.io preview in a SvelteKit project
This article is a complete guide to implementing document preview in Sanity Studio targeting a SvelteKit site. We'll take a look at the setup for Sanity in brief, and we will focus mainly on the SvelteKit part, since there are plenty of resources for the Sanity config.
Before we get started, note that this approach will only work for server rendered SvelteKit sites. It will not work for statically generated sites.
The main idea is to have Sanity Studio point to an api endpoint in the SvelteKit site. This api endpoint will then setup preview for the site and redirect to the correct page, which will then display any available preview data. Preview data in a Sanity context is draft documents.
There are two main parts, the Sanity Studio preview setup and the Setup for the SvelteKit site. Let's get to it!
Sanity Studio preview setup
Here I'll assume that you have already a running Sanity Studio instance ready to go.
First, install the Production Preview plugin and follow the docs to get set up. Url: https://www.sanity.io/docs/preview-content-on-site.
Note that you might prefer to use an iframe plugin to handle preview instead, especially if you want to add preview for only some documents. See this link for more info: https://www.sanity.io/plugins/iframe-pane
The resolveProductionUrl.js|ts
file you created when installing the Preview Plugin will probably need some tweaking.
We will be passing information to the SelteKit site using searchParams/queryParams.
It's usually a good idea to pass a secret key (you can use https://randomkeygen.com to generate one) which will be used in the SvelteKit site to validate that the request is coming from Sanity Studio. This prevents outsiders from calling the preview api route in the SvelteKit site).
When it comes to which params to send to the SvelteKit site, that depends on the structure of you site. You'll need to pass enough info to be able to construct the paths on the SvelteKit site.
For this site, we have pages and blog posts. The pages have slugs that render on the root of the site (like /about). The blog posts are grouped by date and slug. So for this site the resolveProductionUrl.ts
will look something like this:
export default function resolveProductionUrl(doc: {
_type: string;
_id: string;
slug: { current: string };
publishedAt?: string;
}) {
const baseUrl = "url to the seveltkit site";
const previewUrl = new URL(baseUrl);
previewUrl.pathname = `/api/preview`;
// Add static query params
// Note: The «previewSecret» is an env var
previewUrl.searchParams.append(`secret`, previewSecret);
previewUrl.searchParams.append("_type", doc._type);
if (doc?.slug && doc.slug.current) {
// Append the slug as a query param if it exits
previewUrl.searchParams.append("slug", doc.slug.current);
}
if (doc.publishedAt) {
// Append the published date if it exists (for blog posts)
previewUrl.searchParams.append("publishedAt", doc.publishedAt);
}
return previewUrl.toString();
}
The resolveProductionUrl
will now return the correct url and and search params to our SvelteKit site. It will look something like this (for localhost): http://localhost:5173/api/preview?_type="page"&slug="aboutPage"&secret=my-secret
Moving on to the SvelteKit site where we'll use the params from this url eventually.
SvelteKit site preview setup
Again, we will assume that you have a SvelteKit project setup and working. We will also assume that you are using Typescript, but this is not required.
There are several parts to handling preview in the SvelteKit project.
- Writing groq queries that returns both drafts and published documents
- Handling previews in your Sanity data fetching methods
- Filtering documents based on whether preview is active
- Using the preview param in pages
- Creating an api route to handle the request from Sanity
- Setting up a hook to inform pages about the preview status
We'll start with the pages and work our way to handling the url we just created in the Sanity setup.
Setting up preview for your pages
[If you are using Typescript] First go into the `src/app.d.ts` and change `Locals` to the following:
interface Locals {
preview: boolean;
}
This will add proper typing when using locals in pages.
Speaking of pages, open one of the (server) pages where you need to implement preview. In my case I want to have preview enabled for the about page (src/routes/about/+page.server.ts). First we import locals
and extract the preview param from it. For now, locals.preview
will always be undefined. We'll fix that later with a hook.
Note that the steps taken for the about page in this example needs to be repeated for all pages that should support preview.
// src/routes/about/+page.server.ts
export const load: PageServerLoad = async ({ params, locals }) => {
// Extract the preview param from locals
const preview = locals.preview;
// Get data for the about page
const aboutPage = await getAboutPageData(preview);
return { aboutPage };
}
The getAboutPageData
function takes preview
as a parameter. This will let the function know whether to return a published page, or a draft (if it exists). This is how this function might look (please read the comments):
export async function getAboutPage(preview = false): Promise<Page> {
const query = `*[_type == "pages" && slug.current == "about"]`;
// Passing preview to the «getSanityClient» makes it
// not use a cdn if preview is true
// Note that the request returns an array of about pages!
const pages = await getSanityClient(preview).fetch(query);
// If preview is true, return a draft (if it exists)
// Otherwise return a published page
return filterDataToSingleItem(pages, preview);
}
// The filter function (filterDataToSingleItem.ts)
export function filterDataToSingleItem<T>(data: Array<T>, preview: boolean): T {
if (!Array.isArray(data)) {
return data;
}
if (data.length === 1) {
return data[0];
}
if (preview) {
return data.find((item) => (item as any)._id.startsWith(`drafts.`)) || data[0];
}
return data[0];
}
The api route and the hook/middleware
Back in the Sanity setup we implemented the resolveProductionUrl
function. This function returned a url which was something like http://localhost:5173/api/preview?_type="page"&slug="aboutPage"&secret=""...
.
Now we need to create the /api/preview
endpont in our SelteKit app and have it redirect to the correct page. Let's start by creating a new endpoint at src/routes/api/preview/+server.ts
.
// src/routes/api/preview/+server.ts
// Get the same secret we we generated in the Sanity config
// This should probably be a private env variable
import { SANITY_PREVIEW_SECRET } from "$env/static/private";
import type { RequestHandler } from "./$types";
import { redirect } from "@sveltejs/kit";
// this endpoint will only be called from within Sanity Studio
export const GET: RequestHandler = async ({ url, cookies }) => {
// Grab the secret key from the search param
// and validate that it's correct
const secretKey = url.searchParams.get("secret");
if (secretKey !== SANITY_PREVIEW_SECRET) {
return new Response("Invalid token", { status: 401 });
}
// Grab all the search params we need to figure out the
// correct path for our page
const docType = url.searchParams.get("_type");
const docSlug = url.searchParams.get("slug") || undefined;
// Set a preview cookie
// This is a secure, httpOnly cookie
// It can only be read on the server side
// You can optionally store additional preview data in the cookie.
// Here we simply set the cookie as a flag that preview is active.
cookies.set("preview", "true", { path: "/" });
// redirect to the correct page to preview
// Using hardcoded path «about» for this demo
// you would need some logic here to resolve the real path
// using the searchParams
const path = "/about"; // figureOutPathToPage(docType, docSlug);
// redirect to the correct path
throw redirect(307, path);
};
To summarize. This endpint take in all search parameters from Sanity, validates the key and builds a path to the correct url (in this case the about page). It also sets a cookie as a flag that preview is
The last step is the hook. If you haven't used server hooks in SvelteKit, take a look at the docs here: https://kit.svelte.dev/docs/hooks#server-hooks
Remember back when we defined the server route for the about page(src/routes/about/+page.server.ts)? We used the locals.preview
in that route? Now we are going to set the value for locals.preview
using a server hook.
// src/hooks.server.ts
import type { Handle } from "@sveltejs/kit";
/**
* This hook/middleware runs for each request
* @see https://kit.svelte.dev/docs/hooks#server-hooks
*/
export const handle: Handle = async ({ event, resolve }) => {
// If there is a preview cookie, then enable preview for all routes
event.locals.preview = event.cookies.get("preview") ? true : false;
const response = await resolve(event);
if (event.locals.preview) {
// We don't want caching when preview is active
response.headers.set("cache-control", "no-cache, no-store, must-revalidate");
response.headers.set("Pragma", "no-cache");
response.headers.set("Expires", "0");
}
return response;
};
That's it. Now preview will work for any pages you have configured.
Side note. Exiting preview.
If you open a page in preview mode in Sanity you will be "stuck" in preview mode until you close the browser. The preview cookie is true for the duration of the session. It might be a good idea to provide an escape hatch allowing any Sanity editors to exit preview mode without having to close the browser. A simple way of doing this is to provide a link to a route that removes the preview cookie. An example of such a route can be found below.
// src/routes/api/exit-preview/+server.ts
import type { RequestHandler } from "./$types";
import { redirect } from "@sveltejs/kit";
export const GET: RequestHandler = async ({ cookies }) => {
// Remove the preview cookie
cookies.delete("preview", { path: "/" });
// redirect to the home page
throw redirect(307, "/");
};