--- 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é*. ----