Hugo : Page Bundlesルールでの「index.md」以外のマークダウンファイルの扱い

Hugo の Page Bundles のルールでは、記事となる index.md と同じ階層に置かれた index.md 以外のマークダウンファイルはどう扱われるのか?

よく Page Bundles の説明では「配置された index.md 以外のマークダウンファイルは、あくまで Branch Bundles または Leaf Bundle 内のリソースファイルとして扱われる。」と書いてあります。実際のところはどうなのでしょうか?

まず、いくつか前提として...

  • uglyURLs = true は使わない

uglyURLs の設定は推奨もされていないし、記事ファイルに関係する画像ファイルなどの扱いが面倒なことになるので基本 uglyURLs は有効にしません。

  • Permalinks の設定も使わない

またセクションの階層(content以下のフォルダ構造そのまま)をカテゴリー分類として使うので、config.toml での Permalinks の設定も使わないものとします。

するとデフォルトでは Hugo は、例えば content/posts/001/index.md とした記事ファイルはフォルダ構造のまま書き出します。記事のURLもhttp://localhost:1313/posts/001/です。例えば記事のマークダウン文に画像などのリンクがある場合、この記事と同階層に置いた各種の記事関連ファイルはどう扱われるのでしょうか? とくに 記事に別の index.md ではない .mdファイルを関連付けたい場合、どうすればいいのでしょうか?

Page Bundles の仕組み

1. Branch Bundles

まず、Branch Bundles つまりセクションを表す _index.md が置かれた階層と同じフォルダにある .mdファイルはどうなるのか?

これにはさしたる問題は無いようです。例えば sectionA/section-memo.md という .mdファイルは、sectionA/section-memo/index.html という名前でちゃんとレンダリングされたHTMLになります。

2. Leaf Bundles

わかりにくいのはこの Leaf Bundles つまり個々の記事本体となる index.md が置かれたフォルダにある .mdファイルです。

虫のいい考え方だと、index.md と同じようにそれなりにHTMLファイルにレンダリングされて、画像ファイルなどと一緒に index.html のそばに配置されるんだろうと捉えてしまいます。しかしそううまくはいきません。

まず /content/posts 以下に 001 というフォルダを作って、中に index.md を置いて記事を書いてみます。001 というフォルダの中はこうなっていたとします。

/content/posts$ tree -L 3
.
└── 001
    ├── doc.pdf
    ├── img
    │   └── test02.png
    ├── index.md
    ├── memo.md
    ├── movie.mp4
    └── test01.png

index.md のマークダウンにこの中の各ファイルへのリンクを書いたとします。すると次のように PDF、画像、動画の各ファイルへのアクセスはあっさり成功します。

  • PDFファイル -> doc.pdf
  • 画像ファイル -> test01.png, img/test02.png
  • 動画ファイル ー> movie.mp4

特にファイルパスを絶対アドレスにする必要もなく、カレントディレクトリを表す「.」をつけて ./doc.pdf とかしなくても doc.pdf でアクセスできます。内部フォルダがある場合は img/test02.png とします。もちろん「.」をつけて ./movie.mp4./img/test02.png などとしても問題ありません。

しかし memo.md という .mdファイルは index.mdindex.html になるのと同じように memo.html になるのかというとそうならないです。実際 hugoコマンドでビルドしてみると、セクション posts以下は、

/public/posts$ tree -L 3
.
├── 001
│   ├── doc.pdf
│   ├── img
│   │   └── test02.png
│   ├── index.html    <-- メインとなる記事
│   ├── movie.mp4
│   └── test01.png
└── index.html  <-- セクション「posts」のインデックス

となります。/posts/index.html は自動的に書き出される postsセクションの index で、/posts/001/index.html がもとの /posts/001/index.mdレンダリングされたメイン記事です。このとおり memo.html なんてファイルはどこにも書き出されていません。完全無視です。

ではどうやったら memo.mdmemo.html になるのでしょうか?

※ここではファイルの呼び名がいろいろとややこしいので、Leaf Bundles に置いたメインの記事ファイルはindex.mdmemo.md のようなメインの記事から読みたい添付の .mdファイルは、「リーフの.mdファイル」などと呼ぶことにします。

index.md 以外の名前にしてみる

まず記事本体を /posts/001/index.md などという名前を止めて index 以外の名前、例えば main.md にしてみます。uglyURLs の設定がされていなければ、これはデフォルトでは main というフォルダが作られて、そのなかに index.html が書き出されるはず。するとその他のファイルはどうなるか?ビルドしてみます。

