m13o

2019-01-05 Sat 20:18
org-modeのProjectをNetlify上でhtmlにpublishするEmacs

org文書で書かれている技術メモなどを静的サイトとしてまとめておくのに良い処ないかなと思って色々探していたら, NetlifyがそのDocker imageにEmacsを同梱するようになった記録をみつけたのが去年の夏頃. それから自分のorg文書を整理してちゃんとまとめるぞと気付いたら平成も31年(でいいのか)になっていました. お正月休みもそろそろ終わりそうなので, いい加減とっかかっていきたいという気持ちと共にこれを書きます.

リポジトリ構成

NetlifyでDeployするWeb siteの構成ですが, 以下のような形にしています.

root
├── .gitignore
├── assets/
│   └── style.css
├── htmlize.el
├── publish-site.el
├── org-plus-contrib/
├── posts/
└── publish/

各ファイル, ディレクトリの役割は以下の通りです.

assets
画像やcss, jsなどのアセット類を置くディレクトリ
style.css
カスタムスタイルシート
htmlize.el
org-publishでhtmlを出力するために必要
publish-site.el
publishする設定が記述されたEmacs Lisp
org-plus-contrib
9.0以降のorg-modeが必要だったのでelpaから持ってきたorg-mode一式
posts
Deployするorg文書を置くディレクトリ
publish
htmlが出力されるディレクトリ. このディレクトリそのものはpublish時に生成されるので, .gitignoreに登録して除外対象にする.

Netlifyに同梱されているEmacsはこの記事を執筆している現在は25.3.1です. そのEmacsに同梱されているorg-modeのバージョンは8.2.10で少々古いのでバージョン9以降のorg-modeを使いたいのですが, 色々面倒なのでlist-packagesでelpaから落としてきたorg-plus-contribを利用する事にしました.

また, htmlにエクスポートするためにhtmlize.elが必要なのでそれも利用します.

Netlify上でのDeploy

まず, NetlifyのBuild & Deployの設定に上リポジトリを登録します そしてEmacsをバッチモードで起動しpublish-site.elをロード, その中に定義されているpublish-siteという関数を呼び出す事でpublishされるようにしている(後述)ので, Build commandには以下の設定をします.

emacs --batch -Q -l ./publish-site.el -f publish-site

org-publishした結果がpublishディレクトリにDeployされるので, Publish directoryはpublishを設定します.

これで, リポジトリに更新があれば自動的にDelpoyされるようになります.

publish-site.el

Deploy用関数

Deploy用のpublish-site関数は以下のようにorg-publish-allを呼び出しているだけです.

(defun publish-site ()
  "Publish org file to html."
  (org-publish-all t nil))

load-pathとrequire

まず, orgとhtmlizeをloadし, requireして使えるようにします.

(setq load-path (append `(,(expand-file-name "./org-plus-contrib")) load-path))
(require 'org nil t)
(require 'ox nil t)

(load-file "./htmlize.el")
(require 'htmlize nil t)

preamble

html-preambleはpublish時にorg文書のコンテンツの前に挿入される文字列です. 私の場合はここにサイトのタイトル表示を入れています.

(defvar my-html-preamble
  "<h1 class='title'>SITE TITLE</h1>")

上記変数をorg-publish-project-alistの:html-preambleに設定します.

postamble

html-postambleはpreambleとは逆に, コンテンツの後に挿入される文字列です. 私の場合はここにサイトのフッターを入れています.

(defvar my-html-postamble
  "<footer>
<hr>
<div class='footer'>
<p>&copy; 2019 m13o.</p>
<p class=\"creator\">created by %c</p>
</div>
</footer>")

デフォルトのpostambleでも別に問題はないのですが, 情報過多かなと思ったので, 上記のようにしています.

html-head-extra

カスタムのcssやGoogle Fontを使いたかったので, html-head-extraには以下のような設定を入れるようにしました.

(defvar my-html-head-extra
  "<link rel='stylesheet' type='text/css' href='style.css'>
<link href='https://fonts.googleapis.com/css?family=Noto+Sans+JP&amp;subset=japanese' rel='stylesheet'>
<style>body{font-family: 'Noto Sans JP', sans-serif;} </style>")

上記の一行目, style.cssは私が記述したcssでassetsディレクトリに設置しています. cssを置いてあるディレクトリはassetsですが, publish時にはpublishディレクトリに設置されるので, hrefのパスは"style.css"のみです.

sitemap

org-publishはpublishしたorg文書へのリンクを1つのorg文書に自動でまとめる機能があります. 今回はその機能をindex.htmlの作成に利用しています. ただ, デフォルトだとorg文書のtitleをリストアップするだけなので, そこに日時を付加するように変更したいと思い, sitemap-format-entryのカスタム関数を作成しました.

(defun blog-sitemap-format-entry (entry style project)
  (cond ((not (directory-name-p entry))
         (format "%s [[file:%s][%s]]"
                 (format-time-string "%Y-%m-%d (%a) %H:%M - " (org-publish-find-date entry project))
                 entry
                 (org-publish-find-title entry project)))
        ((eq style 'tree)
         (file-name-nondirectory (directory-file-name entry)))
        (t entry)))

この関数の引数 entry はpublishされた文書へのファイルパス, style はsitemapをどのように表示するかのシンボル, projectは publish対象のプロジェクトの各構成要素が詰め込まれたリストです.

org-publish-project-alist

上記を踏まえたorg-publish-project-alistの設定は以下のようにしました.

(custom-set-variables
  '(org-publish-project-alist
    `(("assets"
       :base-directory "./assets"
       :base-extension "css\\|jpg\\|jpeg\\|png\\|gif\\|pdf\\|txt"
       :publishing-directory "./publish"
       :publishing-function org-publish-attachment
       :recursive t
       :author nil)
      ("posts"
       :base-directory "./posts"
       :base-extension "org"
       :html-preamble ,my-html-preamble
       :html-postamble ,my-html-postamble
       :html-viewport nil
       :publishing-directory "./publish"
       :publishing-function org-html-publish-to-html
       :language "ja"
       :recursive t
       :auto-sitemap t
       :sitemap-format-entry blog-sitemap-format-entry
       :sitemap-sort-files anti-chronologically
       :htmlized-source t
       :sitemap-filename "index.org"
       :sitemap-title "m13o.net"
       :html-head-extra ,my-html-head-extra
       :with-author nil
       :with-creator nil
       :with-email nil
       :with-date t
       :with-timestamps nil
       :with-toc nil
       :with-title nil
       :section-numbers nil
       )
      ("website"
       :components ("assets" "posts")))))

