Recent Posts

BlogMore v2.39.0

1 min read; 13 GFI

BlogMore v2.39.0 has a couple of changes, one more or less cosmetic, the other a new feature that I probably should have added right near the start of the project.

First, the cosmetic: in pages and posts, when you use headings (#, ##, etc.), a is added to the end of each heading, along with some show-on-mouse-hover styling, to make it easy to grab the anchor of a particular section of a page1. A problem I'd noticed with this recently is that the shows up in the search text, and also in the content of all the feeds. While not a problem as such, it felt untidy.

So, with this release that particular character is stripped from the search index and from the feeds.

The second change is a facility that should be useful if you have legacy URLs that you really must have land on an actual page or post: redirection support.

This facility is provided by a new redirect_from frontmatter property. From now on, a page or a post can declare which URLs should be redirected to it. So, if you have a post that, given your current post location setup, would land at /2006/06/06/hello-world/, but in some other incarnation of your blog lived at /posts/2006-06-06-hello-world.html, you can now set this in the frontmatter:

redirect_from: /posts/2006-06-06-hello-world.html

and the old URL will be redirected to the new one. This is achieved by simply creating a very minimal HTML file at the old location, which contains a redirection. The content of the file will end up looking something like this:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Redirecting...</title>
    <link rel="canonical" href="https://example.com/2006/06/06/hello-world/">
    <meta http-equiv="refresh" content="0; url=/2006/06/06/hello-world/">
  </head>
  <body>
    <p>Redirecting to <a href="/2006/06/06/hello-world/">/2006/06/06/hello-world/</a>...</p>
  </body>
</html>

If a blog has migrated between locations and/or generation tools over the years, and there are a number of different legacy URLs out there still pointing to a post you really want to keep readable to all, you can also redirect from multiple URLs:

redirect_from:
  - /posts/2006-06-06-hello-world.html
  - /posts/2006/06/06/hello-world.html
  - /my/really/old/blog/structure/hello-world-06-06-06.html

While all of this would have been possible anyway, by creating the appropriate file, with the appropriate content, in the extras directory, this approach helps automate the process and also means that the redirection information lives with the page or post being redirected to.


  1. The recently-added floating ToC is another way to do this now too. 

BlogMore v2.38.0

2 min read; 10 GFI

BlogMore has been bumped to v2.38.0. This release includes a small cosmetic change to the stats page, and also adds a fairly big cosmetic feature with some comprehensive control.

First, the change to the stats page: here I've changed the formatting of numbers a little so that numbers that are in the thousands or higher get better formatting. So rather than a count of something being 1234 it'll now display as 1,234. Hardly a groundbreaking change, but something I'd omitted some time ago and yesterday evening it came to mind. This should make the stats page just a little easier on the eye.

The second change is a reasonably big one, with some extra configuration file and frontmatter additions.

BlogMore has long supported auto-generation of a table of contents in the body of a post by including a [TOC] shortcut. Generally I'm not a fan of using this approach, but I can see the utility. Yesterday evening I added a new approach to showing a table of contents that means you don't need to have the list of links within the post.

Starting with this version, if a page or post has one or more headings in it, a table of contents will appear in the "dead space" on the right-hand side of the page, like this:

ToC to the right

This follows the same kind of style as is used for the year and month index in the archives page. However, whereas in the archives the index just disappears (by design) if you're on a smaller screen, this new table of contents will switch to being inline in the page or post, in a form that is collapsed:

Collapsed inline ToC

If the user clicks on it, it expands:

Expanded inline ToC

Given that this is a fairly big cosmetic change that could affect many pages and posts on a blog, I've built in as much control as makes sense. This new ToC approach is on by default, but can be turned off. First off, there is a show_toc configuration file setting which controls if the feature should be on or off, by default, site-wide. It is set to true by default.

Because there might be times where you want to control this at the page or post level, there is also a show_toc frontmatter property too. This always overrides the global setting in the configuration file.

On top of this, I can imagine that someone1 might also want to have the right-side floating ToC, but never have the inline ToC showing on smaller displays. With this in mind I've also added a show_toc_inline configuration option and frontmatter property. These just control if the inline version of the ToC will ever be shown. To be clear: if the ToC is disabled (either site-wide or via a specific page or post's frontmatter), this inline-specific setting is ignored -- the ToC will never show. But if show_toc is true then show_toc_inline controls if the inline ToC ever appears on smaller displays.

