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 SanityGive your authors the ultimate content creation experience with live updates of draft content to preview content in context with absolute confidence to publish.
The final code for this tutorial is available as a repository.
This guide is for projects using the App router. Go to this guide for projects using Next.js‘ Pages 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.
All the instructions below could also be adapted to an existing Next.js application.
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-app --typescript --tailwind --eslint --app --no-src-dir --import-alias="@/*"
# enter the new project's directory
cd nextjs-app
# 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-app/app/page.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-app/app/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-app
directory you created for your Next.js application and follow the prompts:
# in ./nextjs-app
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?
Yes
> 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-app/.env.local NEXT_PUBLIC_SANITY_PROJECT_ID="your-project-id" NEXT_PUBLIC_SANITY_DATASET="production"
If this file is named .env
, rename to .env.local
and ensure it does not get checked into git.
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-app/app/_components/Posts.tsx
import Link from "next/link";
import type { SanityDocument } from "@sanity/client";
export default function Posts({ posts = [] }: { posts: SanityDocument[] }) {
const title = posts.length === 1 ? `1 Post` : `${posts.length} Posts`;
return (
<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>
);
}
Creating the new Sanity project inside our Next.js application created a Sanity Client inside /nextjs-app/sanity/lib/client.ts
However, to fully benefit from advanced features like draft mode, revalidation and caching we'll need to create a new file that integrates all these features.
Install these additional packages:
npm install suspend-react server-only
Create a new file for this sanityFetch
function. For more information, see the next-sanity readme.
import "server-only";
import type { QueryParams } from "@sanity/client";
import { draftMode } from "next/headers";
import { client } from "@/sanity/lib/client";
const DEFAULT_PARAMS = {} as QueryParams;
const DEFAULT_TAGS = [] as string[];
export const token = process.env.SANITY_API_READ_TOKEN;
export async function sanityFetch<QueryResponse>({
query,
params = DEFAULT_PARAMS,
tags = DEFAULT_TAGS,
}: {
query: string;
params?: QueryParams;
tags?: string[];
}): Promise<QueryResponse> {
const isDraftMode = draftMode().isEnabled;
if (isDraftMode && !token) {
throw new Error(
"The `SANITY_API_READ_TOKEN` environment variable is required."
);
}
const isDevelopment = process.env.NODE_ENV === "development";
return client
.withConfig({ useCdn: true })
.fetch<QueryResponse>(query, params, {
cache: isDevelopment || isDraftMode ? undefined : "force-cache",
...(isDraftMode && {
token: token,
perspective: "previewDrafts",
}),
next: {
...(isDraftMode && { revalidate: 30 }),
tags,
},
});
}
Create a new file to hold all our GROQ queries. If you want to retrieve more fields from each post document, update the queries in this file.
// ./nextjs-app/sanity/lib/queries.ts
import { groq } from "next-sanity";
// Get all posts
export const postsQuery = groq`*[_type == "post" && defined(slug.current)]{
_id, title, slug
}`;
// Get a single post by its slug
export const postQuery = groq`*[_type == "post" && slug.current == $slug][0]{
title, mainImage, body
}`;
// Get all post slugs
export const postPathsQuery = groq`*[_type == "post" && defined(slug.current)][]{
"params": { "slug": slug.current }
}`;
Update the index route to query for every published post
document that has a valid slug
.
// ./nextjs-app/app/page.tsx
import { SanityDocument } from "next-sanity";
import Posts from "@/app/_components/Posts";
import { postsQuery } from "@/sanity/lib/queries";
import { sanityFetch } from "@/sanity/lib/sanityFetch";
export default async function Home() {
const posts = await sanityFetch<SanityDocument[]>({ query: postsQuery });
return <Posts posts={posts} />;
}
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-app/app/_components/Post.tsx
"use client";
import Image from "next/image";
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 (
<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>
);
}
Create a new route to query for the post by its slug:
// ./nextjs-app/app/[slug]/page.tsx
import { SanityDocument } from "@sanity/client";
import Post from "@/app/_components/Post";
import { postPathsQuery, postQuery } from "@/sanity/lib/queries";
import { sanityFetch } from "@/sanity/lib/sanityFetch";
import { client } from "@/sanity/lib/client";
// Prepare Next.js to know which routes already exist
export async function generateStaticParams() {
// Important, use the plain Sanity Client here
const posts = await client.fetch(postPathsQuery);
return posts;
}
export default async function Page({ params }: { params: any }) {
const post = await sanityFetch<SanityDocument>({ query: postQuery, params });
return <Post post={post} />;
}
Update next.config.ts
so that next/image
will load images from the Sanity CDN:
// ./nextjs-app/next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: '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-app/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-app/app/api/preview/route.ts
import { draftMode } from "next/headers";
import { redirect } from "next/navigation";
export async function GET() {
draftMode().enable();
redirect(`/`);
}
// ./nextjs-app/app/api/exit-preview/route.ts
import { draftMode } from "next/headers";
import { redirect } from "next/navigation";
export async function GET() {
draftMode().disable();
redirect(`/`);
}
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_API_READ_TOKEN
# ./nextjs-app/.env NEXT_PUBLIC_SANITY_PROJECT_ID="your-project-id" NEXT_PUBLIC_SANITY_DATASET="production" # 👇 add this line SANITY_API_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 <PreviewProvider />
component so that any child component can benefit from preview mode in the browser.
// ./nextjs-app/app/_components/PreviewProvider.tsx
"use client";
import dynamic from "next/dynamic";
import { suspend } from "suspend-react";
const LiveQueryProvider = dynamic(() => import("next-sanity/preview"));
// suspend-react cache is global, so we use a unique key to avoid collisions
const UniqueKey = Symbol("../../sanity/lib/client");
export default function PreviewProvider({
children,
token,
}: {
children: React.ReactNode;
token: string;
}) {
const { client } = suspend(
() => import("../../sanity/lib/client"),
[UniqueKey]
);
if (!token) {
throw new TypeError("Missing token");
}
return (
<LiveQueryProvider
client={client}
token={token}
// Uncomment below to see debug reports
// logger={console}
>
{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-app/app/_components/PreviewPosts.tsx
"use client";
import type { SanityDocument } from "@sanity/client";
import { useLiveQuery } from "@sanity/preview-kit";
import Posts from "@/app/_components/Posts";
import { postsQuery } from "@/sanity/lib/queries";
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-app/app/page.tsx
import { SanityDocument } from "next-sanity";
import Posts from "@/app/_components/Posts";
import { postsQuery } from "@/sanity/lib/queries";
import { sanityFetch, token } from "@/sanity/lib/sanityFetch";
import { draftMode } from "next/headers";
import PreviewPosts from "@/app/_components/PreviewPosts";
import PreviewProvider from "@/app/_components/PreviewProvider";
export default async function Home() {
const posts = await sanityFetch<SanityDocument[]>({ query: postsQuery });
const isDraftMode = draftMode().isEnabled;
if (isDraftMode && token) {
return (
<PreviewProvider token={token}>
<PreviewPosts posts={posts} />
</PreviewProvider>
);
}
return <Posts posts={posts} />;
}
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 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-app/app/components/PreviewPost.tsx
"use client";
import { useParams } from 'next/navigation'
import type { SanityDocument } from "@sanity/client";
import { useLiveQuery } from "@sanity/preview-kit";
import { postQuery } from '@/sanity/lib/queries';
import Post from "@/app/_components/Post";
export default function PreviewPost({ post }: { post: SanityDocument }) {
const params = useParams();
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-app/app/[slug]/page.tsx
import { SanityDocument } from "@sanity/client";
import { draftMode } from "next/headers";
import Post from "@/app/_components/Post";
import { postPathsQuery, postQuery } from "@/sanity/lib/queries";
import { sanityFetch, token } from "@/sanity/lib/sanityFetch";
import { client } from "@/sanity/lib/client";
import PreviewProvider from "@/app/_components/PreviewProvider";
import PreviewPost from "@/app/_components/PreviewPost";
// Prepare Next.js to know which routes already exist
export async function generateStaticParams() {
// Important, use the plain Sanity Client here
const posts = await client.fetch(postPathsQuery);
return posts;
}
export default async function Page({ params }: { params: any }) {
const post = await sanityFetch<SanityDocument>({ query: postQuery, params });
const isDraftMode = draftMode().isEnabled;
if (isDraftMode && token) {
return (
<PreviewProvider token={token}>
<PreviewPost post={post} />
</PreviewProvider>
);
}
return <Post post={post} />;
}
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-app/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-app/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 componentSummarise 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 componentObject types use a preview property to display contextual information about an item when they are inside of an array; customizing the preview component can make them even more useful for content creators.
Go to Create richer array item previews