Hugo : Feed Atom, JSON, RSS

Article publié, le et modifié le
12 minute(s) de lecture

Cet article contient 2446 mots.
Source brute de l'article : MD

Description

Parmi les flux de données, Hugo est capable de créer plusieurs flux dont RSS (actuellement, à la norme 2.0).

Hugo est donc capable de générer un flux RSS , ou de type JSON , mais pas ATOM - pour ce dernier, nous verrons comment faire dans le chapitre ad hoc.

Documentation

Un petit tour sur la documentation officielle Hugo :

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 RFC 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 :
[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 :
[taxonomies]
tag = "tags"

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 CC - 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 :
    <id>{{ .Permalink }}</id>
  • 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.
    • 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>{{ $id }}website</id>
      • Pour l’identifiant d’entrée : <id>{{ $id }}{{ anchorize .RelPermalink }}</id>
      • Pour RSS, remplacez la balise <id> par <guid>. 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 :
    {{ $uuid := sha1 .Permalink }}<id>urn:uuid:{{substr $uuid 0 8}}-{{substr $uuid 10 4}}-{{substr $uuid 15 4}}-{{substr $uuid 20 4}}-{{substr $uuid 25 12}}</id>

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

Attention

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 - :

{{- $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

Info

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 !

[outputs]
home = ["HTML", "RSS"]
[outputFormats.RSS]
baseName = "rss"

Info

RSS:Template

Attention

J’ai créé mon propre modèle, tenant compte du fait d'être multilangue.


Fichier : layouts/_default/rss.xml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
{{ printf `<?xml version="1.0" encoding="utf-8" standalone="yes" ?>` | safeHTML }}
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
    <title>{{ if eq  .Title  .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}}{{ T "on" }}{{ end }}'{{ .Site.Title }}'{{ end }}</title>
    <link>{{ .Permalink }}</link>
    {{ with .Site.Params.description -}}<description>{{ . }}</description>{{- end }}
    <docs>http://blogs.law.harvard.edu/tech/rss</docs>
    <generator>Hugo {{ .Hugo.Version }}</generator>
    <image>
        <description>Logo</description>
        <height>128</height>
        <link>{{ .Permalink }}</link>
        <title>{{ if eq  .Title  .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}}{{ T "on" }}{{ end }}'{{ .Site.Title }}'{{ end }}</title>
        <url>{{ .Site.BaseURL }}img/Logo.png</url>
        <width>128</width>
    </image>
    {{ with .Site.LanguageCode }}<language>{{.}}</language>{{end}}
    {{ with .Site.Author.email }}<managingEditor>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</managingEditor>
    <webMaster>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</webMaster>{{end}}
    {{ with .Site.Copyright }}<copyright>© {{ $.Date.Format "2006" | safeHTML }} {{ $.Site.Author.name }}; {{.}}</copyright>{{end}}
    {{ if not .Date.IsZero }}<lastBuildDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</lastBuildDate>{{ end }}
    {{ with .OutputFormats.Get "RSS" }}{{ printf `<atom:link href=%q rel="self" type=%q />` .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 }}
    <item>
        <title>{{ .Title }}</title>
        <link>{{ .Permalink }}</link>
        {{ with .Summary }}<description>{{ . | html }}</description>{{end}}
        {{ with .Site.Author.email }}<author>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</author>{{end}}{{ $url := printf "%s" "/tags/" | absLangURL }}
        {{ with .Params.tags }}{{ range . }}<category domain="{{ $url }}{{urlize .}}/">{{.}}</category>{{end}}{{end}}
        <guid>{{ .Permalink }}</guid>
        <pubDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</pubDate>
    </item>
    {{ end }}
</channel>
</rss>

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 :

Atom::MediaType

Hugo >= 0.20

Depuis Hugo 0.20, il faut ajouter :

[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 :

[mediaTypes]
[mediaTypes."application/atom+xml"]
suffixes = ["xml"]

Astuce

Atom::OuputFormat

La déclaration du format de sortie, à ajouter :

[outputs]
[outputFormats.Atom]
baseName = "atom"
isPlainText = false
mediaType = "application/atom+xml"

Puis, il faut ajouter "ATOM" à votre variable home, tel que :

[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.

Fichier : layouts/_default/home.atom.xml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
{{ printf `<?xml version="1.0" encoding="utf-8" standalone="yes" ?>` | safeHTML }}
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="{{ with .Site.Language.Lang }}{{.}}{{end}}">
    <id>{{ .Permalink }}</id>
    <title>{{ if eq  .Title  .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}}{{ T "on" }}{{ end }}'{{ .Site.Title }}'{{ end }}</title>
    {{ with .Site.Params.description -}}<subtitle>{{ . }}</subtitle>{{- end }}
    {{ with .OutputFormats.Get "ATOM" }}{{ printf `<link href=%q rel="self" type=%q />` .Permalink .MediaType | safeHTML }}{{end}}
    {{ range .AlternativeOutputFormats -}}
    <link href="{{ if eq .Name "HTML" }}{{ $.Permalink }}{{ else }}{{ printf `%s%s.%s` $.Permalink (.Name | lower) (index .MediaType.Suffixes 0) }}{{end}}" rel="alternate" {{ printf "type=%q" .MediaType.Type | safeHTMLAttr }} />
    {{end}}{{ if .Site.IsMultiLingual }}{{ range $.Site.Languages }}{{ if ne .Lang $.Site.Language.Lang }}{{ $lang := .Lang }}
    {{ with $.OutputFormats.Get "ATOM" }}{{ printf `<link href=%q hreflang=%q rel="alternate" type=%q />` (print $lang "/" .Name "." (index .MediaType.Suffixes 0) |absURL) $lang .MediaType | safeHTML }}{{end}}
    {{ range $.AlternativeOutputFormats -}}
    <link href="{{ if eq .Name "HTML" }}{{ printf `%s/` $lang | absURL }}{{ else }}{{ printf `%s/%s.%s` $lang (.Name | lower) (index .MediaType.Suffixes 0) | absURL }}{{end}}" hreflang="{{ $lang }}" rel="alternate" {{ printf "type=%q" .MediaType.Type | safeHTMLAttr }} />
    {{end}}{{end}}{{end}}{{end}}{{ with .Site.Copyright }}{{ $lang := $.Site.Language.Lang }}
    <link href="http://creativecommons.org/publicdomain/zero/1.0/legalcode.{{ $lang }}" hreflang="{{ $lang }}" rel="license" />
    <link href="http://creativecommons.org/publicdomain/zero/1.0/deed.{{ $lang }}" hreflang="{{ $lang }}" rel="license" />
    <rights>© {{ $.Date.Format "2006" | safeHTML }} {{ $.Site.Author.name }}</rights>{{end}}
    <icon>/img/favicon.ico</icon>
    {{ with .Site.Params.logo }}<logo>{{.}}</logo>{{end}}
    {{ if not .Date.IsZero }}<updated>{{ .Date.Format "2006-01-02T15:04:05-07:00" | safeHTML }}</updated>{{ end }}
    {{ with .Site.Author.name }}
    <author>
        <name>{{.}}</name>
        {{ with $.Site.Author.email }}<email>{{.}}</email>{{end}}
        <uri>{{ $.Permalink }}</uri>
    </author>{{end}}
    <generator uri="https://gohugo.io" version="{{ .Hugo.Version }}">Hugo</generator>
    {{- $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 }}
    <entry>
        <id>{{ .Permalink }}</id>
        <link href="{{ .Permalink }}" rel="alternate" type="text/html" />
        <title>{{ .Title }}</title>{{ with .Site.Params.Author }}
        <author>
            <name>{{.}}</name>
        </author>{{end}}{{ $url := printf "%s" "/tags/" | absLangURL }}{{ with .Params.tags }}{{ range . }}
        <category term="{{.}}" scheme="{{ print $url (urlize .) | absLangURL }}/" />{{end}}
        {{end}}
        {{ with .Content }}<content type="html">{{ `<![CDATA[` | safeHTML }}{{.}}]]></content>{{end}}
        {{ with .Summary }}<summary type="html">{{ `<![CDATA[` | safeHTML }}{{.}}]]></summary>{{end}}
        <published>{{ .Date.Format "2006-01-02T15:04:05-07:00" | safeHTML }}</published>
        {{ if gt .Lastmod .Date }}<updated>{{ .Lastmod.Format "2006-01-02T15:04:05-07:00" | safeHTML }}</updated>{{end}}
    </entry>
    {{ end }}
</feed>

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 :

[outputs]
[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 :

[outputs]
home = ["HTML", "ATOM", "JSON", "RSS"]

JSON::Template

JSON: Détails

En plus de la 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 ‘,’ :

{{- $length := (len $pages) -}}

Et en fin de boucle range, nous ajoutons :

{{ 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.


Fichier : layouts/index.json

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
{{- $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.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