ブログ更新の前にHugoとEmacs (ox-hugo) を直すことになった

しばらくぶりにブログを更新する。

とはいっても、ブログを更新しようとしたら、ブログ周りのソフトウェアで幾つか問題が見つかり、それに対処していたので、本来書こうとしたことは書けなかった。本末転倒という気もするが、コンテンツを提供するためのソフトウェアを開発する人間としては、こういった問題に対応するのが本質とも思う。

このブログは手元のOrgファイルを原稿としてHugoで生成し、パブリッククラウド上で公開している。原稿を手元のドキュメントと一緒に管理でき、ブログの見た目以外にも機能を自分で変更できる。一箇所にドキュメントを集められるこのスタイルは気に入っている。

このスタイルのデメリットは各ソフトウェアのバージョンアップといったメンテナンスを自分でやらなければならない点だ。問題が起こると、とにかく時間を喰われる。AIエージェントのおかげでトラブルシューティングがし易くなった気もするが、それはそれで時間を喰われる。

今回、遭遇した問題は以下の通り。

No. 問題 原因と対処
1 HugoでOrgを読み込めない v0.166.0のセキュリティポリシー変更
2 ox-hugoでMarkdownを出力できない 古いバイトコンパイル結果が原因と考えられるため再コンパイル
3 org-idリンクを解決できない org-id-extra-files に対象ファイルを追加

ブログ環境のおさらい

本題に入る前に、このブログの環境面について触れたい。

ブログは静的サイトジェネレーターHugo で出力する。ブログの記事はEmacsのOrg mode で書きOrgファイル(拡張子 .org )に保存する。ox-hugo でHugo向けのMarkdownファイルに変換し、HugoでHTMLを生成する。

ローカル

  • macOS v27.2
  • Emacs v30.2 (リリースノート )
  • Org mode v9.7.11 (リリースノート )
    • v9.7.11のリリースノートには辿り着けなかったので、v9.7系のリリースノートのリンクを書いた。
  • ox-hugo v0.12.1 (リリースノート )
    • MELPAからインストールした ox-hugo-20240305.1923 を使用中。
  • Hugo v0.169.0 (リリースノート )
    • hugo v0.169.0+extended+withdeploy darwin/arm64 BuildDate=2026-10-27T14:52:20Z VendorInfo=Homebrew

サーバー

  • Cloudflare Pages
  • Hugo v0.164.0

問題1 HugoがOrgファイルを読み込めない

HugoはMarkdownファイルのほかにOrgファイルを読み、HTMLに変換する機能がある。私はその機能が好きなのだが、Hugo v0.166.0(リリースノート) でセキュリティポリシーの変更が入り、標準動作ではOrgファイルを読み込めない。

config/security: Deny text/org content by default e6abb9c @bep

修正が入ったコミット を確認すると、変更理由はOrgエクスポートブロックとインラインHTMLを出力するスニペット によって、エスケープされていない生のHTMLがHugoに渡りXSS攻撃の標的となるからとのことだ。

実際に手元のHugoを確認するとエラーが発生して静的サイトの生成に失敗する。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
% hugo server -D
Watching for changes in <省略>
Watching for config changes in <省略>
Start building sites …
hugo v0.169.0+extended+withdeploy darwin/arm64 BuildDate=2026-10-27T14:52:20Z VendorInfo=Homebrew

Built in 35 ms
ERROR error building site: assemble: failed to create page from pageMetaSource <省略>: "<省略>.org:1:1": access denied: "text/org" is not whitelisted in policy "security.allowContent"; the current security configuration is:

[security]
  allowContent = ['! ^text/html$', '! ^text/org$']
  enableInlineShortcodes = false

  <以下、省略>

対策として config.toml にセキュリティポリシー設定を追加する方法がある。ただし「信頼できるOrgファイルのみを扱う場合に限る」と強調しておきたい。

1
2
[security]
  allowContent = ['! ^text/html$']

