# Querying content in Astro

## Basic queries

Import the pre-configured client from the `sanity:client` virtual module and call `fetch()` in your component's frontmatter:

**src/pages/index.astro**

```typescript
---
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:

**src/pages/blog/[slug].astro**

```typescript
---
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()`:

**Example**

```typescript
---
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:

**TERMINAL**

```sh
npm add groq
```

**src/lib/queries.ts**

```typescript
import { defineQuery } from 'groq'

export const postsQuery = defineQuery(
  `*[_type == "post" && defined(slug)] | order(publishedAt desc){
    _id,
    title,
    slug,
    publishedAt
  }`
)
```

> [!WARNING]
> **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:

**GROQ query**

```groq
*[_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:

**Example**

```typescript
---
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:

**src/lib/load-query.ts**

```typescript
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:

**src/pages/blog/[slug].astro**

```typescript
---
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](https://www.sanity.io/docs/visual-editing/astro-visual-editing).

