/ astro-integrations / How to Use Sanity with Astro: Complete Guide
astro-integrations 8 min read

How to Use Sanity with Astro: Complete Guide

Step-by-step guide to integrating Sanity with your Astro website. Installation, configuration, and best practices.

How to Use Sanity with Astro: Complete Guide

Sanity is a composable content platform with real-time collaboration and a ridiculously flexible schema system. Pair it with Astro and you get a content-driven site that is fast to build, fast to load, and easy for your team to manage. Astro has an official Sanity integration, @sanity/astro, which makes the setup straightforward.

Versions referenced in this guide (verified against the npm registry on 2026-05-29): Astro 6.4.2, @sanity/astro 3.4.0, @sanity/image-url 2.1.1, astro-portabletext 0.13.0, and the sanity Studio package 5.28.0.

Prerequisites

  • Node.js 22.12.0 or newer (Astro 6 dropped Node 18 and 20)
  • An Astro project (npm create astro@latest)
  • A Sanity account and project (free tier available at sanity.io)

Installation

Install the official Astro Sanity integration. The astro add command from the official plugin docs also pulls in @astrojs/react, which the integration uses when you later embed Sanity Studio on a route:

npx astro add @sanity/astro @astrojs/react

Then add the image URL builder, which you will use for optimized images:

npm install @sanity/image-url

The astro add command automatically updates your astro.config.mjs and registers the React integration. After running it, the Sanity docs recommend adding the integration's types to your tsconfig.json so the sanity:client virtual module resolves cleanly:

// tsconfig.json
{
  "compilerOptions": {
    "types": ["@sanity/astro/module"]
  }
}

Configuration

After running the add command, update your Astro config with your Sanity project details. The integration accepts the same options as @sanity/client plus the integration-specific projectId, dataset, apiVersion, and useCdn:

// astro.config.mjs
import { defineConfig } from "astro/config";
import sanity from "@sanity/astro";
import react from "@astrojs/react";

export default defineConfig({
  integrations: [
    sanity({
      projectId: "your_project_id",
      dataset: "production",
      // For static builds, set useCdn: false. Content is fetched once
      // at build time, so the CDN cache adds no benefit. Flip it to true
      // only for on-demand (server-rendered) pages serving published content.
      useCdn: false,
      // Required for predictable query behavior. Pin a date, never leave it floating.
      apiVersion: "2026-03-01",
    }),
    react(),
  ],
});

The official Sanity docs recommend useCdn: false for static builds. Older guides that hardcode useCdn: true are wrong for the default static output. You can find your project ID in the Sanity dashboard at manage.sanity.io.

A Note on Astro 6 Rendering

Astro defaults to fully static output. The separate output: 'hybrid' mode was removed in Astro 5 and merged into static, so do not set it. With static output, every page is pre-rendered to HTML at build time. If you want a specific page (for example a draft preview or a search endpoint) to render on demand, install an adapter and add export const prerender = false; to that page. The rest of the site stays static.

Basic Usage

With the integration installed, you can use the built-in sanityClient anywhere in your Astro components. Let's fetch a list of blog posts using GROQ, Sanity's query language:

---
// src/pages/blog/index.astro
import { sanityClient } from "sanity:client";
import BaseLayout from "../../layouts/BaseLayout.astro";

const posts = await sanityClient.fetch(
  `*[_type == "post"] | order(publishedAt desc) {
    title,
    slug,
    publishedAt,
    excerpt,
    "imageUrl": mainImage.asset->url
  }`
);
---

<BaseLayout title="Blog">
  <h1>Blog</h1>
  <ul>
    {posts.map((post) => (
      <li>
        <a href={`/blog/${post.slug.current}`}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </a>
      </li>
    ))}
  </ul>
</BaseLayout>

Fetching Data for Dynamic Pages

Generate individual post pages with getStaticPaths:

---
// src/pages/blog/[slug].astro
import { sanityClient } from "sanity:client";
import { PortableText } from "astro-portabletext";
import BaseLayout from "../../layouts/BaseLayout.astro";