この設定ではtext/htmlの拒否を維持し、text/orgを許可する。これでOrg形式の許可は従前のHugoのセキュリティポリシーのままとなる。Orgファイルに生のHTMLを出力する begin_export html などがあると、生のHTMLが出力される。もちろん、エスケープされていない生のHTMLには一定の危険性があることはご承知の通り。

私はHugoに数点のOrgファイルしか置いていないので、そのうち標準のセキュリティポリシーに戻すだろう。

問題2 ox-hugoでMarkdownファイルを出力できない

原稿を一箇所に集約するため、ある時からHugoに記事ごとのOrgファイルを直接置く方式をやめ、一つのOrgファイルに複数の記事をまとめ、ox-hugoで出力する方式に移した。

久しぶりに ox-hugo を使ったら正常に動作しないことに気がついた。

気がついたきっかけ

Org modeでブログ記事をある程度書き進めたので、Hugoでプレビューしようと org-hugo-export-wim-to-md を実行したら「ポイント(カーソル位置)がHugoの記事として有効なサブツリー内にない」とメッセージが出た。

1
Point is not in a valid Hugo post subtree; move to one and try again

実際にサブツリーはあるので、以下の観点で問題の切り分けをした。

  • org-hugo-export-wim-to-md はどのLispのどの定義を使っているのか。
  • ox-hugoがOrgエントリのプロパティを認識しているか。
  • ox-hugoがOrgエントリを有効なサブツリーと認識しているか。

そこで当該Orgエントリにカーソルを置いて、以下のLispを M-: で実行した。

