Post about top Umami posts
Some checks failed
Build and copy to prod / build-and-copy (push) Failing after 34s

This commit is contained in:
Lewis Dale 2024-07-10 12:05:12 +01:00
parent 2706297406
commit 670acc7667
8 changed files with 169 additions and 11 deletions

View File

@ -3,5 +3,5 @@ module.exports = function (eleventyConfig) {
collectionApi.getFilteredByTag("posts") collectionApi.getFilteredByTag("posts")
.filter(p => process.env.DEBUG || !p.data.tags.includes("draft")) .filter(p => process.env.DEBUG || !p.data.tags.includes("draft"))
.filter(p => process.env.DEBUG || p.date < new Date()) .filter(p => process.env.DEBUG || p.date < new Date())
) );
} }

View File

@ -0,0 +1,5 @@
module.exports = function(eleventyConfig) {
eleventyConfig.addFilter('metricsToPosts', function(metrics, posts) {
return metrics.map(metric => posts.find(post => post.url === metric.post));
})
};

View File

@ -2,11 +2,14 @@ const dateFilters = require('./dates');
const arrayFilters = require('./arrays'); const arrayFilters = require('./arrays');
const excerptFilter = require('./excerpt'); const excerptFilter = require('./excerpt');
const filterBy = require('./filterBy') const filterBy = require('./filterBy')
const getPost = require('./getPost');
module.exports = function(eleventyConfig) { module.exports = function(eleventyConfig) {
eleventyConfig.addPlugin(dateFilters); eleventyConfig.addPlugin(dateFilters);
eleventyConfig.addPlugin(arrayFilters); eleventyConfig.addPlugin(arrayFilters);
eleventyConfig.addPlugin(excerptFilter); eleventyConfig.addPlugin(excerptFilter);
eleventyConfig.addPlugin(filterBy); eleventyConfig.addPlugin(filterBy);
eleventyConfig.addPlugin(getPost);
eleventyConfig.addFilter('keys', obj => Object.keys(obj)) eleventyConfig.addFilter('keys', obj => Object.keys(obj))
eleventyConfig.addFilter('json', obj => JSON.stringify(obj, null, 2)); eleventyConfig.addFilter('json', obj => JSON.stringify(obj, null, 2));

27
src/_data/postMetrics.js Normal file
View File

@ -0,0 +1,27 @@
const EleventyFetch = require("@11ty/eleventy-fetch");
const siteId = "4f1be7ef-7fb4-4b08-81f1-68fb807a3063";
const apiKey = process.env.UMAMI_API_KEY;
const postRegex = /^\/post\//;
module.exports = async function(arg) {
const url = new URL(`https://umami.lewisdale.dev/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',
fetchOptions: {
headers: {
"Authorization": `Bearer ${apiKey}`,
"Accept": "application/json"
}
}
})
return metrics.filter(metric => postRegex.test(metric.x))
.map(({ x, y }) => ({ post: x, count: y }));
}

View File

@ -0,0 +1,106 @@
---json
{
"title": "Getting my top posts from Umami",
"date": "2024-07-10T12: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

View File

@ -7,3 +7,7 @@
.grid[data-cols="3"] { .grid[data-cols="3"] {
--grid-col-width: clamp(12rem, 30%, 15rem); --grid-col-width: clamp(12rem, 30%, 15rem);
} }
.grid[data-grid-cols="2"] {
--grid-col-width: clamp(15rem, 45%, 20rem);
}

View File

@ -12,7 +12,7 @@
} }
h2 { h2 {
font-size: var(--text-size-xl); font-size: var(--text-size-l);
} }
picture { picture {

View File

@ -28,15 +28,28 @@ layout: base.njk
</p> </p>
</section> </section>
<section class="stack-md"> <div class="grid" data-grid-cols="2">
<h2>Recent Posts</h2> <section class="stack-md">
<h2>Recent Posts</h2>
<ul class="stack-2xs" role='list'> <ul class="stack-2xs" role='list'>
{% for post in collections.posts | reverse | take(3) %} {% for post in collections.posts | reverse | take(3) %}
<li><a href="{{ post.url }}">{{ post.data.title | safe }}</a> <time datetime="{{ post.date | dateToRfc3339 }}">{{ post.date | dateDisplay }}</time></li> <li><a href="{{ post.url }}">{{ post.data.title | safe }}</a> <time datetime="{{ post.date | dateToRfc3339 }}">{{ post.date | dateDisplay }}</time></li>
{% endfor %} {% endfor %}
</ul> </ul>
<a href="/blog" class="block">View more posts</a> <a href="/blog" class="block">View more posts</a>
</section> </section>
<section class="stack-md">
<h2>Popular posts</h2>
<ul class="stack-2xs" role='list'>
{% for post in postMetrics | take(3) | metricsToPosts(collections.posts) %}
<li><a href="{{ post.url }}">{{ post.data.title | safe }}</a> <time datetime="{{ post.date | dateToRfc3339 }}">{{ post.date | dateDisplay }}</time></li>
{% endfor %}
</ul>
<a href="/blog" class="block">View more posts</a>
</section>
</div>
</main> </main>