Title Image

ブログ制作記 #7

投稿日
(最終更新:)

ブログを書いていて欲しくなった機能を追加していきます。

リンクカード

QiitaやZennなどでは、リンクを書き込むといい感じにリンクカードを表示してくれます。

記事を書いていると、公式ドキュメントなどのリンクを貼ることが多いのでリンクカードが欲しいです。

HugoにはGetRemoteという関数があり、これを使うことで外部のURLからhtmlを取得できます。これを使うとリンクカードを生成するshortcodeを作ることができます。

layouts/shortcodeslinkcard.htmlを作ります。

リンクカードに載せたい情報は以下の4つです。

OGPが設定されたサイトであれば、タイトル・サイト画像・説明はそれぞれmetaタグのog:titleog:imageog:descriptionから取得できます。

ただし、全てのウェブサイトにOGPが設定されているとは限らないため、以下のカッコ内の順序に従って情報を得ます。

作成したコードは以下です。 正規表現を用いて特定のタグの値を抜き出しています。

layouts/shortcodes/linkcard.html
{{- $url := urls.Parse (.Get 0) }}
{{- $title := "" }}
{{- $description := "" }}
{{- $image := "" }}
{{- $siteURL := "" }}
{{- with try (resources.GetRemote $url) }}
  {{ with .Err }} <!-- Error occurred while fetching remote -->
    {{- warnf "%s" . }}
    {{- $title = "Error occurred while fetching remote" }}
    {{- $siteURL = $url.Hostname }}
  {{ else with .Value }} <!-- get OGP remote resource -->
    {{- $content := .Content }}
    <!-- get title -->
    {{- $found := findRESubmatch `og:title["\'].*?content=["\'](.*?)["\']` $content 1 }}
    {{- range $found }}
      {{- $title = index . 1 }}
    {{- end }}
    {{- if eq $title "" }}
      {{- $found := findRESubmatch `<title>(.*?)</title>` $content 1 }}
      {{- range $found }}
        {{- $title = index . 1 }}
      {{- end }}
    {{- end }}

    <!-- get description -->
    {{- $found := findRESubmatch `og:description["\'].*?content=["\'](.*?)["\']` $content 1 }}
    {{- range $found }}
      {{- $description = index . 1 }}
    {{- end }}
    {{- if eq $description "" }}
      {{- $found := findRESubmatch `description["\'].*?content=["\''](.*?)["\']` $content 1 }}
      {{- range $found }}
        {{- $description = index . 1 }}
      {{- end }}
    {{- end }}

    <!-- get image -->
    {{- $found := findRESubmatch `og:image["\'].*?content=["\'](.*?)["\']` $content 1 }}
    {{- range $found }}
      {{- $image = index . 1 }}
    {{- end }}
    {{- if eq $image "" }}
      {{- $found := findRESubmatch `<img.*?src=["\'](.*?)["\']` $content 1 }}
      {{- range $found }}
        {{- $image = index . 1 }}
      {{- end }}
      {{- if eq (slicestr $image 0 1) "/" }}
        {{- $host := urls.JoinPath "https://" $url.Hostname }}
        {{- $image =  urls.JoinPath $host $image }}
      {{- end }}
    {{- end }}

    <!-- get siteURL -->
    {{- $siteURL = $url.Hostname }}

  {{ else }} <!-- 404 -->
    {{ warnf "Unable to get remote resource %q" $url }}
    {{ $title = "Unable to get remote resource" }}
    {{ $siteURL = $url.Hostname }}
  {{ end }}
{{ end }}

<a href="{{ $url }}" target="_blank" class="link-card">
  <div class="link-card__content">
    <p class="link-card__title">{{ $title }}</p>
    <p class="link-card__description">{{ $description }}</p>
    <p class="link-card__url">{{ $siteURL }}</p>
  </div>
  {{ if ne $image "" }}
    <div class="link-card__image">
      <img src="{{ $image }}" alt="サイトの画像">
    </div>
  {{ end }}
</a>

このshortcodeを使ってmdで以下のように書くことでリンクカードを埋め込むことができます。

{{< linkcard "https://www.youtube.com" >}}

以下のように出力されます。

ページネーション

現状では、記事の一覧では1つのページに全ての記事が表示されます。これを、10記事ごとに分割します。

