Skip to content
Astro as content 4 min read

Querying Collections

Once you’ve defined a collection and its schema, you need a way to read entries back out at build time. Astro exposes a tiny, type-safe query API for exactly this: getCollection pulls every entry in a collection, and getEntry fetches a single one by its ID. Both run on the server during the build (or on request in SSR), so the work never ships to the browser — keeping pages zero-JS by default. Because the data flows through your Zod schema, everything you get back is fully typed.

Importing the query helpers

The helpers live in the virtual astro:content module. You import them inside the component script fence of any .astro page or component, or in any .ts/.js file that runs on the server.

---
import { getCollection, getEntry } from 'astro:content';

const posts = await getCollection('blog');
---
<ul>
  {posts.map((post) => (
    <li>
      <a href={`/blog/${post.id}`}>{post.data.title}</a>
    </li>
  ))}
</ul>

Every entry is an object with a stable shape. The most important fields are id (the unique identifier, derived from the filename for file-based loaders), data (your schema-validated frontmatter), and a render() method for content collections that produces the rendered HTML.

FieldTypeDescription
idstringUnique entry identifier (e.g. my-first-post)
datainferred from schemaValidated frontmatter / data, fully typed
bodystringRaw, unparsed body (Markdown/MDX entries)
collectionstringThe collection name this entry belongs to
render()() => Promise<...>Async helper returning Content, headings, and metadata

In Astro 5’s Content Layer, entries are keyed by id rather than the legacy slug. If you’re migrating from older code that read entry.slug, switch those references to entry.id.

Filtering with getCollection

getCollection accepts an optional second argument: a predicate that runs against each entry. Return true to keep the entry. This is the idiomatic way to hide drafts, scope by category, or split content by language.

---
import { getCollection } from 'astro:content';

// Only published posts (drop anything flagged as a draft)
const published = await getCollection('blog', ({ data }) => {
  return data.draft !== true;
});

// Posts in a specific category
const guides = await getCollection('blog', ({ data }) => {
  return data.category === 'guides';
});
---
<p>{published.length} published posts.</p>

The filter is plain JavaScript, so you can compose any condition you like — date ranges, tag membership, author matches. Because data is typed, your editor autocompletes the available fields and flags typos at build time.

Sorting entries

The query API doesn’t sort for you; entries arrive in an unspecified order. Sort the returned array with standard Array.prototype.sort. Sorting by date is the most common case — remember to compare Date values via .valueOf() or arithmetic.

---
import { getCollection } from 'astro:content';

const posts = await getCollection('blog');

// Newest first
const sorted = posts.sort(
  (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
);

const latestThree = sorted.slice(0, 3);
---
<ul>
  {latestThree.map((p) => <li>{p.data.title}</li>)}
</ul>

If your schema coerces dates with z.coerce.date(), pubDate is already a real Date object, so the subtraction above works directly.

Fetching a single entry with getEntry

When you need exactly one entry — for a detail page, a featured callout, or a reference from another entry — use getEntry. Pass the collection name and the entry id.

---
import { getEntry } from 'astro:content';

const about = await getEntry('pages', 'about');

if (!about) {
  // getEntry returns undefined for a missing id
  return Astro.redirect('/404');
}

const { Content } = await about.render();
---
<h1>{about.data.title}</h1>
<Content />

You can also pass a single object: getEntry({ collection: 'pages', id: 'about' }). This object form is what schema reference() fields resolve to, which makes it convenient for following relationships.

import { getEntry, getCollection } from 'astro:content';

// A post whose schema has: author: reference('authors')
const post = await getEntry('blog', 'hello-world');
const author = await getEntry(post.data.author); // resolves the reference

To resolve many references at once, use getEntries:

import { getEntries } from 'astro:content';

// relatedPosts: z.array(reference('blog'))
const post = await getEntry('blog', 'hello-world');
const related = await getEntries(post.data.relatedPosts);

Generating pages from a collection

Pair getCollection with getStaticPaths to statically generate one route per entry. Pass the whole entry through props so the page can render it without a second query.

---
// src/pages/blog/[id].astro
import { getCollection, render } from 'astro:content';
import type { GetStaticPaths } from 'astro';

export const getStaticPaths = (async () => {
  const posts = await getCollection('blog');
  return posts.map((post) => ({
    params: { id: post.id },
    props: { post },
  }));
}) satisfies GetStaticPaths;

const { post } = Astro.props;
const { Content } = await render(post);
---
<article>
  <h1>{post.data.title}</h1>
  <Content />
</article>

Output:

/blog/hello-world  →  static HTML, zero JS shipped
/blog/second-post  →  static HTML, zero JS shipped

Each route is fully prerendered and ships no client JavaScript unless you add an island with a client:* directive.

Best practices

  • Filter inside getCollection rather than after the fact — it keeps draft entries out of the array entirely and reads cleanly.
  • Always handle the undefined case from getEntry; a missing id does not throw, it returns undefined.
  • Sort by comparing date values numerically (b.data.pubDate.valueOf() - a.data.pubDate.valueOf()), not by string comparison.
  • Use id (not the legacy slug) when keying routes and building links in Astro 5.
  • Resolve schema reference() fields with getEntry/getEntries instead of re-querying the whole collection.
  • Keep queries in the component script fence or server-side modules so the work stays at build time and pages remain zero-JS by default.
Last updated June 14, 2026
Was this helpful?