Emacs
HTML
org-mode
org-publish
EmacsDay 14

org-publishを使ってorgでブログを書く話

はじめに

この記事は Emacs Advent Calendar 2018 - Qiita の14日目の記事です。
前日はtakuchalleさんの「Emacs で Dart 開発環境をセットアップ」でした。

モチベーション

私はOrg-Modeで日常のほとんどすべての文書作成を行っており、そもそも私は「Org-Modeを使うためにEmacs入門した」ほど大きな存在です。

最初にOrg-Modeの公式サイトで紹介されている論文から画像を引用します。

org-mode-thesis.png

この画像の通り、Org-Modeはそれ自身が高性能なマークアップ言語でありながら、任意の言語のコード実行、さらには表計算!などを用いて文書を作成し、それを任意のフォーマットへのアウトプットが可能で、すべての文書のハブともいうべき存在になっています。これを万能文書と呼ぶことにします。(21フォーマットのエクスポーターが標準添付(contrib)され、MELPAにはさらに29種ものエクスポーターが登録されています)

例えば現代においてばHTMLをタグ直書きで文書を書いている人はほとんどいないと思います。なぜならHTMLタグは人間が書くには冗長すぎる他(閉じタグなど)、本質的でないメタ情報が文書中に散らばり(head, id, class)、文書の再利用性が極端に低くなるからです。

よって賢明な方ならばPHPのようなテンプレート言語で生成したり、マークダウンからpandocで変換したりすると思います。これによりメタ情報部と文書部が分離でき、再利用性が上がり、保守性も向上します。

つまりHTMLとかいう冗長で扱いづらいフォーマットは動的に生成されるべきターゲットであって、人間はもっと書きやすい別のフォーマットで文書を作成するべきなのです。

一方、マークダウンには根強いファンがあり、そしてpandocにより万能文書としての機能を備えてきたように思えますが、いろいろ苦悩しているようです。

Orgユーザーからすれば、なぜそんな茨の道を進もうとしているのか理解できません。
Org文書で作成しさえすれば豊富なターゲットフォーマット、つまりWebには静的HTMLやHugo, Wordpress, Qiitaにより公開し、印刷にはLaTeXから生成したPDF、スライドにはブラウザで表示するReveal.jsLaTeX+Beamerで生成したスライド投影用のPDFを使用でき、編集時にもOrg-Modeが提供するOrg文書のための様々なコマンドが利用できます。
こんな悲しい事態にはならなくてすむのです。 (この講義、今期末まであるの辛い。。)


さて、あるOrg文書を変換するのはとても簡単で、例えばこのOrg文書からこのPDFファイルをコマンド1発で変換できます。(内容はやっつけなので(ry…)

上の文書には数式や表が含まれていませんが、私は電気系の学生として毎期の実験レポートをOrg文書から生成したLaTeXから変換したPDFですべてのレポートを提出しましたし、卒論を書いた人作成入門を書かれている方もいます。

一方Org文書が複数に渡る場合、(最終的に1つのファイルにするならinclude文を使えばよいだけなのですが、それぞれを違うファイルとして変換したい場合)Org文書同士のリンクやOrg文書へのリンクを張るメタ的なOrg文書を生成したくなったりと途端に難しくなります。

この操作を実現するのがorg-publishです。ただ、設定に癖があるのでその設定を紹介するとともに、もっとOrg-Modeのユーザーが増えたらいいなと布教をするものです。

マニュアル

日本語マニュアルは少々古いですがここ、英語のマニュアルはここにおいてあります。

なにがどうなるのか

<root>/src/ 下に自由にフォルダやOrg文書を作成して配置します。そしてPublishコマンドを発行することにより <root>/src/ 下のすべてのOrg文書を「その階層を保存した上で」 <root>/archives/ にHTMLで書き出すことが出来ます。

~/public_html/orglyth/
   ├── index.inc
   ├── index.org
   ├── archives/
   ├── parts/
   │   └── index.html
   └── src/
       ├── index.inc
       └── index.org
       ├── elisp/
       │   ├── index.inc
       │   ├── index.org
       │   ├── boolean.org
       │   └── with-temp-file.org
       └── emacs/
           ├── index.inc
           ├── index.org
           ├── install.org
           └── org-mode
               ├── index.inc
               ├── index.org
               ├── basis-syntax.org
               ├── org-list.html
               ├── org-list.org
               └── ox-publish.org

org-publishを使用する第一の利点としてsitemapを自動生成させることが出来ます。

別途 <root> に置いた .htaccess の設定をすれば、例えば(https://conao3.com/archives/blog/advent/emacs/org-publish-blog)にアクセスすると普通にHTMLを閲覧させてarchivesの部分をsrcに変更した(https://conao3.com/src/blog/advent/emacs/org-publish-blog)にアクセスするとソースのOrg文書を見せることも出来ます。

AddDefaultCharset utf-8
AddType "text/plain" org

ErrorDocument 503 /archives/error/maintenance.html
ErrorDocument 404 /archives/error/404.html
DirectoryIndex index.html index.org

SetEnvIf REDIRECT_HTTPS (.*) HTTPS=$1 
<IfModule mod_rewrite.c>
RewriteEngine on 
RewriteCond %{ENV:HTTPS} !on 
RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L] 
</IfModule>

# temporary-maintenance
# RewriteCond %{REQUEST_URI} !=/archives/error/maintenance.html
# RewriteRule ^.*$ - [R=503,L]


RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME}\.org -f
RewriteRule ^(/src/.*)$ $1.org [L]

RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME}\.html -f
RewriteRule ^(/archives/.*)$ $1.html [L]

archivesrc に変えてソースのOrg文書を見せることはすでに出来ているのですが、例えば archivepdf に変えたら記事のPDFをダウンロードさせることも簡単にできると思います。

もちろん文書間リンクや画像リンクなどはいつもどおり書いたものをorg-publishがWebで表示できるように、よしなに変更してくれます。具体的には .org へのリンクは .html へのリンクに変更され、メディアはそのままターゲットディレクトリの対応するパスにコピーします。(orglyth.elでは orglyth-html-default-resources-option で制御してます)

init.elの設定

私は今init.elの大改革(.emacs.dの構造から見直したりuse-packageの別実装を作ったり(Qiita記事は21日目に予約済み)、新しいテストフレームワークを作ったり)を行っていて、init.elの編集まで作業が追いついていないのですがとりあえずOrg-Modeに関する今の設定は以下です。

新しいパッケージマネージャ(feather.el)の開発が終わり次第、シームレスに設定できるようになりますが、現状では伝統的なパッケージ管理のように site-lisp にレポジトリをクローンして load-path を通す必要があります。

  • leaf.el - Emacs-22から利用できる use-package の別実装
  • feather.el(開発中) - Emacs-22から利用できる全く新しいパッケージマネージャ
  • orglyth.el - ox-*の設定集
