Adding Maps to my Travel Posts
This post is inspired by Josh Erb’s blog: How I Added Maps to my Travel Posts.
I recently started writing a travel blog on my visit to New Zealand and something was missing. Posts lacked a bit of context as to where I was and where the blog would be taking the reader.
As a daily lurker of ‘Hacker News’, almost immediately after I had posted my first blog on the 15th of September, I came across a post from the day before titled ‘I Added SVG Maps to My Travel Posts’.
As someone who worked in a Geospatial company for a decade I’m ashamed to admit I had not considered maps on my posts. Mostly because ‘maps’ normally equals ‘Google Maps’ and about 5 years ago I gave up wanting to deal with the hassle of sorting out API Keys etc. etc…
Josh goes into detail in their post about why and how they avoided this.
This post dives into how Josh inspired me to solve the same problem; and how I ended up with a different solution.
Requirements
I would encourage you to read through Josh’s post because I have the same base requirements as they did:
- No 3rd party platforms
- Generated at build time
- Looks consistent on mobile & desktop
To address all 3 requirements Josh uses d3, writes a custom tag for his 11ty based site which allows them to specify the following in the Front Matter of each post:
location: mumbai
Which is then passed to a custom function in the post template file:
{% cartographer location %}
This results in a custom JavaScript function in the site configuration being called, which generates a map for the given location. When their site is built the map is generated.
I would have loved to have copy and pasted the solution to my own site, but my setup is a bit different.
I use Jekyll, and my first thought was that I write a custom plugin for it.
However, I do not — currently — pre-build my site before I push it to GitHub and use GitHub Pages to deploy the site. Unfortunately the build pipeline is not-configurable; GitHub has limited plugin support with no support for custom plugins.
So, it was at this point I could either:
- Change to building my site locally and push resultant resources to GitHub
- Generate the images before commit and upload the static images with the post to GitHub
The second option seemed like less of a change to my process, so that’s what I picked.
Abusing pre-commit
As I don’t have a build pipeline I can control, I have opted for pre-commit in my local repository.
If you don’t know what pre-commit is, in short,
it is a way of syncing/revision controlling Git pre-commit
hooks;
so they are the same on any developers machine.
In my .pre-commit-config.yml
file1 I put the following python hook:
repos:
- repo: local
hooks:
- id: generate-maps
name: Generate Maps
description: Generate all the maps used in posts
entry: python3 .generate_maps.py
language: python
additional_dependencies: ["py-staticmaps", "python-frontmatter"]
always_run: true
Here we are telling pre-commit to run python3 .generate_maps.py
as a pre-commit
Git hook.
Because everyone needs dependencies — and everyone hates having those dependencies installed globally on their system —
the additional_dependencies
block will create a virtual environment
with the defined libraries installed.
Neat.
Then, on each commit or if I manually run pre-commit
, my map generation code will run.
If I forget to do this before I commit, I will get the following failure:
$ git commit -m 'Amazing content'
Generate Maps...................................Failed
- hook id: generate-maps
- exit code: 1
New map './assets/images/maps/map.svg' generated
Or, if I update the generate map code (e.g. changing the font of labels), then I will get the following:
$ git commit -m 'Amazing content'
Generate Maps.....................Failed
- hook id: generate-maps
- files were modified by this hook
Currently, this hook will not pick up new maps which were unstaged (always run git status
kids)!
Image Generation
The first part of my solution — and probably the most important part — is the generation of the maps. This is going to consist of 3 main parts:
- Reading the Front Matter from my posts2.
- Generating the SVG map
- Integrating with pre-commit
I found py-staticmaps3, and I’ve gotten some good millage from it; there are probably alternatives out there, but it’s a good library that allowed me to configure all the things I needed to.
Note: py-staticmaps
is not currently being pushed to pypi, so I needed to patch for an API difference in PIL.
Thanks to the awesome community I found a quick patch here.
Reading Front Matter
Everyone of my posts has a Front Matter block which contains metadata on the post itself.
Using python-frontmatter I can pull out structured information from the post.
Using custom Front Matter I could represent a map with a line drawn between London and Auckland like so:
---
title: "I visited New Zealand - Part 1"
date: 2024-09-15 20:00:00 +0000
maps:
- name: london_to_auckland
line: true
points:
- name: London
lat: 51.4775
lon: -0.461389
- name: Auckland
lat: -37.008056
lon: 174.791667
---
To see how I use python-frontmatter
, take a look at the code
but, in short, you can pull out information like so:
with open("./_posts/2024-09-15-new-zealand-1.md") as f:
post = frontmatter.loads(f.read())
title = post["title"]
Failing pre-commit
There are two main cases when I want pre-commit to warn me something has changed. When a new map is created, and when an existing map is updated.
The update case is easier as pre-commit is sensitive to files changing when its running; so there’s nothing to do here.
The ‘new map’ case is a bit harder.
If you have scanned the source code you may have noticed that the python file ends with sys.exit(exit_code)
.
This is a way for me to signal to pre-commit that it needs to abort the in-progress commit, and warn the user.
This is not foolproof because the following events could happen:
- I add a new post with a map definition
- I commit
- pre-commit fails because
generate-maps
exited with a code of 1 as it created a new unstaged file - An unstaged SVG is in the working directory, but I don’t notice and commit again
-
generate-maps
does not fail because it thinks nothing needs to change - The commit is successful
I’m open to a better way of solving this; maybe getting my python code to check for unstaged files?
(Bonus) Waypoint Captions
Out of the box py-staticmaps
gives you a Marker
, Line
, and Area
.
Other entities are left as an exercise for the reader and,
as I wanted to put labels on my Markers, I had to learn how SVG worked.
The example from py-staticmaps
allowed me to get a working version with text captions quite quickly,
but they were not legible when they intersected with the line/map.
So I set about trying to put a white background behind them.
This actually took way more time than I expected, but a couple of hours later I had a basic implementation which put a white ‘flood’ filter on the text (shown in the example above)4.
Embedding Maps
So I have the map stored in ./assets/images/maps/<name>.svg
.
Now I have to get it to show in the post as an image.
The normal way of doing this is with a markdown image reference:
![my-map](/assets/images/maps/<name>.svg "Alt text")
However, all the metadata of the maps are just sat there; right at the top of the file. It would be a shame if I couldn’t do something with it.
Which is when I stumbled upon Jekyll without plugins.
By looking at some of the Plugin-free solutions listed, I was able to cobble together a solution that looks like this:
{% include map.html name="london_to_auckland" %}
This includes a custom HTML file called map.html which then pulls all the map metadata from the Front Matter based on the given name.
This then allows me to do cool things like change the alt text of the image to include the captions of the locations when present. Hover over the example at the top of this page to see it in action!
The Final Result
You can see the final result on my New Zealand - Part 1 post. The source code for which can be found here.
-
The ‘Front Matter’ in posts seemed like the most logical place to store the configuration as we can use it to generate alt text for the maps. ↩
-
I reached for Python as I am more familiar with the tool chain; which meant I had to find an alternative to the JavaScript based D3. ↩
-
Thanks to Robert Longson ↩