
How to reference one collection from another in Astro?
To reference one collection from another in Astro, use the reference() function from astro:content in your collection schema. For example, author: reference("authors") in a blog schema means each post's author frontmatter field must contain the id of an entry in the authors collection, and relatedPosts: z.array(reference("blog")) accepts a list of post IDs from the same collection. Astro turns each value into an object like { collection: "authors", id: "ada" }, which you then pass to getEntry() for a single reference or getEntries() for an array to get the full, typed entry.
References are how you avoid copying the same data into every file. An author's name, bio and avatar live once in an authors collection, and every post just points at them. In this article you'll learn how to define references, write them in frontmatter and JSON, resolve them in pages, build reverse lookups such as "all posts by this author", handle references that point at missing entries, and decide when a reference is the right tool. Everything was tested on an Astro 7.4 project, and the warnings and errors quoted are from those builds.
Setting Up Two Collections
References need at least two collections, or one collection that points at itself. Here's a blog collection whose posts reference an authors collection and other posts:
// src/content.config.ts
import { defineCollection, reference } from "astro:content";
import { glob, file } from "astro/loaders";
import { z } from "astro/zod";
const blog = defineCollection({
loader: glob({ base: "./src/content/blog", pattern: "**/*.{md,mdx}" }),
schema: z.object({
title: z.string(),
pubDate: z.coerce.date(),
// One author from the `authors` collection
author: reference("authors"),
// Any number of posts from this same collection
relatedPosts: z.array(reference("blog")).default([]),
}),
});
const authors = defineCollection({
loader: file("src/data/authors.json"),
schema: z.object({
name: z.string(),
email: z.email(),
website: z.url().optional(),
}),
});
export const collections = { blog, authors };
The two collections use different loaders, Markdown files for posts and a single JSON file for authors, and that's fine. References work between any build-time collections, whatever their loaders. If you're still getting comfortable with schemas, how to define a content collection schema with Zod covers the rest of the field types used here.
The Referenced Data
The authors file gives each entry an id, which is what posts will point at:
[
{ "id": "sajjad", "name": "Sajjad Hossain", "email": "hello@example.com", "website": "https://sajjad.me" },
{ "id": "ada", "name": "Ada Lovelace", "email": "ada@example.com" }
]
With glob() collections, the ID is generated from the file name instead. A file at src/content/blog/second-post.md has the ID second-post, and a file in a subfolder keeps its folder, as in 2025/year-in-review.
Writing References in Frontmatter
In a post, a reference is just the target entry's ID:
---
title: "My first post"
pubDate: 2026-09-01
author: sajjad
relatedPosts: ["second-post"]
---
This is my first post.
That's all an author needs to write. There's no path, no file extension and no collection name, because the schema already knows which collection each field points at.
What Astro Stores
After validation, reference() transforms each ID into an object holding both the collection and the ID. Dumping the fields from my test post gave:
author: {"id":"sajjad","collection":"authors"}
relatedPosts: [{"id":"second-post","collection":"blog"}]
So post.data.author is not the author's data. It's a pointer to it. You can read post.data.author.id directly when the ID is all you need, for example to build a link to /authors/sajjad/, without loading the author at all.
Resolving a Single Reference
To get the referenced entry, pass the reference object to getEntry():
---
// src/pages/blog/[...id].astro
import { getCollection, getEntry, render } from "astro:content";
export async function getStaticPaths() {
const posts = await getCollection("blog");
return posts.map((post) => ({
params: { id: post.id },
props: { post },
}));
}
const { post } = Astro.props;
const author = await getEntry(post.data.author);
const { Content } = await render(post);
---
<article>
<h1>{post.data.title}</h1>
<p>By {author?.data.name}</p>
<Content />
</article>
getEntry() has two call signatures: getEntry("authors", "sajjad") with a collection name and ID, and getEntry(reference) with the object from a reference field. Both return the full entry, typed from the target collection's schema, or undefined if no entry matches. That's why the template uses author?.data.name.
The rendered page showed "By Sajjad Hossain", with the name pulled from the JSON file.
Resolving an Array of References
For arrays, getEntries() resolves every reference in one call:
---
// src/pages/blog/[...id].astro (excerpt)
import { getEntries } from "astro:content";
const related = (await getEntries(post.data.relatedPosts)).filter(
(entry) => entry !== undefined,
);
---
{related.length > 0 && (
<aside>
<h2>Related</h2>
<ul>
{related.map((p) => (
<li><a href={`/blog/${p.id}/`}>{p.data.title}</a></li>
))}
</ul>
</aside>
)}
The .filter() is not decoration. The next section explains why it's there.
What Happens When a Reference Is Broken
What if a post references an author or post that doesn't exist? I tested both cases, and the behaviour is worth knowing because it's not what you might expect.
When I changed a post's author to grace, an ID not in the authors file, content sync logged an error but the build carried on:
[ERROR] [content] Invalid content reference: entry "second-post" in collection "blog" (field: author) references "grace" in collection "authors", but that entry does not exist.
Then, when the page called getEntry() on that reference, Astro logged a warning and returned undefined:
[WARN] [content] Entry authors → grace was not found.
Because my template used author?.data.name, the page built with an empty author name rather than crashing.
Arrays behave the same way, with a twist. When I added a missing-post ID to relatedPosts, getEntries() returned an array containing undefined for the missing entry, even though its TypeScript return type doesn't include undefined. My first version of the template mapped over the array and read p.id, and the build failed:
[ERROR] [build] Caught error rendering /blog/first-post: TypeError: Cannot read properties of undefined (reading 'id')
Filtering with entry !== undefined fixed it, and the page rendered with only the valid related post. astro check still reported 0 errors with the filter in place, because TypeScript narrows the array type from the predicate.
Making Broken References Fail the Build
If you'd rather a broken reference stop the build, check the result and throw:
const author = await getEntry(post.data.author);
if (!author) {
throw new Error(`Post "${post.id}" references a missing author: ${post.data.author.id}`);
}
On a static site, an error thrown during rendering fails astro build, so the mistake never ships. Whichever approach you choose, watch for the Invalid content reference line in your build logs.
Self-References: Related Posts
relatedPosts: z.array(reference("blog")) shows that a collection can reference itself. The same pattern works for:
- Series: a
seriescollection of entries, each post referencing the series it belongs to. - Previous and next:
previous: reference("blog").optional()for hand-curated reading orders. - Parent pages:
parent: reference("docs").optional()for a documentation hierarchy.
Mark references .optional() when not every entry has one, or use .default([]) for arrays so you can always loop over them.
Reverse Lookups: All Posts by an Author
References point one way, from post to author. To go the other way, query the posts and filter on the reference's id:
---
// src/pages/authors/[id].astro
import { getCollection } from "astro:content";
export async function getStaticPaths() {
const authors = await getCollection("authors");
const posts = await getCollection("blog", ({ data }) => !data.draft);
return authors.map((author) => ({
params: { id: author.id },
props: {
author,
posts: posts.filter((post) => post.data.author.id === author.id),
},
}));
}
const { author, posts } = Astro.props;
---
<h1>{author.data.name}</h1>
<p>{posts.length} posts</p>
<ul>
{posts.map((post) => (
<li><a href={`/blog/${post.id}/`}>{post.data.title}</a></li>
))}
</ul>
The page for Ada built with "3 posts" and links to each one. Fetching all posts once, outside the map(), keeps this efficient: one query instead of one per author. Routes generated like this are explained further in dynamic routes with getStaticPaths.
Several Authors Per Post
Co-authored posts need an array of references instead of one:
authors: z.array(reference("authors")).min(1),
---
title: "Writing together"
authors: ["sajjad", "ada"]
---
Resolve them with getEntries(post.data.authors) and join the names for the byline. For reverse lookups, filter with post.data.authors.some((ref) => ref.id === author.id).
References vs Embedding vs Plain Strings
You don't always need a reference. Here's how the options compare:
| Approach | Example | Validated against a collection | Good for |
|---|---|---|---|
| Reference | author: reference("authors") | Yes | Shared entities with their own data and pages |
| Embedded object | author: z.object({ name: z.string() }) | No | Data used in one place only |
| Plain string | tags: z.array(z.string()) | No | Free-form labels such as tags |
Tags are usually better as plain strings, because they have no data of their own beyond the name. If you later want descriptions or images for each tag or category, turn them into a collection and switch the field to a reference. How to add tags and category pages to an Astro blog shows the plain-string approach.
Common Mistakes
- Using the file name with its extension: write
second-post, notsecond-post.md. - Ignoring case:
glob()lowercases generated IDs, soYear-In-Review.mdbecomesyear-in-review. Reference the lowercase form. - Treating the field as data:
post.data.author.namedoesn't exist. The field holds{ collection, id }. Resolve it first. - Forgetting the folder: an entry in a subfolder has the folder in its ID, such as
2025/year-in-review. - Assuming the array is clean: filter
undefinedout ofgetEntries()results.
FAQ: Collection References in Astro
In the schema, reference() validates that the field contains an ID and transforms it into an object with collection and id properties, such as collection authors and id ada. You resolve that object with getEntry() or getEntries().
Yes. A blog schema can use z.array(reference("blog")) for related posts, or reference("docs") for a parent page. The same getEntry() and getEntries() functions resolve them.
Not by itself, in my testing with Astro 7.4. Content sync logs an Invalid content reference error, and getEntry() returns undefined with a warning. Throw an error in your page if you want missing references to stop the build.
Yes. References work between any build-time collections regardless of loader. Entries loaded with file() use the id property from each object, which is the value you write in the referencing field.
Query the posts collection and filter where post.data.author.id equals the author's id. Do the query once and filter per author inside getStaticPaths() for efficiency.
No. The reference object already contains the id, so post.data.author.id is enough for building links. Call getEntry() only when you need the referenced entry's data.
Conclusion
reference() links collections together by ID. Define the field with reference("collection"), or an array of them, write plain IDs in your frontmatter or JSON, and resolve them with getEntry() and getEntries() to get fully typed entries from the target collection. Going the other way is a simple filter on reference.id.
The one thing to plan for is missing entries. A broken reference logs an error but doesn't stop the build, getEntry() returns undefined, and getEntries() can include undefined in its result. Guard for that in your templates, or throw if you want strictness, and references become a reliable way to keep shared data in one place.