I fear I might have made it sound more complicated than it is. Apologies if that's the case. I'm confident that someone needing this level of control will make sense of it easily enough.

Also, just to be clear: the [TOC] shortcut is totally unaffected by any of this. If you have [TOC] in your post, that version of the table of contents will always be generated. I didn't think it made sense to override that explicit markup choice.


  1. Me, I'm thinking it might be me. 

BlogMore v2.37.0

1 min read; 9 GFI

I've just released BlogMore v2.37.0. This is a small update that fixes a bug and also improves the way that warnings and errors are emitted.

The bugfix comes from yesterday's post where I noticed that the stats page was misreporting the number of tags on the blog. The cause of the problem was that, within reason, a single tag can be represented in a number of different ways. In the tags frontmatter you could write any of Python Coding, Python coding, python coding, Python-Coding, etc and they would all be seen as the tag python-coding. This gives some useful flexibility and also ensures that the actual tag is URL-safe.

The problem was that the stats page was counting the list of unique tags, as they were typed into posts, rather than the unique list of "made safe" tags. So that is now fixed, along with categories too.

The other main change in this release is that warnings and errors that might get printed during post parsing and other operations are now printed to stderr. Until now everything was simply printed to stdout, which was fine until I introduced the links dump and main dump commands. With the addition of those commands, you're far more likely to want to redirect the output to a file. So if there's some sort of warning when parsing a post, that would get dropped into the middle of the JSON output (if using dump), resulting in a JSON file that can't be parsed.

Now all warnings and errors will go to stderr.

Recreating my blog stats

4 min read; 13 GFI

Introduction

Having recently added the dump command to BlogMore I've been thinking I should try and learn a little more about jq. It's one of those tools that's been on my radar for ages, which I've used on very rare occasions to get something done quickly, but which I've never really used in anger.

So I thought it might be fun to see about recreating some of the stats from the stats page using jq alone. Well, I say "alone", I mean "from the JSON data that is produced by the BlogMore dump command", and of course that makes it easier given it dumps some of the key calculated values. In other words I won't be using jq to calculate the word count, or reading time, or GFI, etc.

Post count

To start with, working out the number of posts in my blog is simple enough:

jq '. | length'
371

Category count

Getting the list of categories would be:

jq '[.[] .safe_category] | unique'
[
  "ai",
  "coding",
  "creative",
  "emacs",
  "gaming",
  "life",
  "meta",
  "music",
  "python",
  "tech",
  "til"
]

and so getting the count of them is simple enough:

jq '[.[] .safe_category] | unique | length'
11

Tags count

Getting the count of tags takes a little more work, as safe_tags is a list too, so I start out with a list of lists, which I need to flatten first.

jq '[.[] .safe_tags] | flatten | unique | length'
224

This, right away, is an interesting finding. In my stats page, as of the time of writing, the number of tags is reported as 243, but here I'm getting 224. Given I'm using the safe_tags property, which ensures all similar tags end up with the same value (so Hello World, hello world, and all variations, become hello-world), that would suggest the stats page isn't taking that into account. That's an issue to address.

A date/time interlude

Here's where things get a little interesting for a moment. In the output of the dump command from BlogMore, the dates of the posts are given in ISO 8601 format; specifically the date and time with offset format. From what I can tell, while jq does have some date/time parsing support, it can't handle that format specifically.

This means that if I try:

jq '.[0] .date | fromdate'

I just get:

jq: error (at <stdin>:27293): date "2015-06-18T14:53:00+01:00" does not match format "%Y-%m-%dT%H:%M:%SZ"

After some searching around it seems the only approach I can really take is to drop the timezone offset and pretend every time is a Z time:

jq '.[0] .date[:19] + "Z" | fromdate'
1434639180

From here I can then get a fully-parsed list of date/time values using gmtime:

jq '.[0] .date[:19] + "Z" | fromdate | gmtime'
[
  2015,
  5,
  18,
  14,
  53,
  0,
  4,
  168
]

This isn't ideal for what I'd like to do, it's going to skew some of the values related to time, but it's close enough for experimenting.

Posts per year

Now that I have a way of breaking the posting time into a workable array of values, getting the number of posts per year becomes:

jq -r '[.[] .date[:19] + "Z" | fromdate | gmtime[0]] | group_by(.) | .[] | "\(.[0]): \(length)"'
2015: 32
2016: 26
2017: 7
2018: 1
2019: 15
2020: 23
2022: 11
2023: 49
2024: 19
2025: 11
2026: 177

Although, to be fair to jq, that's kind of long-winded when I could just pull the year itself out of the posting time:

jq -r '[.[] .date[:4]] | group_by(.) | .[] | "\(.[0]): \(length)"'

Posts by month

At this point getting the posts by month of year seems obvious too:

jq -r '[.[] .date[5:7]] | group_by(.) | .[] | "\(.[0]): \(length)"'
01: 14
02: 12
03: 53
04: 57
05: 76
06: 33
07: 25
08: 25
09: 13
10: 29
11: 19
12: 15

Posts by weekday

For this, I need to go back to the more involved version of the posting date handling query, where I use gmtime to break down the time. It turns out that the penultimate value is the day of the week as a number. So, while it's not quite as readable in that I don't have day names, I can get the values:

jq -r '[.[] .date[:19] + "Z" | fromdate | gmtime[-2]] | group_by(.) | .[] | "\(.[0]): \(length)"'
0: 48
1: 54
2: 51
3: 48
4: 56
5: 56
6: 58

In this case Sunday is the first day (the 0 day here).

Posts by hour

Getting the posts by the hour is really just a variation on the date-chopping query used for the posts by year and the posts by month; it's all there in the string version of the date.

jq -r '[.[] .date[11:13]] | group_by(.) | .[] | "\(.[0]): \(length)"'
00: 1
06: 1
07: 6
08: 51
09: 35
10: 32
11: 25
12: 14
13: 22
14: 24
15: 25
16: 24
17: 18
18: 9
19: 23
20: 33
21: 21
22: 6
23: 1

First and last posting dates

Getting the date of the first and latest post seems nice and easy:

jq -r '[.[] .date[0:10]] | {first: min, last: max}'
{
  "first": "2015-06-18",
  "last": "2026-06-01"
}

Although, from what I can tell, jq doesn't have anything that makes date arithmetic easy so working out the elapsed time between the two isn't so straightforward. It can be done, but it's not as easy as it might be with a bit of Python code, for example. The best I could come up with was:

jq '[ .[] | .date[:19] + "Z" | fromdate ] | ((max - min) / (365.25 * 24 * 60 * 60))'
10.95438841990519

For an approximate value of "year", of course.

Word counts

From here on in many of the stats that can be pulled out from the JSON, with jq, become easier to handle. Each post has a word_count property, so I only need to do this:

jq -r '[.[] .word_count] | {least: min, most: max, average: (add / length)}'
{
  "least": 24,
  "most": 2792,
  "average": 475.0700808625337
}

Reading times

A post's reading time can be accessed by reading_time, so it's as easy to handle as the word counts:

jq '[.[] .reading_time] | {least: min, most: max, average: (add / length)}'
{
  "least": 1,
  "most": 11,
  "average": 1.8921832884097034
}

Gunning fog index

The Gunning fog index is available as the gfi property so there's no work to do to figure it out. It is, however, a floating point value and I want counts in each integer "bucket". That can be done with round.

