Visual Editing with Next.js App Router and Sanity Studio
Setup interactive live preview with Presentation in a Next.js app router application
Go to Visual Editing with Next.js App Router and Sanity StudioUse the official Sanity Astro integration to build a blog
In this guide, we will dive deeper into what you will need to know in order to make a blog with Astro and Sanity. You will learn how to:
The guide strives to make all the code blocks workable, so if you prefer to try the code first and read after, you should be able to copy-paste the code examples into the right files.
This guide will not add styling to the markup, we will leave that up to you. That said, it‘s often easier to develop the design when the basic markup and content are in place.
This guide does not assume that you know Sanity or Astro. However, it will not go in-depth into Astro concepts (we recommend exploring the rest of the documentation for this). This guide uses light TypeScript. If you don't use TypeScript, you should be able to delete the extra syntax without that much extra effort.
Before taking on the guide, make sure that you have Node.js 18 and npm 9 (or another package manager) or a version above installed.
Run the following in your shell (like Terminal, iTerm, PowerShell):
npm create astro@latest
Follow the instructions. You will be asked to choose a template, where blog
and empty
is two of the options. We recommend that you select empty
to minimize room for error.
Start by installing the official Sanity integration for Astro:
npx astro add @sanity/astro @astrojs/react
The command should add the Sanity and React configuration to your astro.config.mjs
file. This is where you will tell Astro what your Sanity project ID is, as well as the name of your dataset (most likely production
).
The @astrojs/react
dependency is needed to embed the Studio on a route.
In this guide, you are also integrating block content and images from the Sanity CDN using these tools:
npm install astro-portabletext @sanity/image
If you plan to follow along and use TypeScript, then you might want types coming from these libraries:
npm install @portabletext/types @sanity/types
Run the following command to initialize a Sanity project and store the projectId
and dataset
variables in an .env
file. That's the only thing you need to query published content in a dataset that‘s not private.
npx sanity@latest init --env
Follow the instructions from the CLI, and don't worry about messing up, with Sanity, you can make as many projects as you want. You can always go to sanity.io/manage to find information about your projects.
For simplicity, you can copy-paste the projectId
and the dataset
values from the .env
and into the Astro configuration file. You don't need to treat these as secrets. Go here for instructions if you wish to use the .env file
for the configuration.
// astro.config.mjs
import { defineConfig } from "astro/config";
import sanity from "@sanity/astro";
// https://astro.build/config
export default defineConfig({
integrations: [sanity({
projectId: '<your-project-id>',
dataset: '<dataset-name>',
useCdn: false, // See note on using the CDN
apiVersion: "2023-03-20", // insert the current date to access the latest version of the API
})]
});
CDN or not?
Sanity lets you query content through a global CDN. If you plan to keep the site static and set up webhooks that trigger rebuilds when updates are published, then you probably want useCdn
to be false
to make sure you don't hit stale content when the site builds.
If you plan to use Server Side Rendering, then you probably want to set useCdn
to true
for performance and cost. You can also override this setting if you run the site in hybrid, for example:
useSanityClient.config({useCdn: false}).fetch(*[_type == "liveBlog"])
.
Sanity Studio is where you can edit and manage your content. It‘s a Single Page Application that is easy to configure and that can be customized in a lot of ways. It‘s up to you to keep the Studio in a separate repository, in a separate folder (as a monorepo), or embed it into your Astro website.
For the sake of simplicity, this guide will show you how to embed the Studio on a dedicated route (remember /wp-admin
?).
To add a Studio on yoursite.com/admin
, you can set the configuration to the following:
// astro.config.mjs
import { defineConfig } from "astro/config";
import sanity from "@sanity/astro";
import react from "@astrojs/react";
// https://astro.build/config
export default defineConfig({
integrations: [sanity({
projectId: '<your-project-id>',
dataset: '<dataset-name>',
useCdn: false, // See note on using the CDN
apiVersion: "2023-03-20", // insert the current date to access the latest version of the API
studioBasePath: '/admin' // If you want to access the Studio on a route
}), react()]
});
You must also add a configuration file for Sanity Studio in the project root. Make a new file called sanity.config.ts
and add the following, replacing the projectId
and dataset
with the values in the .env
file:
// ./sanity.config.ts
import { defineConfig } from "sanity";
import { deskTool } from "sanity/desk";
export default defineConfig({
name: "project-name", // Can be whatever
title: "Project Name", // Can be whatever
projectId: '<your-project-id>',
dataset: '<dataset-name>',
plugins: [deskTool()],
schema: {
types: [],
},
});
If you start the Astro local development server, you should be able to visit the Studio at http://localhost:4321/admin. The first time you load this URL, you will be asked to add the URL to your project's CORS Origins. This is to enable authenticated requests from the browser to the Sanity APIs. Follow the instructions and reload the Studio route once you have added the setting.
Your project folder should now look like this:
~/Sites/astro-guide-blog ├── README.md ├── astro.config.mjs ├── package-lock.json ├── package.json ├── public │ └── favicon.svg ├── sanity.config.ts ├── src │ ├── env.d.ts │ └── pages │ └── index.astro └── tsconfig.json
Sanity is different from most headless CMSes. Content Lake, where your content is stored, is a schema-less backend that lets you store any JSON document and makes it instantly queryable with GROQ. Sanity Studio is a decoupled application that enables you to define a schema using simple JavaScript objects. The Studio uses the schema to build an editor interface where you can collaborate on content in real-time.
You can continue developing with the Astro development server running. You should see your changes reflected instantly without having to reload the browser.
When the installation is done, you run npm run dev
inside the studio folder. This launches the Studio on a local development server so you can open it in your browser and edit your content. Once you hit publish, this content will be instantly synced to the Content Lake and is available through the public APIs.
By running it, you'll upload the studio and make it available on the web for those with access (you can add users by navigating to sanity.io/manage).
You can go ahead and make your dataset private, but if you do, you will need to mint a token on sanity.io/manage and add it to the client configuration below.
There's a lot you can do with the schemas now stored in your studio folder (in the schemas
folder), but that's for another tutorial. For now, we just want our blog up and running!
npm create astro@latest
Return to the Studio, which will be available on localhost:3333
as long as you have npm run dev
going in your studio
folder.
In your Studio, create a post titled “Hello world”. At the slug field, press “Generate” to make a slug. Then press “Publish”–this makes the content publicly available via the API.
With the content created, the next step is to return to your Astro site and set it up to display your content.
When you use the minimal template, your Astro site has only one route: an index page. To surface posts on our site, you'll want to create routes for each post. They exist as files on the file system and are picked up by Astro as routes.
You could manually create routes for each post, but your posts are dynamic: when everything is up and running, you probably want to publish new content without pushing code. Astro, like most web frameworks, offers dynamic routing to make your life easier: you can create one route to catch them all using parameters.
In your blog's schema, every post has a slug, the unique bit of the URL (eg “hello-world” for your “Hello world” post. To use a slug parameter in the route, you must wrap it in the filename brackets. So, if you want our posts to be on /post/name
, you need to create a folder called post
, which contains a file named [slug].astro
.
In that file, you need to export a function called getStaticPaths
that returns an array of objects. In our case, at the minimum, each object needs to include slug
in its params
. To get started, use this as the contents of your [slug].astro
file:
// my-blog/frontend/src/pages/post/[slug].astro
---
export function getStaticPaths() {
return [
{params: {slug: 'hello-world'}},
{params: {slug: 'my-favorite-things'}},
{params: {slug: 'summertime'}},
];
}
const { slug } = Astro.params;
---
<h1>A post about {slug}</h1>
This code sets up the data in this route within the code fences. When returning the data like this, it is available in the Astro.params
variable. This is a bit of magic the framework does for you. Now you can use the slug
in your template. In this case, it results in a heading that contains whatever the slug is.
With the example above, Astro will generate three files, for 'hello-world', 'my-favorite-things', and 'summertime' in the production build, with a heading that includes the slug. You can now browse to these on your local server. For instance, localhost:3000/post/summertime
will display the heading “A post about summertime”.
Of course, you want to display anything but the slugs, and you don't want to hardcode the content in this file. Let's get your data from Sanity and dynamically populate your post routes with your content.
Head back to your [slug].astro
file, import the Sanity client, and use it to fetch your posts, like this:
---
import { sanityClient } from "sanity:client";
export async function getStaticPaths() {
const posts = await sanityClient.fetch(`*[_type == "post"]`);
return posts.map(({ slug, title }) => {
return {
params: { slug: slug.current },
props: { title },
};
});
}
const { slug } = Astro.params;
const { title } = Astro.props;
---
<h1>{title}</h1>
Within the code fences, we export that same getStaticPaths
function as above, but we've made it automatic so that we can wait for the data before returning the posts. With the client, we fetch the posts using its fetch method (note: this is Sanity's fetch, not the Fetch API).
The argument we're passing into this fetch function, if you've not seen this syntax before, is a GROQ query.
The GROQ syntax in this tutorial can be read like this:
*
👈 select all documents[_type == 'post' && slug.current == $slug]
👈 filter the selection down to documents with the type "post" and those of them who have the same slug to that we have in the parameters[0]
👈 select the first and only one in that listSo that's it! You should now be able to see the title of any of your posts under /post/[the-posts-slug]
.
Now that you've seen how to display the title, continue to add the other bits of our posts: block content and author information.
With Sanity, your blog posts is part of your content model. They can be set up to be whatever you want, but we've used the blog schema that Sanity CLI comes with in this case. Your post's title is a string, the published date is saved as a datetime and so on. Sanity has tooling for images and block content, so we'll add those first.
When you use the image
field type to allow users to upload images in your Studio, the images are uploaded to Sanity's CDN (the Asset Pipeline). It's set up so you can request them however you need them: in specific dimensions, image formats, or crops, just to name a few image transformations. The way this works is that the image is represented as an ID in your data structure. You can then use this ID to construct image URLs.
Use the image URL builder from the @sanity/image-url package for this. First install the dependency:
npm i @sanity/image-url
Then, create a new folder called sanity
with a file called urlForImage.js
:
// /my-blog/frontend/src/sanity/urlForImage.js
import { sanityClient } from 'sanity:client';
import imageUrlBuilder from "@sanity/image-url";
export const imageBuilder = imageUrlBuilder(sanityClient);
export function urlForImage(source) {
return imageBuilder.image(source);
}
The blog template saves your blog content in a array
field of the block
type. This will give you block content with rich text, which Sanity saves in a structured format called Portable Text. From Portable Text, you can generate Markdown, HTML, PDFs, or whatever else you want. It's very flexible. For this tutorial, you'll convert your Portable Text content to Astro components with the astro-portabletext
library:
npm i astro-portabletext
Then, for convenience, create an Astro component to render our Portable Text for us. Create a components
folder alongside sanity
and pages
folders. Inside of that, create a new file called PortableText.astro
:
// /my-blog/frontend/src/components/PortableText.astro
---
import { PortableText as PortableTextInternal } from 'astro-portabletext'
const { portableText } = Astro.props;
---
<PortableTextInternal value={portableText} />
This will render our Portable Text blocks, but we have not yet added a component to handle any custom blocks we added to the Portable Text field, like image
.
In the same components
folder, create a file called PortableTextImage.astro
:
// /my-blog/frontend/src/components/PortableTextImage.js
---
import { urlForImage } from '../sanity/urlForImage';
const {asset, alt} = Astro.props.node
const url = urlForImage(asset).url()
const webpUrl = urlForImage(asset).format('webp').url()
---
<picture>
<source
srcset={webpUrl}
type="image/webp"
/>
<img
class="responsive__img"
src={url}
alt={alt}
/>
</picture>
This component will pass the relevant node from the Portable Text content, and we use our urlForImage
function to calculate the asset URLs to display. Now, you can register this component to be rendered when PortableText encounters an image
block:
// /my-blog/frontend/src/components/PortableText.astro
---
import { PortableText as PortableTextInternal } from 'astro-portabletext'
import PortableTextImage from "./PortableTextImage.astro";
const { portableText } = Astro.props;
const components = {
type: {
image: PortableTextImage,
}
};
---
<PortableTextInternal value={portableText} components={components} />
Now that you've added our tooling for images and block content, your folder structure should be something like this:
~/Sites/my-blog
├── frontend
├── src
├── components
├── pages
├── sanity
├── studio
Now, return to the component that renders your post page, [slug].astro
, and update it to use the PortableText
component to render the post content:
// /my-blog/frontend/src/pages/post/[slug].astro
---
import { useSanityClient } from '@sanity/astro';
import PortableText from '../../components/PortableText.astro';
export async function getStaticPaths() {
const posts = await useSanityClient().fetch(`*[_type == "post"]`);
return posts.map((post) => {
return {
params: {
slug: post.slug?.current || '',
},
props: { ...post },
};
});
}
const { title, body } = Astro.props;
---
<h1>{title}</h1>
<PortableText portableText={body} />
This uses the PortableText
component we just added renders any content you've added, including links, images, and headings. This is an example of what it could look like:
To create an index page, fetch the posts in index.astro
using the Sanity client. Astro fetches your data at build time, so with the fetch
in place, we can loop through the posts in our component's HTML:
// /my-blog/frontend/src/pages/index.astro
---
import { sanityClient } from '@sanity/astro';
const posts = await sanityClient.fetch(`*[_type == "post"]`);
---
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>My blog</title>
</head>
<body>
<h1>My blog</h1>
{posts && <ul>
{posts.map((post) => (
<li><a href='post/{post.slug.current}'>{post.title}</a></li>
))}
</ul>}
</body>
</html>
And there you are: you now have an Astro site to display our blog content and a Sanity Studio to manage it. It uses Astro's dynamic routes feature to generate static files for each of the blog posts in your Studio that you can host wherever. You're only getting the content at build time—when people read your content, they're reading the version built when your build process last ran.
To get started quicker, you can download the example project from GitHub.
Feel free to ask us questions on Slack, or however else you might find us.
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.
Setup interactive live preview with Presentation in a Next.js app router application
Go to Visual Editing with Next.js App Router and Sanity StudioThis guide teaches how to add a custom input component to a field for Sanity Studio v3
Go to How to build an input component for Sanity Studio v3A thorough intro to using GROQ-projections in a webhook contest
Go to GROQ-Powered Webhooks – Intro to ProjectionsA thorough intro to using GROQ-filters in a webhook-context
Go to GROQ-Powered Webhooks – Intro to Filters