Posts from April 2026

REST API clients

REST API clients

REST API's are increasingly important for Web developers.

There are a wide variety of tools that assist developers with accessing, creating, testing and documenting testing REST API's.

The following examples will use Transport For London (TFL)'s report on the current air quality in London. This REST API does not require authentication and returns a manageable amount of data.

The endpoint for this REST API is: https://api.tfl.gov.uk/AirQuality/

Web browsers

As a REST API is simply a URL, the most obvious 1 candidate to access a (simple) REST API is simply, err, a Web browser.

If you just want to quickly check the endpoint is correct, currently accessible and examine the data returned, this may be adequate as a sanity check.

Firefox

Mozilla Firefox can display and format the JSON data returned by a REST API call.

There are options to display the raw data and the headers.

Air Quality - Firefox

Add-Ons

If you need more flexibility or just curious, there are a couple of Firefox add-ons available.

RESTer

RESTer is a Firefox add-on providing a REST API client.

Specify the following parameter values:

Air Quality Firefox RESTer

RESTED

RESTED is another Firefox add-on with similar functionality.

Air Quality Firefox RESTED

Chrome

Out of the box, the JSON displayed by Chrome isn't as readable and user friendly as Firefox as it's just a wall of text with no formatting. That's OK for a computer program but not so good for a human.

Air Quality Chrome

However, you can use the 'Pretty Print' option.

Air Quality Chrome Pretty Print

Alternatively, install the Chrome JSONVue extension which displays the JSON data in a more useful format.

Air Quality Chrome JSONVue

There are also a number of Web sites that display a JSON stream into a readable format with a variety of formatting options.

For example, you can copy/paste the raw Chrome text output into JSON formatter to display the data in a more readable format.

Air Quality JSON formatter

Extensions

REST Client

REST Client is a Chrome extension to test REST API's.

Air Quality Chrome REST Client

Talend

Talend offers a commercial product together with a free Chrome extension that may suffice for simple requirements.

Air Quality Chrome Talend

Applications

Postman

Postman is a very popular desktop REST API application and includes team working, workspaces, Git support, AI integration, data driven testing and many other features. There is a free plan available for individuals.

Postman is available on the Web and there are native applications for Windows, Linux and macOS.

Air Quality Postman Web

Postman is useful as many sites offering REST API's also include a pre-built Postman collection that you can import and experiment with.

Personally, I find the Postman Web site very busy and cluttered which is overkill for my simple requirements. Postman also requires that you sign up for an account.

Postman offers a native Linux application which is 375MB (on Arch Linux).

Air Quality Postman App

Curl

curl is a open source command line utility for testing REST API's which is universally available (Linux, Windows, MacOS).

You can find out whether curl is installed and, if so, which version using the following command. It's worth checking you are using a recent version of the software.

$ curl --version
curl 8.19.0 (x86_64-pc-linux-gnu) libcurl/8.19.0 OpenSSL/3.6.2
  zlib/1.3.2 brotli/1.2.0 zstd/1.5.7 libidn2/2.3.8 libpsl/0.21.5
  libssh2/1.11.1 nghttp2/1.69.0 ngtcp2/1.22.1 nghttp3/1.15.0 mit-krb5/1.21.3
Release-Date: 2026-03-11
Protocols: dict file ftp ftps gopher gophers http https imap imaps
  ipfs ipns mqtt mqtts pop3 pop3s rtsp scp sftp smb smbs smtp smtps
  telnet tftp ws wss
Features: alt-svc AsynchDNS brotli GSS-API HSTS HTTP2 HTTP3
  HTTPS-proxy IDN IPv6 Kerberos Largefile libz NTLM PSL SPNEGO SSL
  threadsafe TLS-SRP UnixSockets zstd

You can use curl to exercise the TFL Air Quality REST API using the following command.

The --silent parameter suppresses download and timing information and the raw output is piped to jq to format the JSON in a prettier format.

