Jekyll で記事の URL を WordPress に合わせる

WordPress から Jekyll への移行で一番問題になったのは、記事の URL を維持することでした。

フィードやタグのページなどはまあリダイレクトでもいいのですが、記事の URL が変わってしまうと、既存のはてなブックマークや Twitter での言及が追えなくなってしまうので、避けたいところです。

この記事では、このブログでの記事 URL の維持のために行った作業について説明します。かなり dirty な方法なので、よりよい方法があれば教えてください。

目標と概要

Jekyll のデフォルトでは記事の URL は /2014/02/10/1234.html のようになります。これを /1234 に変更していきます。

今回は下記の手順で行いました。

  • permalink の設定
  • 生成する HTML のパス変更
  • Apache の MultiViews の利用
  • 開発用サーバの書き換え

permalink の設定

_config.ymlpermalink: :title とすると {{ page.url }} などで目的の URL (/1234) を返すようになります。

この場合、/1234/index.html にファイルを作成するようになります。これにより、標準的な設定の Web サーバなら /1234/ でアクセスできるようになるわけです。

これはなかなかうまい手ではあるのですが、最後のスラッシュが余計です。Apache なら /1234/1234/ へリダイレクトしてくれはするのですが、URL は変わってしまいます。

生成する HTML のパス変更

次の項で説明する MultiViews のために、permalink の設定はそのままで、/1234/index.html でなく /1234.html にファイルを作るよう Jekyll のコードを上書きします。

_plugins 下に下記のコードを配置します。

# _plugins/dont_create_directory_per_post.rb
require "jekyll"

module Jekyll
  class Post
    def destination(dest)
      path = File.join(dest, File.expand_path(CGI.unescape(self.url), "/"))
      # path = File.join(path, "index.html") if path[/\.html$/].nil?
      path = "#{path}.html" if path[/\.html$/].nil?
      path
    end
  end
end

これで /1234.html でアクセスできるようになりました。

Apache の MultiViews の利用

.htaccess に次の記述を追加します。

# Enable content negotiation
Options +MultiViews

MultiViews を有効にすると、要求したパスにファイルがない場合、Accept ヘッダに基づいて、拡張子を付与したファイルを探してくれるようになります。

/1234 へアクセスした場合に、1234 というファイルがなく、1234.html があれば、それを返してくれるわけです。

これで、/1234 でアクセスできるようになりました。

開発用サーバの書き換え

運用サーバはこれでいいのですが、jekyll serve だと (上記の .htaccess は当然無視されるので) /1234 ではアクセスできません。

_plugins 下に下記のコードを配置することで、WEBrick のコードを一部書き換えます。

# _plugins/webrick.rb
# jekyll serve で .html 省略可能に
require "webrick"
WEBrick::HTTPServlet::FileHandler.class_eval do
  def search_file(req, res, basename)
    langs = @options[:AcceptableLanguages]
    path = res.filename + basename
    if File.file?(path)
      return basename
    elsif langs.size > 0
      req.accept_language.each{|lang|
        path_with_lang = path + ".#{lang}"
        if langs.member?(lang) && File.file?(path_with_lang)
          return basename + ".#{lang}"
        end
      }
      (langs - req.accept_language).each{|lang|
        path_with_lang = path + ".#{lang}"
        if File.file?(path_with_lang)
          return basename + ".#{lang}"
        end
      }
    # ここから追加
    elsif File.file?("#{path}.html")
      return "#{basename}.html"
    # ここまで追加
    end
    return nil
  end
end

これで jekyll serve でも /1234 でアクセスできるようになりました。