Creating a Parent/Child Taxonomy - has 5 likes
Create common taxonomy schemas like Categories and Tags with parent/child relationships
Go to Creating a Parent/Child TaxonomyEmpower your authoring teams with live updates to content within your Next.js website or application. Without leaving Sanity Studio. The next-sanity toolkit enables real-time previews without the need to setup a dedicated preview server.
In this guide we'll cover setting up a new Sanity project, Next.js application and streaming live updates to content. The next-sanity
toolkit contains all you'll need to query and display Sanity data. As well as handling your Studio authentication to allow live updates on the frontend.
A serverless function is required to put the app in "preview mode", but no additional services. Preview mode runs on the same hosting as your Production app.
The headline feature of next-sanity
is the usePreviewSubscription()
hook. It takes a few steps to get just right, so we'll walk through each one individually.
The final code you will assemble is available as a public repository on GitHub. Use it to start, or as a reference while you walk through the guide.
To follow this tutorial you will need:
Next.js comes with a Preview Mode which sets a browser cookie. When set, we can use this to activate the preview functionality in the usePreviewSubscription()
hook. So long as your browser is also authenticated (logged into your Sanity Studio) it will continually replace published content with the latest draft content and rerender the page you're looking at. It will even let you change referenced data and have that updated as well.
You'll set up a link in your Sanity Studio to a Next.js API Route, which will set the Preview Mode cookie and show the latest version of your content in your Next.js web app, as you edit it.
If you are not authenticated, the same route will show latest draft content on page load, but will not keep updating as changes are made unless they go through the same link again. This can be a useful link to share with content reviewers.
⏩ If you have an existing Sanity + Next.js studio, skip this step
You can use this guide to add Live Preview to an existing Next.js website, but we will focus on an entirely new project.
Let's start a new project, in a folder my-project
Create a new folder, in it create a studio
folder and run sanity init
inside to start a new Sanity project.
Back in the my-project
folder, run npx create-next-app
and give it the name web
. You should now have a folder structure like this:
/my-project - /studio - /web
In separate terminal windows you can now run:
sanity start
from the studio
folder, and view the studio at http://localhost:3333
npm run dev
from the web
folder, and view the frontend at http://localhost:3000
⏩ If you already have a document type with slugs, skip this step
If you've started from a completely blank dataset, we'll need content schemas to have something to view and edit.
Let's create the most basic document schema:
// ./studio/schemas/article.js
export default {
name: 'article',
title: 'Article',
type: 'document',
fields: [
{name: 'title', type: 'string'},
// Important! Document needs a slug for Next.js to query for.
{name: 'slug', type: 'slug', options: {source: 'title'}},
{name: 'content', type: 'text'},
],
}
And register it to the studio:
// ./studio/schemas/schema.js
import createSchema from 'part:@sanity/base/schema-creator'
import schemaTypes from 'all:part:@sanity/base/schema-type'
import article from './article'
export default createSchema({
name: 'default',
types: schemaTypes.concat([article]),
})
In the web
folder, install the next/sanity utilities...
npm install next-sanity
...and follow the guide in the Readme. Come back once you have these files setup:
./web/lib/ - config.js - sanity.js - sanity.server.js
And with the correct values in an .env.local
file for the Sanity Project ID, Dataset, and an API token. The file should look something like this, and you'll need to restart your dev environment to load them.
// ./web/.env.local
// Find these in sanity.json
NEXT_PUBLIC_SANITY_DATASET='production'
NEXT_PUBLIC_SANITY_PROJECT_ID='...'
// Create this in sanity.io/manage
SANITY_API_TOKEN='...'
// Blank for now, we'll get back to this!
SANITY_PREVIEW_SECRET=''
You'll need to create an API token in sanity.io/manage and allow CORS from your Next.js website.
SANITY_API_TOKEN
in .env.local
http://localhost:3000
with Credentials Allowed, so that your front-end can communicate with the Studio.my-project.com
).Ready?
Let's set up a Dynamic Route to display your content.
Note: In the code below, the normal order of functions you'd usually see in a Next.js page has been inverted, so that it reads more easily from top-to-bottom. It still works the same.
Read through the comments below to see exactly what each function is doing:
// ./web/pages/[slug].js
import React from 'react'
import {groq} from 'next-sanity'
import {usePreviewSubscription} from '../lib/sanity'
import {getClient} from '../lib/sanity.server'
/**
* Helper function to return the correct version of the document
* If we're in "preview mode" and have multiple documents, return the draft
*/
function filterDataToSingleItem(data, preview) {
if (!Array.isArray(data)) {
return data
}
if (data.length === 1) {
return data[0]
}
if (preview) {
return data.find((item) => item._id.startsWith(`drafts.`)) || data[0]
}
return data[0]
}
/**
* Makes Next.js aware of all the slugs it can expect at this route
*
* See how we've mapped over our found slugs to add a `/` character?
* Idea: Add these in Sanity and enforce them with validation rules :)
* https://www.simeongriggs.dev/nextjs-sanity-slug-patterns
*/
export async function getStaticPaths() {
const allSlugsQuery = groq`*[defined(slug.current)][].slug.current`
const pages = await getClient().fetch(allSlugsQuery)
return {
paths: pages.map((slug) => `/${slug}`),
fallback: true,
}
}
/**
* Fetch the data from Sanity based on the current slug
*
* Important: You _could_ query for just one document, like this:
* *[slug.current == $slug][0]
* But that won't return a draft document!
* And you get a better editing experience
* fetching draft/preview content server-side
*
* Also: Ignore the `preview = false` param!
* It's set by Next.js "Preview Mode"
* It does not need to be set or changed here
*/
export async function getStaticProps({params, preview = false}) {
const query = groq`*[_type == "article" && slug.current == $slug]`
const queryParams = {slug: params.slug}
const data = await getClient(preview).fetch(query, queryParams)
// Escape hatch, if our query failed to return data
if (!data) return {notFound: true}
// Helper function to reduce all returned documents down to just one
const page = filterDataToSingleItem(data, preview)
return {
props: {
// Pass down the "preview mode" boolean to the client-side
preview,
// Pass down the initial content, and our query
data: {page, query, queryParams}
}
}
}
/**
* The `usePreviewSubscription` takes care of updating
* the preview content on the client-side
*/
export default function Page({data, preview}) {
const {data: previewData} = usePreviewSubscription(data?.query, {
params: data?.queryParams ?? {},
// The hook will return this on first render
// This is why it's important to fetch *draft* content server-side!
initialData: data?.page,
// The passed-down preview context determines whether this function does anything
enabled: preview,
})
// Client-side uses the same query, so we may need to filter it down again
const page = filterDataToSingleItem(previewData, preview)
// Notice the optional?.chaining conditionals wrapping every piece of content?
// This is extremely important as you can't ever rely on a single field
// of data existing when Editors are creating new documents.
// It'll be completely blank when they start!
return (
<div style={{maxWidth: `20rem`, padding: `1rem`}}>
{page?.title && <h1>{page.title}</h1>}
{page?.content && <p>{page.content}</p>}
</div>
)
}
Phew! All going well you should now be able to view this page, or whatever slug you created...
http://localhost:3000/its-preview-time
And it should look like this...
Now, let's add Preview Context so you can watch edits happen live!
Next.js has a Preview mode which we can use to set a cookie, which will set the preview
mode to true
in the above Dynamic Route. Preview mode can be activated by entering the site through an API Route, which will then redirect the visitor to the correct page.
Create two files inside ./web/pages/api
// ./web/pages/api/preview.js
export default function preview(req, res) {
if (!req?.query?.secret) {
return res.status(401).json({message: 'No secret token'})
}
// Check the secret and next parameters
// This secret should only be known to this API route and the CMS
if (req.query.secret !== process.env.SANITY_PREVIEW_SECRET) {
return res.status(401).json({message: 'Invalid secret token'})
}
if (!req.query.slug) {
return res.status(401).json({message: 'No slug'})
}
// Enable Preview Mode by setting the cookies
res.setPreviewData({})
// Redirect to the path from the fetched post
// We don't redirect to req.query.slug as that might lead to open redirect vulnerabilities
res.writeHead(307, {Location: `/${req?.query?.slug}` ?? `/`})
return res.end()
}
// ./web/pages/api/exit-preview.js
export default function exit(req, res) {
res.clearPreviewData()
res.writeHead(307, {Location: req?.query?.slug ?? `/`})
}
You should now be able to visit the preview route at http://localhost:3000/api/preview
and be greeted with an error.
The Preview route requires two params:
secret
which can be any random string known only to your Studio and Next.jsslug
to redirect to once authenticated – that slug is then used to query the documentDocument doesn't have a slug? No problem! Use the document's _id
!
But, be aware that the usePreviewSubscription()
hook cannot query for drafts.
so update your query parameters for the published version of the _id
and you'll get draft content.
Install the Production Preview plugin and follow the docs to get set up. Your sanity.json
file should now have the following in parts
:
// ./studio/sanity.json
parts: [
// ...all other parts
{
"implements": "part:@sanity/production-preview/resolve-production-url",
"path": "./resolveProductionUrl.js"
},
You'll need to create a suitable resolveProductionUrl()
function for your specific use case, but to match the demo we've created. so far, this does the trick:
// ./studio/resolveProductionUrl.js
// Any random string, must match SANITY_PREVIEW_SECRET in the Next.js .env.local file
const previewSecret = 'j8heapkqy4rdz6kudrvsc7ywpvfhrv022abyx5zgmuwpc1xv'
// Replace `remoteUrl` with your deployed Next.js site
const remoteUrl = `https://your-nextjs-site.com`
const localUrl = `http://localhost:3000`
export default function resolveProductionUrl(doc) {
const baseUrl = window.location.hostname === 'localhost' ? localUrl : remoteUrl
const previewUrl = new URL(baseUrl)
previewUrl.pathname = `/api/preview`
previewUrl.searchParams.append(`secret`, previewSecret)
previewUrl.searchParams.append(`slug`, doc?.slug?.current ?? `/`)
return previewUrl.toString()
}
So now in the studio, you should have this option in the top right of your document, which will open in a new tab, redirect you to the Next.js you were viewing before.
But this time, Preview mode is enabled!
Try making changes in the Studio (without Publishing) and see if they're happening live on the Next.js front end.
Want to make it even more obvious? Drop this into your [slug].js
page.
import Link from 'next/link'
// ... everything else
{preview && <Link href="/api/exit-preview">Preview Mode Activated!</Link>}
This will give you a fast way to escape from Preview Mode, if it is enabled. Usually, you'd style this as a floating button on the page.
You could pass along a ?slug=
param on the link to make the API route redirect back to the page they were on.
Using the Structure Builder and Custom Document Views, you can even display the preview URL inside the Studio inside an <iframe>
.
Your sanity.json
file will need the following added into parts
:
// ./studio/sanity.json
parts: [
// ...all other parts
{
"name": "part:@sanity/desk-tool/structure",
"path": "./deskStructure.js"
},
And you'll need a deskStructure.js
file set up with some basic layout for article
type documents. As well as including a React component to show in the Pane.
There's a handy plugin available to use as a Component. But you could write your own if you prefer.
Notice how we're reusing resolveProductionUrl()
again here to take our document and generate a preview URL. This time instead of creating a link, this will load the URL right inside our view pane.
// ./studio/deskStructure.js
import S from '@sanity/desk-tool/structure-builder'
import Iframe from 'sanity-plugin-iframe-pane'
import resolveProductionUrl from './resolveProductionUrl'
export const getDefaultDocumentNode = () => {
return S.document().views([
S.view.form(),
S.view
.component(Iframe)
.options({
url: (doc) => resolveProductionUrl(doc),
})
.title('Preview'),
])
}
export default () =>
S.list()
.title('Content')
.items([S.documentTypeListItem('article')])
Where you go next is completely up to you!
Let us know if you found this tutorial useful, or if there's anything that can be improved. If you need help with Sanity and Next.js, do join our community and ask in either the #help or the #nextjs channels.
Create common taxonomy schemas like Categories and Tags with parent/child relationships
Go to Creating a Parent/Child TaxonomyCombine Sanity's blazing-fast CDN with Remix's cached at the edge pages.
Go to How to build a Remix website with Sanity.io and live previewSometimes the content you need to reference lives outside of Sanity
Go to Creating a custom input to display and save third party dataA thorough intro to using GROQ-projections in a webhook contest
Go to GROQ-Powered Webhooks – Intro to Projections