How to generate massive amounts of demo content for Sanity
It can be useful for testing plugins, front ends, or other integrations to have a Sanity Studio populated with fake content.
Go to How to generate massive amounts of demo content for SanityLive-as-you-type previewing of draft content is the ultimate upgrade for your content creators so that they can see changes in real-time and gain increased confidence to hit that publish button.
Composable content management is a powerful tool. Content separated from its presentation is the right way to think about content modeling. However, confidence can suffer when the edited content lacks the context of how those edits will affect real-world outputs.
The final code for this tutorial is available as a repository.
This guide is for projects using the Pages router. Go to this guide for projects using Next.js‘ App router.
This guide is deliberately focused on the experience of manually creating a new Next.js 13 application and creating a Sanity project with an embedded Studio inside.
All the instructions below could also be adapted to an existing Next.js application.
app
directory? This guide will only cover the pages
directory approach. There is a similar tutorial using the app router.Create a new project using the command below. Default options such as TypeScript, Tailwind, and ESLint have been selected for you but could be removed if you have different preferences. Just know the code snippets in this guide may no longer be compatible.
# from the command line
npx create-next-app@latest nextjs-pages --typescript --tailwind --eslint --src-dir --import-alias="@/*"
> Use App Router?
No
# enter the new project's directory
cd nextjs-pages
# run the development server
npm run dev
Need more help? See the Next.js docs for getting started.
Visit http://localhost:3000 in your web browser, and you should see this landing screen to show it’s been installed correctly.
The default Next.js project home page comes with some code boilerplate. So that you can more easily see what’s Sanity and what’s Next.js – you will remove almost all of it.
First, update the home page route file to simplify it greatly:
// ./nextjs-pages/src/pages/index.tsx
export default function Home() {
return (
<main className="flex items-center justify-center min-h-screen">
Populate me with Sanity Content
</main>
)
}
Second, update the globals.css
file to just Tailwind utilities:
/* ./nextjs-pages/src/styles/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
Now our Next.js app at http://localhost:3000 should look much simpler:
It's possible to create a new – or connect an existing – Sanity project and configure a Studio inside a Next.js application!
Run the following command from inside the same ./nextjs-pages
directory you created for your Next.js application and follow the prompts:
# in ./nextjs-pages
npx sanity@latest init --env --create-project "Next.js Live Preview" --dataset production
> Would you like to add configuration files for a Sanity project in this Next.js folder?
Yes
> Do you want to use TypeScript?
Yes
> Would you like an embedded Sanity Studio?
Yes
> Would you like to use the Next.js app directory for routes?
No
> What route do you want to use for the Studio?
/studio
> Select project template to use
Blog (schema)
> Would you like to add the project ID and dataset to your .env file?
Yes
Now your Next.js application should contain some Sanity-specific files, including a .env
file with your Sanity project ID and dataset name
Check to see that this file exists with values from your new project:
# ./nextjs-pages/.env NEXT_PUBLIC_SANITY_PROJECT_ID="your-project-id" NEXT_PUBLIC_SANITY_DATASET="production"
Now visit http://localhost:3000/studio to see your new Sanity project's Studio.
Note: When deploying the site to your hosting, you must:
Once logged in, your Studio should look like this with a basic schema to create blog posts. Create and publish a few posts.
Before you set up Live Preview to see draft content from Sanity, you should confirm you can fetch published content.
Create a new component file to display an array of post
documents:
// ./nextjs-pages/src/components/Posts.tsx
import Link from "next/link";
import type { SanityDocument } from "@sanity/client";
import Head from "next/head";
export default function Posts({ posts = [] }: { posts: SanityDocument[] }) {
const title = posts.length === 1 ? `1 Post` : `${posts.length} Posts`
return (
<>
<Head>
<title>{title}</title>
</Head>
<main className="container mx-auto grid grid-cols-1 divide-y divide-blue-100">
<h1 className="text-2xl p-4 font-bold">{title}</h1>
{posts.map((post) => (
<Link key={post._id} href={post.slug.current} className="p-4 hover:bg-blue-50">
<h2>{post.title}</h2>
</Link>
))}
</main>
</>
);
}
Update the index route to include a getStaticProps
function. In it, you’ll import the Sanity Client to query for every published post
document that has a valid slug
.
// ./nextjs-pages/src/pages/index.tsx
import { groq } from "next-sanity";
import type { SanityDocument } from "@sanity/client";
import Posts from "@/components/Posts";
import { client } from "../../sanity/lib/client";
export const postsQuery = groq`*[_type == "post" && defined(slug.current)]{
_id, title, slug
}`;
export const getStaticProps = async () => {
const data = await client.fetch(postsQuery);
return { props: { data } };
};
export default function Home({ data }: { data: SanityDocument[] }) {
return <Posts posts={data} />
}
Your home page at http://localhost:3000 should now look like this:
If so, great … until you click on one of those links.
You can fix that 404 by adding another route.
Install @portabletext/react
to render the portable text field onto the page
npm i @portabletext/react@latest
Create a new component to display the post:
// ./nextjs-pages/src/components/Post.tsx
import Image from "next/image";
import Head from "next/head";
import imageUrlBuilder from "@sanity/image-url";
import { SanityDocument } from "@sanity/client";
import { PortableText } from "@portabletext/react";
import { client } from "../../sanity/lib/client";
const builder = imageUrlBuilder(client);
export default function Post({ post }: { post: SanityDocument }) {
return (
<>
<Head>
<title>{post.title}</title>
</Head>
<main className="container mx-auto prose prose-lg p-4">
<h1>{post.title}</h1>
{post?.mainImage ? <Image
className="float-left m-0 w-1/3 mr-4 rounded-lg"
src={builder.image(post.mainImage).width(300).height(300).url()}
width={300}
height={300}
alt={post?.mainImage?.alt}
/> : null}
{post?.body ? <PortableText value={post.body} /> : null}
</main>
</>
);
}
Then, create a new route to query for the post by its slug:
// ./nextjs-pages/src/pages/[slug].tsx
import { SanityDocument } from "@sanity/client";
import { GetStaticPaths, GetStaticProps } from "next";
import { groq } from "next-sanity";
import { client } from "../../sanity/lib/client";
import Post from "@/components/Post";
export const postQuery = groq`*[_type == "post" && slug.current == $slug][0]{
title,
mainImage,
body
}`;
// Prepare Next.js to know which routes already exist
export const getStaticPaths: GetStaticPaths = async () => {
const paths = await client.fetch(
groq`*[_type == "post" && defined(slug.current)][]{
"params": { "slug": slug.current }
}`
);
return { paths, fallback: true };
};
export const getStaticProps: GetStaticProps = async ({ params }) => {
const queryParams = { slug: params?.slug ?? `` };
const post = await client.fetch(postQuery, queryParams);
return {
props: {
data: { post },
},
};
};
export default function Page({ data }: { data: { post: SanityDocument } }) {
return <Post post={data.post} />
}
Update next.config.ts
so that next/image
will load images from the Sanity CDN:
// ./nextjs-pages/next.config.ts
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {domains: ['cdn.sanity.io']}
// ...other config settings
}
module.exports = nextConfig
Without doing this, Next.js will throw an error.
The Portable Text field in the Studio is being rendered into HTML by the <PortableText />
component.
Install the Tailwind CSS Typography package to quickly apply beautiful default styling:
npm install -D @tailwindcss/typography
Update your tailwind.config.js
file's plugins to include it:
// ./nextjs-pages/tailwind.config.js
module.exports = {
// ...other settings
plugins: [require('@tailwindcss/typography')],
}
This package styles the prose
class names in the <Post />
component.
You should now be able to click links on the home page and see pages just like these:
You should now have the following:
And that's great, but it gets even better!
Let’s see changes made in Sanity Studio on the Next.js website.
next-sanity has already been installed and is a collection of utilities, including @sanity/preview-kit, which powers live-as-you-type preview. To
Create two new API Routes to enter and exit draft mode
// ./nextjs-pages/src/pages/api/preview.ts
import type { NextApiRequest, NextApiResponse } from 'next'
export default function preview(req: NextApiRequest, res: NextApiResponse) {
res.setDraftMode({ enable: true })
res.writeHead(307, { Location: '/' })
res.end()
}
// ./nextjs-pages/src/pages/api/exit-preview.ts
import type { NextApiRequest, NextApiResponse } from 'next'
export default function exit(req: NextApiRequest, res: NextApiResponse) {
res.setDraftMode({ enable: true })
res.writeHead(307, { Location: '/' })
res.end()
}
Visit your Studio at http://localhost:3000/studio, and from the top right in your user icon, click Manage project.
Navigate to the API tab, and under Tokens, add a new token. Give it viewer
permissions and save.
Open your .env
file and add the token on a new line as SANITY_READ_TOKEN
# ./nextjs-pages/.env NEXT_PUBLIC_SANITY_PROJECT_ID="your-project-id" NEXT_PUBLIC_SANITY_DATASET="production" # 👇 add this line SANITY_READ_TOKEN="your-new-token"
It is your responsibility to secure this token, and beware that unencrypted access to it could allow a user to read any document from any dataset in your project. The way it is implemented in this guide should result in the token being encrypted in the browser of any user that views the site in preview mode.
Create a getClient
function so that fetches for content are performed with a differently configured Sanity client, depending on whether preview
mode is active or not.
This helps us fetch the initial preview content server-side.
// ./nextjs-pages/sanity/lib/getClient.ts
import { createClient } from "@sanity/client";
import type { SanityClient } from "@sanity/client";
import { apiVersion, dataset, projectId, useCdn } from "../env";
export function getClient(previewToken?: string): SanityClient {
const client = createClient({
projectId,
dataset,
apiVersion,
useCdn,
});
return previewToken
? client.withConfig({
token: previewToken,
useCdn: false,
ignoreBrowserTokenWarning: true,
perspective: 'previewDrafts'
})
: client;
}
Create a <PreviewProvider />
component so that any child component can benefit from preview mode in the browser.
// ./nextjs-pages/src/components/PreviewProvider.tsx
import { LiveQueryProvider } from "@sanity/preview-kit";
import { useMemo } from "react";
import { getClient } from "../../sanity/lib/getClient";
export default function PreviewProvider({
children,
previewToken,
}: {
children: React.ReactNode;
previewToken: string;
}) {
const client = useMemo(() => getClient(previewToken), [previewToken]);
return <LiveQueryProvider client={client}>{children}</LiveQueryProvider>;
}
Create a component that performs the live-as-you-type data fetching in the browser. In this instance, for the home page's list of posts.
It takes the initial server-side fetch of preview content as initial data and then replaces it with updated preview content as it receives changes.
// ./nextjs-pages/src/components/PreviewPosts.tsx
import type { SanityDocument } from "@sanity/client";
import { useLiveQuery } from '@sanity/preview-kit'
import { postsQuery } from "@/pages";
import Posts from "./Posts";
export default function PreviewPosts({ posts = [] }: { posts: SanityDocument[] }) {
const [data] = useLiveQuery(posts, postsQuery)
return <Posts posts={data} />;
}
Lastly, update your home page route to check for the existence of a preview token – and if found – fetch preview content on the server, and wrap the front end's components in the PreviewProvider.
// ./nextjs-pages/src/pages/index.tsx
import { groq } from "next-sanity";
import type { SanityDocument } from "@sanity/client";
import Posts from "@/components/Posts";
import { getClient } from "../../sanity/lib/getClient";
import dynamic from "next/dynamic";
import PreviewPosts from "@/components/PreviewPosts";
import { GetStaticProps } from "next";
const PreviewProvider = dynamic(
() => import("@/components/PreviewProvider")
);
export const postsQuery = groq`*[_type == "post" && defined(slug.current)]{
_id, title, slug
}`;
export const getStaticProps: GetStaticProps = async (context) => {
const preview = context.draftMode || false;
const previewToken = preview ? process.env.SANITY_READ_TOKEN : ``;
if (preview && !previewToken) {
throw new Error(`Preview mode is active, but SANITY_READ_TOKEN is not set in environment variables`);
}
const client = getClient(previewToken);
const data = await client.fetch(postsQuery);
return { props: { data, preview, previewToken } };
};
export default function Home({
data,
preview,
previewToken,
}: {
data: SanityDocument[];
preview: boolean;
previewToken?: string;
}) {
if (preview && previewToken) {
return (
<PreviewProvider previewToken={previewToken}>
<PreviewPosts posts={data} />
<div className="prose prose-blue p-8">
<a href="/api/exit-preview">
Exit preview
</a>
</div>
</PreviewProvider>
);
}
return <Posts posts={data} />;
}
All done! Let's unpack how all this works:
localhost:3000/api/preview
will be redirected to the home page, with the site put into “Preview Mode” with a viewer token saved to the user's browser.getStaticProps
to fetch static draft content at the time of the page visit.previewToken
is passed to the PreviewProvider
, which allows useLiveQuery()
to fetch live draft content as changes are streamed to the browser.Try it out! Visit your home page at http://localhost:3000, and not only will you see draft documents listed along with published documents – you should see any changes made in your Studio appear in your Next.js application simultaneously!
Neat!
Now add the same live preview capability to the individual post pages.
Create a component to wrap the Post component with a live query.
// ./nextjs-pages/src/components/PreviewPost.tsx
import { useRouter } from "next/router";
import type { SanityDocument } from "@sanity/client";
import { useLiveQuery } from "@sanity/preview-kit";
import { postQuery } from "@/pages/[slug]";
import Post from "./Post";
export default function PreviewPost({ post }: { post: SanityDocument }) {
const params = useRouter().query;
const [data] = useLiveQuery(post, postQuery, params);
return <Post post={data} />;
}
Update the [slug] route to perform a fetch for preview content when in preview mode and pass the token down to the browser:
// ./nextjs-pages/src/pages/[slug].tsx
import { SanityDocument } from "@sanity/client";
import { GetStaticPaths, GetStaticProps } from "next";
import { groq } from "next-sanity";
import { client } from "../../sanity/lib/client";
import Post from "@/components/Post";
import dynamic from "next/dynamic";
import PreviewPost from "@/components/PreviewPost";
import { getClient } from "../../sanity/lib/getClient";
const PreviewProvider = dynamic(() => import("@/components/PreviewProvider"));
export const postQuery = groq`*[_type == "post" && slug.current == $slug][0]{
title, mainImage, body
}`;
// Prepare Next.js to know which routes already exist
export const getStaticPaths: GetStaticPaths = async () => {
const paths = await client.fetch(
groq`*[_type == "post" && defined(slug.current)][]{
"params": { "slug": slug.current }
}`
);
return { paths, fallback: true };
};
export const getStaticProps: GetStaticProps = async (context) => {
const preview = context.draftMode || false;
const previewToken = preview ? process.env.SANITY_READ_TOKEN : ``;
const client = getClient(previewToken);
const data = await client.fetch(postQuery, context.params);
return { props: { data, preview, previewToken } };
};
export default function Page({
data,
preview,
previewToken,
}: {
data: SanityDocument;
preview: boolean;
previewToken?: string;
}) {
if (preview && previewToken) {
return (
<PreviewProvider previewToken={previewToken}>
<PreviewPost post={data} />
<div className="prose prose-lg px-4 prose-blue clear-both py-16 mx-auto">
<a href="/api/exit-preview">Exit preview</a>
</div>
</PreviewProvider>
);
}
return <Post post={data} />;
}
You should now be able to see draft content in the home and individual post pages – as well as changes as they happen, streamed to the browser!
This is great! But it could be even better.
Your setup works excellently so far, but putting these two browser windows side-by-side is more work than it needs to be. Using view panes in the Studio, you can embed the Next.js website into an iframe beside the document form editor.
Install the Iframe Pane plugin.
npm i sanity-plugin-iframe-pane
Create a new file to handle the "Default Document Node", which defines the default settings when the editor for a particular schema type is loaded.
// ./nextjs-pages/sanity/desk/defaultDocumentNode.ts
import {DefaultDocumentNodeResolver} from 'sanity/desk'
import Iframe from 'sanity-plugin-iframe-pane'
export const defaultDocumentNode: DefaultDocumentNodeResolver = (S, {schemaType}) => {
switch (schemaType) {
case `post`:
return S.document().views([
S.view.form(),
S.view
.component(Iframe)
.options({
url: `http://localhost:3000/api/preview`,
})
.title('Preview'),
])
default:
return S.document().views([S.view.form()])
}
}
Now import this into the deskTool()
plugin in your Studio config file:
// ./nextjs-pages/sanity.config.ts
// ...other imports
import { defaultDocumentNode } from './sanity/lib/defaultDocumentNode'
export default defineConfig({
// ...other config settings
plugins: [
deskTool({ defaultDocumentNode }),
// ...other plugins
],
})
Open up any Post-type document now, and you should be able to show the Next.js website side-by-side with the editor and see draft content in real-time.
Write more resilient components by checking existing values before trying to display them. Assume no data exists because the author is previewing a new document. Swap for placeholders or remove the components altogether. For example:
// In preview mode, this title might not yet exist!
<h1>{post.title}</h1>
// Swap with a placeholder
{post?.title ? <h1>{post.title}</h1> : <h1>Untitled</h1>}
// Or just remove the component
{post?.title ? <h1>{post.title}</h1> : null}
Update the options
for your Iframe Pane to a function that will render a different URL based on the current document, valid for if you update your Next.js preview route to redirect a URL other than the home page.
.options({
URL: (doc) => doc?.slug?.current
? `http://localhost:3000/api/preview?slug=${doc.slug.current}`
: `http://localhost:3000/api/preview`,
})
Sanity Composable Content Cloud is the headless CMS that gives you (and your team) a content backend to drive websites and applications with modern tooling. It offers a real-time editing environment for content creators that’s easy to configure but designed to be customized with JavaScript and React when needed. With the hosted document store, you query content freely and easily integrate with any framework or data source to distribute and enrich content.
Sanity scales from weekend projects to enterprise needs and is used by companies like Puma, AT&T, Burger King, Tata, and Figma.
It can be useful for testing plugins, front ends, or other integrations to have a Sanity Studio populated with fake content.
Go to How to generate massive amounts of demo content for SanityAdd live as-you-type preview to any presentational component
Go to Create a reusable live preview componentPreview draft content and changes as-you-type in Next.js from Sanity Studio
Go to Live Preview with Next.js App Router and Sanity StudioSummarise form progression by decorating the entire editing form for a document with a component loaded at the root level.
Go to Create a document form progress component