Unlock seamless workflows and faster delivery with our latest releases - Join the deep dive
Last updated August 02, 2024

Build your blog with Astro and Sanity

By Chris LaRocque & 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:

If you prefer to see the code in your own IDE first, you can find the finished code here.

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. When asked How would you like to start your new project? select Empty . You don't need to use Typescript, but the examples in this guide will be using it.

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.

Update /src/env.d.ts to add the types for the Astro module:

// ./src/env.d.ts
/// <reference types="astro/client" />
/// <reference types="@sanity/astro/module" />

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.

When the init command is completed Astro will have written 2 new environment variables to your .env file: PUBLIC_SANITY_PROJECT_ID and PUBLIC_SANITY_PROJECT_DATASET. These variables are prefixed with PUBLIC_ because they're not considered secrets.

Sanity Client configuration

Astro has a unique limitation where you can't use variables from .env files directly in your astro.config.mjs file. Because your project ID and dataset name aren't considered sensitive you can directly copy + paste their values from your .env file. Go here for instructions if you wish to use the .env file instead.

Update the sanity integration in your astro.config.mjs file to include the information needed by the Sanity client.

// 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: "2024-07-24", // insert the current date to access the latest version of the API
}),
react(), ], });

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

Update astro.config.mjs to add a Studio at yoursite.com/studio

// 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: "2024-07-24", // insert the current date to access the latest version of the API
studioBasePath: '/studio' // 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. Create a new file called sanity.config.ts and add the following, note that we're able to use environment variables here:

// ./sanity.config.ts
import { defineConfig } from "sanity";
import { structureTool } from "sanity/structure";

export default defineConfig({
  projectId: import.meta.env.PUBLIC_SANITY_PROJECT_ID,
  dataset: import.meta.env.PUBLIC_SANITY_DATASET,
  plugins: [structureTool()],
  schema: {
    types: [],
  },
});

Start the Astro local development server, you should be able to visit the Studio at http://localhost:4321/studio. 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:

├── 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.

This guide isn't going to cover schema creation in-depth, for now we'll copy and paste some starting schema definitions.

Create a new directory inside the src directory, called sanity with a directory schemaTypes inside of it. Create the following files inside /src/sanity/schemaTypes :

// ./src/sanity/schemaTypes/author.ts
import { defineField, defineType } from "sanity";

export const authorType = defineType({
  name: "author",
  type: "document",
  fields: [
    defineField({
      name: "name",
      type: "string",
    }),
    defineField({
      name: "slug",
      type: "slug",
      options: {
        source: "name",
        maxLength: 96,
      },
    }),
    defineField({
      name: "image",
      type: "image",
      options: {
        hotspot: true,
      },
      fields: [
        {
          name: "alt",
          type: "string",
          title: "Alternative Text",
        },
      ],
    }),
    defineField({
      name: "bio",
      type: "array",
      of: [
        {
          type: "block",
          styles: [{ title: "Normal", value: "normal" }],
          lists: [],
        },
      ],
    }),
  ],
  preview: {
    select: {
      title: "name",
      media: "image",
    },
  },
});
// ./src/sanity/schemaTypes/blockContent.ts
import { defineType, defineArrayMember } from "sanity";

/**
 * This is the schema type for block content used in the post document type
 * Importing this type into the studio configuration's `schema` property
 * lets you reuse it in other document types with:
 *  {
 *    name: 'someName',
 *    title: 'Some title',
 *    type: 'blockContent'
 *  }
 */

