How I migrated my blog

How I migrated my blog

About a week ago, I migrated my blog to a New blog site! As I explained in that post, I was previously posting on dev.to, and while it mostly worked fine to post the content, I did not like having my content live on a domain name and I didn't have control over. I knew this might be problematic when I started writing over there, but when I started I didn't know if the whole blogging thing would be worthwhile, and I just wanted to start writing with minimal work to figure out logistics.

Well, I haven't done much writing in the last few years, but some of my earlier posts got a bit more popular than I expected. Every now and then I'd wonder what would happen if dev.to suddenly shut down or decided to delete my account and I'd think, I should really figure out a better long-term solution. Two weeks ago I found myself in the right headspace to actually make it happen. This post will walk you through how I found a setup that works for me and feels good to use.

Requirements

Must-haves:

  • The blog must live under my personal domain, zachklipp.com, with URLs that I can customize.
  • Maintaining the site and writing new posts should involve minimal technical work.
  • Not too expensive, although I'm willing to pay a bit.
  • Markdown support: My old blog posts are all in Markdown, and I don't want to rewrite all the formatting tags.

Nice-to-haves:

  • ActivityPub integration.
  • RSS feed.

Using my own domain was important because it turns the content host into an implementation detail. The next time I decide to migrate I can do so without breaking any links. This was my biggest regret from the initial blog because there are quite a lot of links to the old posts that get very regular traffic, and dev.to does not allow you to configure redirects for your own posts. Since they have a vested interest in keeping people on their site, and even consider it a "social network", this makes sense. It's also free, so I can't really complain that much.

Another nice thing about dev.to is that writing your first post is as easy as linking your GitHub account, hitting "Create post", and writing some Markdown. You get nice enough formatting, a human-readable URL, comments, and basic analytics all built-in. I didn't want to have to mess around with a bunch of web code or build systems every time I wanted to write about some other technology. A lot of really great blogs from people I respect use Markdown files with a static site generator hosted on GitHub pages. It's performant, uses familiar tools (text files, Git), and gives you complete control of the site. I just don't want to have to deal with npm or other command-line stuff when I want to write.

For this extra convenience, I am willing to spend a small amount of money. This blog is one of the main components of my personal career "brand", and was probably at least partially responsible for getting me a job one time, so I consider it a worthwhile financial investment.

And if I was spending money on this, I wanted it to be super accessible. While not a hard requirement, I really wanted to be able to support ActivityPub integration. Posting to Twitter/Threads/Mastodon is quick and easy, notifies people who care automatically, and supports full bidirectional discussions. Blogs are better-suited for long-form content, and especially the content I tend to write: technical explainers with lots of links, code snippets, and the occasional diagram. Most blog platforms support RSS publishing, which allows people to follow and read the blog in an app of their choice (RIP Google Reader), but my previous blog also spawned some good discussions in the comments and I wanted to preserve that functionality. Enter ActivityPub. In theory, a blog with ActivityPub integration is the best of both worlds: The writing platform can be all fancy, with all kinds of formatting and embeddings rendered on the canonical page, but it can also be consumed as simply as following an account on Mastodon. Followers on all different kinds of platforms can also then like and comment on posts without creating a new account on whatever comment platform the blog uses. Social networks like Flipboard, Threads, and even a little-known platform called WordPress had already started supporting ActivityPub so I figured there might be a blogging platform out there that did too.

Domain (Part 1)

I've had the domains zachklipp.com (and zachklipp.ca — I'm Canadian!) for years. Until recently I hosted a minimal website at (www.)zachklipp.com that I had hand-coded. It just had a few words about me and some links to my LinkedIn, GitHub, etc. I built a few years ago as an exercise to see how hard it was to write a super simple but reasonably well-behaved website with the latest web technologies, not using any fancy frameworks. It looked pretty bad, probably had terrible accessibility, and making structural changes was tricky because I had a bespoke (if simple) JavaScript-based navigation system.

Screenshot of zach-klippenstein.github.io

I decided to start by leaving the old site up and putting my blog at blog.zachklipp.com, where you're probably reading this. I figured I might eventually want to use the root and/or www domains for more hero-type content, so permalinks to blog posts should have their own subdomain.

