A yak standing on a mountainside.
Image credit: travelwayoflife on Flickr, CC BY-SA 2.0

So if you're reading this now, you might be noticing that my blog looks a little different from how it used to. Hopefully nothing is horribly broken on your end. I've just completed a pretty involved migration process, moving the Azhdarchid blog off of the blog hosting platform Bear and on to a self-hosted server, using a static site generator (Eleventy) and a CDN for unnecessarily fast distribution.

This was in fact a very significant amount of faff, troubleshooting dumb things, and eventually temporarily giving up on some of the more dumb aspects of the project. The main motivation, really, was that I was starting to feel like Bear wasn't meeting my needs. I want to add a few things to my blog that aren't immediately available on Bear, like Webmention support, and Bear gives me limited control. Honestly when I set this blog up on Bear in late 2024, I kind of expected that this day would come; Bear advertises that your blog data is portable enough and you can take it off Bear, which is... mmm, in practice let's say semi-true. It was an expedient solution to get it done when I was hurrying to stand up a new blog in the wake of Cohost closing (pour one out for dead platforms).

But I really, really wanted Webmentions, and I was also enticed by the possibility of just having more control in general over how I was publishing this. Besides, the absolute state of personal computing being what it is now, I don't really envision not using Linux as my daily driver OS for the foreseeable future... which makes all of this a lot more painless, including the future process of updating the the blog. Bear's web interface for posts is convenient, but it's not really where I like to write my posts; I like to write them in Obsidian and then copy them over. A nice thing about this new posting workflow is that I can just set myself up to publish right from Obsidian with scripts on my machine.

The Setup

The blog is now a static website built with Eleventy sitting on a VPN I rent. That VPN also hosts my Forgejo instance, which is how the blog gets built automatically from pushing commits to the remote git repository living in that VPS (through forgejo-runner). The blog is then served by a web server on that VPS, which acts as the origin for a CDN.

I chose Eleventy specifically because it's very unopinionated, relative to other static-site tools. Most of those tools are either designed specifically to build blogs (Like Jekyll, the original static site generator used by Github Pages) or they are designed to build single-page applications around frameworks like Vue or React.

Eleventy, by contrast, assumes very little about what you're building and generally just follows instructions. There's a starter setup if you want to use it, but you can very much be on your own and just write all your own templates and layouts, arrange your files how you like them, and so on. I used Eleventy to build the Game Narrative Reader website, exactly because it doesn't assume a particular structure and is great for that kind of single-serve website. But that design is also invaluable now to help me replicate the original structure of the Bear blog, including not breaking links.

A lot of people publish static website blogs on pure storage solutions (like CDN storage). This is a great solution for almost everybody, although I do value the flexibility of running things out of my own server (without losing the advantages of using a CDN). But also, there is one Extremely Dumb Reason why this doesn't work for a blog migrated off Bear. I'll get to that in a moment.

The Transition

There are three things I needed to migrate off my existing Bear blog:

  • The actual content of my posts;
  • Image files stored on Bear's own CDN;
  • Every existing URL on the Bear blog, to avoid breaking links.

I also needed to build a layout and CSS style that was reasonably compatible with the existing content. The current layout is very much based directly off Bear's, which I think is both quite good and very sufficient for my needs, though I might end up changing things (like moving some of the less important nav links to the footer).

Importing post content

This is the part that Bear explicitly supports; you can download a zipped bundle of all your posts. Bear posts seem like they are perfect to include in a static website generated through a tool like Eleventy, because they come as Markdown files with YAML front matter.[1]

Unfortunately, this is a bit of a lie. The YAML in Bear's front matter is actually malformed and won't parse, because Bear allows things like unescaped colons in page and tag names. So my first hurdle was just getting all of those Markdown files to be successfully ingested by Eleventy to generate something at all. There are a lot of those files, so manually fixing all the YAML issues was out of the question.

Fortunately for me, I know how to use sed. Sed (short for "stream editor") is one of the ancient Unix utilities that have been used as the glue in computer systems for all eternity. Given an input file, it performs a set of editing operations on the file line-by-line then spits out the modified file. sed theoretically is a turing-complete language that can do anything, but 99% of sed scripts are simply a way of running find-and-replace either inline as part of a bigger "pipeline" of different programs, or to a lot of files at once. For example, one of the sed scripts I ran:

s/^title: (.*)$/title: "\1"/
s/^tags: (.*)/tags: [\1]/
/^tags:/s/(Compleat History of the Magic: the Gathering Metagame)/"\1"/
/^tags:/s/(Magic: the Gathering),/"\1",/

I won't bore you with explaining how sed works, but this script basically fixes a bunch of issues around unquoted colons in titles by wrapping all titles in quotes, fixes Bear's nonstandard list syntax by wrapping tag lists in brackets, and then also fixes two tags on my blog that included colons in them for good measure. I used sed probably a dozen times to solve different problems during this whole process.

Importing images

Unlike with your actual posts, Bear doesn't actually provide you with an export of all the images and other media that it stores for you, which sucks. Fortunately for me, I I know how to use ripgrep.

Ripgrep (rg) is a much newer tool; it searches through files and folders recursively and finds instances of a given word or string in all of them. It's basically grep (the original Unix text search tool) but for an entire project rather than individual files. So, having all the markdown files with all the links to images hosted on Bear in them, I could download them all with the one-liner:

rg -INo 'https://bear-images.+webp' exported \
  | sort -u \
  | wget -v -w 1 -P images/exported/ -i -

