This Blog Now Speaks Gemini

A Text-Only Capsule, Generated by the Same Hugo Build

 · 5 min

Every article on this blog is now also available via the Gemini protocol, at gemini://belief-driven-design.com/.

No CSS, no JavaScript, no cookie banner.
Just text.


What’s Gemini?

Gemini is a small internet protocol from 2019 that sits somewhere between Gopher and the web. A client opens a TLS connection on port 1965, sends a single URL, and gets back a status line followed by the content. That’s the whole request/response cycle.

The native document format is gemtext, a line-based markup that looks a lot like Markdown on a strict diet:

text
# A heading
* A list item
> A quote
=> gemini://example.com/ A link, always on its own line

There are no inline links, no images inside the text, no tables, and escpecially no styling. How it looks is entirely up to the reader’s client.

Gemini is intentionally hard to extend, so it can’t slowly turn into the web again. No tracking pixels, no third-party scripts, no 5+MB of JavaScript to display 2,000 words when simple text does the same thing. Don’t get me wrong, personally, I like a lot of the modern web. But it often feels like we lost sight of the foundation, and that the actual content is the thing we should focus on.


Why Not Just Serve the Markdown?

Gemtext looks so much like Markdown that the obvious first idea is simply serving the .md from this Hugo-based blog and calling it a day.

Unfortunately, that falls apart quickly.

Even though Hugo uses markdown as its primary source format, the files are full of things only Hugo understands, like ref shortcodes for linking to other articles, a table of contents, or my custom book promo at the bottom.

So the conversion has to happen after Hugo resolved everything, but before it turns into HTML.


One Build, Two Outputs

Thankfully, Hugo supports custom output formats, meaning every page can be rendered more than once with different templates.

All related files are available on GitHub.

The existing build now writes an index.gmi right next to each index.html:

text
content/posts/*.md ──> hugo ──┬──> index.html   ──> nginx  (HTTPS)
                              └──> index.gmi    ──> Agate  (Gemini)

Define the new media type and output format in hugo.toml and we’re good to go:

toml
[mediaTypes]
  [mediaTypes.'text/gemini']
    suffixes = ['gmi']

[outputFormats]
  [outputFormats.gemini]
    mediaType = 'text/gemini'
    baseName = 'index'
    protocol = 'gemini://'
    isPlainText = true
    permalinkable = true

[outputs]
  page = [ "HTML", "gemini" ]

The protocol makes .Permalink return gemini:// URLs, which the capsule’s Atom feed needs.

The actual work happens in a a new set of template partials. It takes the page’s content from .RenderShortcodes, meaning the Markdown with all shortcodes already replaced by their gemtext variants, and walks it line by line:

  • Paragraphs:
    Soft-wrapped lines are joined, and the links are moved to => lines right below.

  • Headings:
    Capped at three levels, as that’s all gemtext has.

  • Code blocks:
    Passed through untouched, gemtext has preformatted blocks, too.

  • Tables:
    Become preformatted text, which is how they look in the Markdown source anyway.

  • Images:
    Turned into links, so the client decides whether to show them.

Here’s what that does to a typical paragraph:

text
// BEFORE: Markdown
For us as Java developers, a globally unique identifier is just a simple
[`UUID.randomUUID()`](https://docs.oracle.com/...) call away.

// AFTER: gemtext
For us as Java developers, a globally unique identifier is just a simple `UUID.randomUUID()` call away.
=> https://docs.oracle.com/... `UUID.randomUUID()`

As a nice side effect, building the converter flagged a handful of broken links in old articles that had gone unnoticed for years…

The serving side is Agate, a small Gemini server, running in Docker next to the nginx container. It reads the same directory nginx serves, so a single deployment updates both.


What Gets Lost

Nothing comes for free, of course.

As already mentioned, Gemtext can’t do inline links, so links pile up below their paragraph instead of sitting where they belong. In link-heavy paragraphs, that can get a little crowded. The table of contents is gone, too, but most clients offer an outline of the headings anyway.

Then there are certificates. Gemini clients usually don’t rely on certificate authorities but use TOFU, meaning “trust on first use”. The first visit pins the certificate, and any change later on triggers a warning. So the capsule’s certificate is something to back up, not to rotate casually.

The imprint and privacy policy are only available on the web, as they contain contact details I don’t want to put into plain text for every scraper out there.


Give It a Try

All it takes is a Gemini client, like Lagrange for desktop, or Amfora for the terminal.

Or, if you just want to peek at the raw protocol, openssl is enough:

shell
printf 'gemini://belief-driven-design.com/\r\n' \
  | openssl s_client -quiet -connect belief-driven-design.com:1965 -servername belief-driven-design.com

The response starts with 20 text/gemini; lang=en, followed by the capsule’s home page.

The HTML version isn’t going anywhere, and it’s still the primary way to read this blog. But if you prefer your articles without the web around them, the capsule is there now, and it’s updated with every new article.

If you run a capsule yourself, or have a favorite client I should check out, just let me know!