HUGOで作れるCMSっぽいパーツ:関連記事・目次・JSON-LDなど

超爆速でコンパイルができる、Go言語製の静的サイトジェネレーター[HUGO](http://gohugo.io)。

当然、HUGOでは静的HTMLサイトしか作ることができない。しかし、テンプレート機能をうまく作ればWordPressにも負けないようなパーツが結構作れてしまう。この記事では、[HUGOで構築した自分のブログ](http://media-massage.net)を例に、いくつかのCMSっぽいパーツ見本集をまとめる。

# テンプレート見本

* バージョン0.60.1で動作確認
    * `Hugo Static Site Generator v0.60.1/extended darwin/amd64`
* テンプレート機能の見本なので、CSSについては言及しない

##### 補足：MarkdownにHTMLを書くときの注意点

v0.60からMarkdownコンパイル時の挙動が変わり、以下の設定がないと文中のHTMLが出力されないようになった。
Markdownに直接HTMLを書いている場合は注意。

```config.toml
[markup]
  [markup.goldmark]
    [markup.goldmark.renderer]
      unsafe = true

```

## アイキャッチとタグ付きの新着順記事一覧

この例では、記事のMarkdownへアイキャッチ画像用のURLをfront-matterの`image`プロパティを記述し、項目が存在する場合はHTMLに出力するようにしている。

存在の有無でclassも変化する為、レイアウトをそれぞれ適当なものに変えることができる。

ちなみに、`.Summary`は`config.toml`で`HasCJKLanguage = true`を設定しないとおかしな表示になってしまうので、日本語のブログを作るときは必ず設定しておこう。

![新着記事の一覧.png](https://qiita-image-store.s3.amazonaws.com/0/48238/43a7ea73-92f6-c641-bddf-7f417d973b58.png)

### HTML

* ページネーションなしで全件出力

```html:テーマ/layouts/index.html
<!-- article-list -->
<section class="article-list">
  <h2 class="article-list__head" itemprop="name">記事一覧</h2>
  <ol class="article-list__list">
    {{ range .Data.Pages.ByDate.Reverse }}
      {{ .Render "li" }}
    {{ end }}
  </ol>
</section>
<!-- /article-list -->
```

```html:テーマ/layouts/_default/li.html
<li class="article-list__item">
  <!-- article-sneak -->
  <article class="article-sneak {{ if .Params.image }}article-sneak--thumb{{ end }}">
    <div class="article-sneak__link" onclick="location.href='{{ .RelPermalink | safeJS }}'">
      <div class="article-sneak__text article-sneak__wrapper">
        <div class="article-sneak__inner">
          <p class="article-sneak__date">
            <a href="{{ .RelPermalink }}"><time datetime="{{ .Date.Format " 2006-01-02T15:04:05Z07:00" | safeHTML }}">{{ .Date.Format "2006.01.02" }}</time></a>
          </p>
          <h3 class="article-sneak__head">
            <a href="{{ .RelPermalink }}">{{ .Title }}</a>
          </h3>
          {{ if .Params.tags }}
          <dl class="article-sneak__tags">
            <dt><i class="icon icon-tag"></i></dt>
            <dd>
              {{ range .Params.categories }}
              <a href="/categories/{{ . | urlize }}/">{{ . }}</a>
              {{ end }}
            </dd>
            <dd>
              {{ range .Params.tags }}
              <a href="/tags/{{ . | urlize }}/">{{ . }}</a>
              {{ end }}
            </dd>
          </dl>
          {{ end }}
          <p class="article-sneak__body"><a href="{{ .RelPermalink }}">{{ .Summary }}</a></p>
        </div>
      </div>
      {{ if and (.Params.image) (ne .Params.image "") }}
      <div class="article-sneak__figure article-sneak__wrapper">
        <div class="article-sneak__inner">
          <a href="{{ .RelPermalink }}"><p class="article-sneak__thumb" style="background-image: url({{ .Params.image }});">
            <img style="display:none" src="{{ .Params.image }}" alt=""/>
          </p></a>
        </div>
      </div>
      {{ end }}
    </div>
  </article>
  <!-- /article-sneak -->
</li>
```

### Markdown

アイキャッチ画像がある場合は`image`プロパティにパスを記載する。

```markdown
+++
image = "/blog/makeowndigitalbook/stackroom.png"
+++
```

なお`.Summary`の内容を制御する方法はこちらの記事が詳しい。

[Hugoにおける記事要約の仕組み - Qiita](http://qiita.com/DQNEO/items/4cc651aa1c2d0ced9fae)


## ページネーション付の記事一覧と操作UI

前項とほぼ一緒だが、ボタン部分に加えて、一覧の抽出条件の書き方が違ってくる。

### HTML(一覧)

この書き方だと、出力される記事件数はデフォルトの件数となる。ページネーションのデフォルト件数は`config.toml`に設定できる。`li`パーシャルの中身は前項ママでそのまま使えるので省略する。

```html:テーマ/layouts/_default/list.html
<!-- article-list -->
<section class="article-list ">
  <h2 class="article-list__head">{{ .Title }}の記事一覧</h2>
  <ul class="article-list__list">
    {{ range .Paginator.Pages }}
      {{ .Render "li" }}
    {{ end }}
  </ul>
  {{ partial "pagination-nav.html" . }}
</section>
<!-- /article-list -->
```

### config.toml

1ページあたり20件の表示としたい場合はこのように書く。

```toml
paginate = 20
```

### HTML(ボタン部分)

![スクリーンショット 2016-06-08 12.05.36.png](https://qiita-image-store.s3.amazonaws.com/0/48238/996d0c87-2b41-caf1-63c6-53c621ea187c.png)

ページネーションの操作UIは、次のように実装した。

```html
<nav role="pagination" class="pagination-nav">
 
  <p class="pagination-nav__btn pagination-nav__btn--prev">
    <a class="pagination-nav__item pagination-nav__item--link" {{ if .Paginator.HasPrev }} href="{{.Paginator.Prev.URL}}" {{ end }}>
      <i class="icon icon-arrow-left"></i>前のページへ
    </a>
  </p>
 
  <p class="pagination-nav__btn pagination-nav__btn--summary">
    <span class="pagination-nav__item">
      {{.Paginator.PageNumber}}/{{.Paginator.TotalPages}}
    </span>
  </p>
 
  <p class="pagination-nav__btn pagination-nav__btn--next">
    <a class="pagination-nav__item pagination-nav__item--link" {{ if .Paginator.HasNext }} href="{{.Paginator.Next.URL}}" {{ end }}>
      次のページへ<i class="icon icon-arrow-right"></i>
    </a>
  </p>
</nav>
```

押せない時に薄くなる表現はCSSで実装している。

## 本文の途中に挿入する目次

標準の機能では記事の内部に目次を混ぜることはできない。

そこで、あらかじめ本文中に空のHTML構造を挿入しておき、目次のパーツをJavaScriptで移動させる。これで、外観上は目次が途中に挿入されているように見える。

![本文の途中の目次.png](https://qiita-image-store.s3.amazonaws.com/0/48238/4743fed5-be8e-c8e4-5e89-becc7ca744b3.png)

### HTML

```html:テーマ/layouts/_default/single.html
<!-- single.htmlの一部 -->
<section>
  <div class="body">
    {{ if eq .Params.usetoc true }}
      {{ partial "toc.html" . }}
    {{ end }}
    {{ .Content }}
  </div>
</section>
```

```html:テーマ/layouts/partials/toc.html
<section class="js-toc">
  <h2>目次</h2>
  <div class="toc">
  {{ .TableOfContents }}
  </div>
</section>
```

### JavaScript

```js
var $tocPlace = $('#js-toc-place');
if ($tocPlace.length === 1) {
  $('.js-toc').appendTo($tocPlace);
}
```

### Markdown

front-matter内でusetocフラグを使い、必要な記事にだけ目次を挿入できるようにしている。

```markdown
+++
usetoc = true
+++
それでもメリットはかなり大きい。

<!-- この部分に目次を挿入 -->
<div id="js-toc-place"></div>

## 必要機材

本を自炊するためにはどうしても機材が必要になる。
```

## 独自デザインの共有ボタン

facebook, twitter, はてブ, メール送信に対応したリンク要素を生成する。本来の用途と異なるかもしれないが、`safeJS`フィルターが良好に機能した。

![SNS共有ボタン.png](https://qiita-image-store.s3.amazonaws.com/0/48238/88a99f70-1af0-5b8d-3610-220a42e144ef.png)

```html
<!-- content-footer -->
<div class="content-footer ">
  <p class="content-footer__text">最後までお読みいただきありがとうございます!<i class="icon icon-share"></i></p>
  <ul class="content-footer__sns-list">
    <li class="content-footer__sns-item ">
      <p class="button button--sns button--sns--fb">
        <a class="button__link" href="https://www.facebook.com/sharer/sharer.php?u={{ .Permalink | safeJS }}"><i class="icon icon-facebook"></i>Facebookで共有</a>
      </p>
    </li>
    <li class="content-footer__sns-item ">
      <p class="button button--sns button--sns--tw">
        <a class="button__link" href="http://twitter.com/intent/tweet?text={{ .Title | safeJS }}%20%7C%20{{ .Site.Title }}%20{{ .Permalink | safeJS }}%20%40{{ .Site.Params.twitter }}"><i class="icon icon-twitter"></i>Twitterで共有</a>
      </p>
    </li>
    <li class="content-footer__sns-item ">
      <p class="button button--sns button--sns--hb">
        <a class="button__link" href="http://b.hatena.ne.jp/entry/{{ .Permalink | safeJS }}"><i class="icon icon-hatebu"></i>はてなブックマークで共有</a>
      </p>
    </li>
    <li class="content-footer__sns-item">
      <p class="button button--sns button--sns--mail">
        <a class="button__link" href="mailto:?subject={{ .Title }}&amp;body={{ .Permalink | safeURL }}">メールで共有</a>
      </p>
    </li>
  </ul>
</div>
<!-- /content-footer -->
```

## 関連記事

タグ・サムネイル付きの関連記事一覧を最大9件まで表示する。

![関連記事の一覧.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48238/a55303ea-ac33-7548-9034-5bda1c487db0.png)


### HTML

```html
<!-- related-articles -->
{{ $related := .Site.RegularPages.Related . | first 9 }}
{{ with $related }}
<nav class="related-articles">
  <h2 class="content-footer__text">関連記事</h2>
  <ul class="related-articles__links">
    {{ range . }}
    <li class="related-articles__item">
      <article class="related-article">
        <a class="related-article__link" href="{{ .RelPermalink }}">
          <span class="related-article__bg" {{ if .Params.image }} data-normal="{{ .Params.image }}" {{ end }}></span>
          <p class="related-article__stock">{{ .Date.Format "2006.01.02." }}</p>
          <h3 class="related-article__title">{{ .Title }}</h3>
          <p class="related-article__tags">
            {{ range .Params.tags }}
            <span class="related-article__tag">
            {{ . }}
            </span>
            {{ end }} </p>
        </a>
      </article>
    </li>
    {{ end }}
  </ul>
</nav>
{{ end }}
<!-- /related-articles -->
```

## 前の記事／次の記事へのナビゲーション

同一セクションに限定し、前後の投稿がある場合はそれを出力する。

![前後の記事へのナビゲーション.png](https://qiita-image-store.s3.amazonaws.com/0/48238/46d6f3ae-90b0-c1f4-0fd0-f674c65cd6b9.png)

```html
<!-- content-nav -->
<nav class="content-nav ">
  <ol class="content-nav__links">
    {{ if .PrevInSection }}
    <li class="content-nav__item">
      <p class="content-nav__label">前の投稿</p>
      <p class="button button--article-nav">
        <a class="button__link" href="{{.PrevInSection.RelPermalink}}"><i class="icon icon-arrow-left"></i>{{ .PrevInSection.Title }}</a>
      </p>
    </li>
    {{ end }}
    {{ if .NextInSection }}
    <li class="content-nav__item">
      <p class="content-nav__label">次の投稿</p>
      <p class="button button--article-nav">
        <a class="button__link" href="{{.NextInSection.RelPermalink}}"><i class="icon icon-arrow-right"></i>{{ .NextInSection.Title }}</a>
      </p>
    </li>
    {{ end }}
  </ol>
  <div class="content-nav__menu">
    <p class="button button--article-nav button--article-nav--menu">
      <a class="button__link" href="/"><i class="icon icon-menu"></i>記事一覧へ戻る</a>
    </p>
  </div>
</nav><!-- /content-nav -->
```

## 特別な記事だけを抽出して一覧

`pickup`フラグがtrueの記事だけを限定して抽出してアイキャッチ画像を並べる。
あらかじめ各記事のfront-matterに`pickup`、`type`、`image`を記述しておく。

![固定された記事の表示枠.png](https://qiita-image-store.s3.amazonaws.com/0/48238/c1decea5-77de-360d-c7ba-97e92e3eb9f7.png)

### HTML

* 「project」タイプで「pickup」パラメーターがtrueの記事を時系列の逆順に3件まで表示
* 「docs」タイプで「pickup」パラメーターがtrueの記事を時系列順に2件まで表示

```html
<!-- gallery-sneak -->
<div class="gallery-sneak site-hero--footer">
  <div class="gallery-sneak__inner">
    <ul class="gallery-sneak__list">
      {{ range first 3 (where (where .Site.Pages.Reverse ".Params.pickup" "true" ) "Type" "project") }}
      <li class="gallery-sneak__item">
        <a href="{{ .RelPermalink }}" style="background-image: url({{ .Params.image }});">
          <img src="{{ .Params.image }}" alt=""/>
        </a>
      </li>
      {{ end }}
      {{ range first 2 (where (where .Site.Pages ".Params.pickup" "true" ) "Type" "docs") }}
      <li class="gallery-sneak__item">
        <a href="{{ .RelPermalink }}" style="background-image: url({{ .Params.image }});">
          <img src="{{ .Params.image }}" alt=""/>
        </a>
      </li>
      {{ end }}
    </ul>
  </div>
</div>
<!-- /gallery-sneak -->
```

### Markdown

上記の抽出条件にマッチさせるには、front-matter内へ次を記載する。

```markdown
+++
type = "docs"
pickup = "true"
image = "/path/to/image.png"
+++

```

## メタ情報

**[汎用]**

Facebook, Twitter cardに対応したメタタグ。OGPの内容にこだわると結構条件分岐が増える。パーシャル化して運用した方がよいだろう。

### HTML

```html
<meta charset="UTF-8">
<!--
トップページだけ特別ルールにする
-->
{{ if .IsHome }}
<title>{{ .Site.Title }}</title>
{{ else }}
<title>{{ .Title }} | {{ .Site.Title }}</title>
{{ end }}
{{ hugo.Generator }}
<meta name="author" content="{{ .Site.Author.name }}">
<meta property="og:locale" content="{{ .Site.Params.localeOgp }}">
<meta property="fb:app_id" content="{{ .Site.Params.fbAppId }}">
<meta property="og:title" content="{{ .Title }}">
<meta property="og:url" content="{{ .Permalink }}">
<meta property="og:site_name" content="{{ .Site.Title }}">
<!--
1. `.Description`がある場合はそれを使用
2. 「1」が存在せず、nodeの場合は`.Summary`を使用
3. 「1」「2」が存在しない場合は共通の説明文を使用
-->
{{ if .Description }}
<meta name="description" content="{{ .Description }}">
<meta property="og:description" content="{{ .Description }}">
<meta name="twitter:description" content="{{ .Description }}">
{{ else }}
{{ if .IsPage }}
<meta name="description" content="{{ .Summary }}...">
<meta property="og:description" content="{{ .Summary }}...">
<meta name="twitter:description" content="{{ .Summary }}...">
{{ else }}
{{ if .IsHome }}
<meta name="description" content="{{ .Site.Params.description }}">
<meta property="og:description" content="{{ .Site.Params.description }}">
<meta name="twitter:description" content="{{ .Site.Params.description }}">
{{ else }}
<!-- 同じdescriptionのページが重複することを避ける -->
<meta name="description" content="{{ .Title }}に関連する記事の一覧です。{{ .Site.Params.description }}">
<meta property="og:description" content="{{ .Title }}に関連する記事の一覧です。{{ .Site.Params.description }}">
<meta name="twitter:description" content="{{ .Title }}に関連する記事の一覧です。{{ .Site.Params.description }}">
{{ end }}
{{ end }}
{{ end }}
<!--
記事にアイキャッチ画像がある場合はそれをOGP:imageに指定する。
存在しない場合はサイトデフォルトの画像を指定する。
-->
{{ if and (.Params.image) (ne .Params.image "") }}
<meta property="og:image" content="{{ .Params.image | absURL }}">
<meta property="twitter:image" content="{{ .Params.image | absURL }}">
{{ else }}
<meta property="og:image" content="{{ .Site.Params.ogpimage | absURL }}">
<meta property="twitter:image" content="{{ .Site.Params.ogpimage | absURL }}">
{{ end }}
<!--
記事ページはog:typeをarticleにする。公開日時、タグもキーワードとして出力
-->
{{ if .IsPage }}
<meta property="og:type" content="article">
<meta property="og:article:published_time" content="{{ .Date.Format "2006-01-02T15:04:05Z07:00" | safeHTML }}">
{{ range .Params.tags }}<meta property="og:article:tag" content="{{ . }}" />{{ end }}
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@{{ .Site.Params.twitter }}">
<meta name="twitter:creator" content="@{{ .Site.Params.twitter }}">
<meta name="twitter:title" content="{{ .Title }}">
<meta name="twitter:url" content="{{ .Permalink }}">
{{ else }}
<meta property="og:type" content="website">
{{ end }}
<!--
  トップでなければホームページへのリンクを出力
-->
{{ if ne .IsHome true }}
<link rel="index" href="/"/>
{{ end }}
<!--
  ページネーションがある場合は、next/prevを出力
-->
{{ if .IsNode }}
{{ if .Paginator.HasPrev }}
<link rel="prev" href="{{.Paginator.Prev.URL}}"/>
{{ end }}
{{ if .Paginator.HasNext }}
<link rel="next" href="{{.Paginator.Next.URL}}"/>
{{ end }}
{{ end }}
<!--
  前後の記事がある場合は、next/prevを出力
-->
{{ if .IsPage }}
{{if .PrevInSection}}<link rel="prev" href="{{ .PrevInSection.RelPermalink }}">{{end}}
{{if .NextInSection}}<link rel="next" href="{{ .NextInSection.RelPermalink }}">{{end}}
{{ end }}
<!-- HUGOが生成するAtomフィード -->
<link href="{{ with .OutputFormats.Get "RSS" }}{{ .RelPermalink }}{{ end }}" rel="alternate" type="application/rss+xml" title="{{ .Site.Title }}">
```

## 意味情報(JSON-LD)

バージョン0.16から`jsonify`フィルタや、記事からプレーンテキストだけを取得できる`.Plain`変数が追加され、JSON-LDのテンプレートがかなり作成しやすくなった。

### 記事ページのJSON-LD

記事ページを`BlogPosting`としてマークアップするためのJSON-LDを記載する。

注意が必要な点として、[BlogPosting](http://schema.org/BlogPosting)タイプでは、記事の画像(`image`)、著者(`author`)、出版元(`publisher`)フィールドが**必須**となっている。しかも、上の3つはそれぞれ[ImageObject](http://schema.org/ImageObject)、[Person](http://schema.org/ImageObject)、[Organization](http://schema.org/Organization)のタイプでなければならない。

そのため、画像のない記事には、あらかじめ適当なデフォルト画像をあてがう必要がある。OGP対応のためのデフォルト画像を流用するのが適当だろう。

さらに`ImageObject`タイプは`width``height`の値が**必須**となるので、画像の縦横ピクセル値を`Front-matter`へ記載する必要もある。

```html:テーマ/layouts/partial/json-ld-single.html

<!-- ブログ記事の情報 -->
<script type="application/ld+json">
{
  "@context": "http://schema.org",
  "@type": "BlogPosting",
  "@id": "{{ .Permalink }}",
  "name": {{ .Title | jsonify }},
  "headline": {{ .Title | jsonify }},
  "description": {{ .Summary | jsonify }},
  "dateModified": "{{ .Lastmod.Format "2006-01-02" }}",
  "datePublished": "{{ .Date.Format "2006-01-02" }}",
  "mainEntityOfPage": "{{ .Permalink }}",
  "url": "{{ .Permalink }}",
  {{ /* 字数 */ }}
  "wordCount": "{{ .WordCount }}",
  {{ /* プレーンテキストの本文 */ }}
  "articleBody": {{ .Plain | jsonify }},
  "articleSection": {{ .Section | jsonify }},
  {{ /* カテゴリをジャンルとして使用 */ }}
{{if .Params.categories }}  "genre": "{{ range .Params.categories }}{{ . }}{{ end }}",{{ end }}
  {{ /* タグをキーワードとして使用 */ }}
{{if .Params.tags }}  "keywords": "{{ range .Params.tags }}{{ . }}{{ end }}",{{ end }}
  "inLanguage": "{{ .Site.LanguageCode }}",
  "image": {
    "@type": "ImageObject",
    {{ if and (.Params.image) (ne .Params.image "") }}
    "url": "{{ .Params.image | absURL }}",
    "width": "{{ .Params.imagewidth  }}",
    "height": "{{ .Params.imageheight }}"
    {{ else }}
    "@id": "{{ .Permalink }}#ogp"
    {{ end }}
  },
  "author": { "@id": "{{ .Permalink }}#author" },
  "publisher": { "@id": "{{ .Permalink }}#org" }
}
</script>

<!-- 著者の情報 -->
<script type="application/ld+json">
{
  "@context": "http://schema.org",
  "@type": "Person",
  "@id": "{{ .Permalink }}#author",
  "name": "{{ .Site.Author.author }}",
  {{ /* この人物の説明 */ }}
  "description": {{ .Site.Author.profile | jsonify }},
  {{ /* プロフィール画像 */ }}
  "image": "{{ .Site.Author.image | absURL }}"
}
</script>

<!-- 記事にimageが設定されていない場合のデフォルト画像。OGPのデフォルト画像を流用している -->
<script type="application/ld+json">
{
  "@context": "http://schema.org",
  "@type": "ImageObject",
  "@id": "{{ .Permalink }}#ogp",
  "url": "{{ "/images/ogp.png" | absURL }}",
  "width": "{{ .Site.Params.ogpimagewidth }}",
  "height": "{{ .Site.Params.ogpimageheight }}"
}
</script>

<!-- 出版元となる組織。個人ならサイト名とかにしておけば妥当か -->
<script type="application/ld+json">
{
  "@context": "http://schema.org",
  "@type": "Organization",
  "@id": "{{ .Permalink }}#org",
  "name": "{{ .Site.Title }}",
  {{ /* logoは必須。ここにもOGP画像を使用 */ }}
  "logo": { "@id": "{{ .Permalink }}#ogp" }
}
</script>
```

### Markdown

上のテンプレートを使うには、front-matterに次の設定が必要となる。

```
# メイン画像へのパス
image = "/path/to/image.png"
# メイン画像のサイズ(メイン画像がない場合は不要)
imagewidth = 1024
imageheight = 768
```



# config.toml

前項のメタ情報・意味情報を使うための設定ファイルサンプル。

サイト全体で使用するメタ情報は、config.tomlの`[params]`下へ自由に増やせる。テンプレートからは`.Site.Params.xxx`でアクセスできる。

```toml
baseurl = "http://hogehoge.com"
languageCode = "ja"
title = "サイト名を入れる"
HasCJKLanguage = true

[author]
  author = "名前"
  profile = "自己紹介"
  image = "path/to/profile/image.png"

[params]
  localeOgp = "ja"
  description = "トップページ用の説明文"
  fbAppId = "facebookのAppIDを入れる"
  ogpimage = "images/ogp.png"
  ogpimagewidth = "1200"
  opgimageheight = "620"
  twitter = "twitterCard用のユーザー名を入れる"
```

# 参考サイト

* [Hugo :: A fast and modern static website engine](https://gohugo.io/)
* [HUGOとは？|Let's study HUGO!!](http://www.study-hugo.com/basic/whats-hugo/)
* [Hugo v0.60 から既定の Markdown パーサが Goldmark になったようだ — リリース情報 | text.Baldanders.info](https://text.baldanders.info/release/2019/11/hugo-v0_60-with-goldmark-parser/)