Armed with all of these images, I could then just use sed again to change all of the URL references in the actual Markdown files to relative URLs. While I could be hosting all those images separately on a storage bucket somewhere, I don't really post that many images on my blog anyway and having them live alongside the actual blog both means that they're backed up with the blog, and it means that I can just use nice relative url paths to get them instead of adding extra faff.

The fact that Bear doesn't bundle your media files for you into a download is the main reason I think the "exportability" of Bear blogs is a bit of a fib.

Importing URLs

This is the delicate part; I really do not want this migration to break any links (if it did, let me know). For the most part this is just a matter of correctly configuring where Eleventy puts things. Bear blogs put posts right on the root, so its my.blog/my-awesome-post, not my.blog/posts/my-awesome-post; I don't love this convention but I'm sticking with it for link continuity. To check that everything was correct, I ran the Eleventy dev server on my machine and used a script to check each url on the Bear blog's original sitemap.xml file was accessible on the new site. That's right, it's sed again:

#!/bin/bash

pattern="s|</?loc>||g;s|^\s*||;s|https|http|;s|azhdarchid.com|localhost:8080|"

grep loc sitemap.xml |
  sed -r $pattern |
  while read url
  do
    echo "$url: $(curl -Is $url | head -n 1)"
  done

Now, one particular URL is a huge problem. Bear puts the site's RSS feed[2] at https://azhdarchid.com/feed/. No web server is configured to, by default, serve an XML file (like the RSS feed file) out of a directory like the way index.html files work. The behavior of index.html is in fact what static generators exploit to create website with clean URLs, but it doesn't replicate across other file formats.

Remember when I said I ran this off a VPS, rather than dumb CDN storage, for a stupid reason? This is the stupid reason. I have to specifically configure the web server to rewrite requests so that /feed/ gets routed to /feed/index.xml, so that I don't break everyone's rss subscriptions.

There's nothing wrong or evil of Bear to do this; clean URLs like this are in fact what the W3C recommends for all resources, not just html pages. But it's insane to me that there doesn't seem to be some kind of universal standard for serving non-html files out of clean URLs in static websites!

Styling and finalizing

Once all that was done, I could finally—finally— do the actually fun part of redesigning my blog stylistically. Mostly I spent a lot of time on typography; one of the niceties about all this is being able to self-host my fonts and thus use any font I damn well please with extreme care.

I originally wanted to use the font Playfair. The new version of Playfair comes as a variable font with optical sizes. Optical sizing is a technique used in digital typography where the actual shape of each glyph in the font changes depending on the size it's being displayed at. This is really an idea that's as old as metal type; in old metal type fonts, the tiny sizes are drawn differently from the normal sizes, which are themselves drawn differently from the larger display sizes. This means that you can have the same font but have it look appropriately readable at different sizes; a lot of the details in some styles of fonts don't work at smaller sizes.

On the web, though, this has only recently become available; historically, you just couldn't use certain types of fonts as main body fonts on the web. One such type of font is the transitional serif—a type of early 19th century typeface characterized, especially, by their dramatic stroke modulation (the difference between the thick and thin strokes on a letter). On the current version of Azhdarchid, it's the same font file both up there as the main site title, showing off those dramatic letterforms, and down here as the body text being very nicely readable. This used to be not really possible; if you wanted to have a simple and coherent type system for your website, you could use sans-serifs or some of the more forgiving sans-serif fonts, but you couldn't use something like Playfair, because a font like that really need adaptation for different sizes to look good.

Unfortunately, I found Playfair to have some odd rendering issues on Firefox on Linux—I can see it on Google Fonts' website, too, so it's not just my setup—and it really doesn't quite achieve great legibility at small sizes. So I went with Source Serif 4 instead, which is a less dramatic-looking font but it's still a transitional serif with good optical sizing.

Most people opt to use a font service like Google Fonts, which just gives you some CSS and html to slap on your pages to use fonts served directly from their CDN. This is "fine" but I kind of hate the current iteration of the web where you open a website and your browser makes requests to ten different third-party services, all of which are probably tracking you. I also have no interest, in general, in using or relying on a Google service (and their main competitor in this area is Adobe).

So self-hosting the fonts means generating the compressed woff2 files that browsers actually use, myself, from the original ttf font files. Part of this process also involves some more yak shaving in the form of "subsetting" the font, cutting out parts I don't need, like characters in scripts I don't write in.

In conclusion...

Do I recommend doing this, if you have a perfectly good blog on Bear right now? Ehh... ask me again in a few weeks after I get around to actually doing the things I wanted to do this for in the first place, like adding webmention support to this blog. I do greatly enjoy this kind of problem solving and bolting Unix utilities together like legos to do things. On the other hand, I don't enjoy troubleshooting things like Caddy ACME TLS certificate provisioning misconfiguration.[3] And I did eventually have to put my pencil down today and stop doing this so I can actually do real work on real things that I am getting paid real human money to do... I wanted to add some kind of cache busting mechanism so I can change the stylesheet on the website and be assured that users will get the new one from the CDN, but I realized it was actually a much more annoying problem to solve than it seemed at first glance (definitely something that needs better inherent support from Eleventy, though there are community plugins I might try eventually).

Regardless, I definitely recommend picking up some stupid project and shaving some yaks. We could all use a distraction.


  1. Markdown is a text format for writing human-readable documents, which can be "rendered" into HTML; it's widely used by this type of software. Front matter is a bit of YAML (a structured data format) at the head of the Markdown file which contains its metadata, like the title, original date of publication, and so on. ↩︎

  2. Technically it's an Atom feed; most "RSS feeds" are now Atom feeds. Nobody really knows or understands the difference and everyone just calls both file formats RSS anyway. ↩︎

  3. If you don't already know what that is: really don't worry about it. ↩︎