Fareesh

Config to Content: How I Shipped My Blog Without Reinventing The Wheel

· fareesh

Every developer has a set of unique, uncompromising, arcane set of requirements for their own blog. This is a well-known affliction and has become a popular running-joke. Every blogging platform is eliminated from consideration due to obscure nitpicks, leading to one inevitable conclusion - building your own blog engine.

This is a rundown of how, with a little bit of tinkering, I arrived at something that’s almost perfect for my needs without having to build an entire blog engine from scratch.

Bilbo Meme about starting a blog

Earlier this year prior to his departure from OpenAI, @karpathy posted a set of considerations for an “ideal blogging platform” that echoed most of my own preferences. It’s worth reading the tweet thread below for the context, motivations and expectations behind this effort, else the rest of this post could be confusing.

This combination doesn’t exist, although many things come close, and even when they do, they aren’t without some caveats.

My previous attempts at writing regularly failed because the setup wasn’t right, and when the setup isn’t right, there are a myriad of excuses not to write. The lesson here is that not accommodating these eccentricities doesn’t delay writing, it stifles the desire to write.

My list of requirements is similar to the one in the tweet above, however, I would add:

  • 🆓 Free
  • ⚡ Fast (100/100 Lighthouse Scores)
  • 💻 Customizable with code
  • 🐂💩 No licensing
  • 🐦 Easy Twitter/X embeds
  • ➕ Easy Math embeds
  • 👩‍💻 Easy Gist embeds
  • 📽 Easy YouTube embeds
  • 📜 Code blocks (Gists may be good enough but I’d like to have both)
  • 📱 Cross-device publishing, publish from anywhere

It goes without saying that these requirements kill off the non-technical options - Medium, Substack, Ghost, Wordpress. With the exception of Ghost, the others boast a range of annoyances ranging from how they handle data storage, to performance, to bloat.

For a short while, I considered something like Astro. This is often a great choice for many, however, for the thought of engaging with Javascript and enduring framework changes every so often, was enough to lead to an expeditious dismissal of the idea.

The Editor: Obsidian

Obsidian

Obsidian is great. It checks all the boxes for the editing experience and the ownership requirement. It also has vim mode. I’ve been using it for my notes for several years now, and I can’t recommend it enough. Notion is slow, macOS notes is bland, and neovim lacks a good wysiwyg preview mode, or Zettelkasten features.

With this approach, you own all the files. The files are a composed with a simple markdown syntax. They live on your devices and repos. You can sync them via git repository or other means. If you ever switch blogging platforms, the files are yours, forever, and decoupled from the engine in an open format.

Obsidian sells an “Obsidian Publish” service for $8/mo that solves a lot of what I set out to do for a fee, but this wouldn’t qualify for “Free” and it’s not 100/100 lighthouse fast.

We can do better.

Obsidian works quite well out-of-the box. A few minor configuration tweaks were enough to solve most of my problems.

Hugo Interop & Markdown Flavors

By default, Obsidian links are Wikilinks with their own syntax, whereas Hugo uses a more traditional markdown syntax. This is easily solved in Obsidian’s settings. Getting Hugo and Obsidian to work well together is described in this excellent post. The only meaningful addition I made here to the image rendering snippet was the following lazyload change to get 100/100 on lighthouse performance metrics.

<img loading="lazy" ... />

Perfect Lighthouse Score

Fireworks!

I also added rel="noopener nofollow" to external links and target="_blank"

File Locations

I found that pointing my Obsidian Vault to the GitHub repo where the markdown files lived was the best way to ensure everything stays together in harmony. For the uninitiated, you can point Obsidian at a folder on your device and it will treat it as a “vault” of information where you can link notes to each other (similar to links in Wikipedia) and manage them that way.

Image Filenames & Sizes

A growing number of blobs / binaries / images in my GitHub repos irritates me in principle. I was able to overcome this with a compromise that I would exclusively use WebP. This was a sufficient bargain to put my annoyances to rest.

The “Image Converter” Obsidian Community Plugin, does exactly this. Unfortunately, the plugin offered only one type of renaming format where it appends the date to the end of the image file name, which is not ideal since it gives away unnecessary information. To solve this, I made a quick change to the plugin to allow this customization. Obsidian plugins can be written in typescript, which is very convenient for this type of customization. My pull request to the original author is here.

The Generator: Hugo

There are a couple of options here. Jekyll is one. Hugo is better. This was one of the easier decisions.

Hugo is written in Go. It builds fast, it can consume markdown and generate HTML. No nonsense, no fuss, fully customizable, great language. I endorse.

As a bonus, Hugo comes with “shortcodes” that solve all the embed requirements out-of-the-box. For the uninitiated, “shortcodes” are a feature seen in Wordpress, Joomla, Drupal, phpBB etc. where a short snippet (often enclosed in square brackets) enables the embedding of some larger HTML snippet like a YouTube embed. This allows for a lot of media from other places to live in your content on your blog, website, forum, etc.

Hugo Embeds

Hugo’s default tweet embed suffers from one annoying flaw that was easily solved. Once again, thanks to Hugo’s easy-to-customize templates, changing things was a breeze. The annoyance was that embedded tweets always rendered in light mode, regardless of the mode that the blog was in. Hugo fetches the markup at build time using Twitter’s oEmbed API, which allows for a query parameter requesting either the light or dark embed.

This is no-good because the light/dark mode is a run-time toggle via CSS class toggle. As a simple workaround, I edited Hugo’s built-in tweet shortcode which is easily done by overriding layouts/shortcodes/tweet.html and using this markup:

This renders both light & dark mode, but conditional visibility triggered by CSS rules for light and dark mode ensures only one is shown at a time.

Incidentally, Gist embeds don’t appear to have a way of adapting to light/dark mode either. Gists don’t render in iframes, so a trivial CSS solution took care of it.

The Infrastructure: Cloudflare Pages

For a moment, I considered Netlify, but no.

CloudFlare Pages is free for up to 20,000 files, and serves everything from a CDN. I get 500 builds per month and a CDN for free. This eliminates the need for a traditional server of any kind, or managing any kind of infrastructure myself. This is another no-brainer, and, if I really wanted to, it becomes a gateway for more functionality via workers and other offerings.

The Infrastructure

As a result, I also get Cloudflare Analytics, which is one of the least accurate and consequently one of their most troublesome offerings. It will almost always count spambots and all sorts of other nefarious / malicious bot traffic.

I’m OK with this because I have zero actual readers.

The Workflow: git push

On a desktop or laptop, I sync the git repo, fire up Obsidian, write the blog post anywhere, even offline, perhaps on an airplane. When I’m online: git push, and it’s published within seconds.

Thanks to Android superiority and the incredible Termux, I can sync the Git repository with my phone, point the Obsidian mobile app to the folder, write or edit a blog post from the comfort of my phone, and sync it back.

Now all that remains is to write more often.