Hosting

Given my requirements, I didn't actually do much research to find a hosting platform. I'd had just a little bit of experience with it as the host of androiddev.blog and had heard it was even coming up as a less problematic alternative to Substack. I also vaguely remembered hearing something about how they were planning to support ActivityPub integration. A quick Google confirmed this. As of the time of this writing, it does not support the integration yet but their engineering blog is posting weekly updates and immediately after I initially published this I saw they had their first federated account go live.

Let’s fix it in production
One small step for pug, one giant leap for pug kind.

Ghost's first pricing tier was just over $100 USD/year. Even on top of the ~$20/year I'm paying for my domains, that's still less than an Amazon Prime membership. And the best part, if I find a better and cheaper alternative in the future, I can just switch without breaking all my links (i.e. this is more of a Type 2 decision). Sure, Ghost could shut down, price me out, or get bought by a Nazi billionaire with the emotional maturity of a 14-year-old, but now I could simply decide to move my content elsewhere. I signed up for the trial and started moving stuff over.

Ghost's editor is simple and lets you focus on writing, while still providing decent support for embedding content from various well-known services and supporting raw Markdown. Ghost also lets you customize the URL path for each post as well as specify other metadata like publish date and social media cards. Posts can be assigned tags, which can be used for private or public indexing. I use tags to group blog series (a feature which dev.to has explicit support for) and surface my talks. It was super easy to put the blog behind a custom domain: just a CNAME record on my DNS host and entering the domain on a Ghost admin page. Once I was confident I could post the way I wanted, I copied my old posts over and backdated them to the original post dates.

I didn't want to delete my old posts on dev.to since they are still getting lots of traffic, but I did want to move those viewers to my new blog. As I mentioned earlier, dev.to does not let you create actual redirects for posts, but it does let you specify a canonical URL for each post. I quickly learned that this gets published as an HTML <meta> tag that provides a hint for search engines about which URL to favor for pages with duplicate content. After configuring each dev.to post to point to the new blog posts as their canonical URLs, I also added buttons at the top of each post notifying readers that the content had moved. Lastly, I published one last dev.to post that my blog there was deprecated.

After tweaking the navigation and theme a bit in Ghost, I made the migration official by posting about it on Mastodon.

Analytics

As I was exploring the Ghost admin pages I saw a section for analytics. While it functions perfectly well as a blogging platform, Ghost seems to primarily target paid email newsletters. Its analytics are all about subscribers and subscriptions. Dev.to automatically gives you read stats and graphs which have been invaluable in realizing that some of my posts are still read quite often and are worth preserving and keeping updated. Also, dopamine: it's nice to know that people read what you write! I did not want to publish a blog with no idea if anyone was reading it.

I'd used Google Analytics for another site in the past but in the last year I've been trying to de-Googlify my life so that was a non-starter. It's also totally overkill for what I need, which is just page views. I asked around and found two options that looked good: Plausible and TelemetryDeck. Plausible seems to expose slightly more information (e.g. geo region breakdowns) which I have no actual use for other than tickling my brain, but doesn't have a free tier. TelemetryDeck has a free tier with limits that are more than enough for my use case, and the UI is simpler, but it doesn't expose as much information. I decided to pay for Plausible for now and try both concurrently, but I suspect I'll drop Plausible after my subscription runs out. Interestingly, both services are hosted in the EU.

Backfilling

I only had about 10 posts to migrate, so it wasn't a huge pain. However, I figured that, having just set up a shiny new blog, this would be a great time to flesh it out with a bit more of my content. I've given a few talks in recent years but whenever I want to reference them I always end up digging around on the internet to find them. Sometimes I give the same talk at more than one event. My blog seemed like a good place to collect these talks as well—that way, the next time I want to point someone to something I've already explained, I can just send them to my blog, no matter if I wrote about it or talked about it. I collected all my talk links and made one blog post for each talk, grouping repeated presentations and adding the links to my slides. I don't know why I hadn't done this sooner. While I am extremely grateful for Droidcon and the effort they go to to reliably produce high-quality recordings of every talk, their website is notoriously hard to search. Now I can find all my talks simply by searching my blog, and when I want to point someone at a particular talk, I don't have to figure out which version of the talk I want to link them to.