assetsは静的要素をpublishするための項目で, org-publish-attachmentでpublishされるようにします.

postsはorg文書をhtmlにpublishするための設定で, ここにpreambleやpostamble, html-head-extraやsitemapの設定を記述します.

:sitemap-sort-filesはsitemap内で並べる文書の順番をどうするかを設定する項目で, ここでは時系列順に新しいものから並ぶようにしています.

:section-numbersがnilでない場合は各ヘッドラインに番号が付いてしまい邪魔なのでnilを設定しています.

その他の設定

その他, バックアップと取らないようにしたり, htmlのdoctypeを設定したりと雑多な設定も施しておきます.

(setq make-backup-files nil)
(setq auto-save-default nil)
(setq org-html-doctype "html5")
(setq org-html-htmlize-output-type 'css)

org文書のテンプレート

#+TITLE: 
#+DATE: <YYYY-mm-dd HH:MM>

* {{{date}}} {{{title}}} :TAG:

org文書に#+TITLE:は設定しますが, org-publish-project-alistのwith-titleはnilにしています. これは, #+TITLE:で設定したタイトルにはタグを付ける事ができないためです. こうすると, htmlのtitle要素には設定したタイトルが記載されますが, h1要素としてのタイトルは出力されません. そのため, トップレベルのヘッドラインにtitleマクロを記述しpublish時にタイトルが記載されるように設定しています.

タイトルの後に#+DATE:を設定していますが, これはorg-publishとNetlifyの仕様によるもので, これを記載しないまま自動で付与される日時を利用すると, ファイルを最後に変更した時の日時が記載されますが, リポジトリから落としてくる際, ファイルが生成される日時が記載されてしまい, 場合によっては全てのファイルがほぼ同じ日時となってしまいます. そのため, #+DATE:でorg文書の日時を明示的に記載する事でそのような事態を防いでいます. また, dateマクロもトップレベルヘッドラインに記述しているのは, 単純に日時を記載したかったからです. 行く行くはタグと共にリンクをまとめていければと考えています.

org文書のファイル名はDATEと同じ日時にするようにしており, これを手動で行うのは大変面倒なので, org-captureで諸々自動生成できるようにしています.

今後

cssはデフォルトではなくカスタマイズしていきたいけど, 最後にcssを書いたのが5年以上前の事で完全に時代から取り残されているのでお勉強しながら追い追い整えていこうかなと考えている処なので, レイアウトは定まるまでそこそこ時間がかかりそうですが, 記事をDeployする毎に良い感じに調整していこうと思います. またrssへの対応やタグ, 日時毎のsitemapの自動生成も行っていきたいと考えています. 少くともcssについては, org-publishが掃き出すhtmlを元にclassやidなどを調査した上で各種styleを設定し, 形になったら別記事として投稿しようと思います.