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.
| Field | Type | Description |
|---|---|---|
id | string | Unique entry identifier (e.g. my-first-post) |
data | inferred from schema | Validated frontmatter / data, fully typed |
body | string | Raw, unparsed body (Markdown/MDX entries) |
collection | string | The 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
idrather than the legacyslug. If you’re migrating from older code that readentry.slug, switch those references toentry.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
getCollectionrather than after the fact — it keeps draft entries out of the array entirely and reads cleanly. - Always handle the
undefinedcase fromgetEntry; a missingiddoes not throw, it returnsundefined. - Sort by comparing date values numerically (
b.data.pubDate.valueOf() - a.data.pubDate.valueOf()), not by string comparison. - Use
id(not the legacyslug) when keying routes and building links in Astro 5. - Resolve schema
reference()fields withgetEntry/getEntriesinstead 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.