export const blockContentType = defineType({
  title: "Block Content",
  name: "blockContent",
  type: "array",
  of: [
    defineArrayMember({
      type: "block",
      // Styles let you define what blocks can be marked up as. The default
      // set corresponds with HTML tags, but you can set any title or value
      // you want, and decide how you want to deal with it where you want to
      // use your content.
      styles: [
        { title: "Normal", value: "normal" },
        { title: "H1", value: "h1" },
        { title: "H2", value: "h2" },
        { title: "H3", value: "h3" },
        { title: "H4", value: "h4" },
        { title: "Quote", value: "blockquote" },
      ],
      lists: [{ title: "Bullet", value: "bullet" }],
      // Marks let you mark up inline text in the Portable Text Editor
      marks: {
        // Decorators usually describe a single property – e.g. a typographic
        // preference or highlighting
        decorators: [
          { title: "Strong", value: "strong" },
          { title: "Emphasis", value: "em" },
        ],
        // Annotations can be any object structure – e.g. a link or a footnote.
        annotations: [
          {
            title: "URL",
            name: "link",
            type: "object",
            fields: [
              {
                title: "URL",
                name: "href",
                type: "url",
              },
            ],
          },
        ],
      },
    }),
    // You can add additional types here. Note that you can't use
    // primitive types such as 'string' and 'number' in the same array
    // as a block type.
    defineArrayMember({
      type: "image",
      options: { hotspot: true },
      fields: [
        {
          name: "alt",
          type: "string",
          title: "Alternative Text",
        },
      ],
    }),
  ],
});
// ./src/sanity/schemaTypes/category.ts
import { defineField, defineType } from "sanity";

export const categoryType = defineType({
  name: "category",
  type: "document",
  fields: [
    defineField({
      name: "title",
      type: "string",
    }),
    defineField({
      name: "description",
      type: "text",
    }),
  ],
});
// ./src/sanity/schemaTypes/post.ts
import { defineField, defineType } from "sanity";

export const postType = defineType({
  name: "post",
  type: "document",
  fields: [
    defineField({
      name: "title",
      type: "string",
    }),
    defineField({
      name: "slug",
      type: "slug",
      options: {
        source: "title",
        maxLength: 96,
      },
    }),
    defineField({
      name: "author",
      type: "reference",
      to: { type: "author" },
    }),
    defineField({
      name: "mainImage",
      type: "image",
      options: {
        hotspot: true,
      },
      fields: [
        {
          name: "alt",
          type: "string",
          title: "Alternative Text",
        },
      ],
    }),
    defineField({
      name: "categories",
      type: "array",
      of: [{ type: "reference", to: { type: "category" } }],
    }),
    defineField({
      name: "publishedAt",
      type: "datetime",
    }),
    defineField({
      name: "body",
      type: "blockContent",
    }),
  ],

  preview: {
    select: {
      title: "title",
      author: "author.name",
      media: "mainImage",
    },
    prepare(selection) {
      const { author } = selection;
      return { ...selection, subtitle: author && `by ${author}` };
    },
  },
});

Create a file index.ts inside /src/sanity/schemaTypes

// ./src/sanity/schemaTypes/index.ts
import type { SchemaTypeDefinition } from "sanity";
import { authorType } from "./author";
import { blockContentType } from "./blockContent";
import { categoryType } from "./category";
import { postType } from "./post";

export const schema: { types: SchemaTypeDefinition[] } = {
  types: [authorType, blockContentType, categoryType, postType],
};

Update your sanity.config.ts file to include the new schema:

import { defineConfig } from "sanity";
import { structureTool } from "sanity/structure";
import { schema } from "./src/sanity/schemaTypes";
export default defineConfig({ projectId: import.meta.env.PUBLIC_SANITY_PROJECT_ID, dataset: import.meta.env.PUBLIC_SANITY_DATASET, plugins: [ structureTool(), ],
schema,
});

To recap: you created 3 document types - author, category, and post; as well as a reusable array type blockContent for editing Portable Text. If you refresh your Studio at http://localhost:4321/studio you should see the 3 document types listed, and the blockContent array will be visible when creating a post in the next step.

Create some example content

Create a post titled “Hello world” inside your Studio. 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 selected the Empty template while creating this project, your Astro site was generated with only one route: an index page. To surface posts on our site, you'll want to create routes for each post. In Astro, routes exist as files on the file system in src/pages 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 the filename in brackets. So, if you want your posts route to be /post/slug, you need to create a folder called post, which contains a file named [slug].astro.

In [slug].astro, 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:

// ./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 (---). Data returned from getStaticPaths 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, '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:4321/post/summertime will display the heading “A post about summertime”.