/public/posts$ tree -L 3
.
├── 001
│   ├── doc.pdf
│   ├── img
│   │   └── test02.png
│   ├── main
│   │   └── index.html
│   ├── memo
│   │   └── index.html
│   ├── movie.mp4
│   └── test01.png
└── index.html

今度は main.mdmain/index.html になり、同じルールで memo.mdmemo/index.html に書き出されました。このルールがわかっていれば記事ファイルから、リーフの.mdファイルを ../memo/ などというパスでアクセスしても問題は無いように思えます。

しかし元の下書きのファイル構成とは変わってしまうので、かなり理解しにくいですね。

index.md 以外の名前にし、フロントマターのURLパラメータを設定する

では、記事本体に index.md 以外の名前を使い、フロントマターのURLパラメータを設定するとどうなるのでしょうか。新規記事の雛形を書き出す newコマンドで、ファイル名を普通は index.md とするところを 例えば main.md とします。

$ hugo new content/posts/001/main.md

フロントマターの draft: trueコメントアウトするか削除して、URLパラメータを次のように書き換えます。

---
url: "/posts/001/"
---

ビルドしてみると、記事となる index.html は URLパラメータで指定した場所になり、同じ階層に他のファイルもコピーされ、memo.mdmemo/index.html となってレンダリングされます。

$ tree -L 3 ./public/posts/
./public/posts/
├── 001
│   ├── doc.pdf
│   ├── img
│   │   └── test02.png
│   ├── index.html    <-- メインとなる記事
│   ├── memo
│   │   └── index.html    <--  memo.md がレンダリングされたHTML
│   ├── movie.mp4
│   └── test01.png

これならば、下書きの階層をほぼ保ったままで、記事の中から同一階層のファイルにアクセスできるでしょう。

Branch Bundles の仕組みとマークダウンファイルのURLパラメータを併用する

さて 記事ファイルと同じ階層のフォルダに置いたリーフの .mdファイルが、Hugoの仕組みでレンダリングされるようにはなったわけですが、場合によってはフロントマターのURLパラメータに、記事ファイルの移動に影響しない不変のパーマリンクURLを指定したい場合があります。

たとえば /archetypes/default.md に次のように書いておけば、日付をもとにしたパーマリンクURLを指定できます。

[/archetypes/default.md]

---
title: "{{ replace .Name "-" " " | title }}"
thumbnail: ""
description: ""
date: {{ .Date }}
url: "/posts/{{ .Date | dateFormat "2006-01-02T15:04:05" | urlize }}/"
tags: []
draft: true
---

これで記事の雛形を書き出してみます。ただし今度は index.md で

$ hugo new content/posts/001/index.md

この場合はもし index.mdmain.md などと名前を変えると、 memo.md などの添付マークダウンは書き出されるものの、/post/001 以下に記事関連ファイルが書き出され、パーマリンクURLで指定した記事の index.html/posts/パーマリンクURL/index.html となり、またしても別階層に別れてしまうので管理しずらい(リンクが貼りにくい)です。

そこで多少の妥協+考え方を変えてみます。メイン記事のファイル名はあくまで index.md のままで、memo.md などリーフの.mdファイルは /posts/docs/memo.md などといった Branch Bundles の場所に格納し、そのmemo.md などのフロントマターに本体記事のパーマリンクURLに書き出されるようなURLパラメータを指定してみます。ついでにこれらのリーフの.mdファイルがリスト表示されないように _build オプションも加えます。

本体記事(/post/001/index.md)側のフロントマター

---
title: "メイン記事"
thumbnail: ""
description: ""
date: 2022-09-03T19:14:34+09:00
url: "/posts/2022-09-03t191434/"    <- パーマリンクURL指定
tags: []
# draft: true
---

関連マークダウンファイル(/post/docs/memo.md)側のフロントマター

---
title: memo01
url: "/posts/2022-09-03t191434/memo/" <- 記事のパーマリンクURL指定に合わせた指定
_build:
  list: false
---

ビルドしてみると面白いことに、記事のパーマリンクURLから作られたフォルダ階層の中にちゃんと memo/index.html が作られました。これなら記事からのアクセスも簡単です。

$ tree -L 3 ./public/posts/2022-09-03t191434/
./public/posts/2022-09-03t191434/
├── doc.pdf
├── img
│   └── test02.png
├── index.html
├── memo
│   └── index.html
├── movie.mp4
└── test01.png

