Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 16

Displaying Blog Posts

Recommended resources

Hello! Welcome to the third lesson in our module on Dynamic Routing and Layouts.

In our last lesson, you mastered one of Astro's most powerful features: getStaticPaths. You learned how to create a single template file, src/pages/blog/[slug].astro, that can generate an infinite number of individual blog post pages directly from your content collection. This is a massive step towards building a scalable, content-driven website.

However, a collection of individual pages isn't a blog without a central hub. Right now, the only way to visit a post is to know its exact URL. Today, we will fix that. Our goal is to build a blog listing page that queries all your blog posts and displays them in a neat, chronological list, providing the main entry point for your readers.

From Individual Pages to a Blog Index

Just like a physical book needs a table of contents, your blog needs an index page. In Astro, thanks to its file-based routing, creating this page is straightforward. A file located at src/pages/blog/index.astro will automatically become the page available at the /blog/ URL.

On this page, we'll perform three main actions:

  1. Fetch all entries from our blog content collection.
  2. Sort them so that the newest posts appear first.
  3. Display them as a list, with each item linking to the full post page you built in the last lesson.

1. Querying Your Content with getCollection

To get all the posts, Astro provides a handy function called getCollection from the astro:content API. It's an async function that returns a promise, so we'll need to await its result. Your experience with JavaScript promises and async/await will make this feel very natural.

This function takes the name of the collection as a string (in our case, 'blog') and returns an array of all the entries in that collection.

Astro Content Collections: Typed Markdown

The CloudCannon tutorial 'Astro Content Collections: Typed Markdown' provides a concise explanation of the getCollection API and, importantly, shows how to sort the results by date—a crucial step for any blog.

Please read the sections 'Astro collection APIs' and 'Adding the blog roll'. Focus on how getCollection is used and the JavaScript sort method that is applied to the results to order the posts chronologically.


2. Building the Blog Index Page

Let's put this into practice. First, create the new page file: src/pages/blog/index.astro.

Inside this file, we'll start by fetching and sorting the posts in the frontmatter script.

// src/pages/blog/index.astro
---
import { getCollection } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';

// 1. Fetch all blog posts
const allPosts = await getCollection('blog');

// 2. Sort posts by date, newest first
const sortedPosts = allPosts.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
---

<BaseLayout title="My Blog" description="A collection of my thoughts and writings.">
  <section>
    <h1>My Blog</h1>
    <ul>
      {sortedPosts.map(post => (
        <li>
          <a href={`/blog/${post.slug}/`}>
            <h2>{post.data.title}</h2>
            <p>Published on: {post.data.pubDate.toDateString()}</p>
            <p>{post.data.description}</p>
          </a>
        </li>
      ))}
    </ul>
  </section>
</BaseLayout>

Let's break this down:

  • We use getCollection('blog') to get an array of all our post objects.
  • We then use the standard JavaScript Array.prototype.sort() method. Since our pubDate schema field is a Zod date, it becomes a JavaScript Date object. We can subtract their values (.valueOf()) to sort them in descending order (newest first).
  • In the HTML template, we use map() to iterate over our sortedPosts and render the details for each one.
  • Crucially, the href attribute is set to /blog/${post.slug}/. This creates the link between the listing page and the dynamic post pages you created in the last lesson.

3. Refactoring with Reusable Components

The code above works perfectly, but as a UI Engineer focused on clean architecture, you can probably see a place for improvement. The markup for each list item is repeated inside the map function. If we want to make our post preview more complex or reuse it elsewhere, this becomes hard to maintain.

A better approach is to create a dedicated PostCard.astro component. This aligns perfectly with your goal of building with reusable components.

The following video provides an excellent walkthrough of this exact refactoring process. It demonstrates how to create a PostCard component, pass the post data to it as a prop, and ensure everything remains type-safe.

You may not ACTUALLY understand Content Collections…

This video from Coding in Public, 'You may not ACTUALLY understand Content Collections…', clearly shows how to abstract the logic for displaying a single post into a reusable component. This is a core practice for building scalable frontends.

Please watch from 19:15 to 25:25. The first part covers creating the PostCard.astro component and passing props to it. The second part shows how to use the data within that component to display metadata like the date and create the link to the full post.

Inspired by the video, let's create our component.

1. Create src/components/PostCard.astro:

This component will receive a single post object as a prop and handle its presentation.

// src/components/PostCard.astro
---
import type { CollectionEntry } from 'astro:content';

interface Props {
  post: CollectionEntry<'blog'>;
}

const { post } = Astro.props;
---
<article class="post-card">
  <a href={`/blog/${post.slug}/`}>
    <h3>{post.data.title}</h3>
    <time datetime={post.data.pubDate.toISOString()}>
      {post.data.pubDate.toDateString()}
    </time>
    <p>{post.data.description}</p>
  </a>
