Proper websites, done properly

Part 5: Improving our collection

5 minute read time / 674 words

There's still a fair bit missing from our collection setup: published states, dates, and tags/categories at least!

Adding more to our post schema

Let's dive straight in and add some of the missing parts of our schema. Open up the collections config file at src/content/posts/config.ts and update it:

import { z, defineCollection } from 'astro:content';

const postsCollection = defineCollection({
    type: 'content',
    schema: z.object({
        title: z.string(),
        description: z.string(),
        tags: z.array(z.string()),
        isDraft: z.boolean(),
        image: z.object({
            src: z.string(),
            alt: z.string(),
            width: z.number(),
            height: z.number(),
        }),
        publishedDate: z.date(),
    }),
});

export const collections = {
    'posts': postsCollection,
};

Now we're saying that tags, whether or not a post is draft or pubished, an image and a published date are all required as part of the schema. If we check our console output we'll see something like this:

11:13:00 [ERROR] [InvalidContentEntryFrontmatterError] posts → 2024-03-15-my-first-blog-post.mdx frontmatter does not match collection schema.
description: Required
tags: Required
isDraft: Required
image: Required
publishedDate: Required

Add the missing metadata

Let's fix that right now by adding the required metadata to our post page.

---
title: "My first blog post!"
description: "This is the page description. So enticing!"
tags: ["pizza", "sexy", "hello"]
isDraft: false
image: {
    src: "/img/example.png",
    alt: "My First Post Example",
    width: 500,
    height: 350
}
publishedDate: 2024-03-15
---
This is my first blog post, and it is magnificent!

Check back on the console output and you'll see that it's stopped complaining about the missing metadata. Now we can update both our listing page items and our post content page to include some of this new stuff. First, let's add them to our post listings page.

Update our post listings page

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

const postEntries = await getCollection('posts', ({ data }) => {
  return data.isDraft !== true;
});
---
<Layout title="Blog posts">
    <ul>
        {postEntries.map(postEntry => (
            <li>
                <img src={postEntry.data.image.src} alt={postEntry.data.image.lt} width={postEntry.data.image.width} height={postEntry.data.image.height} />

                <a href={`/posts/${postEntry.slug}`}>
                    {postEntry.data.title}
                </a>

                <time datetime={postEntry.data.publishedDate.toISOString()}>
                    {postEntry.data.publishedDate.toDateString()}
                </time>

                <p>
                    {postEntry.data.description}
                </p>

                <ul>
                    Tagged as: {postEntry.data.tags.map(tag => <li>{tag}</li>)}
                </ul>
            </li>
        ))}
    </ul>
</Layout>

The first thing we've done is change what pages the getCollections() function fetches. Now we know if a page is published or draft we can only return pages that do not have isDraft set to true. Easy!

Try changing the value of isDraft in the post article frontmatter and check the post listings page - it will now output nothing. Set it back to false and it'll re-appear in the listings.

We've also added the image, a time stamp, the content description and output a list of tags we've assigned to this particular post. Let's move on to our post content page. We could do with adding some of this new stuff on here as well.

Update our post content page

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

export async function getStaticPaths() {
    const postEntries = await getCollection('posts', ({ data }) => {
        return data.isDraft !== true;
    });

    return postEntries.map(entry => ({
        params: { slug: entry.slug }, props: { entry },
    }));
}

const { entry } = Astro.props;
const { Content } = await entry.render();
---
<Layout title="Blog posts">
    <h1>{entry.data.title}</h1>

    <time datetime={entry.data.publishedDate.toISOString()}>
        {entry.data.publishedDate.toDateString()}
    </time>

    <img src={entry.data.image.src} alt={entry.data.image.lt} width={entry.data.image.width} height={entry.data.image.height} />

    <Content />

    <ul>
        Tagged as: {entry.data.tags.map(tag => <li>{tag}</li>)}
    </ul>
</Layout>

Again, we've used our isDraft value to only create pages for published posts. Then we've added the date, image and tags from the metadata onto the page too.

As you can already see, we're starting to repeat code on both the listing pages and the individual post page. Next we'll look at how we can make things a bit neater, tidier and easier to maintain.