Hugo の RSS 出力条件をカスタマイズする

feature-image

Hugo で生成したサイトの RSS 出力時に不要なページが出てきてしまったので除外する方法を確認しました。

RSS Templates | Hugo ― https://gohugo.io/templates/rss/

Hugo にはいくつかの階層があることを理解しておいた方が理解が捗ります。とはいえ細部は複雑なので、ざっくりまとめるとこの3通りです。そしてサイトの規模がさほど大きくなければ共通的なRSSだけで処理可能だと思います。セクションごとのRSS作成などはコンテンツ規模が大きくなってから考えましょう。

  • 共通的なRSSを作る
  • セクションに特化したRSSを作る
  • カテゴリーなど Taxonomy (分類学)に特化したRSSを作る

RSS作成時のテンプレートの紹介順序

テーマ共通とするのか、ローカルのみで利用するのか、このあたりの考え方はサイト設計により異なりますので一概には言えませんが、まずはローカルに作る( layouts/index.rss.xml )ことを第一に考えて、その完成物が他サイトでも共有できそうならばテーマファイルとして共通化するなどを考えればいいと思います。

The table below shows the RSS template lookup order for the different page kinds. The first listing shows the lookup order when running with a theme (demoTheme).

Example OutputFormat Suffix Template Lookup Order
RSS home RSS xml 1. layouts/index.rss.xml
2. layouts/home.rss.xml
3. layouts/rss.xml
4. layouts/list.rss.xml
5. layouts/index.xml
6. layouts/home.xml
7. layouts/list.xml
8. layouts/_default/index.rss.xml
9. layouts/_default/home.rss.xml
10. layouts/_default/rss.xml
11. layouts/_default/list.rss.xml
12. layouts/_default/index.xml
13. layouts/_default/home.xml
14. layouts/_default/list.xml
15. layouts/_internal/_default/rss.xml

Hugo 標準の埋め込みテンプレート

Hugo に埋め込みの標準RSSテンプレートは以下のとおりです。

The Embedded rss.xml This is the default RSS template that ships with Hugo:

https://github.com/gohugoio/hugo/blob/master/tpl/tplimpl/embedded/templates/_default/rss.xml

{{- $pctx := . -}}
{{- if .IsHome -}}{{ $pctx = .Site }}{{- end -}}
{{- $pages := slice -}}
{{- if or $.IsHome $.IsSection -}}
{{- $pages = $pctx.RegularPages -}}
{{- else -}}
{{- $pages = $pctx.Pages -}}
{{- end -}}
{{- $limit := .Site.Config.Services.RSS.Limit -}}
{{- if ge $limit 1 -}}
{{- $pages = $pages | first $limit -}}
{{- end -}}
{{- 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 }}{{.}} on {{ end }}{{ .Site.Title }}{{ end }}</title>
    <link>{{ .Permalink }}</link>
    <description>Recent content {{ if ne  .Title  .Site.Title }}{{ with .Title }}in {{.}} {{ end }}{{ end }}on {{ .Site.Title }}</description>
    <generator>Hugo -- gohugo.io</generator>{{ with .Site.LanguageCode }}
    <language>{{.}}</language>{{end}}{{ with .Site.Author.email }}
    <managingEditor>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</managingEditor>{{end}}{{ with .Site.Author.email }}
    <webMaster>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</webMaster>{{end}}{{ with .Site.Copyright }}
    <copyright>{{.}}</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 -}}
    {{ range $pages }}
    <item>
      <title>{{ .Title }}</title>
      <link>{{ .Permalink }}</link>
      <pubDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</pubDate>
      {{ with .Site.Author.email }}<author>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</author>{{end}}
      <guid>{{ .Permalink }}</guid>
      <description>{{ .Summary | html }}</description>
    </item>
    {{ end }}
  </channel>
</rss>

まずそもそもで、どの情報をもとに表示を出し分けすればいいのか。

  • {{ .Kind }} 一律で “page” が入ってきたので区別には不十分
  • {{ .Type }} セクション名が入っているので使えそう
    • セクションを指定しない場合は、Hugoのデフォルトセクションは posts で使っているケースが多いと思う
    • 個別ページの場合は posts とはならずに page あるいは個別ページのフロントマタ―で指定した type: の文字列になるので、これで区別できそうである

type: について補足すると例えば、個別ページのフロントマタ―を以下のようにした場合、 {{ .Type }} では archive という文字が取得できる。

---
title: 記事の一覧ページ
layout: archive
type: archive
---

記事本文ほげほげ

テンプレートの書き方

デフォルトのRSSテンプレートをもとに一部修正します。今回の場合は、 range でループするためのページ取得条件をちょっと修正すれば解決できます。

