Gemini et Hugo

Depuis quelques années, j’utilise le générateur de sites statiques Hugo pour produire ces pages. À la fois très rapide et adapté à mes besoins, il est surtout très adaptable.

Je n’ai donc pas été surpris de voir qu’il était relativement simple de convertir, avec peu d’efforts, ce site pour le protocole Gemini. Cela ne se fait pas sans quelques astuces : c’est l’objet de cet article.

Créer un format de sortie pour Gemini

Hugo peut exporter des contenus sous plusieurs formats : la plupart sont déjà prédéfinis, mais il est aussi possible de créer les siens. C’est ce que nous allons faire pour Gemini.

Tout d’abord, dans le fichier de configuration config.yml, on déclare un nouveau type text/gemini avec le suffixe .gmi :

mediaTypes:
  text/gemini:
    suffixes:
      - "gmi"

Une fois cela fait, on déclare un nouveau format de sortie, qui utilise ce type, auquel nous allons donner le nom GEMINI.

C’est l’occasion de spécifier le protocole pour les liens (gemini://), et d’indiquer que l’on souhaite construire le site dans un dossier séparé (gemini), ce qui rendra l’export de nos fichiers vers le serveur plus facile :

outputFormats:
  Gemini:
    name: GEMINI
    isPlainText: true
    isHTML: false
    mediaType: text/gemini
    protocol: "gemini://"
    permalinkable: true
    path: "gemini/"

Finalement, il ne nous reste qu’à demander à Hugo de générer ces pages dans les nouveaux contenus. Par exemple, dans mon cas :

outputs:
  home: ["HTML", "RSS", "GEMINI"]
  page: ["HTML", "GEMINI"]

Désormais, pour pouvoir générer ces fichiers, il ne nous reste qu’à créer des gabarits dans layouts.

Page d’accueil

Commençons avec la page d’accueil, que nous allons créer dans layout/index.gmi. Mettons par exemple un simple texte, suivi d’une liste d’articles, suffit :

## Liste d'articles

{{ range (where .Site.RegularPages "Section" "articles") }}
=> {{ replace .Permalink "/gemini" "" 1}} {{ .Title }}
{{ end }}

Ici, je trie les articles du plus récent au plus ancien, en les regroupant par date. Cela donne le code suivant :

## Articles groupés par date

{{ range .Site.RegularPages.GroupByDate "2006" }}
### {{ .Key }}
{{ range .Pages.ByDate.Reverse }}
=> {{ replace .Permalink "/gemini" "" 1}} {{ .Title }}
{{- end }}
{{ end }}

Comme vous avez pu le remarquer, j’ai manuellement supprimé les références au dossier /gemini, afin d’éviter qu’ils n’apparaissent dans les liens finaux.

Articles

Pour nos pages, nous pouvons créer le gabarit layout/_default/single.gmi. En théorie, il suffirait d’afficher le titre et le contenu :

# {{ .Title }}

{{ .RawContent }}

Suppression du Markdown

Pour enlever les codes, il est possible d’appliquer plusieurs filtres successifs selon vos besoins. Par exemple, les lignes suivantes vont supprimer l’italique :

{{ $content := .RawContent -}}
{{ $content := $content | replaceRE "\\*(.+?)\\*" "$1" -}}
{{ $content }}

Ces lignes peuvent être également utilisées pour enlever les marqueurs de code :

{{ $content := $content | replaceRE "`(.+?)`" "$1" -}}
{{ $content := $content | replaceRE "`" "```*(.+?)" -}}

Images

S’agissant des images, je les transforme en lien :

{{- $content = $content | replaceRE `\!\[(.+?)\]\((.+?)\)` "=> $2 Image: $1" }}

Liens

Pour les liens, c’est presque la même chose, mais il est nécessaire de capturer le texte qui suit, afin de pouvoir afficher les liens après les paragraphes en cours de lecture. J’utilise une boucle pour m’assurer de récupérer tous les liens d’une même ligne :

{{- range findRE `\[.+?\]\(.+?\)` $content }}
    {{- $content = $content | replaceRE `\[(.+?)\]\((.+?)\)(.+)` "$1$3\n\n=> $2 $1 " }}
{{- end }}

J’aime par ailleurs ajouter quelques liens en haut et en bas de la page, pour naviguer facilement vers les articles suivants et précédents :

{{ if .Next }}=> {{ replace .Next.Permalink "/gemini" "" 1}}  Newer: {{ .Next.Title }}{{ end }}
{{ if .Prev -}}=> {{ replace .Prev.Permalink "/gemini" "" 1}}  Older: {{ .Prev.Title }}{{- end }}

Flux

Pour créer des flux ATOM ou RSS, il est possible de créer un nouveau format, puis d’en définir la structure.

Atom

Dans config.yml, on définit :

mediaTypes:
  application/atom:
    suffixes:
      - "xml"

outputFormats:
   GEMINI_ATOM:
     baseName: "atom"
     path: "/gemini/"
     protocol: "gemini://"
     mediaType: "application/atom"

outputs:
  home: ["HTML", "GEMINI", ... "GEMINI_ATOM"]

On créé alors un fichier layout/index.gemini_atom.xml :

<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="{{ .Lang }}">
  <title>{{ .Site.Title }}</title>
  <link href="{{ (replace .Permalink "https://" "gemini://") | safeURL }}atom.xml" rel="self" type="application/atom+xml" />
  <link href="{{ (replace .Permalink "https://" "gemini://") | safeURL }}" rel="alternate" type="text/html" />
  <updated>{{ now.Format "2006-01-02T15:04:05Z" | safeHTML }}</updated>
  <author>
    <name>Sylvain Durand</name>
  </author>
  <id>{{ (replace .Permalink "https://" "gemini://") | safeURL }}</id>

  {{- range (where .Site.RegularPages "Section" "articles") }}
  <entry>
    <title>{{ .Title | markdownify | plainify }}</title>
    <link href="{{ (replace .Permalink "https://" "gemini://") | safeURL }}" rel="alternate" type="text/html" />
    {{- if .IsTranslated -}}
    {{- range .Translations }}
    <link href="{{ (replace .Permalink "https://" "gemini://") | safeURL }}" rel="alternate" hreflang="{{ .Language.Lang }}"/>
    {{- end -}}
    {{ end }}
    <id>{{ (replace .Permalink "https://" "gemini://") | safeURL }}</id>
    <published>{{ .Date.Format "2006-01-02T15:04:05-07:00" | safeHTML }}</published>
  </entry>
  {{- end }}
</feed>

Le flux ATOM est alors disponible sur /atom.xml.

RSS

La démarche sera pratiquement identique !

Dans config.yml, on définit :

mediaTypes:
   application/rss+xml:
     suffixes:
       - "xml"

outputFormats:
  GEMINI_RSS:
    baseName: "rss"
    path: "/gemini/"
    mediaType: "application/rss+xml"
    isPlainText: false

outputs:
  home: ["HTML", "GEMINI", ..., "GEMINI_RSS"]

On créé alors un fichier layouts/index.gemini_rss.xml qui contient :

<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="{{ with .OutputFormats.Get "RSS" }}{{ (replace .Permalink "https://" "gemini://") | safeURL }}{{ end }}" rel="self" type="application/rss+xml" />
    {{- range (where .Site.RegularPages "Section" "articles") }}
    <item>
      <title>{{ .Title | markdownify | plainify }}</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>

Le flux RSS est alors disponible à /rss.xml.

Exportation

J’utilise rsync pour exporter facilement les fichiers vers le serveur. Le dossier public est exporté vers /var/www sur le serveur, sans public/gemini, qui est pour sa part exporté vers /var/gemini. J’utilise pour cela :

hugo

rsync -avz --no-perms --no-owner --no-group \
      --no-times --delete --exclude "gemini" \
      public/ myserver:/var/www/sylvaindurand

rsync -avz --no-perms --no-owner --no-group \
      --no-times --delete \
       public/gemini/ vps:/var/gemini

rm -rf public

Ce dernier dossier est lu par un serveur Gemini, comme expliqué dans l’article À la découverte du protocole Gemini.

Fin !

Ce site peut être consulté à l’adresse gemini://sylvaindurand.fr pour voir le résultat. Vous pouvez vous inspirer de son code source, qui est disponible ici : Code source du site.