Post about top Umami posts
Some checks failed
Build and copy to prod / build-and-copy (push) Failing after 34s
Some checks failed
Build and copy to prod / build-and-copy (push) Failing after 34s
This commit is contained in:
parent
2706297406
commit
670acc7667
@ -3,5 +3,5 @@ module.exports = function (eleventyConfig) {
|
||||
collectionApi.getFilteredByTag("posts")
|
||||
.filter(p => process.env.DEBUG || !p.data.tags.includes("draft"))
|
||||
.filter(p => process.env.DEBUG || p.date < new Date())
|
||||
)
|
||||
);
|
||||
}
|
5
config/filters/getPost.js
Normal file
5
config/filters/getPost.js
Normal 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));
|
||||
})
|
||||
};
|
@ -2,11 +2,14 @@ const dateFilters = require('./dates');
|
||||
const arrayFilters = require('./arrays');
|
||||
const excerptFilter = require('./excerpt');
|
||||
const filterBy = require('./filterBy')
|
||||
const getPost = require('./getPost');
|
||||
|
||||
module.exports = function(eleventyConfig) {
|
||||
eleventyConfig.addPlugin(dateFilters);
|
||||
eleventyConfig.addPlugin(arrayFilters);
|
||||
eleventyConfig.addPlugin(excerptFilter);
|
||||
eleventyConfig.addPlugin(filterBy);
|
||||
eleventyConfig.addPlugin(getPost);
|
||||
|
||||
eleventyConfig.addFilter('keys', obj => Object.keys(obj))
|
||||
eleventyConfig.addFilter('json', obj => JSON.stringify(obj, null, 2));
|
||||
|
27
src/_data/postMetrics.js
Normal file
27
src/_data/postMetrics.js
Normal 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 }));
|
||||
}
|
106
src/blog/posts/2024/7/getting-my-top-posts-from-umami.md
Normal file
106
src/blog/posts/2024/7/getting-my-top-posts-from-umami.md
Normal 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
|
@ -7,3 +7,7 @@
|
||||
.grid[data-cols="3"] {
|
||||
--grid-col-width: clamp(12rem, 30%, 15rem);
|
||||
}
|
||||
|
||||
.grid[data-grid-cols="2"] {
|
||||
--grid-col-width: clamp(15rem, 45%, 20rem);
|
||||
}
|
@ -12,7 +12,7 @@
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: var(--text-size-xl);
|
||||
font-size: var(--text-size-l);
|
||||
}
|
||||
|
||||
picture {
|
||||
|
@ -28,15 +28,28 @@ layout: base.njk
|
||||
</p>
|
||||
|
||||
</section>
|
||||
<section class="stack-md">
|
||||
<h2>Recent Posts</h2>
|
||||
<div class="grid" data-grid-cols="2">
|
||||
<section class="stack-md">
|
||||
<h2>Recent Posts</h2>
|
||||
|
||||
<ul class="stack-2xs" role='list'>
|
||||
{% 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>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<ul class="stack-2xs" role='list'>
|
||||
{% 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>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<a href="/blog" class="block">View more posts</a>
|
||||
</section>
|
||||
<a href="/blog" class="block">View more posts</a>
|
||||
</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>
|
||||
|
Loading…
Reference in New Issue
Block a user