{{- if or $.IsHome $.IsSection -}}
<!-- ↓追記・書き換え -->
{{- $filterSection := "section1" -}}
{{- $pages = where ($pctx.RegularPages) "Section" $filterSection -}}
<!-- ここまで -->
{{- else -}}
{{- if or $.IsHome $.IsSection -}}
<!-- ↓追記・書き換え -->
{{- $filterSections := slice "posts" "section1" "section2" -}}
{{- $pages = where ($pctx.RegularPages) "Section" "in" $filterSections -}}
<!-- ここまで -->
{{- else -}}

あるいは、表示対象とするセクションをコンフィグで定義してしまうのも一案です。SitemapやRSSで最低でも2箇所では同様の定義を用いたいことが多いと思います。

config.toml

availableSections = ["posts", "section1", "section2"]

index.rss.xml

// ...中略

{{- $filterSections := .Site.Params.availableSections | default (slice "posts") -}}
{{- $pages = where ($pctx.RegularPages) "Section" "in" $filterSections -}}

// ...中略

range でのループ中に IF制御式で條件書き足すでもいいのですが、Hugoのテンプレート(Go template)がゴチャゴチャとしていて読みづらいので、できるだけBODY部の外側で変数化をしてしまったうえでループでは素直に表示するのに特化する方が好みです。

HTMLと Go template を綺麗に表示分けてくれる Visual Studio Code 拡張があれば教えてほしいです。軽く探した感じだと見当たらず・・・。

RSS中の記事画像への対応

よくよく見ると、アイキャッチ画像がRSSに含まれていません。以下の通り、フロントマターに featured_image あるいは images: ["画像URL"] がある時には画像を表示するようにします。

images 変数を用いる場合の画像URLは配列形式ですので、フロントマタ―には以下の通り書く必要があります。(背景はわかりませんが Hugo の標準テンプレートでは画像形式をこのように定義しているのを見かけるので、互換性のために倣っています)

---
title: ほげほげ
(・・中略・・)
images:
  - https://画像URL
---

本文ほげほげ

RSS用のXMLは、最終的には以下のようになりました。

{{- $pctx := . -}}
{{- if .IsHome -}}{{ $pctx = .Site }}{{- end -}}
{{- $pages := slice -}}
{{- if or $.IsHome $.IsSection -}}
{{- $filterSections := .Site.Params.availableSections | default (slice "posts") -}}
{{- $pages = where ($pctx.RegularPages) "Section" "in" $filterSections -}}
{{- else -}}
{{- $pages = $pctx.Pages -}}
{{- end -}}
{{- $limit := .Site.Config.Services.RSS.Limit -}}
{{- if ge $limit 1 -}}
{{- $pages = $pages | first $limit -}}
{{- end -}}
{{- 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 }}{{.}} on {{ end }}{{ .Site.Title }}{{ end }}</title>
    <link>{{ .Permalink }}</link>
    <description>Recent content {{ if ne  .Title  .Site.Title }}{{ with .Title }}in {{.}} {{ end }}{{ end }}on {{ .Site.Title }}</description>
    <generator>Hugo -- gohugo.io</generator>{{ with .Site.LanguageCode }}
    <language>{{.}}</language>{{end}}{{ with .Site.Author.email }}
    <managingEditor>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</managingEditor>{{end}}{{ with .Site.Author.email }}
    <webMaster>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</webMaster>{{end}}{{ with .Site.Copyright }}
    <copyright>{{.}}</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 -}}
    {{ range $pages }}
    {{ $eyecatchImage := "" }}
    {{ if .Params.featured_image }}
        {{ $eyecatchImage = .Params.featured_image | absURL }}
    {{ else if .Params.images }}
        {{ $eyecatchImage = index .Params.images 0 | absURL }}
    {{ end -}}
    {{ $pageTitle := .Title }}
    {{ $pagePermalink := .Permalink }}
    <item>
      <title>{{ $pageTitle }}</title>
      <link>{{ $pagePermalink }}</link>
      <pubDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</pubDate>
      {{ with .Site.Author.email }}<author>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</author>{{end}}
      <guid>{{ $pagePermalink }}</guid>
      <description>{{ .Summary | html }}</description>
      {{- with $eyecatchImage -}}
      <image>
        <url>{{ $eyecatchImage }}</url>
        <title>{{ $pageTitle }}</title>
        <link>{{ $pagePermalink }}</link>
      </image>
      {{- end -}}
    </item>
    {{ end }}
  </channel>
</rss>

まとめ

Hugo の標準RSSを上書きして独自の出力条件を定義する方法を書きました。このテクニックは Sitemap の生成にも使えます。自分好みにカスタマイズしましょう。