Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Data collections and collection references #3233

Merged
merged 51 commits into from
May 18, 2023
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
a887012
edit: add YAML and JSON to preamble
bholmesdev May 11, 2023
e46f44f
edit: organizing multi-collections
bholmesdev May 11, 2023
85a8063
edit: defining collections
bholmesdev May 11, 2023
2ba25b9
edit: add types to defining multi-collections
bholmesdev May 11, 2023
e512c1e
edit: add more types to defining multi-collections
bholmesdev May 11, 2023
4874ff8
edit: one more `type: 'content'`
bholmesdev May 11, 2023
6c7ab0e
edit: collection querying updates
bholmesdev May 11, 2023
3faf595
edit: getEntryBySlug() -> getEntry()
bholmesdev May 11, 2023
a41f6d3
edit: one last `type: 'content'`
bholmesdev May 11, 2023
34f95ef
edit: update define instruction comments
bholmesdev May 11, 2023
82fbbde
edit: add data collection example
bholmesdev May 11, 2023
ae796ba
new: add "defining collection references"
bholmesdev May 11, 2023
07e7bea
edit: better reference example
bholmesdev May 11, 2023
366c6dc
new: "resolving references" section
bholmesdev May 11, 2023
72babfd
nit: data -> entry data
bholmesdev May 11, 2023
a983414
new: `type` API reference
bholmesdev May 11, 2023
29bfc00
new: `getEntry()` and `getEntries()` API reference
bholmesdev May 11, 2023
cbdeddf
chore: p tags to be safe
bholmesdev May 11, 2023
f616d9d
nit: simplify wording
bholmesdev May 11, 2023
65a9de0
new: reference() API reference (heh)
bholmesdev May 11, 2023
929e9c2
edit: add third collection to organizing section
bholmesdev May 11, 2023
4eb0a49
edit: update Collection Entry Type section
bholmesdev May 11, 2023
44206dd
nit: ahead of time
bholmesdev May 11, 2023
d66e204
nit: missed closing paren
bholmesdev May 11, 2023
26ffb38
nit: missed closing paren
bholmesdev May 11, 2023
41af67e
nit: bad comma
bholmesdev May 11, 2023
f68b439
nit: bad comma
bholmesdev May 11, 2023
ad552e1
nit: `oneBlogPost` -> `graceHopperProfile`
bholmesdev May 11, 2023
f7b1c72
edit: content-authoring
bholmesdev May 15, 2023
b20e774
fix: single author example
bholmesdev May 15, 2023
dcfbaf6
edit: you can also filter by `slug`
bholmesdev May 15, 2023
94fc260
edit: cleaner querying link
bholmesdev May 15, 2023
8d63281
edit: `getEntries()` cleanup
bholmesdev May 15, 2023
75861a0
edit: clearer `collection` docs
bholmesdev May 15, 2023
6654060
chore: inline defines -> explicit
bholmesdev May 16, 2023
f73cd1f
edit: Astro recommends splitting -> you must
bholmesdev May 16, 2023
a7e0259
chore: caution deprecated
bholmesdev May 16, 2023
ba88b9a
edit: merge `getEntry()` examples into one
bholmesdev May 16, 2023
2e5973a
edit: add `getEntries()` to "query functions"
bholmesdev May 16, 2023
9ad26bf
chore: Content-only header -> available for list
bholmesdev May 16, 2023
5cff403
edit: make references explanation less abstract
bholmesdev May 16, 2023
6e1eb7e
edit: add "top-level"
bholmesdev May 16, 2023
cf1ba03
chore: Markdown or MDX -> `type: 'content'`
bholmesdev May 16, 2023
c2d3a49
.). oh god
bholmesdev May 16, 2023
e91a5d3
edit: way better Content collections lead-in
bholmesdev May 17, 2023
2a063a1
edit: much better collection entry lead-in
bholmesdev May 17, 2023
5420f79
edit: commaaaaaaan
bholmesdev May 17, 2023
a9dd9be
edit: clean up resolving references
bholmesdev May 17, 2023
0cac3a6
Merge branch 'feat/data-collections-references' of github.com:withast…
bholmesdev May 17, 2023
b6f0e48
edit: runnin runnin and runnin runnin and runnin runnin
bholmesdev May 18, 2023
fc64768
Merge branch 'main' into feat/data-collections-references
sarah11918 May 18, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 147 additions & 43 deletions src/content/docs/en/guides/content-collections.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ import TypeScriptSettingTabs from '~/components/tabs/TypeScriptSettingTabs.astro

