Categories, Tags そして Section

(初出: ゼロから始める Hugo — Categories, Tags そして Section - Qiita

前回の続き。 今回は Categories, Tags そして Section について書いてみる。

Categories と Tags

まずは Categories と Tags について。

Hugo では記事に Categories および Tags を設定することができる。 以下のように記述すれば良い。

+++
date = "2015-09-05T16:40:41+09:00"
draft = false
title = "Hello!"
categories = [ "hugo" ]
tags = [ "hello", "world" ]
+++

ようこそ, [Hugo](https://gohugo.io/) の世界へ!

このように Categories および Tags のキーワードを配列で列挙する(キーワードがひとつでも配列に入れること)。 これをビルドすると以下のようになる。

C:\hugo-env\www>hugo
0 draft content
0 future content
1 pages created
0 paginator pages created
2 tags created
1 categories created
in 20 ms

C:\hugo-env\www>tree /f .
C:\HUGO-ENV\WWW
│  config.toml
├─archetypes
├─content
│      hello.md
├─data
├─layouts
│  │  404.html
│  │  index.html
│  │
│  └─_default
│          single.html
├─public
│  │  404.html
│  │  index.html
│  │  index.xml
│  │  sitemap.xml
│  │
│  ├─categories
│  │  └─hugo
│  │          index.html
│  │          index.xml
│  │
│  ├─hello
│  │      index.html
│  │
│  └─tags
│      ├─hello
│      │      index.html
│      │      index.xml
│      │
│      └─world
│              index.html
│              index.xml
└─static

出力先に categories および tags フォルダが作成され,更にその下にキーワードのフォルダが作成されているのがおわかりだろうか。 キーワードのフォルダの index.xml は feed である。 つまり Hugo ではカテゴリ・タグ毎に自動で feed が作成される。

index.html は(テンプレートがないため)この時点では空である。テンプレートは layouts/_default フォルダに list.html ファイルを配置する。 名前からして Categories/Tags 毎に記事を列挙することを期待しているわけやね(笑) とりあえず中身はこんな感じでどうだろう。

<!DOCTYPE html>
{{ with .Site.LanguageCode }}<html lang="{{ . }}">{{ else }}<html>{{ end }}
<head>
<meta charset="utf-8">
<title>List of {{ .Title }} -- {{ .Site.Title }}</title>
</head>
<body>
<h1>List of {{ .Title }}</h1>

<ul style="list-style:none;">
{{ range .Data.Pages }}
    <li><a href="{{ .Permalink }}">{{ .Title }}</a> (<time>{{ .Date.Format "2006-01-02" }}</time>){{ if .Draft }} #Draft{{ end }}</li>
{{ end }}
</ul>

</body>
<html>

ビルド結果はこんな感じ。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>List of Hugo -- Hello World!</title>
</head>
<body>
<h1>List of Hugo</h1>

<ul style="list-style:none;">

    <li><a href="http://hello.example.com/hello/">Hello!</a> (<time>2015-09-05</time>)</li>

</ul>

</body>
<html>

おおう。キーワードの頭文字が勝手に大文字に変換されてるぜ。

前回を読んでいる人はトップページのテンプレート layouts/index.html とほぼ同じ構成であることに気づくと思う。 違うのは {{ .Title }} には Categories/Tags のキーワードが入ることと {{ range }} 構文の対象変数が .Site.Pages ではなく .Data.Pages であることだ。

ついでに記事ページで Categories/Tags を表示できるようにしてみよう。 layouts/_default/single.html を以下のように記述する。

<!DOCTYPE html>
{{ with .Site.LanguageCode }}<html lang="{{ . }}">{{ else }}<html>{{ end }}
<head>
<meta charset="utf-8">
<title>{{ .Title }} -- {{ .Site.Title }}</title>
</head>
<body>
<h1>{{ .Title }}</h1>
<nav>
    {{ with .Params.categories }}<div>Categories:{{ range . }} <a href="/categories/{{ . | urlize }}/">{{ . }}</a>{{ end }}</div>{{ end }}
    {{ with .Params.tags }}<div>Tags:{{ range . }} <a href="/tags/{{ . | urlize }}/">#{{ . }}</a>{{ end }}</div>{{ end }}
</nav>

<div>{{ .Content }}</div>
</body>
<html>

以下がビルド結果。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>Hello! -- Hello World!</title>
</head>
<body>
<h1>Hello!</h1>
<nav>
    <div>Categories: <a href="/categories/hugo/">hugo</a></div>
    <div>Tags: <a href="/tags/hello/">#hello</a> <a href="/tags/world/">#world</a></div>
</nav>

<div><p>ようこそ, <a href="https://gohugo.io/">Hugo</a> の世界へ!</p>
</div>
</body>
<html>

{{ with }} 構文の中に {{ range }} 構文が入ってて分かりにくいが,変数のスコープに注意すれば,それほど難しくはないはず。 注意しないといけないのは, Categories/Tags の変数名が .Categories, .Tags ではなく .Params.categories, .Params.tags になっている点である。

{{ . | urlize }} というのはパイプ機能の一種で, urlize であれば左側の値(文字列)を URL として安全な文字列に変換してくれる。

Categories と Tags との間に機能上の違いはない。 名前が違うだけである。 おそらく他のブログサービスとの互換性の為にあるのだろうが,「ゼロから始める」のであれば Categories と Tags を併記することに意味はない。 それなら後述する Section と組み合わせるほうが合理的である。

.Params のルール

Front matter で指定する変数は,「テンプレート変数」にある既定のもの以外は .Params 以下に自動的に組み換えられる。 なおかつ .Params 以下の変数名は小文字になる決まりである。 Categories/Tags は標準機能なのだが,どういうわけかこれだけ .Params 以下に組み替えられる。 なんだかなぁ。「歴史的経緯」ってやつだろうか1

ちなみに config.toml によるサイト設定では .Site.Params への暗黙的な組み換えは行われないため,明示的に記述する必要がある。

[params]
author = "Spiegel"

この非対称性も分かりにくいんだよなぁ。

Taxonomy

Hugo には Taxonomy と呼ばれる機能があって,標準では Categories/Tags のリストを取り出すことができる2。 たとえば, layouts/index.html をこんな感じに書く。

<h2>Taxonomy Terms</h2>
<ul>{{ range $taxonomyname, $taxonomy := .Site.Taxonomies }}
    <li>{{ $taxonomyname }}
        <ul>{{ range $key, $value := $taxonomy }}
            <li><a href="/{{ $taxonomyname | urlize }}/{{ $key | urlize }}">{{ $key }}</a> ({{ $value.Count }})
                <ul>{{ range $value.Pages }}
                    <li><a href="{{ .Permalink }}">{{ .LinkTitle }}</a> </li>
                {{ end }}</ul></li>
        {{ end }}</ul></li>
{{ end }}</ul>

これをビルドするとこんな感じに展開される。

<h2>Taxonomy Terms</h2>
<ul>
    <li>categories
        <ul>
            <li><a href="/categories/hugo">hugo</a> (1)
                <ul>
                    <li><a href="http://hello.example.com/hello/">Hello!</a> </li>
                </ul></li>
        </ul></li>

    <li>tags
        <ul>
            <li><a href="/tags/hello">hello</a> (1)
                <ul>
                    <li><a href="http://hello.example.com/hello/">Hello!</a> </li>
                </ul></li>

            <li><a href="/tags/world">world</a> (1)
                <ul>
                    <li><a href="http://hello.example.com/hello/">Hello!</a> </li>
                </ul></li>
        </ul></li>
</ul>

Tags の一覧のみを取得したいのであれば,もっと簡単に

<h2>Tags</h2>
<ul>{{ range $key, $value := .Site.Taxonomies.tags.ByCount }}
    <li>#<a href="/tags/{{ $key | urlize }}">{{ $key }}</a> ({{ $value.Count }})</li>
{{ end }}</ul>

でもよい。 ちなみに .Site.Taxonomies.tags.ByCount は Tags の一覧をタグされている記事数の順で sort したものである。 アルファベット順にするには .Site.Taxonomies.tags.Alphabetical とする。

(追記: Categories/Tags の一覧ページを作る方法はこちら

Section

content フォルダの下に practice というフォルダを作り,ここに hello.md を移動させてみよう。 新たに作る場合は path 付きで作成すればよい。

C:\hugo-env\www>hugo new practice/hello.md
C:\hugo-env\www\content\practice\hello.md created

これでビルドしてみる(ファイルを移動した際は出力フォルダの中をいったんクリーンにしてからビルドするとゴミが残らない)。

C:\hugo-env\www>hugo
0 draft content
0 future content
1 pages created
0 paginator pages created
2 tags created
1 categories created
in 19 ms

C:\hugo-env\www>tree /f .
C:\HUGO-ENV\WWW
│  config.toml
├─archetypes
├─content
│  └─practice
│          hello.md
├─data
├─layouts
│  │  404.html
│  │  index.html
│  │
│  └─_default
│          list.html
│          single.html
├─public
│  │  404.html
│  │  index.html
│  │  index.xml
│  │  sitemap.xml
│  │
│  ├─categories
│  │  └─hugo
│  │          index.html
│  │          index.xml
│  │
│  ├─practice
│  │  │  index.html
│  │  │  index.xml
│  │  │
│  │  └─hello
│  │          index.html
│  │
│  └─tags
│      ├─hello
│      │      index.html
│      │      index.xml
│      │
│      └─world
│              index.html
│              index.xml
└─static

hello/index.htmlpractice/hello/index.html に配置されるのは予想通りだと思うが, practiceindex.htmlindex.xml が生成されているのがおわかりだろうか。 practice/index.html の中身はこんな感じ。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>List of Practices -- Hello World!</title>
</head>
<body>
<h1>List of Practices</h1>

<ul style="list-style:none;">

    <li><a href="http://hello.example.com/practice/hello/">Hello!</a> (<time>2015-09-05</time>)</li>

</ul>

</body>
<html>

これは layouts/_default/list.html テンプレートで生成されたページだ。 Hugo ではフォルダ付きの記事を作成すると,そのフォルダが Section として機能する。

当然だが,ひとつの記事はひとつの Section にしか帰属できない。 これは先の Categories/Tags との大きな違いである。 Section と Categories/Tags を組み合わせれば縦串と横串で記事を指示できるようになる。

ついでに記事ページで Section を表示できるようにしてみよう。 たとえば, layouts/_default/single.html をこんな感じに書く。

<!DOCTYPE html>
{{ with .Site.LanguageCode }}<html lang="{{ . }}">{{ else }}<html>{{ end }}
<head>
<meta charset="utf-8">
<title>{{ .Title }}{{ with .Section }} -- {{ . }}{{ end }} -- {{ .Site.Title }}</title>
</head>
<body>
<h1>{{ .Title }}{{ with .Section }} [<a href="/{{ . | urlize }}/">{{ . }}</a>]{{ end }}</h1>
<nav>
    {{ with .Params.categories }}<div>Categories:{{ range . }} <a href="/categories/{{ . | urlize }}/">{{ . }}</a>{{ end }}</div>{{ end }}
    {{ with .Params.tags }}<div>Tags:{{ range . }} <a href="/tags/{{ . | urlize }}/">#{{ . }}</a>{{ end }}</div>{{ end }}
</nav>

<div>{{ .Content }}</div>
</body>
<html>

以下がビルド結果。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>Hello! -- practice -- Hello World!</title>
</head>
<body>
<h1>Hello! [<a href="/practice/">practice</a>]</h1>
<nav>
    <div>Categories: <a href="/categories/hugo/">hugo</a></div>
    <div>Tags: <a href="/tags/hello/">#hello</a> <a href="/tags/world/">#world</a></div>
</nav>

<div><p>ようこそ, <a href="https://gohugo.io/">Hugo</a> の世界へ!</p>
</div>
</body>
<html>

記事のフォルダ階層はいくらでも深くできるが, Section として認識されるのは直下のフォルダのみのようである。 たとえば content/practice/hello.mdcontent/practice/firstcode/hello.md に移動してビルドすると(出力フォルダはクリーンアップしてね)

C:\hugo-env\www>tree /f .
C:\HUGO-ENV\WWW
│  config.toml
├─archetypes
├─content
│  └─practice
│      └─firstcode
│              hello.md
├─data
├─layouts
│  │  404.html
│  │  index.html
│  │
│  └─_default
│          list.html
│          single.html
├─public
│  │  404.html
│  │  index.html
│  │  index.xml
│  │  sitemap.xml
│  │
│  ├─categories
│  │  └─hugo
│  │          index.html
│  │          index.xml
│  │
│  ├─practice
│  │  │  index.html
│  │  │  index.xml
│  │  │
│  │  └─firstcode
│  │      └─hello
│  │              index.html
│  │
│  └─tags
│      ├─hello
│      │      index.html
│      │      index.xml
│      │
│      └─world
│              index.html
│              index.xml
└─static
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>Hello! -- practice -- Hello World!</title>
</head>
<body>
<h1>Hello! [<a href="/practice/">practice</a>]</h1>
<nav>
    <div>Categories: <a href="/categories/hugo/">hugo</a></div>
    <div>Tags: <a href="/tags/hello/">#hello</a> <a href="/tags/world/">#world</a></div>
</nav>

<div><p>ようこそ, <a href="https://gohugo.io/">Hugo</a> の世界へ!</p>
</div>
</body>
<html>

というわけで,あくまでも practice フォルダが Section になっているのがわかると思う。

Section ごとのカスタマイズ

Hugo では Section ごとにカスタマイズすることができる。 layouts フォルダに section フォルダを作成し,その中に <section name>.html ファイルを作成すると,そのテンプレートで Section のトップページ(<section name>/index.html)を作成する。 今回は layouts/section/practice.html を作成してみる。

<!DOCTYPE html>
{{ with .Site.LanguageCode }}<html lang="{{ . }}">{{ else }}<html>{{ end }}
<head>
<meta charset="utf-8">
<title>Hugo の練習 -- {{ .Site.Title }}</title>
</head>
<body>
<h1>Hugo の練習</h1>

<ul style="list-style:none;">
{{ range .Data.Pages }}
    <li><a href="{{ .Permalink }}">{{ .Title }}</a> (<time>{{ .Date.Format "2006-01-02" }}</time>){{ if .Draft }} #Draft{{ end }}</li>
{{ end }}
</ul>

</body>
<html>

ビルド結果。変わり映えしなくてすみません。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>Hugo の練習 -- Hello World!</title>
</head>
<body>
<h1>Hugo の練習</h1>

<ul style="list-style:none;">

    <li><a href="http://hello.example.com/practice/hello/">Hello!</a> (<time>2015-09-05</time>)</li>

</ul>

</body>
<html>

更に Section 内の記事ページもカスタマイズできる。 これは layouts フォルダに Section 名のフォルダを作成し,その中に single.html を配置する。

<!DOCTYPE html>
{{ with .Site.LanguageCode }}<html lang="{{ . }}">{{ else }}<html>{{ end }}
<head>
<meta charset="utf-8">
<title>{{ .Title }} -- Hugo の練習 | {{ .Site.Title }}</title>
</head>
<body>
<h1>{{ .Title }} -- Hugo の練習</h1>
<nav>
    {{ with .Params.categories }}<div>Categories:{{ range . }} <a href="/categories/{{ lower . | urlize }}/">{{ . }}</a>{{ end }}</div>{{ end }}
    {{ with .Params.tags }}<div>Tags:{{ range . }} <a href="/tags/{{ lower . | urlize }}/">#{{ . }}</a>{{ end }}</div>{{ end }}
</nav>

<div>{{ .Content }}</div>
</body>
<html>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>Hello! -- Hugo の練習 | Hello World!</title>
</head>
<body>
<h1>Hello! -- Hugo の練習</h1>
<nav>
    <div>Categories: <a href="/categories/hugo/">hugo</a></div>
    <div>Tags: <a href="/tags/hello/">#hello</a> <a href="/tags/world/">#world</a></div>
</nav>

<div><p>ようこそ, <a href="https://gohugo.io/">Hugo</a> の世界へ!</p>
</div>
</body>
<html>

これも変わり映えしなくてすみません。

しかし,なんでこんなテンプレートの構成なんだろう。 これだと section_default という名前のセクションはカスタマイズ出来ないことになる(まだ説明してないが layouts フォルダには他にも partialsshortcodes といったフォルダもある)。 そうではなくて, layouts/section フォルダの下にセクション名のフォルダを掘ってその中に list.htmlsingle.html を配置すればスッキリするのに。

さきほどの .Params の話といい,どうも Hugo は名前管理がいきあたりばったりな気がする。

categories/hugo という名前の記事はどうなるの?

これも名前に関する話。

categories/hugo.md という名前の記事は categories/hugo/index.html に展開される。 これって Categories の機能と丸かぶりである。 実は Hugo では名前が衝突した際の挙動は明文化されていない(筈)。 強いて言うなら実装依存で状況依存である。またビルド時にエラーになることもない。

C:\hugo-env\www>hugo new categories/hugo.md
C:\hugo-env\www\content\categories\hugo.md created

C:\hugo-env\www>hugo undraft content/categories/hugo.md

C:\hugo-env\www>hugo
0 draft content
0 future content
2 pages created
0 paginator pages created
2 tags created
1 categories created
in 24 ms

C:\hugo-env\www>tree /f .
C:\HUGO-ENV\WWW
│  config.toml
├─archetypes
├─content
│  ├─categories
│  │      hugo.md
│  │
│  └─practice
│          hello.md
├─data
├─layouts
│  │  404.html
│  │  index.html
│  │
│  └─_default
│          list.html
│          single.html
├─public
│  │  404.html
│  │  index.html
│  │  index.xml
│  │  sitemap.xml
│  │
│  ├─categories
│  │  │  index.html
│  │  │  index.xml
│  │  │
│  │  └─hugo
│  │          index.html
│  │          index.xml
│  │
│  ├─practice
│  │  │  index.html
│  │  │  index.xml
│  │  │
│  │  └─hello
│  │          index.html
│  │
│  └─tags
│      ├─hello
│      │      index.html
│      │      index.xml
│      │
│      └─world
│              index.html
│              index.xml
└─static

上の場合は categories/hugo/index.html が記事ページになった。 他にも,このフォルダ構成なら practice.md も名前が衝突する。 このような衝突を避けるにはユーザ側で名前を管理するしかない。 小規模なサイトなら人間が気をつければいいが,中規模以上で複数人が関わるようになると結構危ないかもしれない。

ブックマーク

Hugo に関するブックマークはこちら


  1. Categories/Tags の配置が特殊なのは,これらが Taxonomy として実装されているからのようだ。 ↩︎

  2. Taxonomy の項目は Categories/Tags 以外にも任意に設定することができる(Using Taxonomies 参照)。 ↩︎