From 670acc76675f1eaee881f25851626ee4851dacc3 Mon Sep 17 00:00:00 2001 From: Lewis Dale Date: Wed, 10 Jul 2024 12:05:12 +0100 Subject: [PATCH] Post about top Umami posts --- config/collections/posts.js | 2 +- config/filters/getPost.js | 5 + config/filters/index.js | 3 + src/_data/postMetrics.js | 27 +++++ .../2024/7/getting-my-top-posts-from-umami.md | 106 ++++++++++++++++++ src/css/compositions/grid.css | 4 + src/css/exceptions/home.css | 2 +- src/index.html | 31 +++-- 8 files changed, 169 insertions(+), 11 deletions(-) create mode 100644 config/filters/getPost.js create mode 100644 src/_data/postMetrics.js create mode 100644 src/blog/posts/2024/7/getting-my-top-posts-from-umami.md diff --git a/config/collections/posts.js b/config/collections/posts.js index 1d9e774..65486d0 100644 --- a/config/collections/posts.js +++ b/config/collections/posts.js @@ -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()) - ) + ); } \ No newline at end of file diff --git a/config/filters/getPost.js b/config/filters/getPost.js new file mode 100644 index 0000000..8714be6 --- /dev/null +++ b/config/filters/getPost.js @@ -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)); + }) +}; \ No newline at end of file diff --git a/config/filters/index.js b/config/filters/index.js index 5bb50ca..fdc276e 100644 --- a/config/filters/index.js +++ b/config/filters/index.js @@ -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)); diff --git a/src/_data/postMetrics.js b/src/_data/postMetrics.js new file mode 100644 index 0000000..5aa64df --- /dev/null +++ b/src/_data/postMetrics.js @@ -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 })); +} \ No newline at end of file diff --git a/src/blog/posts/2024/7/getting-my-top-posts-from-umami.md b/src/blog/posts/2024/7/getting-my-top-posts-from-umami.md new file mode 100644 index 0000000..e0ffb70 --- /dev/null +++ b/src/blog/posts/2024/7/getting-my-top-posts-from-umami.md @@ -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": "", + "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//metrics` endpoint, where `` 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) %} + +
+

Popular posts

+ +
+``` +{% 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 \ No newline at end of file diff --git a/src/css/compositions/grid.css b/src/css/compositions/grid.css index 2555f8f..8ea5bdf 100644 --- a/src/css/compositions/grid.css +++ b/src/css/compositions/grid.css @@ -6,4 +6,8 @@ .grid[data-cols="3"] { --grid-col-width: clamp(12rem, 30%, 15rem); +} + +.grid[data-grid-cols="2"] { + --grid-col-width: clamp(15rem, 45%, 20rem); } \ No newline at end of file diff --git a/src/css/exceptions/home.css b/src/css/exceptions/home.css index a12562b..7068ad2 100644 --- a/src/css/exceptions/home.css +++ b/src/css/exceptions/home.css @@ -12,7 +12,7 @@ } h2 { - font-size: var(--text-size-xl); + font-size: var(--text-size-l); } picture { diff --git a/src/index.html b/src/index.html index a7ecce6..099a71e 100644 --- a/src/index.html +++ b/src/index.html @@ -28,15 +28,28 @@ layout: base.njk

-
-

Recent Posts

+
+
+

Recent Posts

- + - View more posts -
+ View more posts +
+
+

Popular posts

+ +
    + {% for post in postMetrics | take(3) | metricsToPosts(collections.posts) %} +
  • {{ post.data.title | safe }}
  • + {% endfor %} +
+ + View more posts +
+