(leaf org
  :init
  (leaf org-plus-contrib :ensure t :require nil)

  :setq
  ((org-directory                         . "~/Documents/org/")
   (org-default-notes-file                . "~/Documents/org/notes.org")
   (org-agenda-files                      . "~/Documents/org/notes.org")
   (org-return-follows-link               . t)
   (org-startup-indented                  . t)
   (org-indent-mode-turns-on-hiding-stars . t)
   (org-indent-indentation-per-level      . 2)
   (org-src-window-setup                  . 'other-window)
   (org-use-sub-superscripts              . '{})
   (org-image-actual-width                . nil)
   (org-highlight-latex-and-related       . '(latex script entities)))

  :config  
  (leaf ob
    :setq ((org-confirm-babel-evaluate . nil))
    :config
    (leaf ob-ipython
      :when (executable-find "jupyter")
      :ensure t
      :config
      ;; depend of jypyter, ipython
      (add-hook 'org-babel-after-execute-hook 'org-display-inline-images 'append))

    (leaf ob-plantuml
      :when (executable-find "plantuml")
      :ensure t
      :setq ((org-plantuml-jar-path . plantuml-jar-path))
      :setq ((org-confirm-babel-evaluate . nil)))

    (org-babel-do-load-languages 'org-babel-load-languages
                                 '((ipython . t)
                                   (plantuml . t)
                                   (org . t)
                                   (R . t)
                                   (C . t)
                                   (emacs-lisp . t))))
  (leaf ox
    :config
    (leaf orglyth
      :config
      (leaf orglyth-html
        :setq
        ((orglyth-html-enable-option    . t)
         (orglyth-html-use-ftp          . nil)
         (orglyth-html-local-root-path  . "~/Documents/sakura/orglyth/")
         (orglyth-html-remote-root-path . "~/Documents/sakura/remote/")
         (orglyth-html-ftp-root-path    . "/ftp:conao3@conao3.com:~/www/orglyth/"))
        :config
        (orglyth-html-init)
        (orglyth-html-project-init))

      (leaf orglyth-latex
        :setq ((orglyth-latex-enable-option . t))
        :config
        (orglyth-latex-init)))))

この設定は拙作のleaf.elにより以下に変換されます。

(progn
  (progn
    (progn
      (leaf-backend/:ensure-package 'org-plus-contrib 'org-plus-contrib)
      (progn)))
  (progn
    (require 'org)
    (setq org-directory "~/Documents/org/")
    (setq org-default-notes-file "~/Documents/org/notes.org")
    (setq org-agenda-files "~/Documents/org/notes.org")
    (setq org-return-follows-link t)
    (setq org-startup-indented t)
    (setq org-indent-mode-turns-on-hiding-stars t)
    (setq org-indent-indentation-per-level 2)
    (setq org-src-window-setup 'other-window)
    (setq org-use-sub-superscripts '{})
    (setq org-image-actual-width nil)
    (setq org-highlight-latex-and-related
          '(latex script entities))
    (progn
      (require 'ob)
      (setq org-confirm-babel-evaluate nil)
      (if
          (executable-find "jupyter")
          (progn
            (leaf-backend/:ensure-package 'ob-ipython 'ob-ipython)
            (progn
              (require 'ob-ipython)
              (add-hook 'org-babel-after-execute-hook 'org-display-inline-images 'append))))
      (if
          (executable-find "plantuml")
          (progn
            (leaf-backend/:ensure-package 'ob-plantuml 'ob-plantuml)
            (progn
              (require 'ob-plantuml)
              (setq org-plantuml-jar-path plantuml-jar-path)
              (setq org-confirm-babel-evaluate nil))))
      (org-babel-do-load-languages 'org-babel-load-languages
                                   '((ipython . t)
                                     (plantuml . t)
                                     (org . t)
                                     (R . t)
                                     (C . t)
                                     (emacs-lisp . t))))
    (progn
      (require 'ox)
      (progn
        (require 'orglyth)
        (progn
          (require 'orglyth-html)
          (setq orglyth-html-enable-option t)
          (setq orglyth-html-use-ftp nil)
          (setq orglyth-html-local-root-path "~/Documents/sakura/orglyth/")
          (setq orglyth-html-remote-root-path "~/Documents/sakura/remote/")
          (setq orglyth-html-ftp-root-path "/ftp:conao3@conao3.com:~/www/orglyth/")
          (orglyth-html-init)
          (orglyth-html-project-init))
        (progn
          (require 'orglyth-latex)
          (setq orglyth-latex-enable-option t)
          (orglyth-latex-init))))))

Orgの命名規則として ob-* はbabel(任意コード実行)などコードブロック関連、ox-* はエクスポート関連のパッケージとなっています。

ox ブロックは実験レポートを日常的に書いていたこともあり、いろいろとカオスだったのですがorglyth.elというパッケージに括りだすことでinit.elの見通しを良くすることが出来ました。

orglyth.elの設定部を抜き出すと以下になります。このように少しの変数のみを定義するのみです。後はorglyth.elに丸投げします。

  • orglyth-html-enable-option : tの時、(orglyth-html-init) をした時に関連する変数を変更します。(何を変えるかはソースを見たほうが早いです)
  • orglyth-html-use-ftp : tの時、プロジェクト:websiteの設定がftp転送したサーバー上になります。 遅いのでnilにしてます。
  • orglyth-html-local-root-path : ローカルのOrg文書ルートフォルダです。
  • orglyth-html-remote-root-path : プロジェクト:websiteのターゲットフォルダです。
  • orglyth-html-ftp-root-path : プロジェクト:websiteのターゲットフォルダです。
  • (orglyth-html-init) : 変数の初期化
  • (orglyth-html-project-init) : orglyth.elのプロジェクトをOrgに登録する。
(leaf orglyth
  :config
  (leaf orglyth-html
    :setq
    ((orglyth-html-enable-option    . t)
     (orglyth-html-use-ftp          . nil)
     (orglyth-html-local-root-path  . "~/Documents/sakura/orglyth/")
     (orglyth-html-remote-root-path . "~/Documents/sakura/remote/")
     (orglyth-html-ftp-root-path    . "/ftp:conao3@conao3.com:~/www/orglyth/"))
    :config
    (orglyth-html-init)
    (orglyth-html-project-init))

  (leaf orglyth-latex
    :setq ((orglyth-latex-enable-option . t))
    :config
    (orglyth-latex-init)))

(orglyth-html-projet-init) はpublishターゲットを複数持っていると、候補が溢れて管理しづらくなる(かもしれない)ので必要な時に登録できるようにしています。私はorg-publishを他の用途には使っていないので起動時に登録するようにしています。

使い方

org-publish-1.png
org-publish-2.png

前段の設定を行うと C-c C-e (org-export-dispatch) を押し、ディスパッチャに入り、P x を押すと登録されているプロジェクトが確認できます。

様々なプロジェクトが登録されていますが、我々が実行するのは一般的にlocalwebsite のメタプロジェクトです。

org-publishの思想から言うと local で変換結果を確認してその後 website を実行してftpで転送させたいのでしょうが、遅すぎてその間Emacsのメインスレッドがブロックされるので、実際には local を実行してその後ターミナルでサーバーに rsync しています。

まとめ

org-publish により静的HTMLサイトを構築する方法を説明しました。

私はもともと org2blog を使ってWordpressに投稿していたのですが、過去記事が編集しづらかったり、そもそもWordpressが嫌いになってしまったりと使うのを諦めてしまいました。

最近Org-Mode界隈では ox-hugo を用いてブログを書くのがブームになっている(takaxpさんなど)ようですが、あまり日の目を見ない org-publish でも同様のことができることを知ってもらえればと。

Org文書変換前や変換後に任意のElisp関数を登録できたりと、とても自由度高そうなのでorg-publish でブログを書いてみるのも面白いかもしれません。

プログラマたる者、素人の方が作られた阿部寛さんのHPより高速なウェブページを構築したいですよね?実際それはけっこう大変らしいのですが、org-publishでは静的HTMLを吐くので、表示速度追求も面白いかと思います。

AMP対応してGoogleのサーバーにホスティングしてもらうのも手かもしれません。(orglyth.elで将来的に対応予定)

(ちなみにるびきちさんに触発されて、私もブログのソースをGitHubに公開してます。実際、URLの archivesrc に変えれば見えるんですが、一覧したいときなどに利用ください。)