Sorting and categorizing by years then months is useful over any collection of posts. The posts are usually displayed via an accordion by year, which expands to more fine-grained categories. Creating this should be straightforward for anyone who is used to using Javascript, but for those who only recently got their feet wet or haven’t used it in a while, like me, there are several useful things to note. We will be creating an Astro component for this, and routing will be done using getStaticPaths to keep the site static.
The routing
The general idea of getStaticPaths has been covered in a prior blog, but I will make a note of things to pay attention to using annotations in the code.
/src/pages/[year]/[month]/[...page].astroastro
1---2importtype{GetStaticPaths}from'astro';3import{typeCollectionEntry,getCollection}from'astro:content';45typeBlogPost=CollectionEntry<'blog'>;67constsortByDateDesc=(a:BlogPost,b:BlogPost):number=>{8returnb.data.pubDate.valueOf()-a.data.pubDate.valueOf();9};1011exportconstgetStaticPaths=(async({paginate})=>{12constposts=(awaitgetCollection('blog')).sort(sortByDateDesc);We want a mapping of years and months to blog posts. However, objects can't easily be used as keys in JS objects without side effects. So the keys have to be joined into strings. We take advantage of the fact that Maps are sorted by insertion.13constdates=newMap<string,BlogPost[]>();14constdateKeys:string[]=[];1516posts.forEach((post)=>{JS Date objects use zero-based months (0-11), so that must be corrected for.17constpostYear=post.data.pubDate.getUTCFullYear();18constpostMonth=post.data.pubDate.getUTCMonth()+1;19constkey=`${postMonth}/${postYear}`;20if(!dates.has(key)){21dates.set(key,[post]);22dateKeys.push(key);23}24else{25dates.get(key)?.push(post);26}27});2829returndateKeys.flatMap((monthYear)=>{30const[month,year]=monthYear.split('/');31constfilteredPosts=dates.get(monthYear)!;32returnpaginate(filteredPosts,{33params:{year,month},34pageSize:1035});36});37})satisfiesGetStaticPaths;3839typeParams={year:string,month:string};40const{year,month}=Astro.paramsasParams;41const{page}=Astro.props;42---
The component
Anyways, the layout of the page isn’t so interesting. What’s important is that we now have a route for each year and month that there are posts. Next we apply a similar technique to get the component to list the categories.
/src/components/Archive.astroastro
1---2import{getCollection}from'astro:content';3import{sortByDateDesc}from'../helpers';45constposts=(awaitgetCollection('blog')).sort(sortByDateDesc);This time, years are mapped to months to get the correct routes. We use a Set for months to automatically ignore duplicates. Again, we leverage that the Set in JS is guaranteed to be ordered by insertion, unlike in e.g. Python.6typePostsMap=Map<number,Set<number>>;7constpostsByYear:PostsMap=newMap();8posts.forEach((post)=>{The month is kept zero-based to make it easier to index month names to display.9constpostMonth=post.data.pubDate.getUTCMonth();10constpostYear=post.data.pubDate.getUTCFullYear();11if(!postsByYear.has(postYear)){12postsByYear.set(postYear,newSet([postMonth]));13}14else{15postsByYear.get(postYear)?.add(postMonth);16}17});1819constyears:number[]=Array.from(postsByYear.keys());2021constmonthNames=[22'January','February','March','April','May','June','July',23'August','September','October','November','December'24];This is used to add an attribute to the HTML <details> tag so that the latest category is open by default. The relevant part is the name of the key, the value can be any truthy value.25constopen=true;26---27<section>28{29years.map((year,index)=>(Only the most recent year is open by default, since the routes are sorted by descending date.30<detailsopen={index===0&&open}>31<summary>{year}</summary>32<ul>33{34Array.from(postsByYear.get(year)!).map((month)=>(35<li>36<ahref={`/blog/${year}/${month+1}`}>37{monthNames[month]},{year}38</a>39</li>40))41}42</ul>43</details>44))45}46</section>