We can use 'slug' in our content

Of course, you want to display more than just the slugs, and you don't want to hardcode the slugs 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

Create a new directory at ./src/sanity called lib and add a new file load-query.ts:

// ./src/sanity/lib/load-query.ts
import { type QueryParams } from "sanity";
import { sanityClient } from "sanity:client";

export async function loadQuery<QueryResponse>({
  query,
  params,
}: {
  query: string;
  params?: QueryParams;
}) {
  const { result } = await sanityClient.fetch<QueryResponse>(
    query,
    params ?? {},
    { filterResponse: false }
  );

  return {
    data: result,
  };
}

Protip

You may ask "Why wouldn't I use the client directly in my Astro template?" and that's a very valid question. We're setting up a wrapper around the Sanity integration's client to make implementing Presentation later easier, but if you don't plan to use Presentation you can feel free to just use the client directly in your Astro files.

Head back to your [slug].astro file, import the loadQuery function, and use it to fetch your posts' slugs, like this:

// ./src/pages/post/[slug].astro
---
import type { SanityDocument } from "@sanity/client";
import { loadQuery } from "../../sanity/lib/load-query";
export async function getStaticPaths() {
const { data: posts } = await loadQuery<SanityDocument[]>({
query: `*[_type == "post"]`,
});
return posts.map(({ slug }) => {
return {
params: {
slug: slug.current,
},
}; }); } const { slug } = Astro.params; --- <h1>A post about {slug}</h1>

Within the code fences, we export that same getStaticPaths function as before, but we've made it automatic so that we can wait for the data before returning the posts. With the loadQuery function, we fetch the posts using the Sanity client's 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

Now you need to fetch the right blog post given a certain slug. Update [slug].astro with the following:

// ./src/pages/post/[slug].astro
---
import type { SanityDocument } from "@sanity/client";
import { loadQuery } from "../../sanity/lib/load-query";

export async function getStaticPaths() {
  const { data: posts } = await loadQuery<SanityDocument[]>({
    query: `*[_type == "post"]`,
  });

  return posts.map(({ slug }) => {
    return {
      params: {
        slug: slug.current,
      },
    };
  });
}

const { params } = Astro;
const { data: post } = await loadQuery({
query: `*[_type == "post" && slug.current == $slug][0]`,
params,
});
---
<h1>A post about {post.title}</h1>

So that's it! You should now be able to see the title of your "Hello World" post under /post/hello-world.

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.

Protip

"Why not return the post during getStaticPaths?" - another excellent question dear reader. Similar to the reasoning for wrapping our client in loadQuery, we're fetching data outside getStaticPaths to allow it to refresh when using Presentation. If you don't plan to use Presentation you can return all the data in getStaticPaths.

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

Background

With Sanity, your blog posts are part of your content model. They can be set up to be whatever you want, but we've used some boilerplate blog post schemas. Your post's title is a string, the published date is saved as a datetime and so on. Sanity has specific 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

Create a new file inside ./src/sanity/lib called urlForImage.ts:

// ./src/sanity/lib/urlForImage.ts
import { sanityClient } from 'sanity:client';
import imageUrlBuilder from "@sanity/image-url";
import type { SanityAsset } from '@sanity/image-url/lib/types/types';

export const imageBuilder = imageUrlBuilder(sanityClient);

