このたび Org Mode に入門しました。そしてあれこれいじっているうちにブログも Org Mode で書きたくなりました。公式サイトでは、Hugo や Org2Blog などが紹介されています。Hugo はネイティブに Org Syntax をサポートしているみたいですね。
手段を選考するなかで、せっかく Org Mode に入門したのだから Org から Markdown に変換するような二度手間は避けたいという思いが強くありました。そこで、ネイティブに Org Syntax をサポートしている Hugo が第一候補に挙がりました。(Hugo と Org Mode で検索すると、Hugo 向け Markdown に変換してくれる ox-hugo の情報が散見しますが、そもそも Hugo はネイティブに Org Syntax を解釈するので Hugo のみで完結できるみたいです。)
ですが、広大な Org Mode の実装にブログを公開する術が備わっていないはずがないだろうといろいろ調べてみると、org-publish という文書公開機能があることが分かったのでこれを使うことにしました。
結論から言うと、ほぼほぼ納得のいくものになりましたが、自分が探した限りではカテゴリでフィルタリングするような機能が見つからず、これだけはちょっと JavaScript で工夫することになります。とはいえ JavaScript は 20 行程度なので、掲題の通り「99% Org Mode でブログを書く」ことになります。
プロジェクトの構成例
myblog
├── 2020
├── 2021
└── categories
該当年のディレクトリ(上の例では 2020 や 2021)に記事用の Org ファイルを格納します。
categories ディレクトリは、カテゴリによってフィルタリングされたサイトマップのファイルを格納します。これはカテゴリ用の DB みたいな用途で使っており詳細は後述します。現時点では手作業で入力していますが、いずれ自動化したいです。
基本設定
設定は org-publish-project-alist
に書いていきます。
(setq base-directory-for-myblog "~/path/to/myblog")
(setq publishing-directory-for-myblog "~/export/to/published")
(setq html-head-for-myblog "<link rel=\"stylesheet\" href=\"/myblog.css\" type=\"text/css\"/>
<script src=\"/myblog.js\"></script>")
(setq html-preamble-for-myblog "<h1 id=\"site-title\">[サイトタイトル]</h1>")
(setq org-publish-project-alist
`(("posts"
:base-directory ,base-directory-for-myblog
:publishing-directory ,publishing-directory-for-myblog
:recursive t
:exclude "categories/*"
:publishing-function org-html-publish-to-html
:with-title nil
:auto-sitemap t
:sitemap-filename "index.org"
:sitemap-title "Articles"
:sitemap-sort-files anti-chronologically
:html-head ,html-head-for-myblog
:html-preamble ,html-preamble-for-myblog)
("categories"
:base-directory ,(concat base-directory-for-myblog "/categories")
:publishing-directory ,(concat publishing-directory-for-myblog
"/categories")
:publishing-function org-html-publish-to-html
:with-title nil
:auto-sitemap t
:sitemap-filename "index.org"
:sitemap-title "Categories"
:html-head ,html-head-for-myblog
:html-preamble ,html-preamble-for-myblog)
("myblog"
:components ("posts" "categories"))))
各オプションについては近々加筆しますが、私は GitHub にある公式ミラーのソースと公式マニュアルを参考にしたのでこれらを参照してみると良いと思います。
記事を書く
記事を書く際の最低限のテンプレートは以下です。ファイル名は日付にしています。
#+TITLE: サンプルタイトル
#+DATE: <2021-03-06 Sat 20:01>
* {{{title}}} :category1:category2:category3:
見出しの {{{title}}}
には #+TITLE:
で設定した サンプルタイトル
が反映されます。
カテゴリには Org Mode のタグ機能を使います。見出しでカテゴリをコロンで挟めばタグになります。カーソルが見出しにある状態で C-c C-c
コマンドを使ってミニバッファからタグを入力することもできます。カテゴリが複数ある場合は上記のように、一つの場合は :category1:
と記述します。
カテゴリを登録する
記事でカテゴリを使用したら、カテゴリ用ファイルにカテゴリを登録します。まず categories ディレクトリ配下に「カテゴリ名.org」というファイルを作成します。例えば、記事中で python というカテゴリを使ったら、categories 配下に python.org というファイルを作成します。
category1 というカテゴリが次の三つのファイル 2020/1207.org, 2020/1227.org, 2021/0211.org で使われている場合のカテゴリ用ファイルの記述例は以下です。
#+TITLE category1
- 2020
- [[file:../2020/1207.org][2020/1207.org で使われているタイトル]]
- [[file:../2020/1227.org][2020/1227.org で使われているタイトル]]
- 2021
- [[file:../2021/0211.org][2021/0211.org で使われているタイトル]]
これは、org-publish で自動生成されるサイトマップ用のファイルと同じ書き方です。
新たなカテゴリが追加されるたびに当該カテゴリ用ファイルを作成します。これはちょっと煩雑なので自動化したいのですが、今のところ良いアイデアが浮かびません。Org Mode のみで作られているであろうブログサイトを国内外問わずたくさん見てみたのですが、ほとんどがカテゴリによるフィルタリング機能を実装しておらず、実装していても正しく機能していないものしか見当たりませんでした。参考になるブログサイトがありましたら教えていただけると嬉しいです。とはいえ、当面そんなに大変な作業でもないのでこれでいきます。漏れがないように注意が必要です。
公開用ファイルを生成する
M-x org-publish
を実行します。ミニバッファでプロジェクト名を聞かれるので、上記の設定例であれば myblog を選択することで公開用の HTML ファイル群が publishing-directory
に生成されます。
カテゴリでフィルタリングするための JavaScript ファイルを用意する
publishing-directory
に以下のファイルを用意します。
function filterByCategory(category) {
location.href = '../categories/' + category + '.html';
}
window.addEventListener('load', () => {
let tags = document.getElementsByClassName('tag');
for (let tag of tags) {
for (let child of tag.children) {
child.setAttribute('onclick', `filterByCategory('${child.className}')`);
child.style.cursor = 'pointer';
}
}
});
window.addEventListener('load', () => {
let siteTitle = document.querySelector('#site-title');
siteTitle.setAttribute('onclick', 'location.href = "/"');
siteTitle.style.cursor = 'pointer';
});
これでカテゴリをクリックすると、カテゴリでフィルタリングされたサイトマップが表示されます。
また、フィルタリングとは関係ないですが、サイトのタイトルをクリックするとホームへ戻るようにもなっています。
公開
先述の myblog.js とサイトデザイン用の myblog.css を publishing-directory
に用意して Netlify などへアップすれば完了です。
サイトマップの詳細設定
サイトマップのリンクから拡張子(.html)を取り除く
上記の手順で公開した場合、サイトマップ上のリンクには拡張子 .html が含まれます。Netlify へ公開した場合は拡張子なしの URL でもページを表示できるので、サイトマップ上のリンクからも拡張子を取り除きたい場合があります。
かつては org-publish-project-alist
に :sitemap-sans-extension
という便利なオプションがあったみたいなのですが、これは廃止になったと公式にてアナウンスされていました。よって、公式のガイドに沿って以下の関数を用意します。これを org-publish-project-alist
のオプション :sitemap-format-entry
に設定することで上手くいきます。
(defun my-sitemap-format-entry (entry style project)
(cond ((not (directory-name-p entry))
(format "[[file:%s][%s]]"
(file-name-sans-extension entry)
(org-publish-find-title entry project)))
((eq style 'tree) (file-name-nondirectory (directory-file-name entry)))
(t entry)))
拡張子 .html を省略して運用していく場合は、先ほどの myblog.js も一箇所修正します。
function filterByCategory(category) {
location.href = '../categories/' + category; // 修正箇所
}
サイトマップに日時を表示させる
サイトマップに日時を表示させたい場合は、my-sitemap-format-entry
を以下のようにします。categories のサイトマップ categories/index.org に日時は必要ないので、条件分岐で処理を分けます。
(defun my-sitemap-format-entry (entry style project)
(cond ((not (directory-name-p entry))
(if (equal (car project) "posts")
(format "%s [[file:%s][%s]]"
(format-time-string "%m-%d %H:%M"
(org-publish-find-date entry project))
(file-name-sans-extension entry)
(org-publish-find-title entry project))
(format "[[file:%s][%s]]"
(file-name-sans-extension entry)
(org-publish-find-title entry project))))
((eq style 'tree) (file-name-nondirectory (directory-file-name entry)))
(t entry)))
これに合わせて、各カテゴリ用ファイル「カテゴリ名.org」にも日時を表示させたいので以下のように修正します。
#+TITLE category1
- 2020
- 12-07 09:27 [[file:../2020/1207.org][2020/1207.org で使われているタイトル]]
- 12-27 15:08 [[file:../2020/1227.org][2020/1227.org で使われているタイトル]]
- 2021
- 02-11 11:05 [[file:../2021/0211.org][2021/0211.org で使われているタイトル]]
my-sitemap-format-entry
を反映させた org-publish-project-alist
の設定は以下になります。
(setq org-publish-project-alist
`(("posts"
:base-directory ,base-directory-for-myblog
:publishing-directory ,publishing-directory-for-myblog
:recursive t
:exclude "categories/*"
:publishing-function org-html-publish-to-html
:with-title nil
:auto-sitemap t
:sitemap-filename "index.org"
:sitemap-title "Articles"
:sitemap-format-entry my-sitemap-format-entry ;; 追記箇所
:sitemap-sort-files anti-chronologically
:html-head ,html-head-for-myblog
:html-preamble ,html-preamble-for-myblog)
("categories"
:base-directory ,(concat base-directory-for-myblog "/categories")
:publishing-directory ,(concat publishing-directory-for-myblog
"/categories")
:publishing-function org-html-publish-to-html
:with-title nil
:auto-sitemap t
:sitemap-filename "index.org"
:sitemap-title "Categories"
:sitemap-format-entry my-sitemap-format-entry ;; 追記箇所
:html-head ,html-head-for-myblog
:html-preamble ,html-preamble-for-myblog)
("myblog"
:components ("posts" "categories"))))