まず、hugo.tomlに以下を追記します。pagerSizeは各ページに何記事表示するか、pathはページネーションの際のパスです。

hugo.toml
[pagination]
  disableAliases = false
  pagerSize = 10
  path = 'list'

デフォルトのページネーション機能がありますが、今後調整する可能性があることを踏まえて、layouts/partials/pagination.htmlを新たに作成し、以下のコードをコピーします。

コピーしたコード上でcustomというフォーマットを使えるようにするため、defaultをコピーした以下のコードを追記します。わかりやすいようにクラス名を一部変更しています。

layouts/partials/pagination.htmlのcustom部分
{{/* Format: custom  
{{/* --------------------------------------------------------------------- */}}
{{- define "partials/inline/pagination/custom" }}
  {{- with .Paginator }}
    {{- $currentPageNumber := .PageNumber }}

    {{- with .First }}
      {{- if ne $currentPageNumber .PageNumber }}
      <li class="pagination-item">
        <a href="{{ .URL }}" aria-label="First" class="pagination-page-link" role="button"><span aria-hidden="true">&laquo;&laquo;</span></a>
      </li>
      {{- else }}
      <li class="pagination-item disabled">
        <a aria-disabled="true" aria-label="First" class="pagination-page-link" role="button" tabindex="-1"><span aria-hidden="true">&laquo;&laquo;</span></a>
      </li>
      {{- end }}
    {{- end }}

    {{- with .Prev }}
      <li class="pagination-item">
        <a href="{{ .URL }}" aria-label="Previous" class="pagination-page-link" role="button"><span aria-hidden="true">&laquo;</span></a>
      </li>
    {{- else }}
      <li class="pagination-item disabled">
        <a aria-disabled="true" aria-label="Previous" class="pagination-page-link" role="button" tabindex="-1"><span aria-hidden="true">&laquo;</span></a>
      </li>
    {{- end }}

    {{- $slots := 5 }}
    {{- $start := math.Max 1 (sub .PageNumber (math.Floor (div $slots 2))) }}
    {{- $end := math.Min .TotalPages (sub (add $start $slots) 1) }}
    {{- if lt (add (sub $end $start) 1) $slots }}
      {{- $start = math.Max 1 (add (sub $end $slots) 1) }}
    {{- end }}

    {{- range $k := seq $start $end }}
      {{- if eq $.Paginator.PageNumber $k }}
      <li class="pagination-item active">
        <a aria-current="page" aria-label="Page {{ $k }}" class="pagination-page-link" role="button">{{ $k }}</a>
      </li>
      {{- else }}
      <li class="pagination-item">
        <a href="{{ (index $.Paginator.Pagers (sub $k 1)).URL }}" aria-label="Page {{ $k }}" class="pagination-page-link" role="button">{{ $k }}</a>
      </li>
      {{- end }}
    {{- end }}

    {{- with .Next }}
      <li class="pagination-item">
        <a href="{{ .URL }}" aria-label="Next" class="pagination-page-link" role="button"><span aria-hidden="true">&raquo;</span></a>
      </li>
    {{- else }}
      <li class="pagination-item disabled">
        <a aria-disabled="true" aria-label="Next" class="pagination-page-link" role="button" tabindex="-1"><span aria-hidden="true">&raquo;</span></a>
      </li>
    {{- end }}

    {{- with .Last }}
      {{- if ne $currentPageNumber .PageNumber }}
      <li class="pagination-item">
        <a href="{{ .URL }}" aria-label="Last" class="pagination-page-link" role="button"><span aria-hidden="true">&raquo;&raquo;</span></a>
      </li>
      {{- else }}
      <li class="pagination-item disabled">
        <a aria-disabled="true" aria-label="Last" class="pagination-page-link" role="button" tabindex="-1"><span aria-hidden="true">&raquo;&raquo;</span></a>
      </li>
      {{- end }}
    {{- end }}
  {{- end }}
{{- end -}}

さらに、 一番上のコードを以下のように変更し、customが使えるようにします。

layouts/partials/pagination.html
{{- $validFormats := slice "default" "terse" "custom" }}

以下はコード全文です。defaultとの変更点はほとんどないので、組み込みのテンプレートでも十分かもしれません。

コード全体
layouts/partials/pagination.html
{{- $validFormats := slice "default" "terse" "custom" }}

