Querying content in Astro
Use the @sanity/astro integration to fetch content with GROQ in your Astro components. Covers basic queries, dynamic routes, typed queries, and the loadQuery helper for Visual Editing.
Basic queries
Import the pre-configured client from the sanity:client virtual module and call fetch() in your component's frontmatter:
---
import { sanityClient } from 'sanity:client'
const posts = await sanityClient.fetch(
`*[_type == "post" && defined(slug)] | order(publishedAt desc){
_id,
title,
slug,
publishedAt
}`
)
---
<h1>Blog</h1>
<ul>
{posts.map((post) => (
<li>
<a href={`/blog/${post.slug.current}`}>{post.title}</a>
</li>
))}
</ul>The client is available in any .astro file's frontmatter, in API routes, and in server-side scripts. Queries use GROQ, Sanity's query language.
Dynamic routes with getStaticPaths
For statically generated pages, use Astro's getStaticPaths to create a page for each document:
---
import { sanityClient } from 'sanity:client'
export async function getStaticPaths() {
const posts = await sanityClient.fetch(
`*[_type == "post" && defined(slug)]{
"slug": slug.current,
title,
body
}`
)
return posts.map((post) => ({
params: { slug: post.slug },
props: { post },
}))
}
const { post } = Astro.props
---
<h1>{post.title}</h1>Astro calls getStaticPaths at build time and generates one HTML file per route. Content changes require a rebuild to appear on the site.
Parameterized queries
Use GROQ parameters to avoid string interpolation in queries. Pass parameters as the second argument to fetch():
---
import { sanityClient } from 'sanity:client'
const slug = Astro.params.slug
const post = await sanityClient.fetch(
`*[_type == "post" && slug.current == $slug][0]{
_id,
title,
body,
"author": author->{name, image}
}`,
{ slug }
)
---Parameters are sanitized by the client, preventing GROQ injection. Always prefer parameters over template literal interpolation.
Using Sanity TypeGen
For generated types that stay in sync with your schema, use Sanity TypeGen. Define queries with defineQuery in separate .ts files and import them into your Astro components:
npm install groqpnpm add groqyarn add groqbun add groqimport { defineQuery } from 'groq'
export const postsQuery = defineQuery(
`*[_type == "post" && defined(slug)] | order(publishedAt desc){
_id,
title,
slug,
publishedAt
}`
)Known limitation: Sanity TypeGen doesn't fully support .astro file syntax. The type generator's parser can't handle Astro's frontmatter fences and may produce errors. Define queries in separate .ts files as a workaround.
Querying references and joins
GROQ supports following references inline with the -> operator:
*[_type == "post" && slug.current == $slug][0]{
title,
body,
"author": author->{
name,
"imageUrl": image.asset->url
},
"categories": categories[]->{
title,
slug
}
}This fetches the post along with its referenced author and category documents in a single query. For more on GROQ query patterns, see the GROQ documentation.
Typing query results
For basic type safety, use a generic on the fetch() call:
---
import { sanityClient } from 'sanity:client'
interface Post {
_id: string
title: string
slug: { current: string }
publishedAt: string
}
const posts = await sanityClient.fetch<Post[]>(
`*[_type == "post" && defined(slug)] | order(publishedAt desc)`
)
---For generated types that stay in sync with your schema, use Sanity TypeGen with defineQuery as shown in the section above.
The loadQuery helper for Visual Editing
When using Visual Editing, queries need additional configuration: a viewer token, the perspective, source maps, and stega encoding. Rather than adding these options to every fetch() call, create a shared loadQuery helper:
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<T>({
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 ? 'drafts' : 'published'
const { result, resultSourceMap } = await sanityClient.fetch<T>(
query,
params ?? {},
{
filterResponse: false,
perspective,
resultSourceMap: visualEditingEnabled
? 'withKeyArraySelector'
: false,
stega: visualEditingEnabled,
...(visualEditingEnabled ? { token } : {}),
useCdn: !visualEditingEnabled,
}
)
return {
data: result,
sourceMap: resultSourceMap,
perspective,
}
}Then use it in your components:
---
import { loadQuery } from '../../lib/load-query'
const { data: post } = await loadQuery<Post>({
query: `*[_type == "post" && slug.current == $slug][0]{
_id,
title,
body
}`,
params: { slug: Astro.params.slug },
})
---
<h1>{post.title}</h1>The helper transparently switches between published and draft content based on the PUBLIC_SANITY_VISUAL_EDITING_ENABLED environment variable. See Visual Editing for Astro for a full setup.