2014-09-26 ,

JekyllからWordPressへ移行

JekyllからWordpressへ移行しました。

Jekyllは生成が遅すぎて耐えきれなかったというのが第一の理由です。一つ記事を書き終わってから更新をかけると、ほぼすべてのページを再生成するため、どうしても数十秒かかります。すべてのページを再生成しなくても良いように思うかもしれませんが、各ページにあるナビゲーションリンクやタグごとの記事数なんかを書き直さなければなりません。このサイトには1500くらい記事があるのですが1分くらいはかかります。関連記事や記事数といった情報はいったん全部の記事を読み込んでからで無いと求まりませんし。incrementalな再生成も検討されているようなのですが、できるのはまだ先のようなので。

さくらのレンタルサーバで簡単にWordpressを入れる方法が紹介されていたのも移行を後押ししました。

外出先からなど、もっと自由に記事を書きたかったですし。

移行手順は次のような感じでした。

記事の変換はいくつかハマったところがありました。

まずはインポートできる形式を調べたのですが、めぼしいものが見当たりません。結局WordPress間でやりとりするのに使われるであろう形式にこれまでの記事を変換することにしました。
これまでの記事はJekyllのyamlヘッダー付きhtml形式です(昔の独自マークアップ形式やorg-mode形式からhtmlへ変換されたものです)。それをWordpressでインポートできるWXR(WordPress eXtended RSS)へ変換しました。変換に使ったEmacs Lispは次の通り。

(defun jtow-parse-html (file)
  "yamlヘッダー付きhtmlファイルを解析して(記事URL slug タイトル 時刻 タグ文字列リスト 内容文字列)を返します。"
  (with-temp-buffer
    (insert-file file)
    ;; ---
    (beginning-of-buffer)
    (if (not (looking-at "^---\n")) (error "syntax error. '---'"))
    ;; yaml header
    (next-line)
    (let ((yaml-header nil))
      (while (not (looking-at "^---"))
        (if (not (looking-at "^\\([^:]+\\): *\\(.*\\)$"))
            (error "syntax error. %s %s" file (buffer-substring (point-at-bol) (point-at-eol))))
        (push (cons (match-string-no-properties 1) (match-string-no-properties 2)) yaml-header)
        (next-line))
      (next-line)

      ;; check
      (let ((title (cdr (assoc "title" yaml-header)))
            (date (cdr (assoc "date" yaml-header)))
            (tags (cdr (assoc "tags" yaml-header)))
            (content (buffer-substring-no-properties (point) (point-max))))
        (if (not title) (error "title not found. %s" file))
        (if (not date) (error "date not found. %s" file))

        ;; result
        (list
         ;; link
         (concat "http://example.jp/blog/" file)
         ;; postname
         (if (string-match "^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]-\\([^.]+\\)\\.html$" file ) (match-string 1 file) nil)
         ;; title
         (substring title 1 -1)
         ;; date
         (jtow-parse-date date)
         ;; tags
         (if tags (split-string tags " " t))
         ;; content
         content))
      )))

(defun jtow-parse-date (d)
  "2014-01-02という形式の文字列からEmacsの時刻形式へ変換します。"
  (let ((tm (parse-time-string d)))
    (encode-time 0 0 0 (nth 3 tm) (nth 4 tm) (nth 5 tm) (* 9 3600))))

(defun jtow-format-date-rfc2822 (time)
  (format-time-string "%a, %d %b %Y 00:00:00 %z" time))

(defun jtow-format-time (time &optional universal)
  (format-time-string "%Y-%m-%d %H:%M:%S" time universal))

(defun jtow-make-item (item)
  "jtow-parse-htmlが返したリストをWXRのitem要素へ変換します。"
  (let ((link (nth 0 item))
        (postname (nth 1 item))
        (title (nth 2 item))
        (date (nth 3 item))
        (tags (nth 4 item))
        (content (nth 5 item)))
    (concat
     "<item>"
     (if link (concat "<link>" link "</link>\n"))
     (if title (concat "<title>" title "</title>\n"))
     (if date (concat "<pubDate>" (jtow-format-date-rfc2822 date) "</pubDate>\n"))
     (if tags (loop for tag in tags concat (format "<category domain=\"post_tag\" nicename=\"%s\"><![CDATA[%s]]></category>\n" (downcase tag) tag)))

     (format "<wp:post_date>%s</wp:post_date>\n" (jtow-format-time date))
     (format "<wp:post_date_gmt>%s</wp:post_date_gmt>\n" (jtow-format-time date t))
     (concat "<wp:post_name>" postname "</wp:post_name>\n")
     "<wp:post_type>post</wp:post_type>\n"
     "<wp:status>publish</wp:status>\n"

     ;; content
     "<content:encoded><![CDATA[" content "]]></content:encoded>\n"
     "</item>\n")))