{{- $msg1 := "When passing a map to the internal pagination template, one of the elements must be named 'page', and it must be set to the context of the current page." }}
{{- $msg2 := "The 'format' specified in the map passed to the internal pagination template is invalid. Valid choices are: %s." }}

{{- $page := . }}
{{- $format := "default" }}

{{- if reflect.IsMap . }}
  {{- with .page }}
    {{- $page = . }}
  {{- else }}
    {{- errorf $msg1 }}
  {{- end }}
  {{- with .format }}
    {{- $format = lower . }}
  {{- end }}
{{- end }}

{{- if in $validFormats $format }}
  {{- if gt $page.Paginator.TotalPages 1 }}
    <ul class="pagination pagination-{{ $format }}">
      {{- partial (printf "partials/inline/pagination/%s" $format) $page }}
    </ul>
  {{- end }}
{{- else }}
  {{- errorf $msg2 (delimit $validFormats ", ") }}
{{- end -}}

{{/* Format: custom  
{{/* --------------------------------------------------------------------- */}}
{{- define "partials/inline/pagination/custom" }}
  {{- with .Paginator }}
    {{- $currentPageNumber := .PageNumber }}

    {{- with .First }}
      {{- if ne $currentPageNumber .PageNumber }}
      <li class="pagination-item">
        <a href="{{ .URL }}" aria-label="First" class="pagination-page-link" role="button"><span aria-hidden="true">&laquo;&laquo;</span></a>
      </li>
      {{- else }}
      <li class="pagination-item disabled">
        <a aria-disabled="true" aria-label="First" class="pagination-page-link" role="button" tabindex="-1"><span aria-hidden="true">&laquo;&laquo;</span></a>
      </li>
      {{- end }}
    {{- end }}

    {{- with .Prev }}
      <li class="pagination-item">
        <a href="{{ .URL }}" aria-label="Previous" class="pagination-page-link" role="button"><span aria-hidden="true">&laquo;</span></a>
      </li>
    {{- else }}
      <li class="pagination-item disabled">
        <a aria-disabled="true" aria-label="Previous" class="pagination-page-link" role="button" tabindex="-1"><span aria-hidden="true">&laquo;</span></a>
      </li>
    {{- end }}

    {{- $slots := 5 }}
    {{- $start := math.Max 1 (sub .PageNumber (math.Floor (div $slots 2))) }}
    {{- $end := math.Min .TotalPages (sub (add $start $slots) 1) }}
    {{- if lt (add (sub $end $start) 1) $slots }}
      {{- $start = math.Max 1 (add (sub $end $slots) 1) }}
    {{- end }}

    {{- range $k := seq $start $end }}
      {{- if eq $.Paginator.PageNumber $k }}
      <li class="pagination-item active">
        <a aria-current="page" aria-label="Page {{ $k }}" class="pagination-page-link" role="button">{{ $k }}</a>
      </li>
      {{- else }}
      <li class="pagination-item">
        <a href="{{ (index $.Paginator.Pagers (sub $k 1)).URL }}" aria-label="Page {{ $k }}" class="pagination-page-link" role="button">{{ $k }}</a>
      </li>
      {{- end }}
    {{- end }}

    {{- with .Next }}
      <li class="pagination-item">
        <a href="{{ .URL }}" aria-label="Next" class="pagination-page-link" role="button"><span aria-hidden="true">&raquo;</span></a>
      </li>
    {{- else }}
      <li class="pagination-item disabled">
        <a aria-disabled="true" aria-label="Next" class="pagination-page-link" role="button" tabindex="-1"><span aria-hidden="true">&raquo;</span></a>
      </li>
    {{- end }}

    {{- with .Last }}
      {{- if ne $currentPageNumber .PageNumber }}
      <li class="pagination-item">
        <a href="{{ .URL }}" aria-label="Last" class="pagination-page-link" role="button"><span aria-hidden="true">&raquo;&raquo;</span></a>
      </li>
      {{- else }}
      <li class="pagination-item disabled">
        <a aria-disabled="true" aria-label="Last" class="pagination-page-link" role="button" tabindex="-1"><span aria-hidden="true">&raquo;&raquo;</span></a>
      </li>
      {{- end }}
    {{- end }}
  {{- end }}
{{- end -}}

