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) %}
+
+
+```
+{% 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
-