$ curl --request GET \
       --url https://api.tfl.gov.uk/AirQuality/ | jq
{
  "$id": "1",
  "$type": "Tfl.Api.Presentation.Entities.LondonAirForecast, Tfl.Api.Presentation.Entities",
  "updatePeriod": "hourly",
  "updateFrequency": "1",
  "forecastURL": "http://londonair.org.uk/forecast",
  "disclaimerText": "This forecast is intended to provide information on expected pollution levels in areas of significant public exposure. It may not apply in very specific locations close to unusually strong or short-lived local sources of pollution.",
  "currentForecast": [
    {
      "$id": "2",
      "$type": "Tfl.Api.Presentation.Entities.CurrentForecast, Tfl.Api.Presentation.Entities",
      "forecastType": "Current",
      "forecastID": "53210",
      "forecastBand": "Low",
      "forecastSummary": "Low air pollution forecast valid from Friday 24 April to end of Friday 24 April GMT",
      "nO2Band": "Low",
      "o3Band": "Low",
      "pM10Band": "Low",
      "pM25Band": "Low",
      "sO2Band": "Low",
      "forecastText": "A sunny warm day on Friday.<br/><br/>Back trajectories indicate air arriving from the North Sea. This should be a relatively 'clean' air feed. A light and cooler breeze should ensure dispersion of any local emissions.<br/><br/>Air pollution is expected to remain 'Low' for the following pollutants:<br/><br/>Nitrogen dioxide<br/>Ozone<br/>Sulphur dioxide<br/>PM10 Particulates<br/>PM2.5 Particulates<br/><br/>"
    },
    {
       <snip>
    }
  ]
}

Insomnia

Insomnia is another option although the free version doesn't support synchronisation to the cloud.

Insomnia is an Electron based application which is great for platform portability but does increase the size of the program (which is a hefty 725MB on Arch Linux).

Insomnia also requires an account (although there is a 'scratchpad' available with limited functionality).

Air Quality Insomnia

Bruno

Bruno is another Electron based REST API client (although the size of the AppImage is 158MB). Bruno expressly does not offer a cloud synchronisation option or require an account. Your work is stored locally and sharing is via Git repositories. Nor will Bruno use your data to train AI models.

Bruno supports collections and each API request is stored as a text file.

Air Quality Bruno

Hoppscotch

Hoppscotch is yet another option that offers a cloud service (requires login) as well as native clients for Linux, macOS and Windows.

Hoppscotch Community Edition is open source (MIT licence) and can be used on personal and commercial projects. Hoppscotch can also be self-hosted.

Air Quality Hoppscotch

HTTPie

HTTPie is another REST client (similar to Hoppscotch) which supports importing API's from 'curl', 'Postman' and 'Insomnia'.

Some prospective users may be discouraged by the imminent promise of 'HTTPie AI'.

Air Quality HTTPie

Editors/IDEs

Emacs

Emacs includes a package restclient (available in MELPA) which is a REST client that presents the output in a separate buffer. The package still works fine but appears to be without an owner as the repository was archived in April 2024.

Air Quality Emacs restclient

Visual Studio

Thunder Client

Thunder Client is a VS Code extension for the widely used VS Code editor/IDE.

Air Quality VSCode ThunderClient

Golden Retriever

If you want an alternative to ThunderClient and like dogs, try the Golden Retriever extension.

'Think of it as Postman’s loyal cousin who stays local, syncs with Git, and doesn’t demand a cloud subscription'.

Air Quality VSCode GoldenRetriever


  1. Seems obvious, but it took two years for this simple, obvious fact to dawn on me. 

converting JPEG and PNG images to WebP format

People tell me the 'WebP' format is the recommended format for images displayed on a Web site as WebP files are smaller than PNG and JPEG formats. My site uses JPEG and PNG image formats. The advent of WebP is hardly news but I'm always late to the party.

There is a WebP converter available in the Arch Linux AUR repository.

This looks promising so I immediately go to install it.

$ yay -S webp-converter-git
Sync Make Dependency (7): gendesk-1.0.10-1, gn-0.2324.304bbef6-2,
    rust-bindgen-0.72.1-2, gperf-3.3-2, python-httplib2-0.31.2-1,
    python-pyparsing-3.3.2-1, patchutils-0.4.5-1
