Ryan Trimble
Code displaying JavaScript function for list formatting

JavaScript List Formatting and Astro

Front End Development

Sometimes you write a bit of JavaScript that makes you remember why you like using JavaScript. I had one of these moments while working on a site conversion from Hugo to Astro.

The problem

I ran into a dilemma where I needed to output a list of authors in the byline of a post, links on each of their names, and comma formatted.

There are probably hundreds of ways to accomplish this, but I chose to use the Intl.ListFormat (reference) object.

The functionality this provides is great as you basically just need to pass in an array and it automagically formats it in a variety of ways:

// This:
const array = ['Apples', 'Strawberries', 'Bananas'];
const listFormatter = new Intl.ListFormat();
listFormatter.format(array);

// Outputs as:
Apples, Strawberries, and Bananas

It even includes an Oxford comma!

The solution

Using Astro functionality and vanilla JavaScript, we can figure out a pretty impressive solution for this.

Content collections

Astro has a great feature called content collections which was introduced in version 2.0. Content collections let you group relevant markdown files inside one folder like so:

/src/content/authors
  bruce-wayne.md
  clark-kent.md
  peter-parker.md

Structuring the data

So with the authors content collection created, I made a new Astro component called PostAuthors.astro and inside imported the getCollection function provided by Astro. Now I was able to return an array of authors from the content collection:

import { getCollection } from 'astro:content';
const authorCollection = await getCollection('authors');

I also want to be able to include this inside a layout where it can accept a list of the specific post’s authors, so I set up an authors prop:

const { authors } = Astro.props;

Now I could create a new array using the .map array method by filtering down the authors collection compared to the list of post authors, then flattening the new array:

const postAuthors = entry.data.authors
  .map(author => authorsCollection.filter(a => {
    return a.data.name == author
  }))
  .flat();

This gives me a list of just the authors on the post but also contains all the necessary information from the author’s entry in the content collection.

Intl.ListFormat in action

Now here’s where the fun part comes in. Using Intl.ListFormat, we can pass in an array of HTML which returns a comma formatted list containing links for each other:

const authorNames = new Intl.ListFormat('en').format(
  postAuthors.map(author => {
    return `
      <a href="/author/${author.slug}">
      ${author.data.name}
      </a>`
  })
);

Inside the Astro template, we can utilize Fragment and set:html to output the results:

<p>Written by: <Fragment set:html={authorNames}` />.</p>

Why is this cool?

So why this is so fascinating to me because of how Astro itself works - much like backend scripting languages, such as PHP, it runs all this JavaScript before building the page.

The client sees none of this and only receives a well-formatted piece of HTML:

<p>
  Written by <a href="/author/bruce-wayne">Bruce Wayne</a>, <a href="/author/clark-kent">Clark Kent</a>, and <a href="/author/peter-parker">Peter Parker</a>.
</p>

Moving this type of thing to the build process means we get to deliver a lot leaner experience for our users, and I think that is very cool.

Full component code

---
import { getCollection } from 'astro:content';
const authorCollection = await getCollection('authors');

const { authors } = Astro.props;

const postAuthors = entry.data.authors
  .map(author => authorsCollection.filter(a => {
    return a.data.name == author
  }))
  .flat();

const authorNames = new Intl.ListFormat('en').format(
  postAuthors.map(author => {
    return `
      <a href="/author/${author.slug}">
      ${author.data.name}
      </a>`
  })
);
---

<p>Written by: <Fragment set:html={authorNames}` />.</p>

Let's work together!