jq -r '[.[] .gfi | round] | group_by(.) | .[] | "\(.[0]): \(length)"'
3: 1
4: 2
5: 3
6: 7
7: 30
8: 46
9: 67
10: 70
11: 75
12: 35
13: 18
14: 11
15: 1
16: 3
17: 2

As for working out the mean, median and mode... while I worked out the above queries by reading the docs, experimenting, and using Gemini on occasion to either help me understand an error message or to explain why an approach works the way it did, I'm going to have to leave this one 100% to Gemini. Here's its approach to using jq to work out those averages:

jq '
  [ .[] | .gfi | select(. != null) ] as $raw_gfi
  | [ $raw_gfi[] | round ] as $rounded_gfi
  | ($raw_gfi | length) as $count

  # 1. Mean Calculation
  | (($raw_gfi | add) / $count) as $mean

  # 2. Median Calculation
  | ($raw_gfi | sort) as $sorted_gfi
  | (if $count % 2 == 1 then
       $sorted_gfi[($count - 1) / 2]
     else
       ($sorted_gfi[($count / 2) - 1] + $sorted_gfi[$count / 2]) / 2
     end) as $median

  # 3. Mode Calculation (using the rounded values)
  | [ $rounded_gfi
      | group_by(.)
      | map({gfi: .[0], frequency: length})
      | sort_by(.frequency)
      | reverse
      | .[]
    ] as $frequencies
  | [ $frequencies[] | select(.frequency == $frequencies[0].frequency) | .gfi ] as $modes

  # Final Object Assembly
  | {
      count: $count,
      mean: $mean,
      median: $median,
      mode: $modes
    }
'
{
  "count": 371,
  "mean": 9.908842231503396,
  "median": 9.979198312236287,
  "mode": [
    11
  ]
}

As of the time of writing: that's bang on what I get in the stats. Honestly though, by this point, I think I'd be reaching for Python or something similar to do this sort of work. For sure, I can't say if this is a good jq query, if it's in any way idiomatic, or even if it's error-free. The numbers match what BlogMore says though.

Conclusion

This has been a useful exercise in getting to know a little more about jq, and I can see myself reaching for it to do quick little jobs now that I've finally taken some time to dive into it. As it turns out, it's also been a useful little audit of the content of the stats page because I've even found a bug that needs addressing; so that's a bonus.

New GitHub Copilot billing is popular

1 min read; 8 GFI

So today is the day, today is when GitHub Copilot swaps to its new billing system. Watching the relevant subreddit suggests this might not be popular.

Some folk think it isn't the smartest move.

Not a good choice

Some don't feel too friendly towards it any more.

Friendship ended

It looks like some of those friendships have lasted a while.

2021-2026

Some saw the opportunity to create content out of the situation.

A cancellation video

Some have figured out that the thing that costs money, costs money.

It is too expensive

Someone used up half their monthly allowance on just 8 requests.

Half used after 8 requests

Although, of course, there's always someone who has to do it better.

Half after 1 request

To be fair though, at least one person loves the new system.

Love the new system

As for my subscription, which came about after I initially experimented with free access to the tool, I've not actually cancelled yet, but I can't see me making use of it much more. I might try a couple of prompts with it, along the lines of what I was doing while working on BlogMore, just to get a feel for how different the usage is now.

Meanwhile, though, I've found that I'm getting on a lot better with Antigravity and getting the bits done I want to do. I suspect this is how I'll keep tinkering with BlogMore, until Google come to their senses anyway.

BlogMore v2.36.0

1 min read; 10 GFI

Another quick update to BlogMore: this time doing a little bit of tidying up in support of my use of it over on my photoblog.

The new items I've added to the stats page are working out really well, but over on the photoblog they were lacking a bit in a couple of areas. Because no post on the photoblog has any body text -- it's just title, category, tags and image -- things like the word count and yearly focus didn't really have any content. The minimum and maximum word counts were zero, and every single year of the span of the blog had no focus whatsoever.