export function urlForImage(source: SanityAsset) {
  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

If you're using TypeScript it may be helpful to include the types for Portable Text:

npm install @portabletext/types

Then, for convenience, create an Astro component to render our Portable Text for us. Create a components folder alongside sanity and pages folders.

.
└── src/
    ├── components
    ├── pages
    └── sanity

Create a new file called PortableText.astro inside of ./src/components:

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

Create a file called PortableTextImage.astro in the same components folder:

// ./src/components/PortableTextImage.astro
---
import { urlForImage } from "../sanity/lib/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:

// ./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} />

Update [slug].astro to use the PortableText component to render the post content:

// ./src/pages/post/[slug].astro

---
import type { SanityDocument } from "@sanity/client";
import { loadQuery } from "../../sanity/lib/load-query";
import PortableText from "../../components/PortableText.astro";
export async function getStaticPaths() { const { data: posts } = await loadQuery<SanityDocument[]>({ query: `*[_type == "post"]`, }); return posts.map(({ slug }) => { return { params: { slug: slug.current, }, }; }); } const { params } = Astro; const { data: post } = await loadQuery<{ title: string; body: any[] }>({ query: `*[_type == "post" && slug.current == $slug][0]`, params, }); --- <h1>A post about {post.title}</h1>
<PortableText portableText={post.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

Enable Presentation

Live Visual Editing is made possible via Sanity's Presentation Tool. To enable Presentation we'll follow the steps outlined in the documentation for the Astro integration. Presentation provides 2 key benefits:

  1. Overlays - All content stored in Sanity has an overlay added that when clicked brings users directly to editing that content in the Studio
  2. Live mode - Edits made in the Studio are reflected on the front-end to provide authors immediate feedback

Create a layout file with the VisualEditing component

Create a directory inside the src directory called layouts with a file Layout.astro with the following:

// ./src/layouts/Layout.astro
---
import { VisualEditing } from "@sanity/astro/visual-editing";

export type props = {
  title: string;
};
const { title } = Astro.props;
const visualEditingEnabled =
  import.meta.env.PUBLIC_SANITY_VISUAL_EDITING_ENABLED == "true";
---

<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>{title}</title>
  </head>
  <body>
    <slot />
    <VisualEditing enabled={visualEditingEnabled} />
  </body>
</html>

In Layout.astro you're importing the VisualEditing component, which enables overlays and live mode for Presentation. Note the visualEditingEnabled constant tied to an environment variable PUBLIC_SANITY_VISUAL_EDITING_ENABLED set to true. If you haven't already, update your .env file to include this variable. When you're ready to deploy your site you'll want to have this variable set to false in production, but have another environment that's a copy of production with this variable set to true.

Update your [slug].astro template to be wrapped in the new layout:

---
import type { SanityDocument } from "@sanity/client";
import { loadQuery } from "../../sanity/lib/load-query";
import Layout from "../../layouts/Layout.astro";
import PortableText from "../../components/PortableText.astro"; export async function getStaticPaths() { const { data: posts } = await loadQuery<SanityDocument[]>({ query: `*[_type == "post"]`, }); return posts.map(({ slug }) => { return { params: { slug: slug.current, }, }; }); } const { params } = Astro; const { data: post } = await loadQuery<{ title: string; body: any[] }>({ query: `*[_type == "post" && slug.current == $slug][0]`, params, }); ---
<Layout>
<h1>A post about {post.title}</h1> <PortableText portableText={post.body} />
</Layout>

Update settings in astro.config file

Update the Sanity integration settings in astro.config.mjs to include stega.studioUrl

import { defineConfig } from "astro/config";

import sanity from "@sanity/astro";
import react from "@astrojs/react";

import { loadEnv } from "vite";
const { PUBLIC_SANITY_PROJECT_ID, PUBLIC_SANITY_DATASET } = loadEnv(
  process.env.NODE_ENV,
  process.cwd(),
  "",
);

// https://astro.build/config
export default defineConfig({
  integrations: [
    sanity({
      projectId: PUBLIC_SANITY_PROJECT_ID,
      dataset: PUBLIC_SANITY_DATASET,
      useCdn: false, // See note on using the CDN
      apiVersion: "2023-07-24", // insert the current date to access the latest version of the API
      studioBasePath: "/studio",
stega: {
studioUrl: "/studio",
},
}), react(), ], });

Adding this to the configuration allows the overlays to link to the appropriate place.

Generate a viewer token

In Sanity, drafts are considered private and are not accessible without a token.

In the top right of the Studio click on your user avatar, and click "Manage project"

Select "manage project" from this drop down

In your manage dashboard, navigate to "API" and down to "Tokens". Click "Add token", give it any name you wish, ensure it has "Viewer" permissions, and click "Save"

Navigate to the token settings in manage and create a viewer token

Add this token to your .env file with the name SANITY_API_READ_TOKEN.

Update loadQuery to work with Presentation

Update ./src/sanity/lib/load-query.ts to the following:

import { type QueryParams } from "sanity";
import { sanityClient } from "sanity:client";

const visualEditingEnabled =
import.meta.env.PUBLIC_SANITY_VISUAL_EDITING_ENABLED === "true";
const token = import.meta.env.SANITY_API_READ_TOKEN;
export async function loadQuery<QueryResponse>({ query, params, }: { query: string; params?: QueryParams; }) {
if (visualEditingEnabled && !token) {
throw new Error(
"The `SANITY_API_READ_TOKEN` environment variable is required during Visual Editing.",
);
}
const perspective = visualEditingEnabled ? "previewDrafts" : "published";
const { result, resultSourceMap } = await sanityClient.fetch<QueryResponse>( query, params ?? {}, { filterResponse: false,
perspective,
resultSourceMap: visualEditingEnabled ? "withKeyArraySelector" : false,
stega: visualEditingEnabled,
...(visualEditingEnabled ? { token } : {}),
}, ); return { data: result,
sourceMap: resultSourceMap,
perspective,
}; }

There are a few things going on here, you're:

  • Modifying the perspective setting in the client to use previewDrafts when Visual Editing is enabled
  • Returning a resultSourceMap for the overlays to know where to link to
  • Passing the token to the client to view drafts and enable Stega encoding (which powers the overlays)

Add the Presentation tool to the Studio

Update your sanity.config.ts file to include the Presentation tool in the plugins array:

import { defineConfig } from "sanity";
import { structureTool } from "sanity/structure";
import { schema } from "./src/sanity/schema";
import { presentationTool } from "sanity/presentation";
export default defineConfig({ projectId: import.meta.env.PUBLIC_SANITY_PROJECT_ID, dataset: import.meta.env.PUBLIC_SANITY_DATASET, plugins: [ structureTool(),
presentationTool({
previewUrl: location.origin,
}),
], schema, });

Note the previewUrl, set to location.origin.

If you navigate to http://localhost:4321/studio/presentation you should see the Presentation tool, and if you put the path to your blog post (/post/hello-world) in the tool's address bar you should see your front-end with overlays that bring you directly to your blog post.

Add Document Location Resolver

The Document Locations Resolver API allows you to define where data is being used in your application(s), and it also allows you to quickly preview a document from the Structure.

For example if you have an author document open, enabling locations puts a widget at the top of the document with links to all documents on the site where this author is linked to.

Location resolver adds this widget on top of the document

Create a new location resolver

Create a new file in ./src/sanity/lib/ , called resolve.ts

// ./src/sanity/lib/resolve.ts

import { defineLocations } from "sanity/presentation";
import type { PresentationPluginOptions } from "sanity/presentation";

export const resolve: PresentationPluginOptions["resolve"] = {
  locations: {
    // Add more locations for other post types
    post: defineLocations({
      select: {
        title: "title",
        slug: "slug.current",
      },
      resolve: (doc) => ({
        locations: [
          {
            title: doc?.title || "Untitled",
            href: `/post/${doc?.slug}`,
          },
          { title: "Posts", href: location.origin },
        ],
      }),
    }),
  },
};

Add location resolver to the Studio

Update your sanity.config.ts file to include the Location tool (resolve) inside the presentationTool

import { defineConfig } from "sanity";
import { structureTool } from "sanity/structure";
import { schema } from "./src/sanity/schema";
import { presentationTool } from "sanity/presentation";
import { resolve } from "./src/sanity/lib/resolve";
export default defineConfig({ projectId: import.meta.env.PUBLIC_SANITY_PROJECT_ID, dataset: import.meta.env.PUBLIC_SANITY_DATASET, plugins: [ structureTool(), presentationTool({
resolve,
previewUrl: location.origin, }), ], schema, });

Demo of Visual Editing with Astro

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.

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

Managing redirects with Sanity

Official(made by Sanity team)

How to use Sanity to control redirects in your JavaScript framework of choice.

Chris LaRocque
Go to Managing redirects with Sanity