bholmesdev marked this conversation as resolved.
Show resolved Hide resolved
## 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 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.
bholmesdev marked this conversation as resolved.
Show resolved Hide resolved

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`). 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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`). 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`). You can use any filename you want, or begin your filename with an underscore to [exclude pages from the build](/en/core-concepts/routing/#excluding-pages). We recommend using a consistent naming scheme (lower-case, dashes instead of spaces) to make it easier to find and organize your content, but this is not required.

Just had an issue with someone over the underscore thing this morning, so I think this is a good place to qualify "you can use any filename you want"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! I agree documenting the underscore pattern is useful, but this isn't quite right. Underscore files will still be included in the build if you're using them for, say, relative image paths or MDX component imports. They're just excluded from collection queries. Wondering if this is the best place to explain that given it's the intro?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, so rather than get into more detail here, what about dropping that you can name them whatever you want entirely:

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.


<FileTree>
- src/content/
Expand Down Expand Up @@ -52,7 +52,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.

Expand All @@ -61,11 +61,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
sarah11918 marked this conversation as resolved.
Show resolved Hide resolved
</FileTree>


Expand Down Expand Up @@ -134,28 +135,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:
bholmesdev marked this conversation as resolved.
Show resolved Hide resolved

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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nicely done here, Ben!

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,
Expand All @@ -169,9 +173,18 @@ You can use `defineCollection()` as many times as you want to create multiple sc
```ts
// src/content/config.ts
export const collections = {
'blog': defineCollection({ /* ... */ }),
'newsletter': defineCollection({ /* ... */ }),
'profile-authors': defineCollection({ /* ... */ }),
'blog': defineCollection({
type: 'content',
schema: z.object({ /* ... */ })
}),
'newsletter': defineCollection({
type: 'content',
schema: z.object({ /* ... */ })
}),
'authors': defineCollection({
type: 'data',
schema: z.object({ /* ... */ })
}),
bholmesdev marked this conversation as resolved.
Show resolved Hide resolved
};
```

Expand All @@ -181,14 +194,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,
};
```

Expand All @@ -200,7 +221,8 @@ 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 });
const blogCollection = defineCollection({ type: 'content', schema: blogSchema });

// Export the blog collection, using an external schema from 'my-blog-theme'
export const collections = {
'blog': blogCollection,
Expand Down Expand Up @@ -245,9 +267,52 @@ defineCollection({
})
```

### Defining collection references

Collection entries can also "reference" other related entries. A common example is a blog post that references reusable author profiles stored as JSON, or related post URLs stored in the same collection.

You can define these relationships using the `reference()` utility from your collection schema. This accepts a collection name and validates the entry identifier(s) in your frontmatter or data file.
bholmesdev marked this conversation as resolved.
Show resolved Hide resolved

```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.

Expand All @@ -261,26 +326,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
### Resolving references

When using [collection references](#defining-collection-references), you can use the `getEntry()` and `getEntries()` functions to retrieve the referenced entry `data`.
bholmesdev marked this conversation as resolved.
Show resolved Hide resolved

```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);
---

<h1>{blogPost.data.title}</h1>
<p>Authors: {authors.map(a => a.data.name).join(', ')}</p>
bholmesdev marked this conversation as resolved.
Show resolved Hide resolved

<!-- ... -->

<h2>You might also like:</h2>
{relatedPosts.map(p => (
<a href={p.slug}>{p.data.title}</a>
))}
```

### Filtering collection queries

`getCollection()` takes an optional "filter" callback that allows you to filter your query based on an entry's `id`, `slug`, or `data` (frontmatter) properties.
bholmesdev marked this conversation as resolved.
Show resolved Hide resolved

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:
:::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 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;
Expand Down Expand Up @@ -342,16 +445,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 `<Content />` 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 `<Content />` 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();
---
<p>Written by: {entry.data.author}</p>
<p>Published on: {entry.data.published.toDateString()}</p>
<Content />
```

Expand Down Expand Up @@ -391,20 +494,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");
Expand All @@ -429,8 +532,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(),
Expand Down Expand Up @@ -564,7 +668,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.
Expand Down Expand Up @@ -619,8 +723,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();
---
<!--
Expand All @@ -631,5 +735,5 @@ const { remarkPluginFrontmatter } = await blogPost.render();
<p>{blogPost.data.title} — {remarkPluginFrontmatter.readingTime}</p>
```

The remark and rehype pipelines only runs when your content is rendered, which explains why `remarkPluginFrontmatter` is only available after you call `render()` on your content entry. In contrast, `getCollection()` and `getEntryBySlug()` cannot return these values directly because they do not render your content.
The remark and rehype pipelines only runs when your content is rendered, which explains why `remarkPluginFrontmatter` is only available after you call `render()` on your content entry. In contrast, `getCollection()` and `getEntry()` cannot return these values directly because they do not render your content.
bholmesdev marked this conversation as resolved.
Show resolved Hide resolved

Loading