To clean this up I've made it so that the word count section just doesn't show at all if the minimum and maximum word counts are both zero. I've also changed how the corpus of words for the yearly focus works too. If a particular post has no body text, the text that is used falls back to using the title of the post and the tags. With that change, the yearly focus list for the photoblog goes from being completely empty, to looking quite informative.

Year focus for the photoblog

One other change in v2.36.0 is a small fix to headings as they appear in user-supplied pages. While it was possible to grab an anchor for a heading so you could link to it, the character that should appear on hover, to help make this obvious, wasn't appearing on those pages. That's now fixed.

BlogMore v2.35.0

1 min read; 9 GFI

Well, I wasn't expecting to release a new version of BlogMore quite so soon, given I made a release earlier today. But an issue came in that the Bluesky icon wasn't showing in the socials section on someone's blog, despite the fact that the docs suggested it should work.

Sure enough, the icon wasn't there despite the docs giving Bluesky as an example that should work. A quick check, and the reason for the problem was clear: the code was using a version of FontAwesome that didn't have that icon. An easy fix.

It's interesting to note that Copilot/Claude (which is what did this work originally) produced one thing in the code, and then made an unsupportable claim in the documentation. Furthermore, it didn't construct the tests to at least match the documentation's claims. None of this is a surprise, of course, but it is a good illustration of the fact that agent-based coding is just fancy auto-complete with no real ability to maintain a coherent view of the whole project.

Anyway, v2.35.0 is now available and I too can have a Bluesky link in my socials. Oddly, I hadn't thought to add it before now.

A full month of blogging

1 min read; 11 GFI

I've just realised that, somehow, I've managed to post something on this blog, every single day this month.

May 2026

A large part of this is, of course, because I've been doing a lot of stuff on BlogMore, but there's no getting away from the fact that BlogMore exists and I feel compelled to make use of it, and also that blogmore.el helps make it a lot easier to kick off and edit a post.

Thinking back to other blogs I've maintained over the past couple of decades, I don't think I've ever come close to this. Last month did come close, broken only by the couple of days I was chilling in Whitby. The month before also came close, minus 2 days where I just didn't have something to write.

I don't imagine this will last; in fact, I know for sure that there will be fewer posts next month (I have a trip coming up). What I do know is that I feel more compelled to jot something down when an idea turns up, and I'm enjoying the habit of blogging more frequently. While I expect this run to calm down, I hope I don't fall back to leaving it months at a time before opening a fresh Emacs buffer and kicking off some new Markdown.

BlogMore v2.34.0

1 min read; 12 GFI

I've released BlogMore v2.34.0. This is a small update to make some changes to the newly-added dump command.

The first change is a small fix to the url_path property, which wasn't being populated; now it is.

The second change adds two new properties to the output which relate to links that can be found inside posts: internal_links and external_links. As the names suggest, the first gives you a list of all the internal links that can be found in a given post, with the values given being the same format as the id used for every post in the dump. For example:

"internal_links": [
  "posts/2026/05/2026-05-20-blogmore-v2-25-0.md",
  "posts/2026/05/2026-05-22-blogmore-v2-26-0.md"
]

This should give everything needed to write tools that do things similar to the back-links system in BlogMore itself.

The list of external links is, obviously, a list of all the links in the post that are external to the blog. It looks like this:

"external_links": [
  "https://blogmore.davep.dev/",
  "https://validator.w3.org",
  "https://json-ld.org/",
  "https://microformats.org/wiki/microformats2",
  "https://microformats.org/wiki/rel-me"
]

There is, of course, some overlap with the link dumping command, but given that the information is available it seemed to make sense to provide it here; it also means that it's available in a more structured form.

Also providing this sort of information in the JSON output means there's a lot of flexibility when it comes to analysing all the posts in my blog. For example, I can now easily satisfy my curiosity if I want to know exactly which posts in my blog have no links whatsoever.