{{/* Format: default
{{/* --------------------------------------------------------------------- */}}
{{- define "partials/inline/pagination/default" }}
  {{- with .Paginator }}
    {{- $currentPageNumber := .PageNumber }}

    {{- with .First }}
      {{- if ne $currentPageNumber .PageNumber }}
      <li class="page-item">
        <a href="{{ .URL }}" aria-label="First" class="page-link" role="button"><span aria-hidden="true">&laquo;&laquo;</span></a>
      </li>
      {{- else }}
      <li class="page-item disabled">
        <a aria-disabled="true" aria-label="First" class="page-link" role="button" tabindex="-1"><span aria-hidden="true">&laquo;&laquo;</span></a>
      </li>
      {{- end }}
    {{- end }}

    {{- with .Prev }}
      <li class="page-item">
        <a href="{{ .URL }}" aria-label="Previous" class="page-link" role="button"><span aria-hidden="true">&laquo;</span></a>
      </li>
    {{- else }}
      <li class="page-item disabled">
        <a aria-disabled="true" aria-label="Previous" class="page-link" role="button" tabindex="-1"><span aria-hidden="true">&laquo;</span></a>
      </li>
    {{- end }}

    {{- $slots := 5 }}
    {{- $start := math.Max 1 (sub .PageNumber (math.Floor (div $slots 2))) }}
    {{- $end := math.Min .TotalPages (sub (add $start $slots) 1) }}
    {{- if lt (add (sub $end $start) 1) $slots }}
      {{- $start = math.Max 1 (add (sub $end $slots) 1) }}
    {{- end }}

    {{- range $k := seq $start $end }}
      {{- if eq $.Paginator.PageNumber $k }}
      <li class="page-item active">
        <a aria-current="page" aria-label="Page {{ $k }}" class="page-link" role="button">{{ $k }}</a>
      </li>
      {{- else }}
      <li class="page-item">
        <a href="{{ (index $.Paginator.Pagers (sub $k 1)).URL }}" aria-label="Page {{ $k }}" class="page-link" role="button">{{ $k }}</a>
      </li>
      {{- end }}
    {{- end }}

    {{- with .Next }}
      <li class="page-item">
        <a href="{{ .URL }}" aria-label="Next" class="page-link" role="button"><span aria-hidden="true">&raquo;</span></a>
      </li>
    {{- else }}
      <li class="page-item disabled">
        <a aria-disabled="true" aria-label="Next" class="page-link" role="button" tabindex="-1"><span aria-hidden="true">&raquo;</span></a>
      </li>
    {{- end }}

    {{- with .Last }}
      {{- if ne $currentPageNumber .PageNumber }}
      <li class="page-item">
        <a href="{{ .URL }}" aria-label="Last" class="page-link" role="button"><span aria-hidden="true">&raquo;&raquo;</span></a>
      </li>
      {{- else }}
      <li class="page-item disabled">
        <a aria-disabled="true" aria-label="Last" class="page-link" role="button" tabindex="-1"><span aria-hidden="true">&raquo;&raquo;</span></a>
      </li>
      {{- end }}
    {{- end }}
  {{- end }}
{{- end -}}

{{/* Format: terse
{{/* --------------------------------------------------------------------- */}}
{{- define "partials/inline/pagination/terse" }}
  {{- with .Paginator }}
    {{- $currentPageNumber := .PageNumber }}

    {{- with .First }}
      {{- if ne $currentPageNumber .PageNumber }}
      <li class="page-item">
        <a href="{{ .URL }}" aria-label="First" class="page-link" role="button"><span aria-hidden="true">&laquo;&laquo;</span></a>
      </li>
      {{- end }}
    {{- end }}

    {{- with .Prev }}
      <li class="page-item">
        <a href="{{ .URL }}" aria-label="Previous" class="page-link" role="button"><span aria-hidden="true">&laquo;</span></a>
      </li>
    {{- end }}

    {{- $slots := 3 }}
    {{- $start := math.Max 1 (sub .PageNumber (math.Floor (div $slots 2))) }}
    {{- $end := math.Min .TotalPages (sub (add $start $slots) 1) }}
    {{- if lt (add (sub $end $start) 1) $slots }}
      {{- $start = math.Max 1 (add (sub $end $slots) 1) }}
    {{- end }}

    {{- range $k := seq $start $end }}
      {{- if eq $.Paginator.PageNumber $k }}
      <li class="page-item active">
        <a aria-current="page" aria-label="Page {{ $k }}" class="page-link" role="button">{{ $k }}</a>
      </li>
      {{- else }}
      <li class="page-item">
        <a href="{{ (index $.Paginator.Pagers (sub $k 1)).URL }}" aria-label="Page {{ $k }}" class="page-link" role="button">{{ $k }}</a>
      </li>
      {{- end }}
    {{- end }}

    {{- with .Next }}
      <li class="page-item">
        <a href="{{ .URL }}" aria-label="Next" class="page-link" role="button"><span aria-hidden="true">&raquo;</span></a>
      </li>
    {{- end }}

    {{- with .Last }}
      {{- if ne $currentPageNumber .PageNumber }}
      <li class="page-item">
        <a href="{{ .URL }}" aria-label="Last" class="page-link" role="button"><span aria-hidden="true">&raquo;&raquo;</span></a>
      </li>
      {{- end }}
    {{- end }}
  {{- end }}
{{- end -}}

