Posts tagged with "BlogMore"

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.

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.

BlogMore v2.33.0

1 min read; 11 GFI

I've released v2.33.0 of BlogMore, which extends the stats page some more, and also adds a tool so a user can do all sorts of fun things with the raw data of their posts.

The addition to the stats page is a list of years along with the top five words that characterise the focused subject for those years. This is done using TF-IDF. While the results for my blog don't come as a surprise, I am pleased to see that it does turn out pretty much how I would have expected:

Blogging focus per year

This feels like another fun way to learn something about the post history for your own blog.

Which got me thinking: there's probably any number of other niche and bizarre things that could be done with the content of a blog to gain some insight as to its history, and I really shouldn't just keep adding more and more things to the stats page. But what if I wanted a way to run some code over all the posts? Wouldn't it be useful if I could get all of the parsed post data in JSON format so I can play with it?

With that idea in mind, I added the dump command. When run, it will print to stdout a full dump of all of your posts, as JSON, so you can then write your own nifty tool to read it back in and do any number of interesting checks, tests, reformats or manipulations. For example, if I wanted to use jq to pull out the metadata for this particular post I could:

blogmore dump | jq '.[] | select(.id == "posts/2026/05/2026-05-29-blogmore-v2-33-0.md") | .metadata'

which gives me this result:

{
  "title": "BlogMore v2.33.0",
  "date": "2026-05-29 14:14:10+0100",
  "category": "Coding",
  "tags": "BlogMore, Coding, PyPI, Python",
  "cover": "/attachments/2026/05/29/focus.webp"
}

I can see this being pretty useful to blogmore.el at some point, if I want to add some better querying tools or similar (not that I'd want to run a dump every time, it does require that a full parse and render has to happen).

Hopefully this will be useful to someone else. I know I'll be toying with it to find out other things about my posting history.

BlogMore v2.32.0

1 min read; 11 GFI

Now that BlogMore is so obviously feature-complete and there's nothing else I could possibly need to add to it... here's v2.32.0 with a quick new feature.

The work to add the relatedness of posts got me wondering about other things that could be measured about the content of posts, and a bit of reading and looking for ideas resulted in my (re)discovering the Gunning fog index. That got me thinking it might be interesting to see what the highest and lowest values were for my blog, and perhaps what the average is, perhaps even get a feel for the distribution.

So now BlogMore has a with_gfi configuration setting (and a matching command line switch). When enabled, it adds the score for a post next to the reading time (if that's enabled, if not it's in the same location as the reading time would be). It also adds a load of stats to the stats page.

The GFI stats for my blog

As well as this change, I've also tweaked the stats page so it's easier to get a link to a particular section. As with headings in posts, when you hover over a section heading you'll get a little anchor you can click on that will result in you visiting that particular heading. That should make it easier to highlight a particular statistic, if you need to.