Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
102
Help us understand the problem. What is going on with this article?
@y_hokkey

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

超爆速でコンパイルができる、Go言語製の静的サイトジェネレーターHUGO

当然、HUGOでは静的HTMLサイトしか作ることができない。しかし、テンプレート機能をうまく作ればWordPressにも負けないようなパーツが結構作れてしまう。この記事では、HUGOで構築した自分のブログを例に、いくつかの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も変化する為、レイアウトをそれぞれ適当なものに変えることができる。

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

新着記事の一覧.png

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 -->
テーマ/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プロパティにパスを記載する。

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

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

Hugoにおける記事要約の仕組み - Qiita

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

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

HTML(一覧)

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

テーマ/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件の表示としたい場合はこのように書く。

paginate = 20

HTML(ボタン部分)

スクリーンショット 2016-06-08 12.05.36.png

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

<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

HTML

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

JavaScript

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

Markdown

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

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

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

## 必要機材

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

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

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

SNS共有ボタン.png

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

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

<!-- 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にpickuptypeimageを記述しておく。

固定された記事の表示枠.png

HTML

  • 「project」タイプで「pickup」パラメーターがtrueの記事を時系列の逆順に3件まで表示
  • 「docs」タイプで「pickup」パラメーターがtrueの記事を時系列順に2件まで表示
<!-- 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内へ次を記載する。

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

メタ情報

[汎用]

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

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タイプでは、記事の画像(image)、著者(author)、出版元(publisher)フィールドが必須となっている。しかも、上の3つはそれぞれImageObjectPersonOrganizationのタイプでなければならない。

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

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

テーマ/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でアクセスできる。

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用のユーザー名を入れる"

参考サイト

102
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
y_hokkey
1988年生まれ。多摩美グラフィックデザイン学科を中退。デザイナーとしてグラフィック・エディトリアル・Web・UIのデザインを経験した後、Webフロントエンドエンジニアも経験。現在はITコンサルタントとして外資系SIerに在籍中。※投稿内容は私個人の意見であり、所属企業・部門見解を代表するものではありません。

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
102
Help us understand the problem. What is going on with this article?