Hugo のショートコード内で Markdown を使う

Hugo のショートコード内の Markdown もレンダーさせる際にちょっとハマったのでメモです。

やりたいこと

よく技術系の Web サイトで以下のようなメモカード (一般的になんていうのかわからない) ってノーティス目的やワーニング目的で使ったりするじゃないですか。このサイトでも前回から使い始めています。

メモカード

これを Hugo のショートコードで作って、カード内のコンテンツで Markdown を使えるようにしたかったのですが、ハマって色々調べたり試してみました。

結論

今回色々調べてはうまくいかなかったりと記事が長くなってしまったので、結論を先に書いておきます。

Hugo v0.100.0 以降でショートコード内で Markdown をレンダーするには config.tomlmarkup.goldmark.renderer.unsafe オプションを true にし、ショートコードの呼び出しは {{% shortcode %}} で行う。(検証できていませんが、v0.100.0 より前のバージョンでは config.tomlmarkup.defaultMarkdownHandlerblackfriday にすればレンダーできるかもしれません)

順序付きリスト内でショートコード利用時に順序付きリストをブレイクさせないためには、ショートコード定義内でコンテンツを渡す際に、{{ .Inner -}} と後ろにハイフンを付けて、空白を除去する。

詳細や、NG 事例は以下で紹介していますのでよかったら読んでください。

Shortcode とは

Hugo ではレイアウトファイルを使ってサイト自体のデザインを自由度高くカスタマイズできますが、コンテンツ内容は純粋に Markdown のパースおよび規定の HTML への置換と CSS でのデザインが限界です。

Hugo v0.62.0 以降は Markdown Render Hooks 機能が追加され、一部の要素についてカスタマイズが可能となりましたが、利用できる要素は限定的です。

そこで登場するのがショートコード機能です。ショートコード機能を使えば任意の要素をコンテンツ内に挿入することができます。また Hugo のビルトインショートコードも多く用意されており、ショートコードを利用することで、コンテンツデザインについても高い自由度が得られます。

ビルトインショートコードは Shortcodes | Hugo で紹介されています。

環境

OS
Windows 10 21H2
Hugo
v0.101.0

準備

自分のサイトだとテンプレートも自作しているので、ショートコードの問題なのかテンプレートの問題なのかわからなくなるので、Quick Start | Hugo に沿ってテスト用サイトを作り、既存テーマを適用します。

  1. 新しいサイトを作ります。

    hugo new site shortcodetest
  2. Ananke theme を適用します。

    cd shortcodetest
    git init
    git submodule add https://github.com/theNewDynamic/gohugo-theme-ananke.git themes/ananke
  3. テーマを config.toml に定義します。

    echo theme = "ananke" >> config.toml

ショートコード作成と使用

  1. layouts/shortcodes/memocard.html というファイルを作成し、以下のコードを記載します。

    <div class="memocard {{ .Get "label" }}">
      {{ .Inner }}
    </div>

    Create Your Own Shortcodes | Hugo に説明がありますが、ショートコードの定義は layouts/shortcodes ディレクトリ配下に HTML ファイルを作成し、定義します。

  2. 新しいコンテンツを作成します。

    hugo new shortcodecontent.md
  3. 作成したコンテンツファイル content/shortcodecontent.md に以下を記載します。

    1. 順序付きリスト 1
    
        `{{< shortcode >}}` でショートコード呼び出し。
        {{< memocard label="warning" >}}
        Markdown は**レンダーされません**。HTML は<strong>レンダーされます</strong>    {{< /memocard >}}
    
    1. 順序付きリスト 2
    
        `{{% shortcode %}}` でショートコード呼び出し。
        {{% memocard label="warning" %}}
        Markdown は**レンダーされます**。HTML は<strong>レンダーされません</strong>    {{% /memocard %}}
    
    1. 順序付きリスト 3
    
        順序付きリストがブレイクされてしまいます。
  4. サーバーを起動します。

    hugo server -D
  5. ブラウザで http://localhost:1313/shortcodecontent にアクセスします。

    他の Hugo server を起動している場合、ポートは 1313 以外になりますので、適宜置き換えてください。