blogmore dump | jq -r '.[] | select((.internal_links | length) == 0 and (.external_links | length) == 0) | .id'

posts/2015/2015-06-18-a-mild-chrome-annoyance.md
posts/2015/2015-06-23-and-now-for-some-ios.md
posts/2015/2015-07-01-odd-ipod-update.md
posts/2015/2015-08-03-best-update-ever.md
posts/2015/2015-09-04-unknown-promo.md
posts/2015/2015-10-19-microsoft-accounts.md
posts/2015/2015-11-11-voice-search-failing-on-nexus-6.md
posts/2015/2015-11-12-i-miss-until-next-alarm.md
posts/2016/2016-04-28-i-now-own-a-macbook.md
posts/2017/2017-12-12-on_to_something_new.md
posts/2020/2020-06-24-swift-til-3.md
posts/2020/2020-06-26-switch-til-5.md
posts/2020/2020-06-28-swift-til-7.md
posts/2020/2020-07-05-swift-til-10.md
posts/2022/2022-06-03-failed-successfully.md
posts/2023/2023-07-21-encouragement-i-guess.md
posts/2023/2023-07-29-home-pod-stuck-installing.md
posts/2023/2023-10-20-constant-siri-voice-loss.md
posts/2026/05/2026-05-04-my-new-favourite-game-on-steam.md
posts/2026/05/2026-05-11-steam-controller-is-close.md
posts/2026/05/2026-05-25-this-is-not-fun.md

Sure, I don't know what I'd do with this information, but at least I can easily ask the question.

Tidying and spelling

2 min read; 11 GFI

Since kicking off the work on BlogMore and blogmore.el, I've absolutely found that I've reduced the friction involved when it comes to writing a quick (or not so quick) blog post. I've also found that I want to go back and tidy up lots of my old posts. Over the past few weeks I've gone and cleaned up the size and positioning of images; converted most images to WebP format; cleaned and consolidated the tags used; hunted down and fixed broken internal links; and a few other things besides.

Another thing I want to do is go back and hunt down, and clean up, typos and spelling mistakes, and the like. While I'm careful to try and not make any errors when typing out a post, and while I've always made a point of reading my posts back to try and catch problems, I've not always been successful. Sometimes I'm just blind to the errors, sometimes I'm just rushing. There's over a decade of mistakes on this blog.

So, with this in mind, I've added a couple of little tools to the build environment for this blog to help me go back and catch problems that might need addressing.

The main tool is a script for running aspell over all the Markdown and building a list of errors. This shows the names of the Markdown files that have errors, and lists the unknown words for them. For example:

=== content/posts/2019/2019-11-04-my-pylint-shame.md ===
flycheck
prepending
whitespace

=== content/posts/2020/2020-08-23-the-pep-8-hill-i-will-die-on.md ===
parens
whitespace

=== content/posts/2020/2020-06-22-swift-til-1.md ===
backticks

=== content/posts/2020/2020-06-14-my-journey-to-the-dark-side-is-complete.md ===
Macbook
Macbooks
scrollbars

=== content/posts/2020/2020-01-19-dnote-el.md ===
dnote
Dnote

=== content/posts/2020/2020-01-11-where-i-live-and-work.md ===
adwaita
eshell
powerline

This alone makes it nice and easy to go back and clean up some obvious issues. A problem I ran into though was that I was getting a lot of false reports for things in the front matter of the files (especially parts of the cover: file name) and also in the end-of-file comments I like to use. So, with a little help from Gemini (because it's a moment since I last wrote any awk in anger), I wrote a filter to "clean" the Markdown content before running it through aspell.

Already, using this setup, I've caught a few things that deserved cleaning up, and because there will be a lot of words that are correct but particular to this blog and what I write about, I'm also building up a local ignore list.

While this setup isn't going to make the content of this blog error-free, it should give me everything I need to go back and slowly improve some of the older text, and to harmonise some of the spellings of some technical terms.