---json
{
  "title": "Getting my top posts from Umami",
  "date": "2024-07-10T11:00:00.232Z",
  "tags": [
    "eleventy",
    "umami"
  ],
  "excerpt": "I recently started using umami.is for website analytics, and figured I could use the API to output some stats about my blog"
}
---

I recently started using [Umami](https://umami.is) for website analytics, and figured I could use the API to output some stats about my blog. Among them was a list of my most popular posts, which I've not had any information about before[^1].

## Accessing the API

Firstly, I need to be able to actually access the [API](https://umami.is/docs/api). The way this is done is by sending a POST request to the `/api/auth/login` endpoint with a `username` and `password` in the request body, which returns an auth token. This was pretty straightforward, I just did the call in [Restfox](https://docs.restfox.dev), but here's the equivalent cURL:

```bash
curl --request POST \
    --url https://umami.lewisdale.dev/api/auth/login \
    --header 'content-type: application/json' \
    --data '{
    "username": "<your username>",
    "password": "<your password>"
}'
```

This returns the token, as well as a bit of information about the user. The docs don't specify how long the token is valid for, but I originally generated mine in the middle of June before getting distracted for a month, so I'm guessing they're fairly long-lived.

## Getting the data

This is just a straightforward GET request to the `/api/websites/<site id>/metrics` endpoint, where `<site id>` is the ID of the website you want to get data for. You also need to provide `startTime` and `endTime` timestamps as query parameters. Because I want to match the results to my blog posts, I'm using the `url` type, and filtering the results to only include URLs that start with `/post/`, but you can choose one of the many other types too, e.g. referrer[^2], browser, device, etc.

Here's my Eleventy data file, which uses [Eleventy Fetch](https://www.11ty.dev/docs/plugins/fetch/) to make the request and cache the result for an hour, for no other reason than I didn't want it to slow down my builds all the time. I just needed to make sure that I set `removeUrlQueryParams` to true, because otherwise I'm caching the entire URL which includes the current timestamp, which is a bit pointless:

```javascript
// src/_data/postMetrics.js

const EleventyFetch = require("@11ty/eleventy-fetch");

const siteId = ".."; // The site ID from Umami
const umamiUrl = "https://umami.lewisdale.dev";
const apiKey = process.env.UMAMI_API_KEY;

module.exports = async function(arg) {
    const url = new URL(`${umamiUrl}/api/websites/${siteId}/metrics`);
    url.searchParams.append("startAt", 0);
    url.searchParams.append("endAt", Date.now());
    url.searchParams.append("type", "url");

    const metrics = await EleventyFetch(url.toString(), {
        duration: '1h',
        type: 'json',
        removeUrlQueryParams: true,
        fetchOptions: {
            headers: {
                "Authorization": `Bearer ${apiKey}`,
                "Accept": "application/json"
            }
        }
    })

    return metrics.filter(metric => metric.x.startsWith("/post/"))
        .map(({ x, y }) => ({ post: x, count: y }));
}
```

The API was easy enough to use, so this worked more-or-less out of the box, thankfully. As a bonus, the data is already in descending order, so I didn't even need to sort it.

## Displaying the data

This was slightly more convoluted. In Eleventy, data files can't access the collections API as they sit higher up in the [data cascade](https://www.11ty.dev/docs/data-cascade/), and likewise there's no way to access the data object from a config function. Instead, I created a new filter that takes both the metrics and the posts, and just maps the two together:

```javascript
// config/filters/getPost.js

module.exports = function(eleventyConfig) {
    eleventyConfig.addFilter('metricsToPosts', function(metrics, posts) {
        return metrics.map(metric => posts.find(post => post.url === metric.post));
    })
};
```

And then I can use it in my template:

{% raw %}
```twig
{% set popularPosts = postMetrics | take(3) | metricsToPosts(collections.posts) %}

<section class="stack-md">
    <h2>Popular posts</h2>
    <ul class="stack-2xs" role='list'>
    {% for post in popularPosts %}
    <li><a href="{{ post.url }}">{{ post.data.title | safe }}</a> <time datetime="{{ post.date | dateToRfc3339 }}">{{ post.date | dateDisplay }}</time></li>
    {% endfor %}
    </ul>
</section>
```
{% endraw %}

And that's it! The list of top posts is fairly static right now - I've only been running Umami for about a month and I've had one fairly popular posts, and then a couple of normal low-traffic posts. I imagine that the top post will be there for a long time - or I'll shorten the timespan to the last month so that it's more fluid[^3].

[^1]: Well, I've got server logs but there's so much cruft in there that sifting through it is a pain
[^2]: Or referer, refferrer, rrefferrerr or however it's misspelled everywhere
[^3]: Until I have another month where I don't write and this section winds up blank