Skip to content

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

Each 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-01
tags: ["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.ts
import { defineCollection } from "astro:content";
// if it give error run `npx astro sync`
const blogCollection = defineCollection({});
export const collections = {
blog: blogCollection,
};

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.ts
import { 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.

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.

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.ts
export function formatDate(date: Date): string {
return new Intl.DateTimeFormat("en-IN", {
dateStyle: "medium",
}).format(date);
}
---
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>
))}

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.

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>

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} />
))}

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} />

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.ts
export function capitalize(word: string): string {
return word.charAt(0).toUpperCase() + word.slice(1);
}
---
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>
---
// src/pages/tags/[tag].astro
import { 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.

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.