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")
|
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())
|
||||||
)
|
);
|
||||||
}
|
}
|
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 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
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
|
@ -6,4 +6,8 @@
|
|||||||
|
|
||||||
.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);
|
||||||
}
|
}
|
@ -12,7 +12,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
font-size: var(--text-size-xl);
|
font-size: var(--text-size-l);
|
||||||
}
|
}
|
||||||
|
|
||||||
picture {
|
picture {
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user