Content Collections
Astro’s Content Collections feature helps you manage a lot of Markdown files in an organized way, such as blog posts.
src/content/blog/ ├─ post-1.md ├─ post-2.mdEach Markdown file has its own frontmatter (the metadata at the top) and then the actual content below it:
---title: "My First Post"date: 2024-01-01tags: ["astro", "webdev"]author: "Kumar Sahil"slug: "my-first-post"---
Content goes here.To tell Astro that this folder is a content collection, you create a config file:
// src/content/config.tsimport { defineCollection } from "astro:content";// if it give error run `npx astro sync`const blogCollection = defineCollection({});
export const collections = { blog: blogCollection,};Collection Schema
Section titled “Collection Schema”A schema is just a set of rules that says exactly what fields each post should have, and what type each field should be (text, date, list, etc). This helps catch mistakes early, instead of your site breaking later.
// src/content/config.tsimport { defineCollection, z } from "astro:content";// if it give error run `npx astro sync`const blogCollection = defineCollection({ type: "blog", // optional but help to distinguish between multiple collections // it is default to "content" if not specified schema: z.object({ title: z.string(), date: z.date(), tags: z.array(z.string()), author: z.string(), slug: z.string().optional(), }),});
export const collections = { blog: blogCollection,};In simple words: this schema says “title must be text, date must be a date, tags must be a list of text values, author must be text, and slug is optional text.” If a post doesn’t match this shape, Astro will warn you, instead of letting a broken page slip through.
Querying Collections
Section titled “Querying Collections”To actually read your blog posts and show them on a page, you use getCollection().
---import { getCollection } from "astro:content";
import type { CollectionEntry } from "astro:content";
const posts: CollectionEntry<"blog">[] = await getCollection("blog");---
{posts.map((post) => ( <article> <h2>{post.data.title}</h2> <time>{post.data.date.toDateString()}</time> {post.data.tags.map(tag => <span>{tag}</span>)} </article>))}In plain words: this code fetches every post in the “blog” collection, then loops through each one and displays its title, date, and tags.
Formatting and Sorting Dates
Section titled “Formatting and Sorting Dates”A Helper to Format Dates Nicely
Section titled “A Helper to Format Dates Nicely”Dates stored in your Markdown files are plain date objects. To show them in a nice, readable format, it helps to create a small reusable helper function.
// src/utils/formatDate.tsexport function formatDate(date: Date): string { return new Intl.DateTimeFormat("en-IN", { dateStyle: "medium", }).format(date);}Using the Helper
Section titled “Using the Helper”---import { getCollection } from "astro:content";
import type { CollectionEntry } from "astro:content";
import { formatDate } from "../utils/formatDate";
const posts: CollectionEntry<"blog">[] = await getCollection("blog");---
{posts.map((post) => ( <article> <h2>{post.data.title}</h2> <time>{formatDate(post.data.date)}</time> <!-- here is use formated date --> {post.data.tags.map(tag => <span>{tag}</span>)} </article>))}Sorting Posts by Date
Section titled “Sorting Posts by Date”If you want your newest blog posts to show up first, you need to sort them by date, from newest to oldest.
---import { getCollection } from "astro:content";
import type { CollectionEntry } from "astro:content";
import { formatDate } from "../utils/formatDate";
const posts: CollectionEntry<"blog">[] = await getCollection("blog");
posts.sort((a: CollectionEntry<"blog">, b: CollectionEntry<"blog">) => { return b.data.date.valueOf() - a.data.date.valueOf();});---
{posts.map((post) => ( <article> <h2>{post.data.title}</h2> <time>{formatDate(post.data.date)}</time> <!-- here is use formated date --> {post.data.tags.map(tag => <span>{tag}</span>)} </article>))}In simple words: b.data.date.valueOf() - a.data.date.valueOf() simply means “put the newer date first.” Note that the method name is valueOf() with a capital “O” - this is a common typo to watch out for.
Article Card Component
Section titled “Article Card Component”Instead of repeating the same article markup again and again, it’s a good idea to make one reusable “Article Card” component.
---const { post } = Astro.props;import { formatDate } from "../utils/formatDate";import type { CollectionEntry } from "astro:content";
interface Props { post: CollectionEntry<"blog">;}---
<article> <h2>{post.data.title}</h2> <time>{formatDate(post.data.date)}</time> {post.data.tags.map(tag => <span>{tag}</span>)}</article>Showing Articles on the Homepage
Section titled “Showing Articles on the Homepage”Here’s how you’d use the Article Card component to show only the 6 latest posts on your homepage.
---import { getCollection } from "astro:content";import type { CollectionEntry } from "astro:content";import ArticleCard from "../components/ArticleCard.astro";
const posts: CollectionEntry<"blog">[] = (await getCollection("blog")) .sort((a: CollectionEntry<"blog">, b: CollectionEntry<"blog">) => { return b.data.date.valueOf() - a.data.date.valueOf(); }) .slice(0, 6); // get only 6 latest posts---
{posts.map((post) => ( <ArticleCard post={post} />))}Showing Only the Most Recent Article
Section titled “Showing Only the Most Recent Article”Sometimes you just want to highlight the single newest post, for example in a “featured post” section.
---import { getCollection } from "astro:content";import type { CollectionEntry } from "astro:content";import ArticleCard from "../components/ArticleCard.astro";
const latestPost: CollectionEntry<"blog"> = (await getCollection("blog")) .sort((a: CollectionEntry<"blog">, b: CollectionEntry<"blog">) => { return b.data.date.valueOf() - a.data.date.valueOf(); })[0]; // get only latest post---
<ArticleCard post={latestPost} />A Helper to Capitalize Words
Section titled “A Helper to Capitalize Words”Tags are usually stored in lowercase (like astro), but you’ll often want to display them with the first letter capitalized (like Astro).
// src/utils/capitalize.tsexport function capitalize(word: string): string { return word.charAt(0).toUpperCase() + word.slice(1);}Tags Component
Section titled “Tags Component”---import { capitalize } from "../utils/capitalize";const { tags } = Astro.props;interface Props { tags: string[];}---<div> {tags.map((tag) => ( <a href={`/tags/${tag}`}>#{capitalize(tag)}</a> ))}</div>A Page That Filters Posts by Tag
Section titled “A Page That Filters Posts by Tag”---// src/pages/tags/[tag].astroimport { getCollection } from "astro:content";import type { CollectionEntry } from "astro:content";
const { tag } = Astro.params;
if (tag === undefined) { throw new Error("Tag is required");}
const posts: CollectionEntry<"blog">[] = (await getCollection("blog")) .filter((post) => post.data.tags.includes(tag));---
{posts.map((post) => ( <article> <h2>{post.data.title}</h2> <time>{post.data.date.toDateString()}</time> </article>))}In plain words: this page looks at the URL (for example /tags/astro), grabs the word “astro” from it, and then shows only the posts that have that tag.
Showing All Unique Tags (for a Footer)
Section titled “Showing All Unique Tags (for a Footer)”If you want a list of every tag used across all your posts (with no repeats), shown somewhere like a footer:
---import { getCollection } from "astro:content";import type { CollectionEntry } from "astro:content";
const posts: CollectionEntry<"blog">[] = await getCollection("blog");
const allTags: string[] = posts.flatMap((post) => post.data.tags);
const uniqueTags: string[] = Array.from(new Set(allTags));---
<div> {uniqueTags.map((tag) => ( <a href={`/tags/${tag}`}>#{tag}</a> ))}</div>In simple words: flatMap takes the tags from every post and combines them into one single, flat list. Then Array.from(new Set(...)) removes any duplicate tags, so each tag is shown only once.