記事の index.md があるフォルダの上位の所属セクションフォルダ配下に .md を置くので、若干記事との関連がわかりづらいですが、なんでもかんでも static 配下に置くよりマシです。本当は Branch Bundle 直下にフォルダを作るのは Leaf Bundle だけなので、ルール上は「不正なリソース」とみなされるのでしょうが、その場合は memo.md/posts 直下に置けばいいかと思います。ただし/posts 直下に Leaf Bundle が多く関連ファイルも多い場合は管理しにくいでしょうね。「不正なリソース」かもしれませんが、画像などではなくマークダウンファイルです。記事となるマークダウンファイルのファイル名は index.md 以外も許されているわけですし、特にエラーになるわけでもないので気にせず管理しやすい方法を選べばいいのではないでしょうか。

ちなみに、/posts/memo.md のURLパラメータを次のように uglyURL にしてしまえば、フォルダに内包されずに 直接 /posts/2022-09-03t191434/memo.html が書き出されます。このへんは好みの問題でしょうか?

---
url: "/posts/2022-09-03t191434/memo.html"
---

関連マークダウンファイルに貼る画像などの置き場

この場合のリーフの .mdファイルに貼る画像などは、指定したURLに結びつける必要があります。

たとえば 上位階層のセクションフォルダに置いた関連 .md ファイルに、記事ファイルのimgフォルダにあるtest.pngを表示させたいなら、マークダウンの画像のパスは元の記事本体ファイル(index.md)からのパスでないと認識しません。

記事本体index.mdのURLパラメータがurl: "/posts/2022-09-03t191434/"であり、上記のmemo.mdのURLパラメータが

---
url: "/posts/2022-09-03t191434/memo.html"
---

ならば、そのmemo.mdの中にimg/test.pngを表示させるのには、次のようにパスを元記事に合わせます。

![テスト画像](/posts/2022-09-03t191434/img/test.png)

すべてをhugoコマンドでビルドした場合に書き出されるパスに合わせる と考えればわかりやすいでしょう。

リーフの .md ファイルに独自のレイアウトを適用する

このままではリーフの .mdファイルにも、記事と同じ/layouts/_default/single.htmlといったレイアウトが適用されてしまいます。もし不都合なら、独自のレイアウトを作り適用したいです。

そこで.mdファイルのフロントマターでtypelayoutを指定します。

ここではlayout/layouts/special/html-singleという中身だけを表示する単純なレイアウトファイルを適用します。

[/layouts/special/html-single]

<!--Content-->
{{with .Content}}
  {{.}}
{{end}}

結果的に関連ファイルとしての.mdファイルのフロントマターの雛形はこんな感じになります。.mdファイルはフォルダを作らせず直接HTMLファイル名にします。

---
url: /posts/2022-09-03t191434/memo.html
type: special
layout: html-single
_build:
  list: false
---

ちなみに HugoのPage Bundlesでは、生のHTML(拡張子が.html)ファイルを関連ファイルとして配置すると「plain HTML documents not supported」といったエラーがでます。しかしHTMLファイルであっても先頭にフロントマターがあればこのエラーは回避できます。上記のフロントマターを書いて、例えば/post/mdfiles/といった場所にmemo.mdと一緒にdoc.htmlといった感じで配置すれば、本体記事からファイル名だけで関連ファイルとして参照できます。ネットからダウンロードしたページなどを参考にしたりできるようになります。

記事と同じCSSをリーフの .md ファイルにも適用する

.Contentだけのレイアウトでは、記事本文で使っているCSSスタイルは適用されないので、記事と同じスタイルをリーフの .mdファイルにも適用させたかったら、partialで分離した head タグ部分を読み込ませればいいかと思います。ここでは記事のレイアウトと揃えるためにmd-contentなどのクラス名も付けました。

[/layouts/special/html-single]

{{- partial "site/head" . -}}

<!--Content-->
{{with .Content}}
<section id="post-content" class="post-content md-content">
  {{.}}
</section>
{{end}}

もっと簡単な方法もあるが...

実際はこんなめんどうなことをしなくても、例えば VSCode+Markdown Preview Enhancedの環境を作って、VSCodeで書いた .mdファイルをMarkdown Preview Enhancedの機能でHTMLとして書き出せば、なぜか?Hugoの記事があるフォルダ(index.mdのある Leaf Bundles のフォルダ)の中に直接HTMLファイルがあってもHugoから「生のHTMLを置くな!」とか怒られたりしません。フォルダに直接HTMLをコピーすると怒られるのにそのへんの違いが謎ですが、たぶん元になる .mdファイルがある場合は扱いが違うんでしょう。

これだとわざわざ Branch Bundles 下に置かなくても記事から直接参照できるし楽ですが、ただしMarkdown Preview Enhancedの機能でHTMLを書き出すので、変数や関数はもちろんショートコードやスタイルなどHugo側の機能は使えません。ここは目的に応じて使い分ければいいと割り切ります。