1
2
3
4
5
6
7
(list
 :org-version (org-version)
 :ox-hugo (symbol-file 'org-hugo-export-wim-to-md)
 :export-name (org-entry-get nil "EXPORT_FILE_NAME" t)
 :valid-subtree
 (save-excursion
   (not (null (org-hugo--get-valid-subtree)))))

実行結果は以下の通りで、 org-hugo-export-wim-to-md はバイトコンパイル されたox-hugo.elcから読み込まれていた。プロパティ取得とサブツリー判定も成功した。

1
2
3
4
(:org-version "9.7.11"
 :ox-hugo "/Users/hiroakit/.emacs.d/elpa/ox-hugo-20240305.1923/ox-hugo.elc"
 :export-name "index"
 :valid-subtree t)

org-hugo–get-valid-subtree はox-hugoが提供する関数だ。この関数は EXPORT_FILE_NAME プロパティを持つ記事のサブツリーを探し、見つかればそのOrg要素を返す。なければnilを返す。

1
2
3
Return the Org element for a valid Hugo post subtree.
The condition to check validity is that the EXPORT_FILE_NAME
property is defined for the subtree element.

Orgエントリが有効なら、どうしてHugoに出力されないのか。

Point is not in a valid Hugo post subtree の原因は特定できなかったため、簡単な記事でも正常に出力できるかを調べることにした。

追加の調査

今度は手元の init.el に設定不備がある可能性を疑い、コマンドラインでEmacsを -Q オプション付きで指定のLispを実行することにした。以下の内容を ox-hugo-test.el として保存する。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
(setq user-emacs-directory "~/.emacs.d/")
(package-initialize)

(require 'cl-lib)
(require 'ox-hugo)
(setq debug-on-error t)

(princ (format "Org=%s; ox-hugo=%s\n"
               (org-version) (symbol-file 'org-hugo-export-wim-to-md)))

(let* ((root (make-temp-file "ox-hugo-test-" t))
       (source (expand-file-name "article.org" root))
       (site (expand-file-name "site" root)))
  (make-directory (expand-file-name "content" site) t)
  (find-file source)

  ;; 簡単なOrgエントリの作成
  (insert (format "#+HUGO_BASE_DIR: %s\n#+HUGO_SECTION: posts\n* Test article\n:PROPERTIES:\n:EXPORT_FILE_NAME: ox-hugo-check\n:EXPORT_DATE: 2026-11-10\n:END:\nExpected body.\n** Child heading\nChild body.\n" site))

  (goto-char (point-min))
  (re-search-forward "^Expected body")

  ;; このテストではorg-idのIDインデックス更新を考慮しない。
  ;;
  ;; なお、org-hugo-export-wim-to-mdはorg-id-locationsがnilもしくは空のハッシュテーブルなら自動生成を図る。
  ;; https://github.com/kaushalmodi/ox-hugo/blob/v0.12.1/ox-hugo.el#L4790-L4793
  (let ((org-id-locations nil))
    (cl-letf (((symbol-function 'org-id-update-id-locations) #'ignore))
      (org-hugo-export-wim-to-md)))
  (let ((out (expand-file-name "content/posts/ox-hugo-check.md" site)))
    (unless (file-exists-p out) (error "[ERROR] org-hugo-export-wim-to-md -- Markdown file was not produced: %s" out))
    (with-temp-buffer
      (insert-file-contents out)
      (unless (and (search-forward "Expected body." nil t)
                   (search-forward "Child body." nil t))
        (error "[ERROR] org-hugo-export-wim-to-md -- generated Markdown is missing expected body text")))
    (princ (format "[SUCCESS] org-hugo-export-wim-to-md -- direct export produced Markdown: %s\n" out))))

このox-hugo-test.elを実行する。

1
2
3
/Applications/Emacs.app/Contents/MacOS/Emacs \
  --batch -Q \
  -l ox-hugo-test.el

実行するとエラーになった。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
[ox-hugo] Exporting ‘[org-element-deferred org-element-property-2 (:raw-value) nil]’ ..

Debugger entered--Lisp error: (error "[ERROR] org-hugo-export-wim-to-md -- generated Markdown is missing expected body text")
  error("[ERROR] org-hugo-export-wim-to-md -- generated Markdown is missing expected body text")

  <中略>

Org=9.7.11; ox-hugo=/Users/hiroakit/.emacs.d/elpa/ox-hugo-20240305.1923/ox-hugo.elc

Error: error ("[ERROR] org-hugo-export-wim-to-md -- generated Markdown is missing expected body text")
  error("[ERROR] org-hugo-export-wim-to-md -- generated Markdown is missing expected body text")

<以下、省略>

どうやら、Orgエントリの本文がMarkdownファイルに含まれていない。Orgエントリの見出しではなく [ox-hugo] Exporting ‘[org-element-deferred org-element-property-2 (:raw-value) nil]’ となっており、内部のデータが表示されているように見える。

そのため、遅延解決される前の値が表示されているのではないかという仮説が浮かんだ。もしかするとバイトコンパイルでox-hugo.elcにOrg modeのインライン定義が埋め込まれ、ox-hugo.elcが現在のOrg modeの意図した通りに値を遅延解決しないのかもしれない。

そこで次を ox-hugo-source-test.el として保存し、素のox-hugo.elを読み込み同じテストを実行することにした。

1
2
3
4
(setq user-emacs-directory "~/.emacs.d/")
(package-initialize)
(load-file (expand-file-name "elpa/ox-hugo-20240305.1923/ox-hugo.el" user-emacs-directory))
(load "/path/to/your/ox-hugo-test.el" nil t)

実行する。

1
2
3
/Applications/Emacs.app/Contents/MacOS/Emacs \
  --batch -Q \
  -l ox-hugo-source-test.el

ox-hugo.elをload-fileするとox-hugoがOrgエントリの本文をMarkdownファイルに出力した。

1
2
3
[ox-hugo] Exporting ‘Test article’ ..
Org=9.7.11; ox-hugo=/Users/hiroakit/.emacs.d/elpa/ox-hugo-20240305.1923/ox-hugo.el
[SUCCESS] org-hugo-export-wim-to-md -- direct export produced Markdown: /var/folders/zs/f7xjhm_157959t0chxsmzz900000gn/T/ox-hugo-test-LlzMtW/site/content/posts/ox-hugo-check.md

このため、現在のOrg modeを読み込んだ環境で再コンパイルすれば改善すると考えた。

ふと思えば、以前はEmacs v29.3を使っており、そのEmacsにはOrg v9.6系が同梱されていたと思う。ox-hugoのバイトコンパイルはそのときにしたので、Emacs v30に移行したときにバイトコンパイルをし直してなかったのかもしれない。

ox-hugoの再バイトコンパイル

前述の状況からox-hugoに関するバイトコンパイルをやり直すことにした。

通常の M-x byte-recompile-directory は、ソースが更新されていなければ既存の.elcを再コンパイルしない。そこで第3引数にtを指定して強制的に再コンパイルする。

そのためのLispは以下で、これを M-: で実行する。

1
2
3
4
5
6
(progn
  (require 'org)
  (require 'ox)
  (byte-recompile-directory
   (expand-file-name "~/.emacs.d/elpa/ox-hugo-20240305.1923/")
   0 t))

再バイトコンパイルが終わったら、シェルで次のコマンドを実行する。

1
2
3
/Applications/Emacs.app/Contents/MacOS/Emacs \
  --batch -Q \
  -l ox-hugo-test.el

ox-hugo-test.el が成功している。

1
2
3
4
[ox-hugo] Exporting ‘Test article’ ..
Org=9.7.11; ox-hugo=/Users/hiroakit/.emacs.d/elpa/ox-hugo-20240305.1923/ox-hugo.elc
[SUCCESS] org-hugo-export-wim-to-md -- direct export produced Markdown:
/var/folders/zs/f7xjhm_157959t0chxsmzz900000gn/T/ox-hugo-test-4XepdV/site/content/posts/ox-hugo-check.md

これで ox-hugo からMarkdownファイルを出力できるようになった。

ただ、まだ問題は残っていた。

余談

org-hugo-export-wim-to-md の wim はWhat I Meanのこと。

https://ox-hugo.scripter.co/doc/usage#export-bindings

Export “What I Mean”. This is same as calling the org-hugo-export-wim-to-md function interactively or via (org-hugo-export-wim-to-md) in Emacs Lisp.

問題3 org-idのリンク解決ができない

Emacsに戻り org-hugo-export-wim-to-md を実行すると *Messages* バッファに次のメッセージが出力された。

1
2
3
[ox-hugo] Exporting ‘私のブログ記事’ ..
6 files scanned, 5 files contains IDs, and 156 IDs found.
if: Unable to resolve link; aborting: "FBC92631-D6DE-41D9-B8FF-1DEA6BA5CCF8"

メッセージを見て、OrgエントリのプロパティのIDのことと想像したが、具体的にどのリンクが壊れているのかはこれだけではわからなかった。

IDをキーにdeadgrepで検索したところ、 リンク先のファイルは存在していたが、Org IDの検索対象に含まれていなかった。そこで init.el に org-id-extra-files の検索対象の設定を追加した。

1
2
3
4
5
6
(with-eval-after-load 'org-id
  (when (symbolp org-id-extra-files)
    (setq org-id-extra-files (symbol-value org-id-extra-files)))
  (dolist (file '("~/path/to/your/file-a.org"
                  "~/path/to/your/file-b.org"))
    (add-to-list 'org-id-extra-files (expand-file-name file))))

Emacsを再起動して(init.elを再読み込みができていればいいと思う)設定を反映し、 M-x org-id-update-id-locations でorg-idのID索引を更新した。その結果、 org-hugo-export-wim-to-md を実行できた。

振り返ると、前述のfile-a.orgとfile-b.orgはOrgアジェンダには表示しないので org-agenda-files から除外したような気がする。

教訓

  • Emacs Lispのパッケージで不可解な挙動が発生したら、バイトコンパイル済みコードとソースコードの動作を比較する。
  • EmacsやOrg modeの更新後、依存するパッケージに不可解な挙動が生じたら、再バイトコンパイルを試す。
  • org-agenda-filesに含めないがorg-idを持つOrgファイルはorg-id-extra-filesに登録する。