事象と対処

すでにコンテンツの中で事象を書いてしまっていますが、以下のような表示結果になってしまいます。

ブラウザでの表示結果

生成された HTML を見てみると以下のようになっています。

<ol>
  <li>
    <p>順序付きリスト 1</p>
    <p><code>{{< shortcode >}}</code> でショートコード呼び出し。</p>
    <div class="memocard warning">
    Markdown は**レンダーされません**。HTML は<strong>レンダーされます</strong>    </div>
    <p></p>
  </li>
  <li>
    <p>順序付きリスト 2</p>
    <p><code>{{% shortcode %}}</code> でショートコード呼び出し。</p>
    <!-- raw HTML omitted -->
    <p>Markdown は<strong>レンダーされます</strong>。HTML は<!-- raw HTML omitted -->レンダーされません<!-- raw HTML omitted --></p>
  </li>
</ol>
<!-- raw HTML omitted -->
<ol>
  <li>
    <p>順序付きリスト 3</p>
    <p>順序付きリストがブレイクされてしまいます。</p>
  </li>
</ol>

まず順序付きリスト 1 に記載したショートコードは {{< shortcode >}} という記法で記載しています。この記法の場合、ショートコードに渡した Markdown はレンダーされないようです。ただ何故か HTML はレンダーされるようです。

The < character indicates that the shortcode’s inner content does not need further rendering. Often shortcodes without markdown include internal HTML - Shortcodes | Hugo

次に順序付きリスト 2 に記載したショートコードは {{% shortcode %}} という記法で記載しています。この記法の場合、ショートコードに渡した Markdown はレンダーされるのですが、ショートコード定義に記載した HTML がすべて無視されています。生成された HTML にも <!-- raw HTML omitted --> とコメントされています。加えて、順序付きリストがブレイクされ、順序付きリスト 3 の連番が 1 に戻ってしまっています。HTML でも <ol> が分かれてしまっています。

In Hugo 0.55 we changed how the % delimiter works. Shortcodes using the % as the outer-most delimiter will now be fully rendered when sent to the content renderer. - Shortcodes | Hugo

ショートコード内の Markdown と HTML をレンダーさせる解決方法

HTML が無視される問題については、config.toml に次のように記載すれば解消されます。

[markup]
  [markup.goldmark]
    [markup.goldmark.renderer]
      unsafe = true

ブラウザでの表示結果

markup.goldmark.renderer.unsafe オプションは コンテンツ内の HTML をレンダーさせるかを決めるオプションです。インライン JavaScript も埋め込み可能になりますので、セキュリティーの観点からデフォルトでは false (=レンダーしない) になっています。

unsafe By default, Goldmark does not render raw HTMLs and potentially dangerous links. If you have lots of inline HTML and/or JavaScript, you may need to turn this on.
Configure Markup | Hugo

しかし、ショートコードのためにセキュリティーリスクを高めてまで、markup.goldmark.renderer.unsafe オプションを true にするのも嫌ですよね。 (結論、この方法しかありませんでした。)

ショートコード内の Markdown と HTML をレンダーさせる NG その 1

{{< shortcode >}} は Markdown はレンダーされないが HTML はレンダーされる、{{% shortcode %}} は Markdown はレンダーされるが、HTML はレンダーされる。であれば、渡されたコンテンツをただ表示するだけのショートコードを作成しネストすればどうだ、と考え以下のようにやってみました。

コンテンツを表示するだけのショートコード (inner.html):

{{ .Inner }}

ショートコードをネストさせる (順序付きリスト 2 のところ):

1. 順序付きリスト 1

    `{{< shortcode >}}` でショートコード呼び出し。
    {{< memocard label="warning" >}}
    Markdown は**レンダーされません**。HTML は<strong>レンダーされます</strong>    {{< /memocard >}}

1. 順序付きリスト 2

    `{{% shortcode %}}``{{< shortcode >}}` でラップしてショートコード呼び出し。
    {{< memocard label="warning" >}}
    {{% inner  %}}
    Markdown は**レンダーされます**。HTML は<strong>レンダーされません</strong>    {{% /inner %}}
    {{< /memocard >}}