(defun jtow-dir-html-to-xml (dir outfile)
  "ディレクトリ内のyamlヘッダー付きhtmlファイルからWXRファイルを作成します。"
  (with-temp-file outfile
    (insert
     "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>
<rss version=\"2.0\"
	xmlns:excerpt=\"http://wordpress.org/export/1.2/excerpt/\"
	xmlns:content=\"http://purl.org/rss/1.0/modules/content/\"
	xmlns:wfw=\"http://wellformedweb.org/CommentAPI/\"
	xmlns:dc=\"http://purl.org/dc/elements/1.1/\"
	xmlns:wp=\"http://wordpress.org/export/1.2/\"
>
<channel>
	<title>Example Blog</title>
	<link>http://example.jp/blog</link>
	<description></description>
	<pubDate>Thu, 25 Sep 2014 05:56:52 +0000</pubDate>
	<language>ja</language>
	<wp:wxr_version>1.2</wp:wxr_version>
	<wp:base_site_url>http://example.jp/blog</wp:base_site_url>
	<wp:base_blog_url>http://example.jp/blog</wp:base_blog_url>

	<wp:author>
	<wp:author_id>1</wp:author_id>
	<wp:author_login>foo</wp:author_login>
	<wp:author_email>foo@example.jp</wp:author_email>
	<wp:author_display_name><![CDATA[foo]]></wp:author_display_name>
	<wp:author_first_name><![CDATA[]]></wp:author_first_name>
	<wp:author_last_name><![CDATA[]]></wp:author_last_name>
	</wp:author>

	<generator>http://wordpress.org/?v=4.0</generator>
")

    (loop for file in (directory-files dir nil "\.html$") do (insert (jtow-make-item (jtow-parse-html file))))

    (insert "</channel></rss>")))

;; ex)カレントディレクトリにある.htmlからposts.xmlを作る。
;;(jtow-dir-html-to-xml "." "posts.xml")

私が使っていたyamlヘッダーは主にtags, date, titleだけでしたので、それだけ考慮してあります。

CDATAの障害になる]]>という文字列が使われていないことも確認しました。

一番困ったのが、slug(postname)が重複してはいけないということでした。昔の記事では/blog/2006-01-01-a.htmlのようなURLが多いのですが、aの部分は他の日付でも沢山使われているので重複してしまいます。重複すると自動的にa-2、a-3のように補正されてしまいます。仕方ないので/blog/2006-01-01-060101a.htmlのように確実にユニークになるようにファイル名を置換しました。以前のURLと変わってしまいますが諦めました。

外観のカスタマイズはBootpressテーマをベースにして、気に入らないところをひたすら書き換えては表示してみる繰り返しでした。
それにしてもphpは久しぶりです。しかしまぁ、見えないグローバル変数に依存するコードが多くて分かりづらいですね。WP_Queryが割と万能な模様。

一通りカスタマイズが終わったらテスト用ディレクトリから本番ディレクトリへ移し替えて完成です。

投稿は今まで事実上Emacsからしか出来ませんでしたが、これからはブラウザ、Androidアプリ、Emacsと色々なところから出来るようになりました。

AndroidのWordPressアプリは面白いですね。写真を撮ったそばから投稿できます。

Emacsからの投稿はOrg2blogを導入。el-get経由でインストールしました。bzrが実行できないと言われたのでCygwinのインストーラからbzrをインストール(xml-rpc-elのインストールに必要)。でもWindowsのEmacsを使っていて、かつ、bzrはPythonスクリプトなのでEmacsからexecutable-findで見つからず。bzr.batを作ってsh -c '/bin/bzr %*'のようにしてなんとかel-getからインストールできました。(MELPAをpackageに追加してそこからインストールした方が良かったかも)

Org2blogもAndroidのWordPressアプリもですがslugが入力できません。URLが日本語になってしまいますが、まぁ、これも諦めるしかないでしょうか。(Org2blogでは#+PERMALINKでslugを設定できました)