😎 Discover cool tips and tricks for customization in our next Developer Deep Dive virtual event - sign up now!
Last updated September 05, 2023

Build your blog with Astro and Sanity

By Knut Melvær & Rune Botten

Use 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:

  • Set up static and dynamic routes based on content from your Sanity project
  • How to implement block content with Portable Text, and add custom block types
  • How to work with images from the Sanity CDN
  • How to use GROQ-powered Webhooks to trigger rebuilds for static sites

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.

Prerequisites

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.

Initialize a new Astro project

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.

Add dependencies

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

Initialize a new Sanity Project

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.

Sanity Client configuration

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
  })]
});

Protip

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"]).

Embedding Sanity Studio

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

Defining the Studio schema

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).

Gotcha

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

Create some example content

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.

What's a better way to get started with your blog than creating some Hello World content?

With the content created, the next step is to return to your Astro site and set it up to display your content.

Set up a blog post route in Astro

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”.

We can use 'slug' in our content

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.

Integrate your blog posts from Sanity in Astro

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.

Protip

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 list

So that's it! You should now be able to see the title of any of your posts under /post/[the-posts-slug].

If you have a post titled “Hello world” with “hello-world” as the slug, you should be able to find it in localhost:3000/post/hello-world.

Render images and block content

Now that you've seen how to display the title, continue to add the other bits of our posts: block content and author information.

Background

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.

Images

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);
}

Block content and rich text

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:

Bold text, links, images: authored in one rich text field and rendered in one PortableText component

List all posts on an index page

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>

Next steps

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 – build remarkable experiences at scale

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.

Other guides by authors