Sync Dependency (1): libvips-8.18.2-1
AUR Explicit (1): webp-converter-git-1.0.0.r2.gce5d96c-1
AUR Dependency (1): electron33-33.4.11-2
:: PKGBUILD up to date, skipping download: webp-converter-git
:: PKGBUILD up to date, skipping download: electron33
  2 webp-converter-git                       (Build Files Exist)
  1 electron33                               (Build Files Exist)
==> Packages to cleanBuild?
==> [N]one [A]ll [Ab]ort [I]nstalled [No]tInstalled or (1 2 3, 1-3, ^4)

Oh no, hang on, wait. it's a bloated Electron application with a lot of dependent Rust and Python packages. Forget that.

However, I recall that Emacs uses the 'imagemagick' package which is already installed and includes a utility (mogrify) that can perform the same conversion (from JPEG/PNG to WebP).

$ mogrify -format webp -quality 80 2026-04-14-BlogMore-DraftPost.png
$ ls -lh 2026*
100k     2026-04-14-BlogMore-DraftPost.png
44k      2026-04-14-BlogMore-DraftPost.webp

Half the size. Compare the PNG versus the WebP image. Copy the WebP file to the images directory. Update the image link in the blog post. Job done (apart from the existing 135 images).

blogging more with BlogMore

This is my experience of migrating this blog from Hugo to BlogMore (prompted by various reasons that initiated yet another blog migration).

The aim of the exercise was to streamline the process of publishing a blog post to encourage me to post more frequently than once a year.

My small, personal blog is hosted on Codeberg pages and I use Arch Linux (BTW).

This post is not a substitute for the the excellent, comprehensive documentation which fully describes the installation, configuration and usage of BlogMore.

Installation

Install uv

uv is a modern, powerful Python development tool which includes the virtualenv, pyenv and pip functionality (and much more besides).

The 'uv' package is available in the Arch Linux repository.

$ yay -S uv
Sync Explicit (1): uv-0.11.3-1
resolving dependencies...
looking for conflicting packages...

Packages (1) uv-0.11.3-1

Total Download Size:   15.41 MiB
Total Installed Size:  57.14 MiB

