From b18535bb3c541bad3a4f82fb8309869ec6fd6523 Mon Sep 17 00:00:00 2001
From: Ben Holmes
Date: Thu, 18 May 2023 12:45:48 -0400
Subject: [PATCH] Data collections and collection references (#3233)
Co-authored-by: Yan Thomas <61414485+Yan-Thomas@users.noreply.github.com>
Co-authored-by: Sarah Rainsberger
---
.../docs/en/guides/content-collections.mdx | 205 ++++++++++++++----
.../docs/en/reference/api-reference.mdx | 133 +++++++++++-
2 files changed, 284 insertions(+), 54 deletions(-)
diff --git a/src/content/docs/en/guides/content-collections.mdx b/src/content/docs/en/guides/content-collections.mdx
index 9b5957c340be3..db7a0ec5b9599 100644
--- a/src/content/docs/en/guides/content-collections.mdx
+++ b/src/content/docs/en/guides/content-collections.mdx
@@ -15,14 +15,13 @@ import TypeScriptSettingTabs from '~/components/tabs/TypeScriptSettingTabs.astro
-**Content collections** are the best way to work with Markdown and MDX in any Astro project. Content collections are a feature of Astro that help manage your content files in a project. Collections help to organize your content, validate your frontmatter, and provide automatic TypeScript type-safety for all of your content.
-
+**Content collections** are the best way to manage and author content in any Astro project. Collections help to organize your documents, validate your frontmatter, and provide automatic TypeScript type-safety for all of your content.
## What are Content Collections?
-A **content collection** is any directory inside the reserved `src/content` project directory, such as `src/content/newsletter` and `src/content/blog`. Only content collections are allowed inside the `src/content` directory. This directory cannot be used for anything else.
+A **content collection** is any top-level directory inside the reserved `src/content` project directory, such as `src/content/newsletter` and `src/content/authors`. Only content collections are allowed inside the `src/content` directory. This directory cannot be used for anything else.
-A **content entry** is any piece of content stored inside of your content collection directory. Content entries are stored as either Markdown (`.md`) or MDX (`.mdx`) files. You can use any filename you want, but we recommend using a consistent naming scheme (lower-case, dashes instead of spaces) to make it easier to find and organize your content.
+A **collection entry** is any piece of content stored inside of your content collection directory. Entries can use content authoring formats including Markdown (`.md`) and MDX (`.mdx`) or as data formats including YAML (`.yaml`) and JSON (`.json`). We recommend using a consistent naming scheme (lower-case, dashes instead of spaces) for your files to make it easier to find and organize your content, but this is not required.
- src/content/
@@ -52,7 +51,7 @@ echo "\n.astro" >> .gitignore
### Organizing with multiple collections
-If two files represent different kinds of content (e.g. a blog post and an author profile), they most likely belong in different collections. This is important because many features (frontmatter validation, automatic TypeScript type-safety) require that all entries in a collection share a similar frontmatter structure.
+If two files represent different kinds of content (e.g. a blog post and an author profile), they most likely belong in different collections. This is important because many features (frontmatter validation, automatic TypeScript type-safety) require that all entries in a collection share a similar structure.
If you find yourself working with different types of content, you should create multiple collections to represent each type. You can create as many different collections in your project as you'd like.
@@ -61,11 +60,12 @@ If you find yourself working with different types of content, you should create
- **newsletter/**
- week-1.md
- week-2.md
- - week-3.md
- - **authors/** split different content types into new collections
- - grace-hopper.md
- - alan-turing.md
- - batman.md
+ - **blog/**
+ - post-1.md
+ - post-2.md
+ - **authors/**
+ - grace-hopper.json
+ - alan-turing.json
@@ -89,7 +89,7 @@ For example, you can use the following directory structure to organize i18n tran
The `src/content/config.ts` file is optional. However, choosing not to define your collections will disable some of their best features like frontmatter schema validation or automatic TypeScript typings.
:::
-To get the most out of your content collections, create a `src/content/config.ts` file in your project (`.js` and `.mjs` extensions are also supported). This is a special file that Astro will automatically load and use to configure your content collections.
+To get the most out of your content collections, create a `src/content/config.ts` file in your project (`.js` and `.mjs` extensions are also supported.) This is a special file that Astro will automatically load and use to configure your content collections.
```ts
@@ -134,28 +134,31 @@ If you use `.js` or `.mjs` files in an Astro project, you can enable IntelliSens
### Defining a collection schema
-Schemas enforce consistent frontmatter within a collection. A schema **guarantees** that your frontmatter exists in a predictable form when you need to reference or query it. If any file violates its collection schema, Astro will provide a helpful error to let you know.
+Schemas enforce consistent frontmatter or entry data within a collection. A schema **guarantees** that this data exists in a predictable form when you need to reference or query it. If any file violates its collection schema, Astro will provide a helpful error to let you know.
Schemas also power Astro's automatic TypeScript typings for your content. When you define a schema for your collection, Astro will automatically generate and apply a TypeScript interface to it. The result is full TypeScript support when you query your collection, including property autocompletion and type-checking.
-To create your first content schema, create a `src/content/config.ts` file if one does not already exist (`.js` and `.mjs` extensions are also supported). This file should:
+To define your first collection, create a `src/content/config.ts` file if one does not already exist (`.js` and `.mjs` extensions are also supported.) This file should:
-1. Import the proper utilities from `astro:content`.
-2. Define each collection that you'd like to validate with a schema.
-3. Export a single `collections` object to register your collections.
+1. **Import the proper utilities** from `astro:content`.
+2. **Define each collection that you'd like to validate.** This includes a `type` specifying whether the collection contains content authoring formats like Markdown (`type: 'content'`) or data formats like JSON or YAML (`type: 'data'`). It also includes a `schema` that defines the shape of your frontmatter or entry data.
+3. **Export a single `collections` object** to register your collections.
```ts
// src/content/config.ts
// 1. Import utilities from `astro:content`
import { z, defineCollection } from 'astro:content';
-// 2. Define a schema for each collection you'd like to validate.
+
+// 2. Define a `type` and `schema` for each collection
const blogCollection = defineCollection({
+ type: 'content',
schema: z.object({
title: z.string(),
tags: z.array(z.string()),
image: z.string().optional(),
}),
});
+
// 3. Export a single `collections` object to register your collection(s)
export const collections = {
'blog': blogCollection,
@@ -168,10 +171,23 @@ You can use `defineCollection()` as many times as you want to create multiple sc
```ts
// src/content/config.ts
+const blogCollection = defineCollection({
+ type: 'content',
+ schema: z.object({ /* ... */ })
+});
+const newsletter = defineCollection({
+ type: 'content',
+ schema: z.object({ /* ... */ })
+});
+const authors = defineCollection({
+ type: 'data',
+ schema: z.object({ /* ... */ })
+});
+
export const collections = {
- 'blog': defineCollection({ /* ... */ }),
- 'newsletter': defineCollection({ /* ... */ }),
- 'profile-authors': defineCollection({ /* ... */ }),
+ 'blog': blogCollection,
+ 'newsletter': newsletter,
+ 'authors': authors,
};
```
@@ -181,14 +197,22 @@ As your project grows, you are also free to reorganize your codebase and move lo
// src/content/config.ts
// 1. Import your utilities and schemas
import { defineCollection } from 'astro:content';
-import {blogSchema, newsletterSchema} from '../schemas';
+import { blogSchema, authorSchema } from '../schemas';
+
// 2. Define your collections
-const blogCollection = defineCollection({ schema: blogSchema });
-const newsletterCollection = defineCollection({ schema: newsletterSchema });
+const blogCollection = defineCollection({
+ type: 'content',
+ schema: blogSchema,
+});
+const authorCollection = defineCollection({
+ type: 'data',
+ schema: authorSchema,
+});
+
// 3. Export multiple collections to register them
export const collections = {
'blog': blogCollection,
- 'newsletter': newsletterCollection,
+ 'authors': authorCollection,
};
```
@@ -199,8 +223,9 @@ You can import collection schemas from anywhere, including external npm packages
```ts
// src/content/config.ts
-import {blogSchema} from 'my-blog-theme';
-const blogCollection = defineCollection({ schema: blogSchema });
+import { blogSchema } from 'my-blog-theme';
+const blogCollection = defineCollection({ type: 'content', schema: blogSchema });
+
// Export the blog collection, using an external schema from 'my-blog-theme'
export const collections = {
'blog': blogCollection,
@@ -245,9 +270,54 @@ defineCollection({
})
```
+### Defining collection references
+
+Collection entries can also "reference" other related entries.
+
+With the `reference()` function from the Collections API, you can define a property in a collection schema as an entry from another collection. For example, you can require that every `space-shuttle` entry includes a `pilot` property which uses the `pilot` collection's own schema for type checking, autocomplete, and validation.
+
+A common example is a blog post that references reusable author profiles stored as JSON, or related post URLs stored in the same collection:
+
+```ts
+import { defineCollection, reference, z } from 'astro:content';
+
+const blog = defineCollection({
+ type: 'content',
+ schema: z.object({
+ title: z.string(),
+ // Reference a single author from the `authors` collection by `id`
+ author: reference('authors'),
+ // Reference an array of related posts from the `blog` collection by `slug`
+ relatedPosts: z.array(reference('blog')),
+ })
+});
+
+const authors = defineCollection({
+ type: 'data',
+ schema: z.object({
+ name: z.string(),
+ portfolio: z.string().url(),
+ })
+});
+
+export const collections = { blog, authors };
+```
+
+This example blog post specifies the `slug`s of related posts and the `id` of the post author:
+
+```yaml title="src/content/blog/welcome.md"
+---
+title: "Welcome to my blog"
+author: ben-holmes # references `src/content/authors/ben-holmes.json`
+relatedPosts:
+- about-me # references `src/content/blog/about-me.md`
+- my-year-in-review # references `src/content/blog/my-year-in-review.md`
+---
+```
+
### Defining custom slugs
-Every content entry generates a URL-friendly `slug` property from its [file `id`](/en/reference/api-reference/#id). The slug is used to query the entry directly from your collection. It is also useful when creating new pages and URLs from your content.
+When using `type: 'content'`, every content entry generates a URL-friendly `slug` property from its [file `id`](/en/reference/api-reference/#id). The slug is used to query the entry directly from your collection. It is also useful when creating new pages and URLs from your content.
You can override an entry's generated slug by adding your own `slug` property to the file frontmatter. This is similar to the "permalink" feature of other web frameworks. `"slug"` is a special, reserved property name that is not allowed in your custom collection `schema` and will not appear in your entry's `data` property.
@@ -261,26 +331,64 @@ Your blog post content here.
## Querying Collections
-Astro provides two functions to query a collection and return one (or more) content entries: [`getCollection()`](/en/reference/api-reference/#getcollection) and [`getEntryBySlug()`](/en/reference/api-reference/#getentrybyslug).
+Astro provides two functions to query a collection and return one (or more) content entries: [`getCollection()`](/en/reference/api-reference/#getcollection) and [`getEntry()`](/en/reference/api-reference/#getentry).
```js
-import { getCollection, getEntryBySlug } from 'astro:content';
-// Get all entries from a collection. Requires the name of the collection as an argument.
+import { getCollection, getEntry } from 'astro:content';
+
+// Get all entries from a collection.
+// Requires the name of the collection as an argument.
+// Example: retrieve `src/content/blog/**`
const allBlogPosts = await getCollection('blog');
-// Get a single entry from a collection. Requires the name of the collection and the entry's slug as arguments.
-const oneBlogPost = await getEntryBySlug('blog', 'enterprise');
+
+// Get a single entry from a collection.
+// Requires the name of the collection and either
+// the entry `slug` (content collections) or `id` (data collections)
+// Example: retrieve `src/content/authors/grace-hopper.json`
+const graceHopperProfile = await getEntry('authors', 'grace-hopper');
```
Both functions return content entries as defined by the [`CollectionEntry`](/en/reference/api-reference/#collection-entry-type) type.
-#### Filtering collection queries
+### Accessing referenced data
+
+Any [references defined in your schema](#defining-collection-references) must be queried separately after first querying your collection entry. You can use the `getEntry()` function again, or `getEntries()`, to retrieve the referenced entry from the returned `data` object.
-`getCollection()` takes an optional "filter" callback that allows you to filter your query based on an entry's `id`, `slug`, or `data` (frontmatter) properties.
+```astro title="src/pages/blog/welcome.astro"
+---
+import { getEntry, getEntries } from 'astro:content';
+
+const { data } = await getEntry('blog', 'welcome');
+
+// Resolve a singular reference
+const author = await getEntry(data.author);
+// Resolve an array of references
+const relatedPosts = await getEntries(data.relatedPosts);
+---
+
+{blogPost.data.title}
+Author: {author.data.name}
+
+
+
+You might also like:
+{relatedPosts.map(p => (
+ {p.data.title}
+))}
+```
+
+### Filtering collection queries
+
+`getCollection()` takes an optional "filter" callback that allows you to filter your query based on an entry's `id` or `data` (frontmatter) properties. For collections of `type: content`, you can also filter based on `slug`.
+
+:::note
+The `slug` property is specific to content collections, and will not be available when filtering collections of JSON or YAML.
+:::
-You can use this to filter by any content criteria you like. For example, you can filter by frontmatter properties like `draft` to prevent any draft blog posts from publishing to your blog:
+You can use this to filter by any content criteria you like. For example, you can filter by properties like `draft` to prevent any draft blog posts from publishing to your blog:
```js
-// Example: Filter content entries with `draft: true` frontmatter
+// Example: Filter content entries with `draft: true`
import { getCollection } from 'astro:content';
const draftBlogEntries = await getCollection('blog', ({ data }) => {
return data.draft !== true;
@@ -342,16 +450,16 @@ const { post } = Astro.props;
### Rendering content to HTML
-Once queried, you can render a collection entry to HTML using the entry `render()` function property. Calling this function gives you access to rendered content and metadata, including both a `` component and a list of all rendered headings.
+Once queried, you can render Markdown and MDX entries to HTML using the entry `render()` function property. Calling this function gives you access to rendered content and metadata, including both a `` component and a list of all rendered headings.
```astro {5}
---
// src/pages/render-example.astro
-import { getEntryBySlug } from 'astro:content';
-const entry = await getEntryBySlug('blog', 'post-1');
+import { getEntry } from 'astro:content';
+const entry = await getEntry('blog', 'post-1');
const { Content, headings } = await entry.render();
---
-Written by: {entry.data.author}
+Published on: {entry.data.published.toDateString()}
```
@@ -391,20 +499,20 @@ This will generate a new page for every entry in the `blog` collection. For exam
### Building for server output (SSR)
-If you are building a dynamic website (using Astro's SSR support), you are not expected to generate any paths ahead-of-time during the build. Instead, your page should examine the request (using `Astro.request` or `Astro.params`) to find the `slug` on-demand, and then fetch it using [`getEntryBySlug()`](/en/reference/api-reference/#getentrybyslug).
+If you are building a dynamic website (using Astro's SSR support), you are not expected to generate any paths ahead of time during the build. Instead, your page should examine the request (using `Astro.request` or `Astro.params`) to find the `slug` on-demand, and then fetch it using [`getEntry()`](/en/reference/api-reference/#getentry).
```astro
---
// src/pages/posts/[...slug].astro
-import { getEntryBySlug } from "astro:content";
+import { getEntry } from "astro:content";
// 1. Get the slug from the incoming server request
const { slug } = Astro.params;
if (slug === undefined) {
throw new Error("Slug is required");
}
// 2. Query for the entry directly using the request slug
-const entry = await getEntryBySlug("blog", slug);
+const entry = await getEntry("blog", slug);
// 3. Redirect if the entry does not exist
if (entry === undefined) {
return Astro.redirect("/404");
@@ -429,8 +537,9 @@ This guide shows you how to convert an existing Astro project with Markdown file
```ts title="src/content/config.ts"
// Import utilities from `astro:content`
import { z, defineCollection } from "astro:content";
- // Define a schema for each collection you'd like to validate.
+ // Define a `type` and `schema` for each collection
const postsCollection = defineCollection({
+ type: 'content',
schema: z.object({
title: z.string(),
pubDate: z.date(),
@@ -564,7 +673,7 @@ This guide shows you how to convert an existing Astro project with Markdown file
```
:::note
- Any individual Markdown or MDX file imports should be replaced by [`getEntryBySlug()`](/en/reference/api-reference/#getentrybyslug).
+ Any individual Markdown or MDX file imports should be replaced by [`getEntry()`](/en/reference/api-reference/#getentry).
:::
8. Update the code that uses the publish date in the `layouts/MarkdownPostLayout.astro` file.
@@ -619,8 +728,8 @@ Astro supports remark or rehype plugins that [modify your frontmatter directly](
```astro "{ remarkPluginFrontmatter }"
---
-import { getEntryBySlug } from 'astro:content';
-const blogPost = await getEntryBySlug('blog', 'post-1');
+import { getEntry } from 'astro:content';
+const blogPost = await getEntry('blog', 'post-1');
const { remarkPluginFrontmatter } = await blogPost.render();
---