Sorting Articles by Year with Astro

As you might see on the articles page, my articles are sorted by year. But how did I achieved that? First of all we have to start with an Astro Content Collection. Let’s create one inside src/content.config.ts.

import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";

const Article = z.object({
  title: z.string(),
  description: z.string().optional(),
  pubDate: z.coerce.date(),
  updatedDate: z.coerce.date().optional(),
  author: z.string().default("Thomas"),
  isDraft: z.boolean().default(true),
});

const articles = defineCollection({
  loader: glob({ pattern: "**/*.md", base: "./src/data/articles" }),
  schema: Article,
});

export const collections = { articles };

My articles are stored inside src/data/articles and the pattern will match every markdown files, in every subfolders. Then let’s create src/pages/articles.astro who will present the articles split by year. First, let’s fetch the collection.

---
import { getCollection } from "astro:content";

const publishedArticles = (
  await getCollection("articles", ({ data }) => {
    return data.isDraft !== true;
  })
).sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
---

Here, the articles are gathered from the articles collection, filtered by their draft status and finally sorted by time. Then we split our articles by year.

---
// ...

type ArticleSubstract = {
  id: number;
  pubDate: Date;
  title: string;
};

const articlesByYear = new Map();
publishedArticles.forEach((article) => {
  const year = article.data.pubDate.getFullYear();
  if (!articlesByYear.get(year)) {
    articlesByYear.set(year, []);
  }
  articlesByYear.get(year).push({
    id: article.id,
    pubDate: article.data.pubDate,
    title: article.data.title,
  });
});

const flattenedArticles = new Array();
articlesByYear.forEach((articles, year) => {
  flattenedArticles.push([year, articles]);
});
---

There must be a better way than to convert my articlesByYear into an array. However I had a hard time using it as is in the template part. Speaking of which:

---
import { getCollection } from "astro:content";

const publishedArticles = (
  await getCollection("articles", ({ data }) => {
    return data.isDraft !== true;
  })
).sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());

type ArticleSubstract = {
  id: number;
  pubDate: Date;
  title: string;
};

const articlesByYear = new Map();
publishedArticles.forEach((article) => {
  const year = article.data.pubDate.getFullYear();
  if (!articlesByYear.get(year)) {
    articlesByYear.set(year, []);
  }
  articlesByYear.get(year).push({
    id: article.id,
    pubDate: article.data.pubDate,
    title: article.data.title,
  });
});

const flattenedArticles = new Array();
articlesByYear.forEach((articles, year) => {
  flattenedArticles.push([year, articles]);
});
---

<header>
  <h1>Articles</h1>
</header>
{
  flattenedArticles.map((year) => {
    const articles = year[1].map((article: ArticleSubstract) => {
      return (
        <article>
          <time>{Intl.DateTimeFormat("en-US").format(article.pubDate)}</time>
          <div class="article-title">
            <a href={`/articles/${article.id}`}>{article.title}</a>
          </div>
        </article>
      );
    });
    return (
      <section>
        <header>
          <h2>An {year[0]}</h2>
        </header>
        <div class="article-title">{articles}</div>
      </section>
    );
  })
}

I loop through every year, and for every year I do the same with the articles.