:: Proceed with installation? [Y/n]
:: Retrieving packages...
 uv-0.11.3-1-x86_64     15.4 MiB  14.0 MiB/s 00:01 [######################] 100%
(1/1) checking keys in keyring                     [######################] 100%
(1/1) checking package integrity                   [######################] 100%
(1/1) loading package files                        [######################] 100%
(1/1) checking for file conflicts                  [######################] 100%
(1/1) checking available disk space                [######################] 100%
:: Processing package changes...
(1/1) installing uv                                [######################] 100%
:: Running post-transaction hooks...
(1/1) Arming ConditionNeedsUpdate...

Check the version of the newly installed 'uv' package.

$ uv --version
uv 0.11.3 (45da18ac3 2026-04-01 x86_64-unknown-linux-gnu)

Install BlogMore

Once 'uv' is available, installation of BlogMore is trivial as 'uv' automatically takes care of installing all the dependencies and required Python packages.

$ uv tool install blogmore
Resolved 16 packages in 145ms
Installed 16 packages in 101ms
 + blogmore==2.9.0
 + feedgen==1.0.0
 + jinja2==3.1.6
 + lxml==6.0.2
 + markdown==3.10.2
 + markupsafe==3.0.3
 + minify-html==0.18.1
 + pillow==12.2.0
 + pygments==2.20.0
 + python-dateutil==2.9.0.post0
 + python-frontmatter==1.1.0
 + pyyaml==6.0.3
 + rcssmin==1.2.2
 + rjsmin==1.2.5
 + six==1.17.0
 + watchdog==6.0.0

Add the ~/.local directory to $PATH to make the blogmore command available.

$ export PATH="$HOME/.local/bin:$PATH"

Updating BlogMore

BlogMore releases can be frequent (new functionality, bug fixes) and the 'uv' package is also used to update BlogMore. Any updates to dependent packages are automatically handled.

$ uv tool update blogmore
Updated blogmore v2.11.1 -> v2.13.0
 - blogmore==2.11.1
 + blogmore==2.13.0
 - lxml==6.0.2
 + lxml==6.0.3

Site structure

As part of the migration, I reviewed the existing structure of my Hugo site.

./archetypes
./content
./content/about                           # 'About' page
./content/archive.md                      # 'Archive' tab on Home page
./content/search.md                       # 'Search' tab on Home page
./content/posts                           # 1,000 posts in single directory
./content/posts/aboriginal-culture.md
./content/posts/a-brief-history-of-inane-drivel.md
./content/posts/absolute-disgrace.md
./content/posts/a-cause-for-concern.md
./content/posts/a-day-in-the-life.md
# <snip>
./content/posts/writing-style.md
./content/posts/yak-shaving.md
./content/posts/your-papers-please.md
./content/posts/you-were-kindness.md
./content/posts/zohowriter.md
./orgposts                                # Orgmode posts
./orgposts/fun-with-static-site-generators.org
./orgposts/improving_fwp_apex_app.org
./orgposts/oracle-sql-config.org
./static
./static/images                           # Images
./themes
./themes/PaperMod                         # Papermod theme
./public                                  # Generated site

There were a couple of things that irritated me about this setup:

  • Orgmode posts are stored separately from Markdown content.
  • Markdown files derived from Orgmode posts are essentially (but understandably) duplicated.
  • All 1,000 posts are stored in a single directory.

The decision to store every single post in the 'content/posts' directory was purely historical. I started that way and never changed. This flat structure had its advantages. Looking for patterns (posts without any tags defined) in the front matter was easy using 'grep'.

However, grep (and ripgrep) can search recursively and so I decided to move 1,000 blog posts spanning 20 years to sub directories for each year so this year's posts now reside under 'content/posts/2026'.

Bizarrely, I found this simple change was very helpful. Previously, it was hard to identify draft posts and find recent content. Having less than 10 posts in the 2026 directory was more manageable.

Front Matter

Any blog migration will necessitate conversion of the front matter.

Hugo front matter (TOML)

+++
title = "fun with static site generators"
date = 2025-10-16T09:46:10
lastmod = 2025-10-22T12:52:18+01:00
categories = ["software"]
tags = ["Hugo", "11ty", "BSSG", "Zola", "Nikola", "Pelican", "Jekyll"]
draft = false
+++

BlogMore front matter (YAML)

---
title: fun with static site generators
date: 2025-10-16 09:46:10
modified: 2025-10-22 12:52:09
category: software
tags: Hugo, 11ty, BSSG, Zola, Nikola, Pelican, Jekyll
---

The conversion of the front matter was relatively straightforward as I already had a bunch of sed scripts as a result of previous migrations.

I like BlogMore's use of a single 'Category' with multiple 'Tags' as that's how my brain works and it helps me categorise content better.

BlogMore is flexible and supports three formats to specify the 'Tags' meta-data

  1. YAML list ('tags: [football, united]')
  2. Comma separated list ('tags: football, united')
  3. Multi-line list
tags:
  - football
  - united

I prefer the CSV option as the square brackets in the YAML list is just noise and visual clutter. The single line format is also easier for any subsequent conversions to the next shiny blog platform.

Similarly, the fact that ox-hugo enclosed tags in double quotes was undoubtedly sensible from a purist, developer standpoint but was just more irritating, visual clutter when reviewing the generated Markdown files.

BlogMore is more forgiving on the precise date and timestamp format which has always been another source of irritation. I don't want to know or need to learn the 'ISO 8601 with timezone' standard simply to quickly create a blog post.

Creating a post

You can easily create a new post in BlogMore using your preferred text editor. You can either create the post manually, copy the previous post or use a shell script or templates. For Emacs, YASnippet and tempo are useful.

Here is my sample template for a new post.

---
title:
date: 2026-04-13 12:20:46+0100
category:
tags:
draft: true
---

Dave Pearson has created a marvellous Emacs package (blogmore.el) that is invaluable in managing content, timestamps and meta-data (categories and tags). However, this package merits a separate post in due course.

Code blocks

BlogMore is Python based and uses Pygments for syntax highlighting. Pygments supports many different languages and styles.

BlogMore allows you to select your preferred style (for light and dark themes) using the light/dark_mode_code_style configuration file parameters.

To use the popular 'monokai' theme for all code blocks, add the following lines to the BlogMore configuration file config.yaml.

light_mode_code_style: monokai
dark_mode_code_style: monokai

Building

When I work on a blog post, I tend to take my time. Write, edit, review, write, edit, preview and iterate. Rinse and repeat.

I normally build the BlogMore site to check for any errors in the front matter.

By default, the BlogMore build command only considers published posts so use the --include-drafts parameter to see work currently in progress.

$ blogmore build --include-drafts
Parsing posts from content...
Found 1033 posts
Found 1 pages
Optimizing FontAwesome CSS...
Generating post pages...
Generating static pages...
Generating custom 404 page...
Generating index page...
Generating archive page...
Generating date-based archive pages...
Generating tag pages...
Generating tags overview page...
Generating category pages...
Generating categories overview page...
Generating RSS and Atom feeds...
Generating search index and search page...
Generating blog statistics page...
Copied bundled static assets
Generated minified CSS as styles.min.css
Generated minified CSS as search.min.css
Generated minified CSS as stats.min.css
Generated minified CSS as archive.min.css
Generated minified CSS as tag-cloud.min.css
Generated minified CSS as calendar.min.css
Generated minified code CSS as code.min.css
Generated minified JS as theme.min.js
Generated minified JS as codeblocks.min.js
Generated minified JS as search.min.js
Generated minified FontAwesome CSS as fontawesome.min.css
Copied 134 extra file(s) from content/extras
Generating XML sitemap...
Site generation complete! Output: <site>/blogmore/site

The build command identifies any problems, typically in the front matter. For example

Warning: Skipping content/posts/2026/blogging-more-with-blogmore.md:
Post missing required 'title' in frontmatter:
    content/posts/2026/blogging-more-with-blogmore.md

Previewing

Once the site has been generated without errors, you can then preview the site using blogmore serve which makes your local site available at http://localhost:8000/.

$ blogmore serve --include-drafts
Generating site from /home/andy/software/codeberg-repos/blogmore/content...
Removing output directory: /home/andy/software/codeberg-repos/blogmore/site
Parsing posts from /home/andy/software/codeberg-repos/blogmore/content...
Found 1033 posts
Found 1 pages
Optimizing FontAwesome CSS...
Generating post pages...
Generating static pages...
Generating custom 404 page...
Generating index page...
Generating archive page...
Generating date-based archive pages...
Generating tag pages...
Generating tags overview page...
Generating category pages...
Generating categories overview page...
Generating RSS and Atom feeds...
Generating search index and search page...
Generating blog statistics page...
Copied bundled static assets
Generated minified CSS as styles.min.css
Generated minified CSS as search.min.css
Generated minified CSS as stats.min.css
Generated minified CSS as archive.min.css
Generated minified CSS as tag-cloud.min.css
Generated minified CSS as calendar.min.css
Generated minified code CSS as code.min.css
Generated minified JS as theme.min.js
Generated minified JS as codeblocks.min.js
Generated minified JS as search.min.js
Generated minified FontAwesome CSS as fontawesome.min.css
Copied 134 extra file(s) from /home/andy/software/codeberg-repos/blogmore/content/extras
Generating XML sitemap...
Site generation complete! Output: /home/andy/software/codeberg-repos/blogmore/site
Watching for changes in /home/andy/software/codeberg-repos/blogmore/content...
Watching for changes in config file: blogmore.yaml
Serving site at http://localhost:8000/
Press Ctrl+C to stop the server
127.0.0.1 - - [14/Apr/2026 11:19:24] "GET /posts/blogging-more-with-blogmore/ HTTP/1.1" 200 -
127.0.0.1 - - [14/Apr/2026 11:19:24] "GET /static/fontawesome.min.css?v=1776161935 HTTP/1.1" 200 -
127.0.0.1 - - [14/Apr/2026 11:19:24] "GET /static/styles.min.css?v=1776161935 HTTP/1.1" 200 -
127.0.0.1 - - [14/Apr/2026 11:19:24] "GET /static/code.min.css?v=1776161935 HTTP/1.1" 200 -
127.0.0.1 - - [14/Apr/2026 11:19:24] "GET /static/theme.min.js HTTP/1.1" 200 -
127.0.0.1 - - [14/Apr/2026 11:19:24] "GET /static/codeblocks.min.js HTTP/1.1" 200 -

The 'blogmore serve' command will also generate the site (implies a 'blogmore build') so include the --include-drafts parameter to see all content.

BlogMore provides a helpful, visual indicator to distinguish drafts from published posts.

BlogMore draft post

The 'blogmore serve' command is a live preview that automatically detects any changes to content and dynamically regenerates the site which accelerates the iterative 'write-preview-fix' process.

Detected change in <site>/content/posts/2026/blogging-more-with-blogmore.md, regenerating site...
Removing output directory: <site>/blogmore/site
Parsing posts from <site>/blogmore/content...
Found 1027 posts
Found 1 pages
Optimizing FontAwesome CSS...
Generating post pages...
<snip>
Generating XML sitemap...
Site generation complete! Output: <site>//blogmore/site
Regeneration complete!

Deployment

Codeberg pages is currently in a state of flux but you can deploy the BlogMore site to a Codeberg 'pages' repository. Previously, I used a branch and CI actions but I actually prefer this method as it's simpler, less risky and more portable.

$ cat ~/bin/deploy_codeberg.sh
#!/bin/bash

# Deploy the Blogmore site to Codeberg
CODEBERG=$HOME/software/codeberg-repos
BLOGMORE=$CODEBERG/blogmore
PAGES=$CODEBERG/pages

cd $BLOGMORE

blogmore build

# Copy the contents of the Blogmore 'site' folder to the Codeberg
# 'pages' repository
rsync --archive $BLOGMORE/site/ $PAGES/

cd $PAGES

git add --all

NOW=`date '+%Y-%m-%d-%H:%M:%S'`
git commit -m "Published Blogmore site on $NOW"

git push

I tried to tune this script by only sync'ing files with different sizes (not timestamps) as there is no point in pushing 950 static, unchanged blog posts to the Codeberg repository.

rsync --archive --size-only $BLOGMORE/site/ $PAGES/

This seemed like a good idea but doesn't always update the Blogmore version which is included in the footer on every single post.

I normally check the the newly published blog post has appeared on the site. Occasionally Codeberg pages seems to cache data but this normally resolves after a few minutes.

Publishing

BlogMore also includes a 'publish' command that supports publishing the generated site to a GitHub branch for GitHub pages. This command also works fine on Codeberg.

Add the following entries to the 'blogmore.yaml' configuration file.

# Publish options
branch: cb-pages
remote: origin

Publishing the site on Codeberg

$ blogmore publish
Building site before publishing...
Removing output directory: <site>/blogmore/site
Parsing posts from content...
Found 1026 posts
Found 1 pages
Optimizing FontAwesome CSS...
Generating post pages...
Generating static pages...
Generating custom 404 page...
Generating index page...
Generating archive page...
Generating date-based archive pages...
Generating tag pages...
Generating tags overview page...
Generating category pages...
Generating categories overview page...
Generating RSS and Atom feeds...
Generating search index and search page...
Generating blog statistics page...
Copied bundled static assets
<snip>
Generated minified FontAwesome CSS as fontawesome.min.css
Copied 134 extra file(s) from content/extras
Generating XML sitemap...
Site generation complete! Output: <site>/blogmore/site
Site built successfully
Publishing site to branch 'cb-pages' on remote 'origin'...
Created worktree with new orphan branch 'cb-pages'
Created .nojekyll file
Changes committed
Successfully pushed to origin/cb-pages

BlogMore configuration

For reference, here is my BlogMore configuration file.

$ cat blogmore.yaml
# BlogMore configuration
#
# Heavily based on
# https://github.com/davep/davep.github.com/blob/main/blogmore.yaml

# Locations.
content_dir: content
output: site

# Main site details.
site_title: "Blog In Isolation"
site_subtitle: "There's a radiant darkness upon us"
site_url: "https://www.yakshaving.co.uk/"
site_description: "Personal blog of Roman Totale XVII"
site_keywords:
  - Emacs
  - Oracle
  - APEX
  - UK
default_author: andyc
include_drafts: false

# Building
clean_first: true
# icon_source: davep.jpeg
minify_css: true
minify_js: true
minify_html: true
clean_urls: true
post_path: "posts/{slug}/index.html"
page_path: "pages/{slug}/index.html"
archive_path: /archive/index.html
categories_path: /categories/index.html
search_path: /search/index.html
stats_path: /stats/index.html
tags_path: /tags/index.html

# Content and styling.
with_search: true
with_sitemap: true
with_stats: true
with_read_time: false
light_mode_code_style: default

# The links section in the sidebar.
links:
  - title: BlogMore
    url: https://blogmore.davep.dev/

# The "socials" in the sidebar.
socials:
  - site: mastodon
    url: https://mastodon.me.uk/@andyc

# Publish options
branch: cb-pages
remote: origin

Emacs 31.x Orgmode version mismatch

The latest development version of GNU Emacs contains features coming in the next major release so I build Emacs from source. This development release is generally stable and very reliable.

The source code for GNU Emacs is available on Savannah and there is also a GitHub mirror available. I tend to use the GitHub mirror as it seems more reliable and faster.

Clone the latest, bleeding edge Emacs code.

$ git clone https://github.com/emacs-mirror/emacs.git    # GitHub mirror

Configure and build Emacs

$ cd emacs
$ ./autogen.sh
$ ./configure --with-mailutils   # silence warning
$ make -j 4                      # 4 parallel threads
$ sudo make install

Occasionally, after upgrading to the latest version, Emacs produces a warning caused by a version mismatch for the Orgmode package. This helpful error message gives four possible causes.

Warning (emacs): Org version mismatch.
This warning usually appears when a built-in Org version is loaded
prior to the more recent Org version.

Version mismatch is commonly encountered in the following situations:

1. Emacs is loaded using literate Org config and more recent Org
   version is loaded inside the file loaded by ‘org-babel-load-file’.
   ‘org-babel-load-file’ triggers the built-in Org version clashing
   the newer Org version attempt to be loaded later.

   It is recommended to move the Org loading code before the
   ‘org-babel-load-file’ call.

2. New Org version is loaded manually by setting ‘load-path’, but some
   other package depending on Org is loaded before the ‘load-path’ is
   configured.
   This "other package" is triggering built-in Org version, again
   causing the version mismatch.

   It is recommended to set ‘load-path’ as early in the config as
   possible.

3. New Org version is loaded using straight.el package manager and
   other package depending on Org is loaded before straight triggers
   loading of the newer Org version.

   It is recommended to put

    (straight-use-package 'org)

   early in the config.  Ideally, right after the straight.el
   bootstrap.  Moving ‘use-package’ :straight declaration may not be
   sufficient if the corresponding ‘use-package’ statement is
   deferring the loading.

4. A new Org version is synchronized with Emacs git repository and
   stale .elc files are still left from the previous build.

   It is recommended to remove .elc files from lisp/org directory and
   re-compile.

In my case, none of these fixes resolved my issue but I discovered that there is a package cache stored in the ~/.emacs.d/elncache directory.

This 'elncache' directory stores natively compiled .eln files for Emacs packages with sub-directories for each Emacs version.

$ ls ~/.emacs.d/eln-cache/
30.2-6a8b22b2
31.0.50-be59b212
$ ls -1 ~/.emacs.d/eln-cache/31.0.50-be59b212/
autorevert-841d6890-e5e85ba9.eln
benchmark-0e5ff0c6-b45151e2.eln
blogmore-bd298bd5-140db3f8.eln
bookmark-8667481e-0391040a.eln
browse-url-87f72988-d18440ad.eln
<snip>
org-capture-31aeb3cb-5a0ab108.eln
org-compat-8f643782-38135399.eln
org-d208de9c-51cfc11b.eln
org-faces-fdfdb79f-6e1f5bdc.eln
org-footnote-a50a8027-86895802.eln
org-id-b4e96473-012fc1e0.eln
org-indent-91cb060a-1219f44c.eln
org-keys-d9945960-5b58cda5.eln
org-roam-56f7ff31-fcc8f708.eln
<snip>

You can either remove all files or remove individual packages.

$ rm -fr $HOME/.emacs.d/eln-cache

The eln cache is rebuilt on the next Emacs startup and this fix silences the irritating message on startup.