Screenshot of one of my blog posts for a talk I gave multiple times.

SE… O?

After letting analytics stream in for a few days, I realized the vast majority of the hits on my original blog were coming from Google. Sure enough, searching a few Compose keywords turned up my old blog, but not the new one. A long time ago, probably back in high school or college, I remembered seeing a way to explicitly submit URLs for Google to index to jumpstart the crawling process for new sites. Despite my desire to de-Googlify as much as possible, I couldn't avoid it this time. I found the Google Search Console and added the necessary DNS records to verify my ownership of the domain. It turns out they had already crawled it back when it still pointed to a Hashnode experiment, so I requested a re-index and watched over the next few days to see if they'd pull the new content. It took about a week, but eventually the new pages started showing up.

Screenshot of Google Search Console showing page indexing stats for blog.zachklipp.com.

Domain (Part 2)

With my blog up and running smoothly, I was still annoyed that simply typing "zachklipp.com" into a web browser took you to my shitty little homepage. After spending the last week and a half polishing up this new site, I figured there was no reason to keep the old site around. As I mentioned earlier, I wanted to keep the blog hosted at the blog subdomain, and just forward the other domains to it. To my surprise, after some research, I found there was no way to do this with pure DNS. My DNS provider had a proprietary feature that let me forward subdomains to arbitrary URLs, but it only forwarded HTTP requests, not HTTPS, and wasn't supported on the apex domain (zachklipp.com in my case).

I knew I could stand up a web server to serve redirect codes at the HTTP level, but still trying to avoid doing any more devops than absolutely necessary I did a bit of research and found the simplest way to do the redirect I wanted was to add a <meta> tag to my homepage. This tells browsers to load a different page immediately, but doesn't prevent them from rendering the old page for a frame or two on the first load. The redirect gets cached, so subsequent loads immediately pull up the redirect target, but I hated my old page flashing up even for a split second. I needed real HTTP redirects and my DNS host couldn't help me.

I went back to my web server plan. I started looking into server hosts, starting with DigitalOcean and eventually finding Netlify, fly.io, and others. I still really didn't want to actually bother with a full server though.

Some people recommended Cloudflare, which I'd only known as a CDN. Turns out they have a DNS hosting product that includes a very easy-to-configure and extremely flexible redirect service, served by their extensive network. Configuring subdomains to redirect involves one more step than my previous host: instead of just making a special record type that points to the new URL, I had to first create an A record with a placeholder IP which Cloudflare would then automatically proxy to their own redirect service, and then configure a redirect rule. These rules can be written in a little scripting language but they provide a simple UI for writing basic ones, which was more than enough for my use case. Even better, Cloudflare has a free tier with up to ten rules. I like to make little convenience aliases for certain personal profiles (e.g. github.zachklipp.com) so I've already used about six of the rules, but I expect the free tier to last me for the next few years pretty easily.

Screenshot of Cloudflare configuration

I've now got my domains set up exactly how I want them:

  • blog.zachklipp.com points directly to Ghost's servers (via a CNAME record).
  • zachklipp.com and www.zachklipp.com both return HTTP code 301 with a link to https://blog.zachklipp.com.
    • Both the apex and www subdomain have their own A record that points to Cloudflare’s proxy service.
    • Visiting either of these domains will show the blog, but also ensure that the URL shown in the browser is blog.zachklipp.com, so anyone sharing links to a post will always get the blog subdomain ensuring they still work even if I decide to stop redirecting the www or apex in the future.

Conclusion

When I started blogging, I wasn't sure if it would be worth it, or if I'd even enjoy it, so I wanted the simplest possible way to get some words on the web. A few years, thousands of words, and hours of talks later, I feel like I have enough content to warrant putting at least a small amount of effort into ensuring it's easy to find and at least partially under my control.

I think the setup described in this post should work well for at least the next few years, but more importantly, if it doesn't, I can now remedy that without breaking old links.