Wed Mar 18 2026

Overview

Here, we will focus on the content collection framework to create web pages. The key components for a functional website consists of the following.

  • BaseLaout.astro: Defines the base page upon which each page would be defined. The homepage directly inherits the BaseLayout.
  • PostLayout.astro: It inherits the BaseLayout and defines additional structure common to each pages that are listed in the homepage.
  • index.astro: It defines the homepage. It lists all the web pages with their links. The layout of the page is inherited from the BaseLayout.
  • [...slug].astro: It renders each md or mdx pages in the collections into complete html pages. It injects the pages created into the PageLayout elements.
  • content.config.ts: The md and mdx pages are configured by defining astro:content module. The schema validation of each md[x] page defined in its frontmatter is done, and if any mismatch is found, the error is thrown at the build time.
  • content collection: The actual web pages.
  • global.css: The style page for all components.

Content collection

source: Content collections

A content collection is set of related data. It can be blog posts stored as md or mdx files, JSON, yaml, toml, etc. Content collection APIs allow to work with collections and, thus, helps to manage the files in a structured way.

A collection can be present in local storage or in the remote storage. The collection in the local storage are retrieved at the build time and are called build-time collections, whereas the collections in the remote, which are fetched during run-time are called live content collections.

Collections created in the directories cannot be used directly within astro. Using collection APIs, we first configure and register the collections to convert the collections into workable modules. Those modules are, then, used within the astro system.

content.config.ts

To work with the collection modules, the collections need to be defined in the config file. The config file configures the collections and validates them against a pre-defined schema to ensure that the collections loaded in the astro meet the specified criteria. The schema validator also allows to filter the pages based on certain features of the pages.

The content collection is defined using a utility function defineCollection. The function takes as arguments two objects: a loader (required) and a schema validator (optional).

import {defineCollection} from 'astro:content';
//loader
import {glob} from 'astro/loaders';
//schema validator
import {z} from 'astro/zod';

//blog content collection
const blog = defineCollection({
  //define the path of the content collection in base
  loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/blog" }),
  schema: z.object({
    title: z.string(),
    pubDate: z.coerce.date(),
    tags: z.array(z.string()),
  })
});

export const collections = {blog};

We can define multiple content collections. The collection objects are, then, exported for use in the project. The objects can be used in the project by querying them using the getCollection() and getEntry() functions.

The loaders allow to load data from sources. The loaders are of two types: those that load data from local sources, or those from remote sources. The remote loaders are called live loaders, and the local loaders are called build-time content loaders.

  • Build-time content loaders
    • glob(): It takes the pattern on the files to load and the location of the files.
    • file(): loads json, yaml, and toml files.
  • Live content loaders
    • storeLoader():

The golb() loader creates a unique id for each loaded content, which can be used to query the content. A custom id can be defined using slug property in the frontmatter in the md files.

---
title: "Title
slug: "Custom ID"
---

The file() loader doesn’t creates an id automatically and a custom id needs to be defined for each entry. The id can be defiend as a property for each object in the entry, or as an array of object with unique keys as ids.

BaseLayout.astro

This component defines a boilerplate html page inside which every pages inherit either directly or indirectly through PostLayout.astro. A basic BaseLayout.astro is defined as follows.

---
const headTitle = "Notes";
const {pageTitle} = Astro.props;
---

<html lang="en-us">
    <head>
        <title>{headTitle}</title>
    </head>

    <body>
        <h1>{pageTitle}</h1>
        <slot/>
    </body>
</html>

The assignment const {pageTitle} = Astro.props; means every import of BaseLayout needs an argument pageTitle to be passed to it.

The elements defined inside the <BaseLayout> tag would be injected in the <slot/> tag in the above example.

PostLayout.astro

---
import BaseLayout from './BaseLayout.astro';
const {title, date} = Astro.props;
---

<BaseLayout>
  <div class="content">
    <div class="title-bar">
      <p class="title-div">{title}</p>
    </div>
    <div class="date-div">
      <p class="date">{date}</p>
    </div>

    <div class="post">
      <slot />
    </div>
  </div>
</BaseLayout>

index.astro

---
import BaseLayout from '../layouts/BaseLayout.astro';
import { getCollection } from 'astro:content';

const posts = await getCollection('blog');
---
<BaseLayout>
  <div class="homepage">
    <div class="title-bar">
      <p class="title-div">Posts</p>
    </div>

  <div class="post-list">
    <input type="text" id="search" class="search" placeholder="Search..." />

    <ul id="list">
      {posts.map(post => (
        <li class="post-item">
          <a href={`/posts/${post.id}`}>{post.data.title}</a>

          <div class="tag">
            {post.data.tags?.map(tag => (
              <span>{tag}</span>
            ))}
          </div>
        </li>
      ))}
    </ul>
  </div>
</BaseLayout>

[slug].astro

---
import {getCollection, render} from 'astro:content';
import PostLayout from '../../layouts/PostLayout.astro';

export async function getStaticPaths() {
  const posts = await getCollection('blog');

  return posts.map(post => ({
    params: {slug: post.id},
    props: {post}
  }));
}

const {post} = Astro.props;
const {Content} = await render(post);
---

<PostLayout title={post.data.title}, date={post.data.pubDate.toDateString()}>
  <Content />
</PostLayout>