超爆速でコンパイルができる、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を書いている場合は注意。
[markup]
[markup.goldmark]
[markup.goldmark.renderer]
unsafe = true
アイキャッチとタグ付きの新着順記事一覧
この例では、記事のMarkdownへアイキャッチ画像用のURLをfront-matterのimage
プロパティを記述し、項目が存在する場合はHTMLに出力するようにしている。
存在の有無でclassも変化する為、レイアウトをそれぞれ適当なものに変えることができる。
ちなみに、.Summary
はconfig.toml
でHasCJKLanguage = true
を設定しないとおかしな表示になってしまうので、日本語のブログを作るときは必ず設定しておこう。
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 -->
<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
の内容を制御する方法はこちらの記事が詳しい。
ページネーション付の記事一覧と操作UI
前項とほぼ一緒だが、ボタン部分に加えて、一覧の抽出条件の書き方が違ってくる。
HTML(一覧)
この書き方だと、出力される記事件数はデフォルトの件数となる。ページネーションのデフォルト件数はconfig.toml
に設定できる。li
パーシャルの中身は前項ママでそのまま使えるので省略する。
<!-- 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(ボタン部分)
ページネーションの操作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で移動させる。これで、外観上は目次が途中に挿入されているように見える。
HTML
<!-- single.htmlの一部 -->
<section>
<div class="body">
{{ if eq .Params.usetoc true }}
{{ partial "toc.html" . }}
{{ end }}
{{ .Content }}
</div>
</section>
<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
フィルターが良好に機能した。
<!-- 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 }}&body={{ .Permalink | safeURL }}">メールで共有</a>
</p>
</li>
</ul>
</div>
<!-- /content-footer -->
関連記事
タグ・サムネイル付きの関連記事一覧を最大9件まで表示する。
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 -->
前の記事/次の記事へのナビゲーション
同一セクションに限定し、前後の投稿がある場合はそれを出力する。
<!-- 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
を記述しておく。
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つはそれぞれImageObject、Person、Organizationのタイプでなければならない。
そのため、画像のない記事には、あらかじめ適当なデフォルト画像をあてがう必要がある。OGP対応のためのデフォルト画像を流用するのが適当だろう。
さらにImageObject
タイプはwidth``height
の値が必須となるので、画像の縦横ピクセル値をFront-matter
へ記載する必要もある。
<!-- ブログ記事の情報 -->
<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用のユーザー名を入れる"