1. 順序付きリスト 3
    順序付きリストがブレイクされてしまいます。

Markdown も HTML もレンダーされないどころか、<pre><code></code></pre> でラップされてしまいました。

ブラウザでの表示結果

この挙動は上で書いた config.tomlmarkup.goldmark.renderer.unsafetrue にしても同じです。

なお、ショートコード内に HTML を書いて {{< shortcode >}} で呼び出した外側のショートコード内の HTML はちゃんとレンダーされています。

<li>
  <p>順序付きリスト 2</p>
  <div class="memocard warning">
    <pre><code>Markdown は**レンダーされます**。HTML は&lt;strong&gt;レンダーされません&lt;/strong&gt;</code></pre>
  </div>
</li>

いずれにしてもこのやり方は NG です。

ショートコード内の Markdown と HTML をレンダーさせる NG その 2

そもそも config.tomlmarkup.goldmark.renderer.unsafe オプションは Markdown レンダリングエンジンに Goldmark が採用されているからです。 Goldmark は Hugo v0.60.0 からデフォルトレンダリングエンジンとして採用されています。それ以前は BlackFriday というレンダリングエンジンが使われていました。ではレンダリングエンジンを BlackFriday に戻してやればいいのではと思いましたが…。

config.tomlmarkup.defaultMarkdownHandlerblackfriday に変更。

[markup]
  defaultMarkdownHandler = 'blackfriday'

レンダーしてみると Hugo v0.100.0 で Blackfriday は削除されたとのことで、以下のようなエラーが出てしまいます。

Failed to reload config: add site dependencies: create deps: markup: Configured defaultMarkdownHandler "blackfriday" not found. Did you mean to use goldmark? Blackfriday was removed in Hugo v0.100.0. 
hugo v0.101.0-466fa43c16709b4483689930a4f9ac8add5c9f66 windows/amd64 BuildDate=2022-06-16T07:09:16Z VendorInfo=gohugoio

Reload Page

まあそもそもこのやり方は config.tomlmarkup.goldmark.renderer.unsafetrue にするのと同義だと思いますので、あまり意味ないですね。

ショートコード内の Markdown と HTML をレンダーさせる NG その3

その 3 と言いますかそもそも Hugo のビルトインショートコードがたくさんあるので、その実装を見れば答えがあるのでは?と考えたのですが、そもそも Hugo のビルトインショートコードにコンテンツを渡すタイプのものがない (シンタックスハイライトぐらい)ということに気づきました。ショートコードに Markdown 渡してレンダーさせるの邪道なのか。

順序付きリストがブレイクされる問題の解決方法

さて、ショートコード内の Markdown と HTML 両方をレンダーさせる方法は config.tomlmarkup.goldmark.renderer.unsafetrue にするしかなかったわけですが、もう一つ順序付きリストがブレイクされてしまう問題がありました。

こちらはショートコード HTML 内で .Inner を渡すときにハイフン - をつけることで解消されました。

<div class="memocard {{ .Get "label" }}">
{{ .Inner -}}
</div>

ブラウザでの表示結果

- は空白を除去してくれるようです。

Go 1.6 includes the ability to trim the whitespace from either side of a Go tag by including a hyphen (-) and space immediately beside the corresponding {{ or }} delimiter. - Introduction to Hugo Templating | Hugo

なお、両サイドに - をつけると {{< shortcode >}} でショートコードを呼び出したときと同じ挙動 (Markdown はレンダーされない。HTML はレンダーされる)になります。

<div class="memocard {{ .Get "label" }}">
{{- .Inner -}}
</div>

あとがき

ちょっと釈然としませんが、Hugo のショートコードで Markdown をレンダーする方法と、順序付きリスト内でショートコードを使ってもブレイクさせない方法を調査、紹介しました。今後は Markdown Render Hooks 機能がもっと追加されればショートコードの利用を最小限にできるのかなと思います。