For a few years, I have been using Hugo, a static site generator, to produce these pages. At the same time very fast and corresponding perfectly to my needs, it is above all very modular.
I was therefore not surprised to see that it was quite easy to convert, with little effort, my site for the Gemini protocol. This was not done without some tricks. Let’s see how!
Declaring Gemini as an output format
Hugo can output content in multiple formats: most of them are already predefined, but it is also possible to create your own. This is what we are going to do for Gemini.
First, in the configuration file config.yml
we will declare a new type text/gemini
with the file suffix .gmi
:
mediaTypes:
text/gemini:
suffixes:
- "gmi"
Once this is done, we declare a new output format, which uses this type, which is given the name GEMINI
.
outputFormats:
GEMINI:
name: GEMINI
isPlainText: true
isHTML: false
mediaType: text/gemini
protocol: "gemini://"
permalinkable: true
Finally, it only remains to ask Hugo to generate pages for the different contents. For example, in my case:
outputs:
home: ["HTML", "RSS", "GEMINI"]
page: ["HTML", "GEMINI"]
To be able to generate the files, it is now necessary to create layouts to see how to display them!
Index page
To start with the index, we can start with layout/index.gmi
. For example, here is a simple text, followed by a list of posts:
## List of posts
{{ range .RegularPages }}
=> {{ .RelPermalink }} {{ .Title }}
{{- end }}
Here, I sort the articles in descending chronological order, grouping them by date. This gives the following code:
## Posts grouped by year
{{ range .RegularPages.GroupByDate "2006" }}
### {{ .Key }}
{{ range .Pages.ByDate.Reverse }}
=> {{ .RelPermalink }} {{ .Title }}
{{- end }}
{{ end }}
Posts
For posts, we can create a layout/_default/single.gmi
. Basically, it would suffice to display the title and content:
# {{ .Title }}
{{ .RawContent }}
Images
For images, I extract them with a simple regex and show them as a link:
{{- $content := .RawContent -}}
{{- $content = $content | replaceRE `\!\[(.+?)\]\((.+?)\)` "=> $2 Image: $1" }}
{{ $content }}
Links
For the links, I decided to simply not use inline links on the site, but only put the links on a single paragraph. This allows me, as before, a very simple regex:
{{- range findRE `\[.+?\]\(.+?\)` $content }}
{{- $content = $content | replaceRE `\[(.+?)\]\((.+?)\)(.+)` "$1$3\n\n=> $2 $1 " }}
{{- end }}
However, this is not a very satisfactory method when you have a site that has a lot of links online. A solution, proposed by the site Brain Baking, allows you to reference each link with a number ([1], [2]…) and then to put the links underneath, automatically, thanks to a clever code from Brainbaking.
Navigation to other pages
If you want to add links for previous and next articles with:
{{ if .Next }}=> {{ .Next.RelPermalink }} ← Newer: {{ .Next.Title }}{{ end }}
{{ if .Prev -}}=> {{ .Prev.RelPermalink }} → Older: {{ .Prev.Title }}{{- end }}
Feeds
To create RSS feeds, we can create a new output format, then define its layout.
RSS
We will do the same here! In config.yml
, we define:
outputFormats:
GEMINI_RSS:
baseName: "feed"
mediaType: "application/rss+xml"
isPlainText: false
outputs:
home: ["HTML", "GEMINI", ..., "GEMINI_RSS"]
Then, we create layouts/index.gemini_rss.xml
with the following content:
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>{{ .Site.Title }}</title>
<description>{{ i18n "description" }}</description>
<link>{{ (replace .Permalink "https://" "gemini://") | safeURL }}</link>
<atom:link href="{{ .Permalink | safeURL }}feed.xml" rel="self" type="application/rss+xml" />
{{- range .RegularPages }}
<item>
<title>{{ .Title }}</title>
<link>{{ (replace .Permalink "https://" "gemini://") | safeURL }}</link>
<pubDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</pubDate>
<guid>{{ (replace .Permalink "https://" "gemini://") | safeURL }}</guid>
</item>
{{ end }}
</channel>
</rss>
The RSS feed is now available on /feed.xml
.
Export
I use rsync
to easily export my files to the server:
hugo
rsync -avz --no-perms --no-owner --no-group \
--no-times --delete public/ vps:/var/gemini
rm -rf public
This last folder is then read by a gemini server, as explained in the previous article “Discovering the Gemini protocol”.