</article>

<style>
  .post-card {
    border: 1px solid #e0e0e0;
    border-radius: 8px;
    padding: 1.5rem;
    margin-bottom: 1rem;
    transition: box-shadow 0.2s ease-in-out;
  }
  .post-card:hover {
    box-shadow: 0 4px 12px rgba(0,0,0,0.08);
  }
  .post-card a {
    text-decoration: none;
    color: inherit;
  }
  h3 {
    margin-top: 0;
    color: #333;
  }
  time {
    font-size: 0.875rem;
    color: #666;
    display: block;
    margin-bottom: 0.5rem;
  }
</style>

Notice we use CollectionEntry<'blog'> to get full TypeScript type safety and autocompletion for our post prop. This is the same technique you've seen before and is excellent practice.

2. Update src/pages/blog/index.astro:

Now we can simplify our blog index page significantly.

// src/pages/blog/index.astro
---
import { getCollection } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
import PostCard from '../../components/PostCard.astro'; // <-- Import the new component

const allPosts = await getCollection('blog');
const sortedPosts = allPosts.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
---
<BaseLayout title="My Blog" description="A collection of my thoughts and writings.">
  <section class="post-list">
    <h1>My Blog</h1>
    {sortedPosts.map(post => (
      <PostCard post={post} />
    ))}
  </section>
</BaseLayout>

<style>
  .post-list {
    max-width: 720px;
    margin: 0 auto;
    padding: 2rem 1rem;
  }
</style>

This is much cleaner! The index.astro page is now only responsible for fetching data and orchestrating the layout, while the PostCard.astro component handles the presentation of a single item. This separation of concerns is fundamental to building maintainable applications.

The final result should look something like this, a clean list of posts ready for your visitors.

Astro Blog Listing Page Example
This image shows a well-designed blog listing page. Our `PostCard` component is the building block for a layout like this, displaying each post's title, description, and other metadata.
Test your understanding!

For your accounting website, it would be professional to show the author of each blog post. Let's add this feature.

  1. Update the Schema: Go to your src/content/config.ts file and add an author: z.string() field to your blog collection's schema.
  2. Update Content: Add the author frontmatter property to one or two of your .md or .mdx blog post files (e.g., author: 'John Doe').
  3. Update the Component: Modify your src/components/PostCard.astro component to display the author's name underneath the publication date.

This task will test your ability to work across the full content pipeline: from schema definition to content authoring to component rendering.

Show answer

1. Updated src/content/config.ts:

// src/content/config.ts
import { defineCollection, z } from 'astro:content';

const blog = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    description: z.string(),
    pubDate: z.date(),
    // Add the new field
    author: z.string(), 
  }),
});

export const collections = { blog };

Remember to run npm run astro sync or restart your dev server to have TypeScript recognize the new schema field.

2. Updated Markdown Frontmatter:

---
title: "Understanding Tax Deductions"
description: "A guide for small businesses."
pubDate: 2024-08-20
author: "Jane Smith, CPA"
---

Content of the blog post...

3. Updated src/components/PostCard.astro:

// src/components/PostCard.astro
---
import type { CollectionEntry } from 'astro:content';

interface Props {
  post: CollectionEntry<'blog'>;
}

const { post } = Astro.props;
---
<article class="post-card">
  <a href={`/blog/${post.slug}/`}>
    <h3>{post.data.title}</h3>
    <div class="meta">
      <time datetime={post.data.pubDate.toISOString()}>
        {post.data.pubDate.toDateString()}
      </time>
      <span>by {post.data.author}</span>
    </div>
    <p>{post.data.description}</p>
  </a>
</article>

<style>
  /* ... previous styles ... */
  .meta {
    font-size: 0.875rem;
    color: #666;
    margin-bottom: 0.5rem;
  }
  .meta span {
    margin-left: 0.5rem;
  }
</style>

Now, your post cards will display the author's name, pulling the data all the way from your Markdown files.


Conclusion

Excellent work today! You have successfully built the central hub for your blog. By creating a blog listing page, you've connected all the individual articles into a cohesive and navigable section of your site.

Key Takeaways:

  • An index.astro file inside a directory (e.g., /pages/blog/) creates the root page for that path (/blog).
  • The getCollection() function is the primary tool for fetching all content from a specific collection.
  • You can use standard JavaScript array methods like .sort() and .map() to process the data before rendering.
  • Encapsulating repeated markup into reusable Astro components like PostCard.astro is a key strategy for writing clean, maintainable, and scalable code.

Up Next:
Our blog listing page works beautifully, but what happens when you have 20, 50, or 100 posts? Displaying them all on one page would be slow and overwhelming for the user. In the next lesson, we will solve this by implementing pagination, splitting the list of posts into multiple, manageable pages.