CSSで見た目を整えます。HTMLをいじらずにCSSを書くだけで十分だったかもしれません。

assets/css/main.css
/* ページネーション */
.pagination-custom {
  display: flex;
  list-style: none;
  padding: 0;
  margin: 20px 0;
  justify-content: center;
  align-items: center;
  gap: 5px;
}

.pagination-item {
  display: inline-block;
}

.pagination-page-link {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  padding: 5px;
  text-decoration: none;
  color: #333;
  font-size: 16px;
  font-weight: bold;
  background-color: #fff;
  border: 1px solid #ccc;
  border-radius: 5px;
}

.pagination-item.active .pagination-page-link {
  background-color: #007bff;
  color: #fff;
  border-color: #007bff;
  pointer-events: none;
}

.pagination-item.disabled .pagination-page-link {
  color: #aaa;
  background-color: #f5f5f5;
  border-color: #ddd;
  pointer-events: none;
  cursor: default;
}

最後に記事一覧の表示部分であるlayouts/_default/list.htmlを以下のようにして完了です。

{{ define "main" }}
  <h1>{{ .Title }}</h1>
  {{ .Content }}

  {{ $paginator := .Paginate .Pages }}

  {{ partial "pagelist.html" (dict "pages" $paginator.Pages) }}

  {{ partial "pagination.html" (dict "page" . "format" "custom") }}
{{ end }}

出来上がりはこのような感じです。かなりいい感じだと思います。

完成したpagination

headを整備する

リンクカードの実装時にOGPタグから情報を得ていましたが、このサイトのheadタグ内はデフォルトのままだったのでmetaタグなどがほとんどありません。

head内の情報を増やします。layouts/partials/head.htmlに追記して以下のようにしました。

アイコン画像はまだないので保留です。

layouts/partials/head.html
{{- $title := "" }}
{{ if .IsHome }}
  {{ $title = site.Title }}
{{ else }}
  {{ $title = printf "%s | %s" .Title site.Title }}
{{ end }}

<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<meta http-equiv="X-UA-Compatible" content="IE=edge">

<meta name="description" content="{{ .Summary }}">
<meta name="robots" content="index, follow">  <!-- クローラーにページをインデックスさせる -->
{{ with .GitInfo }}
  <meta name="author" content="{{ .AuthorName }}">  <!-- 作成者情報 -->
{{- end }}
<meta name="generator" content="Hugo">  <!-- 生成ツール -->

<meta property="og:title" content="{{ $title }}">
<meta property="og:description" content="{{ .Summary }}">
<!-- <meta property="og:image" content="https://example.com/image.jpg"> --> <!-- TODO: アイコン画像の追加  -->
<meta property="og:url" content="{{ urls.JoinPath site.BaseURL .RelPermalink }}">
<meta property="og:type" content="website">
<meta property="og:site_name" content="{{ site.Title }}">

<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="{{ $title }}">
<meta name="twitter:description" content="{{ .Summary }}">
<!-- <meta name="twitter:image" content="https://example.com/image.jpg"> --> <!-- TODO: アイコン画像の追加 -->
<!-- <meta name="twitter:site" content="@Twitterアカウント"> --> <!-- のちほど追加するかも -->



<title>{{ $title }}</title>
{{ partialCached "head/css.html" . }}
{{ partialCached "head/js.html" . }}

おわりに

欲しい機能が追加できました。少し難易度は高そうですが、検索機能も付けてみたいです。