---
title: "Hugo : Feed Atom, JSON, RSS"
date: 2019-12-11T18:35:25+01:00
description: "Comment mettre en place un flux de données RSS, ET Atom, voire JSON !"
draft: false
lastmod: 2019-12-12T20:17:25+01:00
tags: ["Hugo","Feed","RSS","Atom","JSON"]
---
## Description
Parmi les flux de données, {{< tag Hugo >}} est capable de créer plusieurs
flux dont {{< abbr RSS "Really Simple Syndication" >}}
*(actuellement, à la norme 2.0)*.
**Hugo** est donc capable de générer un flux {{< anchor RSS rss >}}, ou
de type {{< anchor JSON json >}}, mais pas {{< anchor ATOM atom >}} -
*pour ce dernier, nous verrons comment faire dans le chapitre ad hoc*.
## Documentation
Un petit tour sur la documentation officielle Hugo :
* {{< gohugo n="rss" s="templates" >}}
* {{< gohugo n="output-formats" s="templates" >}}
* {{< gohugo n="site" s="variables" a="site-variables-list" >}}
* {{< gohugo n="hugo" s="variables" >}}
* {{< gohugo s="functions" n="sha" >}}
* {{< gohugo n="range" s="functions" >}}
## Détails Techniques
Commençons par les détails techniques suivants à-propos de la gestion de
différents éléments ; chaque format de flux a ses propres spécificités,
certaines sont communes ou peuvent se ressembler. Merci de lire les
{{< anchor RFC "documentations tierces" >}} adéquates.
### author
Les trois formats de flux gérent un élément `author` dans les différentes
entrées générées. Quant à l'auteur du site, là où ATOM et JSON ont leur
élément `author` avec leurs spécificités, RSS a son propre élément
`managingEditor`, voire `webMaster`.
Il faut configurer le bloc `author` dans le fichier de configuration,
de telle manière :
```toml
[params]
[params.author]
email = "courriel@domaine.tld"
name = "Nom Prénom"
```
### category
Là où ATOM et RSS sont capables de générer un élément `catégory`, JSON
utilise l'élément `tag`.
Dans tous les cas, j'ai utilisé la taxonomie des tags pour les créer.
Il faut configurer la taxonomie dans le fichier de configuration, de
telle manière :
```toml
[taxonomies]
tag = "tags"
```
### copyright
Là où RSS a son élément `copyright`,
* ATOM peut annoncer par le biais d'un élément `link` qui permet de
cibler un fichier de licence - *telle une licence {{< abbr CC "Creatives Commons" >}}* -
ainsi que son élément `rights`.
* JSON n'a rien de prévu.
Il faut configurer la variable `copyright` dans le fichier de configuration.
### description
Là où RSS et JSON ont leur élément `description` du flux, ATOM n'en a pas,
mais il est possible d'utiliser l'élément `subtitle`.
Concernant l'usage de l'élément `description`, j'utilise personnellement
la variable `site.Params.description`.
Il faut configurer la variable `description` dans le fichier de configuration.
### generator
Seul ATOM et RSS sont capables de générer un élément `generator`, bien
qu'ATOM le fasse plus finement.
On peut utiliser les variables spécifiques à Hugo.
### id
Pour RSS, l'identifiant d'une entrée est l'élément `guid`. ATOM et JSON
ont un élément `id` . ATOM a aussi son identifiant de site.
Il est important de veiller à ce que cet identifiant soit unique ; il y
a plusieurs manières de les générer :
* Soit tout simplement par le biais de l'URL du site, ou de l'article
correspondant à l'entrée :
`{{ .Permalink }}`
* Soit en utilisant le schéma de notation `tag`, définie par la RFC 4151,
de type `tag:identifiant,DATE:alphanumerique` et où `tag:identifiant,DATE`
ne doit pas changer sur l'ensemble du site, seule la partie `alphanumerique`
sera l'objet de l'unicité de l'article ou de l'identifiant du site :
* où l'identifiant peut être soit une adresse mail, soit le nom de
domaine ; ce dernier semble être la préférence.
* où DATE doit correspondre à la norme ISO 8601, à minima l'année,
codée sur 4 chiffres, telle que `2019` ; la préférence peut être
donnée à la forme `YYYY-MM-DD` où l'année, le mois et le jour
sont séparés par un tiret `-`, telle que `2019-10-07` qui peut
correspondre à la date de création de votre domaine, par exemple.
* où `alphanumerique` est un ensemble de lettres et de chiffres.
* Pour exemple :
* Pour l'identifiant du site :
`{{ $url := urls.Parse .Permalink }}{{ $id := print "tag:" $url.Host ",2019-10-07:" }}{{ $id }}website`
* Pour l'identifiant d'entrée :
`{{ $id }}{{ anchorize .RelPermalink }}`
* Pour RSS, remplacez la balise `` par ``.
De même, si vous utilisez la notation par tag ou par UURI,
il vous faudra ajouter l'attribut `isPermalink`, tel que :
`isPermaLink="false"`
* Soit en utilisant le schéma de notation `UURI` définie par la RFC 4122 -
*qui est humainement incompréhensible* ; l'avantage avec Hugo est qu'on
peut la générer dynamiquement en utilisant la fonction `sha`, tout
particulièrement `sha1` conforme à la version 5 de ladite numérotation,
telle que :
```hugo
{{ $uuid := sha1 .Permalink }}urn:uuid:{{substr $uuid 0 8}}-{{substr $uuid 10 4}}-{{substr $uuid 15 4}}-{{substr $uuid 20 4}}-{{substr $uuid 25 12}}
```
### image
Là où RSS gère l'élément `image` pour afficher le logo,
* ATOM et JSON gèrent deux éléments différents, `icon` pour l'image de
favicon, et `logo` pour votre… logo !
* JSON est capable, en plus, de gérer un avatar pour l'auteur.
### Limite
{{< note warning >}}Il semble nécessaire d'utiliser au moins Hugo v0.55.x{{< /note >}}
Le code Hugo suivant, *que j'utilise dans chacun des modèles*, permet de
limiter le nombre d'entrées selon :
* La limite du service RSS,
* Si les pages ont le paramètre `disable_feed` sur `true`, elles ne sont
pas incluses.
* On capture le nombre de pages,
* Puis on utilise la fonction Hugo `range` pour générer dynamiquement le
nombre d'entrées - *l'usage de la fonction dans le modèle pour JSON
différe légérement de celui pour les modèles ATOM et RSS* - :
```hugo
{{- $limit := (cond (le site.Config.Services.RSS.Limit 0) 65536 site.Config.Services.RSS.Limit) -}}
{{- $pages := where site.RegularPages ".Params.disable_feed" "!=" true -}}
{{- if ge $limit 1 -}}{{- $pages = $pages | first $limit -}}{{- end -}}
```
## Configuration
* Le fichier de configuration principal : `config.toml`
### RSS
{{< note info >}}Il est possible de spécifier dans le fichier de
configuration le format de sortie RSS pour `home`, `section` et les deux
`taxonomy`, `taxonomyTerm`.{{< /note >}}
Personnellement, je renomme le nom du flux RSS en modifiant dans le
fichier de configuration, la variable `baseName` à `rss` parce que je ne
pense pas que celui-ci doit porter le nom `index`.
En effet, le nom `rss.xml` est plus parlant !
```toml
[outputs]
home = ["HTML", "RSS"]
[outputFormats.RSS]
baseName = "rss"
```
{{< note info >}}Sachez qu'il est possible de surcharger la génération
native du flux RSS, en créant un modèle dans `_default`. {{< /note >}}
#### RSS:Template
{{< note warning >}}Il n'est pas nécessaire de créer ce modèle du fait
que Hugo génére correctement, mais pour de l'anglais, le fichier RSS,
nommé `index.xml`.{{< /note >}}
J'ai créé mon propre modèle, tenant compte du fait d'être multilangue.
----
```xml
{{ printf `` | safeHTML }}
{{ if eq .Title site.Title }}{{ site.Title }}{{ else }}{{ with .Title }}{{.}}{{ T "on" }}{{ end }}'{{ site.Title }}'{{ end }}
{{ .Permalink }}
{{ with site.Params.description -}}{{ . }}{{- end }}
http://blogs.law.harvard.edu/tech/rss
Hugo {{ Hugo.Version }}
Logo
128
{{ .Permalink }}
{{ if eq .Title site.Title }}{{ site.Title }}{{ else }}{{ with .Title }}{{.}}{{ T "on" }}{{ end }}'{{ site.Title }}'{{ end }}
{{ site.BaseURL }}img/Logo.png
128
{{ with site.LanguageCode }}{{.}}{{end}}
{{ with site.Params.author.email }}{{.}}{{ with site.Params.author.name }} ({{.}}){{end}}
{{.}}{{ with site.Author.name }} ({{.}}){{end}}{{end}}
{{ with site.Copyright }}© {{ $.Date.Format "2006" | safeHTML }} {{ site.Author.name }}; {{.}}{{end}}
{{ if not .Date.IsZero }}{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}{{ end }}
{{ with .OutputFormats.Get "RSS" }}{{ printf `` .Permalink .MediaType | safeHTML }}{{ end }}
{{- $limit := (cond (le site.Config.Services.RSS.Limit 0) 65536 site.Config.Services.RSS.Limit) -}}
{{- $pages := where site.Pages ".Params.disable_feed" "!=" true -}}
{{- if ge $limit 1 -}}{{- $pages = $pages | first $limit -}}{{- end -}}
{{- range first $limit $pages }}
-
{{ .Title }}
{{ .Permalink }}
{{ with .Summary }}{{ . | html }}{{end}}
{{ with site.Params.author.email }}{{.}}{{ with site.Params.author.name }} ({{.}}){{end}}{{end}}{{ $url := printf "%s" "/tags/" | absLangURL }}
{{ with .Params.tags }}{{ range . }}{{.}}{{end}}{{end}}
{{ .Permalink }}
{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}
{{ end }}
```
### Atom
Nativement Hugo ne génére pas de flux Atom. Il faut tout créer ; ce n'est pas bien difficile !
Il est nécessaire de modifier le fichier de configuration pour :
- créer un nouveau {{< anchor "type de média" "atom-mediatype" >}}
- créer un nouveau {{< anchor "format de sortie" "atom-ouputformat" >}}
- créer le {{< anchor modèle "atom-template" >}} pour le flux Atom
#### Atom::MediaType
##### Hugo >= 0.20
Depuis Hugo 0.20, il faut ajouter :
```toml
[mediaTypes]
[mediaTypes."application/atom+xml"]
suffix = "xml"
```
Là, nous avons donc implémenté un nouveau type de format ayant pour mime
type : `application/atom+xml`, et pour nom d'extension : `xml`.
##### Hugo >= 0.44
Depuis Hugo 0.44, pour que cela fonctionne correctement il faut ajouter :
```toml
[mediaTypes]
[mediaTypes."application/atom+xml"]
suffixes = ["xml"]
```
{{< note tip >}}Si votre ancienne configuration précédait la 0.44, il
faut adapter/transformer la variable `suffix` en `suffixes = ['xml']` !
{{< /note >}}
#### Atom::OuputFormat
La déclaration du format de sortie, à ajouter :
```toml
[outputFormats.Atom]
baseName = "atom"
isPlainText = false
mediaType = "application/atom+xml"
```
Puis, il faut ajouter `"ATOM"` à votre variable `home`, tel que :
```toml
[outputs]
home = ["HTML", "ATOM", "RSS"]
```
#### Atom::Template
Le modèle peut simplement être créé dans le répertoire `layouts/` et se
nommer `index.atom.xml`, ou être dans son sous-répertoire `_default/` et
se nommer, par exemple : `home.atom.xml`.
* Si le site est multilangue, les liens alternatifs vers la version de
langue correspondante à l'entrée du site, aux flux atom, RSS, voire JSON
sont générés.
----
```xml
{{ printf `` | safeHTML }}
{{ .Permalink }}
{{ if eq .Title site.Title }}{{ site.Title }}{{ else }}{{ with .Title }}{{.}}{{ T "on" }}{{ end }}'{{ site.Title }}'{{ end }}
{{ with site.Params.description -}}{{ . }}{{- end }}
{{ with .OutputFormats.Get "ATOM" }}{{ printf `` .Permalink .MediaType | safeHTML }}{{end}}
{{ range .AlternativeOutputFormats -}}
{{end}}{{ if site.IsMultiLingual }}{{ range site.Languages }}{{ if ne .Lang site.Language.Lang }}{{ $lang := .Lang }}
{{ with $.OutputFormats.Get "ATOM" }}{{ printf `` (print $lang "/" .Name "." (index .MediaType.Suffixes 0) |absURL) $lang .MediaType | safeHTML }}{{end}}
{{ range $.AlternativeOutputFormats -}}
{{end}}{{end}}{{end}}{{end}}{{ with site.Copyright }}{{ $lang := site.Language.Lang }}
© {{ $.Date.Format "2006" | safeHTML }} {{ site.Params.author.name }}{{end}}
/img/favicon.ico
{{ with site.Params.logo }}{{.}}{{end}}
{{ if not .Date.IsZero }}{{ .Date.Format "2006-01-02T15:04:05-07:00" | safeHTML }}{{ end }}
{{ with site.Params.author.name }}
{{.}}
{{ with site.Params.author.email }}{{.}}{{end}}
{{ $.Permalink }}
{{end}}
Hugo
{{- $limit := (cond (le site.Config.Services.RSS.Limit 0) 65536 site.Config.Services.RSS.Limit) -}}
{{- $pages := where site.RegularPages ".Params.disable_feed" "!=" true -}}
{{- if ge $limit 1 -}}{{- $pages = $pages | first $limit -}}{{- end -}}
{{- range first $limit $pages }}
{{ .Permalink }}
{{ .Title }}{{ with site.Params.author }}
{{.}}
{{end}}{{ $url := printf "%s" "/tags/" | absLangURL }}{{ with .Params.tags }}{{ range . }}
{{end}}
{{end}}
{{ with .Content }}{{ `{{end}}
{{ with .Summary }}{{ `{{end}}
{{ .Date.Format "2006-01-02T15:04:05-07:00" | safeHTML }}
{{ if gt .Lastmod .Date }}{{ .Lastmod.Format "2006-01-02T15:04:05-07:00" | safeHTML }}{{end}}
{{ end }}
```
### JSON
Pour générer le flux JSON, nous nous conformerons à la norme JSON Feed v1.
#### JSON::OutputFormat
La déclaration de sortie de format à ajouter :
```toml
[outputFormats.JSON]
mediaType = "application/json"
baseName = "feed"
suffix = "json"
IsHTML = false
IsPlainText = true
noUgly = false
rel = "alternate"
```
Puis, il faut ajouter la déclaration `JSON` à votre variable `home`, tel que :
```toml
[outputs]
`home = ["HTML", "ATOM", "JSON", "RSS"]
```
#### JSON::Template
##### JSON: Détails
En plus de la {{< anchor limite limite >}} mentionnée plus haut, nous
récupèrons le nombre de page, pour boucler correctement car le dernier
`item` ne doit pas être suivi du symbole ',' :
```hugo
{{- $length := (len $pages) -}}
```
Et en fin de boucle `range`, nous ajoutons :
```hugo
{{ if ne (add $index 1) $length }},{{ end }}
```
Ainsi tant qu'il y a un élément suivant, il est précédé du symbole ','
jusqu'au dernier.
---
```json
{{- $limit := (cond (le site.Config.Services.RSS.Limit 0) 65536 site.Config.Services.RSS.Limit) -}}
{{- $pages := where site.RegularPages ".Params.disable_feed" "!=" true -}}
{{- if ge $limit 1 -}}{{- $pages = $pages | first $limit -}}{{- end -}}
{{- $length := (len $pages) -}}
{
"version": "https://jsonfeed.org/version/1",
"title": "{{ site.Title }}",
"home_page_url": "{{ site.BaseURL }}",
{{ with .OutputFormats.Get "JSON" -}}"feed_url": "{{.Permalink}}",{{- end }}
{{ with site.Params.description -}}"description": "{{ . }}",{{- end }}
{{ with site.Params.author.name }}
"author": {
"avatar": "{{ with site.Params.logo }}{{ . }}{{ end}}",
"name": "{{ . }}",
"url": "http://huc.fr.eu.org"
},
{{- end }}
{{ with site.Params.logo }}"icon": "{{ . }}",{{- end }}
"favicon": "/img/favicon.ico",
"items": [ {{- range $index, $elements := $pages -}}
{
"id": "{{ .Permalink }}",
"url": "{{ .Permalink }}",
"title": "{{ .Title | plainify }}",
{{ with site.Params.Author }}"author": { "name": "{{ . }}" }{{ end }}{{ with .Content }},
"content_text": {{ . | plainify | jsonify }},
"content_html": {{ . | safeHTML | jsonify }}{{ end }}{{ with .Summary }},
"summary": {{ . | plainify | jsonify }}{{ end }}{{ with .Params.tags }},
"tags": [{{ range $tindex, $tag := . }}{{ if $tindex }}, {{ end }}"{{ $tag| htmlEscape }}"{{ end }}]{{ end }}{{ if .PublishDate }},
"date_published": "{{ .PublishDate.Format "2006-01-02T15:04:05-01:00" | safeHTML }}"{{ end }}{{ if gt .Lastmod .Date }},
"date_modified": "{{ .Lastmod.Format "2006-01-02T15:04:05-01:00" | safeHTML }}"{{ end }}
}{{ if ne (add $index 1) $length }},{{ end }}{{- end }}
]
}
```
## Documentations tierces
* RFC 3339 : https://tools.ietf.org/html/rfc3339
* RFC 4122 : https://tools.ietf.org/html/rfc4122
* RFC 4151 : https://tools.ietf.org/html/rfc4151
* RFC 4287 : https://tools.ietf.org/html/rfc4287
* RFC 5988 : https://tools.ietf.org/html/rfc5988
* RFC 8288 : https://tools.ietf.org/html/rfc8288
* Norme ISO 8601 : https://www.w3.org/TR/1998/NOTE-datetime-19980827
* L'article d'OpenWeb : [Comment construire un flux Atom ?](https://openweb.eu.org/articles/comment-construire-un-flux-atom)
* Plus d'informations sur la norme d'identification `tag` sur le site [Tag URI](http://www.taguri.org/)
* Plus d'informations sur les UUID : {{< wp "Universal_Unique_Identifier" >}}
* La spécification JSON Feed 1 : https://jsonfeed.org/version/1
* La spécification RSS 2.0 : https://cyber.harvard.edu/rss/index.html
* Le sujet [Atom and JSON feeds](https://discourse.gohugo.io/t/atom-and-json-feeds/13572) sur le forum de la communauté Hugo - *qui m'a bien aidé*.
----