===============
== bacardi55 ==
===============
ἕν οἶδα ὅτι οὐδὲν οἶδα

From hugo markdown to gemtext with lowdown

- Permalink

I’m still working on deploying again my blog to gemini, and now that I can export gemini links from orgmode to markdown, it was time for the next step(s). I decided years ago to kiln, a simple capsule generator, because it was simple yet close enough to my ways of working with Hugo for my website. The important points are:

  • Content is in gemtext with a frontmatter header, like in hugo
  • Output is customized via template files
  • URL pattern are configurable (important for later)

For the past year, I searched multiple times the best way to convert markdown to gemtext (or even html to gemtext). The best I found was md2gemini (by makew0rld) but I had some issues with it. Also, it didn’t took into consideration the frontmatter format so it was breaking more than expected. I could have removed the frontmatter content, run the script, and then copied the frontmatter content at the top of the generated output, or edited the tool, but never had the motivation to do last year.

Then, doing another random search recently, I stumbled on a lost reddit page that mention lowdown… And my brain exploded with happiness (well maybe not, but i was happy after some quick tests)! Lowdown is a great tool to transform markdown files in different format. It can export it to html5, roff, man, latex… and gemtext! And it works with frontmatter content too! Going to write a bit more about lowdown later, but the tldr; is: it is an awesome tool to export markdown in another format like gemtext while using data from the frontmatter header :-).

The main issue I had with lowdown was that it can only parse frontmatter content in the yaml format. Hugo accept frontmatter data in multiple format, yaml of course, but others too like toml. And of course at this stage I was exporting my orgmode post to markdown with the frontmatter data in the toml format (the default used by ox-hugo)…

Well, step one was to change the frontmatter format from toml to yaml, which was as easy as adding a configuration line at the top of my orgmode file:

#+HUGO_FRONT_MATTER_FORMAT: yaml

Then I had to re-export every posts, which was a bit painful but easy.

So at this point, I had markdown files (as before) but with yaml formated frontmatter, which changes nothing to Hugo to generate my blog. But is the first step towards generating gemtext that kiln will use.

Second steps was to actually convert those markdown files to gemtext. As said, I used lowdown. Lowdown allows to use a template file to generate the desired output. Again, I’ll write more about this great CLI tool later, but in a nutshell, I created the following template (called lowdown_gemtext.tpl):

---
title: $title$
date: $meta(date)$
params:
  categories: $categories$
  tags: $tags$
---

$body$

Using this template, it will generate the gemtext file with the frontmatter parameters I want to keep from the markdown files and then the body. That is an improvement from my previous way of doing things as I was loosing the categories and tags data (more on that later), but not anymore!

Then, to transform a markdown file into a gemtext one, I used the following command:

lowdown -t gemini --gemini-link-noref --template ./lowdown_gemtext.tpl markdown_file.md > gemtext_file.gmi
  • -t gemini indicates that the markdown content is exported in gemtext
  • --gemini-link-noref add links without reference, as i didn’t like to see [a], [b], … in the middle of my paragraphs being referenced after. Using this option, it just show the links below each paragraph as just links without title, which I find better than just letters or numbers
  • --template /path/to/template indicate the template file to use
  • markdown_file.md is the file to transform
  • gemtext_file.gmi is the destination file.

And it worked great!… Well almost great :D. There was one remaining problem: images.

When I add an image within my orgmode post, ox-hugo will generate something like that in the exported markdown:

{ {< figure src="/images/posts/2025/01/poll.png" caption="<span class=\"figure-number\">Figure 1: </span>Image showing a fediverse poll about web pages vs external apps. 47% preferred \"always or usually brower\" vs 53% preferred \"always or usually separate program\"" >} }

(without the spaces between the curly brackets, but that was being interpreted by hugo…)

Lowdown don’t do anything to this (as it should, it isn’t anything markdown related) and keeps it as is. Of course, that is not valid gemtext… Instead, I wanted something like:

=> /path/to/image/poll.png

What to do then? Use one of the greatest thing in programming: regexp <3. I used sed after the lowdown command to rewrite this content to the appropriate gemtext format:

lowdown -t gemini --gemini-link-noref --template ./lowdown_gemtext.tpl markdown_file.md \
  | sed -E 's/\{\{< figure src=”\/images\/posts([^”]*)”.*>\}\}/=> \/blog\1/g' \
  > gemtext_file.gmi

And voilà, the gemtext was now correct.

I’ll write some additional details around automating all of this in the part 3 of my series about my “unncessary complex deployment workflow”, a series started a year ago to fully automate my writing deployments that was eagerly waiting for the last part about the automation of my capsule deployment. Now with this step completed, I’m finally getting there.

For the impatient people, the gist of the deployment is easy once you’ve read the part 1 and 2. In the build file, there is build_capsule.sh file that needs to:

  • generate blog posts gemtext from markdown
  • copy blog posts images from the hugo directory to kiln directory
  • build with kiln

Using kiln configuration, I used the same URL for posts in the web and gemini, which means that links for blog posts will now be in the form of /<year>/<month>/<day>/<title>/ and not /blog/<year>/<month>/<day>/<title>/ as it used to be. It may break some search engine results pages for some time but should not be a big problem in the long run. By doing so, I fixed easily all internal links from one blog posts to another in the gemini format, so worth it :-).

There are still some issues remaining in the gemini version of my blog, but I consider those minors. I’ll work on it after finishing automating its deployment as they are not blockers (to me at least). But more on that later in a dedicated post about bringing back my blog to gemini!


Contact

If you find any issue or have any question about this article, feel free to reach out to me via webmentions, email, mastodon, matrix or even IRC, see the About page for details.