export async function getStaticPaths() {
  const posts = await sanityClient.fetch(
    `*[_type == "post"]{ slug }`
  );

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

const { slug } = Astro.params;

const post = await sanityClient.fetch(
  `*[_type == "post" && slug.current == $slug][0] {
    title,
    body,
    publishedAt,
    "imageUrl": mainImage.asset->url
  }`,
  { slug }
);
---

<BaseLayout title={post.title}>
  <article>
    <h1>{post.title}</h1>
    {post.imageUrl && <img src={post.imageUrl} alt={post.title} />}
    <PortableText value={post.body} />
  </article>
</BaseLayout>

For rendering Portable Text (Sanity's rich text format), use the astro-portabletext package. This is the package the Sanity team recommends for Astro projects in their agent-toolkit best-practices reference. Note that there is no @portabletext/astro package on npm, so do not try to install one. The official @portabletext/* family ships framework-specific renderers like @portabletext/react and the framework-agnostic @portabletext/to-html, but for native .astro components reach for astro-portabletext:

npm install astro-portabletext

Import the PortableText component and pass the body array to its value prop, exactly as shown in the dynamic page above:

---
import { PortableText } from "astro-portabletext";
---

<PortableText value={post.body} />

Production Tips

  1. Enable the CDN. Set useCdn: true in your config for production builds. This serves cached responses from Sanity's edge network and is significantly faster than hitting the API directly.

  • Use GROQ projections. Only fetch the fields you actually need. Instead of *[_type == "post"], project specific fields. It reduces payload size and speeds up queries.

  • Set up on-demand revalidation. Configure a Sanity webhook to trigger rebuilds when content changes. Point it at your deploy hook URL from Vercel or Netlify.

  • Optimize images with the image URL builder. Use @sanity/image-url to generate responsive, optimized image URLs with width, height, and format parameters built in.

  • Use Sanity's Visual Editing. The integration exposes a stega option for stega-encoded source maps, which power the click-to-edit overlays that let editors jump from the live site straight into the Studio. Because Astro components do not re-render on the client, the Sanity docs note that live, real-time updates may require a small React wrapper or the View Transitions API around the editable region.

  • Alternatives to Consider

    • Contentful if you need enterprise-grade content infrastructure with strict content modeling and approval workflows.
    • TinaCMS if you want a git-backed CMS with visual editing and Markdown/MDX support. Good for developer blogs.
    • Keystatic if you prefer zero external services. Content lives in your git repo.

    Common Errors and Fixes

    • Cannot find module 'sanity:client' or no types on the virtual module. Add the integration's types to tsconfig.json with "types": ["@sanity/astro/module"]. The sanity:client import is a virtual module the integration injects, so TypeScript will not resolve it until the types are referenced.

    • Cannot find package '@portabletext/astro'. That package does not exist on npm. Install astro-portabletext instead and import PortableText from it. Mixing up the package name is the single most common setup failure for this stack.

    • Stale content on a static build even after publishing in Sanity. A static Astro site fetches content once at build time. Publishing in the Studio does not update a deployed static site on its own. Trigger a rebuild with a Sanity webhook pointed at your host's deploy hook, or move that route to on-demand rendering with export const prerender = false; plus an adapter.

    • Setting output: 'hybrid' throws a config error. That mode was removed in Astro 5 and folded into static. Delete the output option (static is the default) and opt individual pages into server rendering with export const prerender = false;.

    • On-demand pages fail to build or 404 in production. Any page using export const prerender = false; requires an adapter (Node, Vercel, Netlify, Cloudflare). Without one, Astro has no server runtime to render the route. Install the adapter for your host first.

    • projectId can only contain a-z, 0-9 and dashes at build time. You left a placeholder like your_project_id in the config. Underscores are invalid. Paste the real project ID from manage.sanity.io.

    • Empty results from a known-good query. Confirm apiVersion is pinned to a real date (for example 2026-03-01). An unpinned or future-dated API version can change query behavior. Also check that dataset matches the dataset you published into (often production).

    Official Docs and Examples

    Wrapping Up

    Sanity and Astro are one of the best CMS and framework combos for content sites in 2026. The official integration handles the boring setup, GROQ makes querying intuitive, and the Studio gives your content team a great editing experience. Pin your apiVersion, keep useCdn: false for static builds, render rich text with astro-portabletext, and you are on solid, documented ground.

    Sources

    All versions and facts below were checked on 2026-05-29.