Hugoでaタグとimgタグに属性を追加したかったのでRender hooksを作成した

Posted:

HugoがMarkdownをHTMLへレンダリングする際に書き出すaタグやimgタグは、本当に必要最低限のタグでしかなくて。
ショートコードを使う頻度を下げたかったので、既存のレンダリングをオーバーライドするRender hooksを作成したよ、という備考録です。


HugoのMarkdown

厳密に言えば、Hugoが書き出すのではなく、Hugoが標準使用するMarkdownレンダラーであるGoldmarkが最低限しか書き出さない、なんですけど。

どれだけ最低限かというと、例えばMarkdownでテキストリンクと画像挿入をこう書いたとして、

1
2
[Retrovirus](https://www.retrovirus.xyz/ "れとうぃへのリンク")
![我が家のねこちゃん](/note/postimg/250207_01.webp)

書き出されるHTMLタグはこう。

1
2
<a href="https://www.retrovirus.xyz/" title="れとうぃへのリンク">Retrovirus</a>
<img src="/note/postimg/250207_01.webp" alt="我が家のねこちゃん" />

aタグはtitle属性はつけられるのに、target属性はつけられず全部target="_self"扱いでrel属性もないのです。

ウチのサイトでは、内部リンクは_self(つまり、target属性の指定をしない)で、外部リンクは_blankで開くようにしています。なので、target属性を指定できないと不便。

Markdownによっては{:target="_blank"}で別窓になるとかありますが、それは他のMarkdownの方言なのでHugoが採用しているGoldmarkでは使えません。

imgタグも、必須であるalt属性はつけられるのに、それ以上に必須だと個人的に思うwidth属性とheight属性がない。
なんならloading属性もない(これはブラウザ対応されたのがやや最近なので、なくても不思議じゃないけど)。

width属性とheight属性は本当に必要だと思うのですよね。
よく、サイトを見ている時にページを読み込んだと思って見ていたら途中でいきなり画像が出てきて、クリックするつもりもないどうでもいい広告バナーをクリックした、みたいな経験をしたことがある人ってかなりいると思うんですが。
あれはレイアウトシフトが起こっている状態のサイトによくある光景です。
画像が表示されるスペースが予め確保されていたら起こらない現象なので、画像のサイズを指定するwidth属性とheight属性はとても大事だと思うのです。

……と、こんな感じなので自作のショートコードを使っていたんですけどね。
本文を書く際、折角色々と取り回しが利くMarkdownで書いているのに、HTMLタグで書くのも本末転倒感があり、かといってショートコードを連発しすぎるのもなぁと思ってしまって。

そういえば組み込みのレンダリングの挙動を変えることができるものがあったなと思い出したので、そいつを一発ビシッと決めてみました。

ちな、上にある我が家のねこちゃんの画像はこれ。

あくびの瞬間を撮ってしまって、人相悪くなってるやつ。


Render hooksとは

HugoがMarkdownからHTMLをレンダリングする際のデフォルトの挙動を、カスタムテンプレートで上書きして挙動を変えられる機能です。

Hugoでは以下の7つの要素タイプをフックできるようで。

  • Blockquotes(引用ブロック)
  • Code blocks(コードブロック)
  • Headings(見出し)
  • Images(画像)
  • Links(リンク)
  • Passthrough elements(パススルー要素)
  • Tables(テーブル)

挙動に不満があるのは、今のところ上記の中では画像とリンクだけなので、この2つをカスタマイズしていきます。

他のRender hooksの詳細はマニュアルをどうぞ。


aタグをカスタマイズする

まずはリンクタグをカスタマイズします。

Markdownで記述するリンクには3つ要素があります。

1
2
3
[Post 1](/posts/post-1 "My first post")
 ------  -------------  -------------
  text    destination       title

引用:https://gohugo.io/render-hooks/links/#markdown

リンクテキストになるText要素、リンク先になるDestination要素、リンクタイトルになるTitle要素です。これらをカスタマイズ済みのHTMLのaタグへ組み込んでいきます。

ウチのブログでは、用途によってCSSで装飾したリンク表示と、本文内に書くプレーンなリンクを使い分けています。つまり、用途によってHTMLの組み方も違うということで。

外部リンク(装飾リンク)、外部リンク(インラインリンク)、内部リンク(NOTE内記事にリンクする装飾リンク)と3種類あるので、それをどう振り分けるかをまず悩みました。
外部リンクと内部リンクはURLの行頭がhttpではじまるかどうかで振り分ければ簡単なんですけど、装飾のあるリンクとインラインリンクはどちらも外部リンクで使用するので、どこで分けるか、みたいな。

で、色々考えた結果、HTMLマークアップとしては邪道な使い方になるのですが、リンクタイトルをその判別に使用することにしました。

できたテンプレートはこちら。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
{{/* layouts\_default\_markup\render-link.html */}}
{{/* インラインリンク */}}
{{- if eq .Title "inner" -}}
<a href="{{ .Destination | safeURL }}" target="_blank" rel="noopener">{{ .Text }}</a>
{{/* 装飾つきリンク */}}
{{- else -}}
<div class="linkcard">
  <div class="linkcard-mark">
  {{/* 内部リンク */}}
  {{- if eq .Title "inlink" -}}
   <a href="{{ .Destination | safeURL }}">{{ .Text }} - Retrovirus NOTE</a>
  {{/* 外部リンク */}}
  {{- else -}}
   <a href="{{ .Destination | safeURL }}" target="_blank" rel="noopener">{{ .Text }}</a>
  {{- end -}}
  </div>
</div>
{{- end -}}

リンクタイトルを使ってインラインリンクと装飾つき内部リンクを振り分けています。
なので、実際のリンクにはタイトルをつけていません。
まぁ、リンクタイトルってほとんどつけないし、いいかなって。

装飾つきリンクの方はaタグだけ挙動が違うので、ここでもリンクタイトルを使って内部リンクか外部リンクかを判別して振り分けました。

これでMarkdownで

1
2
3
[装飾付き外部リンク](https://www.retrovirus.xyz/)
[装飾付き内部リンク](/note "inlink")
インライン[リンク](/note "inner")です。

と書くと、HTMLでこう表示されます。

インラインリンクです。

これでMarkdownでのリンクまわりの記述がスッキリしたのでヨシ。


imgタグをカスタマイズする

Hugoにはfigureタグで画像を挿入できる組み込みのショートコードがあるのですが、このfigureタグは「本文のから参照される図版」を表すタグで。
つまり、本文から図を取り除いても文章が成立する場合でのみ使用することが望まれるもの で、図を伴った説明文などでの使用は避けた方がよいタグです。難しいよね。

なので、pictureタグで画像を挿入するショートコードを自作して使っていました。
WordpressからHexoに乗り替えた頃は、まだ一部の主要ブラウザでしかWebPが正式対応していなかったので、ショートコードで対応していたんですよ。

で、HexoからHugoへ乗り替える時も、そのショートコードをそのまま移植しただけで使っていました。
少し前にWebP対応を調べたら、もうほぼすべてのブラウザでWebP対応が済んでいたので、ショートコード内でpictureタグでの画像表示をやめて、imgタグ+srcset属性だけの簡潔なものへ変更したのです。
でも、imgタグだけにするならショートコードにしなくてもよくね?と思い当たって。
あとは、AppleデバイスのRetinaディスプレイ対応に本当に嫌気が差したのでsrcset属性も取っ払って、単純に1枚だけ用意した画像を表示するimgタグへ書き換えるついでに、Markdownの画像挿入記述のままでなんとかしてぇなと思ったのでした。

サイズが違うだけの同じ画像を何枚も用意するのバカらしくね?
いや、仕事で給料もらって運営するサイトならきっちりやりますけども、ここは個人の趣味サイトなのでそこまでせんでもよくね?って。

長々と語りましたが、次は待望の画像まわりのカスタマイズです。

Markdownで記述する画像挿入には3つ要素があります。

1
2
3
![white kitten](/images/kitten.jpg "A kitten!")
  ------------  ------------------  ---------
  description      destination        title

引用:https://gohugo.io/render-hooks/images/#markdown

画像の代替テキストになるDescription要素、画像パスになるDestination要素、画像タイトルになるTitle要素です。
imgタグへ画像のサイズ(width属性とheight属性)とloading属性を追加したHTMLタグへ組み込んでいきます。

ショートコードを使用していた時は、画像のサイズはwidth属性やheight属性へデフォルトの値を入れることができたのですけども。
フックするテンプレートでは、表示する画像のサイズを取得する関数を使って画像のサイズを挿入したいと思います。
全部を同じサイズに固定すればめんどくさくないんだけど、それも難しい場面とかあるしね。

また、ウチのサイトでは画像表示にFancyboxを使っているので、それにも対応するように組んでいきたいと思います。

まず画像のサイズを調べるための準備をします。
使用するリソースメソッドassetsディレクトリのみで使用できるメソッドなので、画像を置くディレクトリをassetsディレクトリにしなければなりません。
でも、画像はstaticディレクトリへ置いておきたい。
ということで、assetsディレクトリへstaticディレクトリをマウントします。

サイト設定(ここではhugo.yaml)へ、以下の設定を追加します。

1
2
3
4
5
6
module:
  mounts:
  - source: assets
    target: assets
  - source: static
    target: assets

これで、staticディレクトリの画像などのリソースをグローバルリソースとして、リソースメソッドで操作できるようになりました。

次に、オーバーライドするRender hooksのテンプレートを作ります。

1
2
3
4
5
6
7
8
{{/* layouts\_default\_markup\render-image.html */}}
{{- $noteImage := path.Join "note/postimg/" (.Destination | safeURL) -}}
{{- $noteImgDir := "/note/postimg/" -}}
<div class="notepage-text-images">
<a href="{{ $noteImgDir }}{{ .Destination | safeURL }}" data-caption="{{ .PlainText }}" data-fancybox="rnote">
  <img src="{{ $noteImgDir }}{{ .Destination | safeURL }}" {{- with resources.Get $noteImage }}width="{{ div .Width 2 }}" height="{{ div .Height 2 }}"{{ end }} alt="{{ .PlainText }}" loading="lazy" />
</a>
</div>

これから先は例として以下のMarkdownで説明します。
利用する画像のサイズはWidth=1600px、Height=900pxとします。

1
![我が家のねこちゃん](250207_01.webp)

まずは、リソースメソッド操作するための画像ファイルパスをpath.Join関数で作ります。

1
{{- $noteImage := path.Join "note/postimg/" (.Destination | safeURL) -}}

画像ファイルはassetsディレクトリにあるという前提なので、上の関数だと、assetsディレクトリからの相対パス+Markdownからの画像パスになるDestination要素を結合して、変数$noteImageへ代入します。

$noteImageの中は以下のパスが代入されています。

1
note/postimg/250207_01.webp

もうひとつ、Fancybox用のリンクのためのディレクトリパスの変数を作りました。

1
{{- $noteImgDir := "/note/postimg/" -}}

これはサイトのルートからの指定です。

次に、画像を表示するタグを作成。

1
2
3
4
5
<div class="notepage-text-images">
<a href="{{ $noteImgDir }}{{ .Destination | safeURL }}" data-caption="{{ .PlainText }}" data-fancybox="rnote">
  <img src="{{ $noteImgDir }}{{ .Destination | safeURL }}" {{- with resources.Get $noteImage }}width="{{ div .Width 2 }}" height="{{ div .Height 2 }}"{{ end }} alt="{{ .PlainText }}" loading="lazy" />
</a>
</div>

aタグのhref属性とimgタグのsrc属性には$noteImgDirDestination要素でパスを作ります。

1
href="{{ $noteImgDir }}{{ .Destination | safeURL }}"

Fancyboxのdata-caption属性とimgタグのalt属性にDescription要素を.PlainTextで挿入。

1
2
data-caption="{{ .PlainText }}"
alt="{{ .PlainText }}"

imgタグのwidth属性とheight属性はリソースメソッドの.Width.Heightで画像サイズを挿入するのですが、元画像は2倍サイズで作ってあって、ブログ記事内で表示するのはCSSで50%~縮小したサイズになります。なので、imgタグのwidth属性とheight属性には1/2のサイズを記載したい。

ということで、テンプレートの中で計算をしています。
該当箇所はここ。(見やすいように改行をいれています)

1
2
3
4
{{- with resources.Get $noteImage }}
 width="{{ div .Width 2 }}"
 height="{{ div .Height 2 }}"
{{ end }}

まず、対象になる画像のパスをresource関数を使って変数から取得します。
次に、幅と高さそれぞれを除算のmath.Div関数で2で割ります。

上のMarkdownの例でレンダリングして書き出されたHTMLはこちら。

1
2
3
4
5
<div class="notepage-text-images">
<a href="/note/postimg/250207_01.webp" data-caption="我が家のねこちゃん" data-fancybox="rnote">
  <img src="/note/postimg/250207_01.webp" width="800" height="450" alt="我が家のねこちゃん" loading="lazy" />
</a>
</div>

必要な要素がたっぷり詰まったHTMLタグになりました。


画像リンクもカスタマイズしたかったけど

ウチでは本文を書く際に、画像に外部へのWebリンクを貼る画像リンクも使用することがあります。
なので、今回のフックで画像リンクもカスタマイズできるのでは?と試行錯誤したのですけども。

結論から言えば、画像リンクはどう要素を取り出せばいいのか迷ってしまったので、今回はカスタマイズを見送り。

HugoのMarkdownでは画像リンクってこう書くんですよ。
でもって、要素的はこんな感じだと思うんですね。

1
2
3
[![れとうぃバナー](/imgs/retrovirus.png "Retrovirus")](https://www.retrovirus.xyz)
  ---------------  --------------------  ----------    --------------------------
 (img)description    (img)destination    (img)title        (link)destination

コンテキストを取りだそうにも、リンクパスであるDestination要素と画像パスであるDestination要素が衝突してうまくいきませんでした。
プレースホルダー的に取り出せるならできそうなんだけど、その辺がよくわからなかったので、画像リンクはこれまでと同じくショートコードで対応することにしました。


総じて

最初はめんどくさそうだな~と思っていたRender hooksですが、作ってみると意外とあっさりしていて、簡単に実装できました。
Hugoの公式ドキュメントにあるコードは本当に必要最低限しか書かれていないので、もう少し例とか増やしてほしいな……って思いました。
淡泊にしか書かれてないから、逆に見づらいんだよね……。



SEARCH:

ARCHIVE:

▲PageTOPへ