February 09, 2023 (Updated August 09, 2023)

Build your blog with Astro and Sanity

By Hidde de Vries & Rune Botten

Use the official Sanity Astro integration to build a blog

0. Create a folder and monorepo

In this project, you'll have two web separate apps, one to manage content and one to render the content:

  1. Sanity Studio—a Single Page Application in which you manage your content, it connects to the hosted API that has all your blog's content
  2. The front-end—a website generated with Astro in which your readers will read the content

Set these up so that we have one studio folder and one frontend folder:

~/Sites/my-blog 
├── studio
├── frontend

1. Install Sanity Studio with the preconfigured blog schema

Start by setting up the Sanity Studio using node package manager (how to install npm). To set up, run:

npm create sanity@latest

You'll be asked to create an account with your Google or Github login, or you can choose to log in with a dedicated email and password. Afterward, you can create a new project, where you'll be asked to choose a project template. Select the blog schema template. First, though, you'll need to give your project and dataset a name (you can add more datasets if you need one for testing) and choose the path to your studio folder (let's pick studio). You can also choose if you want to use TypeScript and which package manager to use.

$ Select project to use Create new project
$ Your project name: my-blog
$ Use the default dataset configuration? Yes
$ Project output path: studio
$ Select project template Blog (schema)
$ Do you want to use TypeScript? Yes
$ Package manager to use for installing dependencies? npm

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 start editing your content. This content will be instantly synced to the Content Lake and is available through the public APIs once you hit publish.

By running 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!

2. Install Astro and get it running

For the front-end, we'll use Astro, a framework for building websites that is focused on speed. It renders on the server where possible and works together with a number of different UI frameworks, like Vue, Svelte, and React.

To install Astro in our project, use a frontend folder that lives alongside the studio folder you've just created. Return your projects folder and run:

npm create astro@latest

The prompt will take you through some questions. Type “y” to confirm you want to install Astro, then pick a folder (frontend), choose “an empty project”, install dependencies and pick your TypeScript preference (eg “Relaxed”).

$ npm create astro@latest
$ Ok to proceed? y
$ Where would you like to create your new project? frontend
$ How would you like to setup your new project? an empty project
$ Would you like to install npm dependencies? y
$ Would you like to initialize a new git repository? y
$ How would you like to setup TypeScript? relaxed

After this setup, you should now have a frontend folder alongside your studio folder. Enter it (cd frontend) and run npm run dev to serve your newly created front-end projects on a development server. Now you can open it in the browser at localhost:3000.

In the code, there is an index page under src/pages/index.astro. Feel free to change the contents of the title and h1 element there, eg to “My blog”.

You'll notice that the file starts with two lines that just say ---, with plain HTML below. These two lines below are “code fences“, an Astro-specific thing used to include code with a specific file or component. It's a bit like how Yaml is included as front matter in a lot of tools. You'll use this feature soon, but first, let's create some posts in our Studio.

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

4. 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 having to push code. To make your life easier, Astro, like most web frameworks, offers dynamic routing: 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 need to wrap it in brackets in the filename. So if we want our posts to be on /post/name, we 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 we use 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 a 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.

5. Integrate your blog posts from Sanity in Astro

To integrate your Sanity content with your Astro blog, install the astro-sanity package:

npx astro add @sanity/astro

This will update astro.config.mjs to import sanity from @sanity/astro and add it as an integration, so that it looks like this:

// my-blog/frontend/astro.config.mjs

import { defineConfig } from 'astro/config';
import sanity from "@sanity/astro";

export default defineConfig({
  integrations: [sanity()]
});

For it to work, you'll need to pass in your configuration as an object:

// my-blog/frontend/astro.config.mjs

import { defineConfig } from 'astro/config';
import sanity from "@sanity/astro";

export default defineConfig({
  integrations: [sanity({
    projectId: 'YOUR_PROJECT_ID',
    dataset: 'YOUR_DATASET_NAME',
    apiVersion: '2023-02-08',
    useCdn: false,
  })]
});

Your project ID and dataset name can be copied from sanity.config.ts in the studio folder.

With that set up, let's head back to your [slug].astro file, import the Sanity client and use it to fetch your posts, like this:

---
import { useSanityClient } from "@sanity/astro";

export async function getStaticPaths() {
  const posts = await useSanityClient().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 async, so that we can wait for the data before returning the posts. With useSanityClient, we fetch the posts using its fetch function (note: this is Sanity's fetch function, 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.

6. Render images and rich text

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

Background

With Sanity, your blog posts have their own content model. They can be set up to be whatever you want, but in this case, we've used the blog schema that Sanity CLI comes with. Your post's title is a string, the published date is saved as a datetime and so on. Sanity has some specific tooling for images and rich text, 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 that 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 { useSanityClient } from '@sanity/astro';
import imageUrlBuilder from "@sanity/image-url";

export const imageBuilder = imageUrlBuilder(useSanityClient());

export function urlForImage(source) {
  return imageBuilder.image(source);
}

Rich text

The blog template saves your blog content in a field of the block type. This will give the content editor a rich text field, which Sanity saves in a structured format called Portable Text. From Portable Text, you can generate Markdown, HTML, PDFs, or whatever else we 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 be passed the relevant node from the Portable Text content, and we use our urlForImage function to calculate the asset URLs to display. Now we 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 rich text, 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, which renders any content that you've added, including links, images and headings. This is an example of what it could like like:

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

8. 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 { useSanityClient } from '@sanity/astro';

const posts = await useSanityClient().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 that was built when your build process last ran.

If you'd like, you can deploy your studio using npx sanity deploy, and deploy your website on any hosting service, including Netlify or Vercel.

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