WordPress to Jekyll: handling redirects

[Tweet : nvALT]

If you happen to be converting a blog from WordPress to Jekyll, this tip might be of use, especially if you want to make sure links to your existing posts continue to work. While this is most likely to be an issue if you’re changing your permalink structure, you can still run into a few hiccups even if you maintain it.

Note that this post assumes you have a working knowledge of Ruby and can get the WordPress importer script to run on its own, including installing the sequel and MySQL gems. If you’re not that far yet, check back once you have it working.

I’m posting this to document my own discoveries, and I highly doubt it will be of much use to anyone else. While I have this working for my particular needs, this post only details enough to give you an idea how to implement your own. If you’ve read this far and don’t know what’s going on, you should probably skip this one.

I converted from having no dates in the url (/post-name) to having full dates (/2013/04/04/post-name). I handled this with Apache redirects. If you’re using another server platform, you’ll need to adjust the rule output accordingly.

I used a heavily modified version of a migration script borrowed from the original Jekyll package. Among myriad conversions it runs, it gathers permalinks and creates rules for all existing posts as it reads them from the WordPress database. The following is the concept, but you’ll need to reassemble in your own import script.

First, I set up a variable in the class initialization to hold the redirects as they’re gathered.

class WordPressImporter
    def initialize(dbname, user, pass, host = 'localhost', domain = '')
        @redirects = []

Collecting and generating rules

During the process function in the import script, it gathers all of the posts and writes out the Markdown files for you. At the end of this function I add to my @redirects array using the information in the post variable.

@redirects << {
    'source' => "^" + post[:post_name].to_s + "/?$",
    'target' => slug

The source line needs to generate a regular expression that matches the original URL of the post on your site. In my case this is just “^post-title/?”. If you have an existing permalink structure using dates or categories (or anything else), you’ll need to add the bits in to create a matching rule. For example, if your link structure is “yoursite.net/2013/04/post-title”, your regular expression needs to be “^2013/04/post-title/?”. You would generate it with:

date = post[:post_date]
regex = "^%02d/%02d/%s/?$" % [date.year, date.month, post[:post_name]]
@redirects << { 'source' => regex, 'target' => slug }

Adding the redirects to htaccess

I created an .htaccess file for the site in my “source” folder. It just needs to be at the root of the site source so that it’s copied over when the site is generated. Whether you create a new one or are using an existing one, you’ll need to add some markers so the script knows where to insert/update the rules. Add two lines to the htaccess anywhere that makes sense for page redirects:


The start and end markers are the same.

Here’s the part of the script that reads the htaccess in, inserts the generated rules between those markers and writes the result back out to .htaccess. Note that the File.open("source/.htaccess",'r') needs to be modified if your htaccess exists in a folder other than “source”. This should be defined within the WordPressImporter class.

def process_redirects
  $stderr.puts "Processing redirects"
  content = []
  File.open("source/.htaccess",'r') do |f|
    content = f.read.split(/\#===== PAGE REMAPS/)
  before = ''
  after = ''
  if content.length == 2
    before = content[0]
  elsif content.length == 3
    before = content[0]
    after = content[2]
  File.open("source/.htaccess", 'w+') do |f|
    f.puts("#===== PAGE REMAPS")
    @redirects.each { |redirect|
      f.puts("RewriteRule #{redirect['source']} #{redirect['target']} [R=301,L]")
    f.puts "#===== PAGE REMAPS"
    f.puts after

Now just run the process_redirects function right after the process function and — if you hacked it all together properly — you should get an .htaccess file with all of your old links mapped to new ones. I highly recommend testing it on a local server before deploying anything.

Double checking

You can use a sitemap from your old site to check against the new site for missing links and repair and redirect them as needed. Here’s a basic script which may require some adjustment. Run it with sitemapchecker.rb yourstagingsite.com http://yourwordpresssite.com/yoursitemap.xml. If you do happen to use this and you have a large site, let me know. I have a version with progress meters…

#!/usr/bin/env ruby
# sitemapchecker.rb
# Brett Terpstra 2013, no rights reserved
require 'rexml/document'
require 'net/http'
require 'uri'

def get_xml(url)
  if File.exists?(url)
    f = File.open(url,'r')
    res = f.read
    url = "http://" + url unless url =~ /^https?:\/\//
    res = Net::HTTP.get_response(URI.parse(url)).body

def test_url(url)
  res = Net::HTTP.get_response(URI.parse(url))
  res.nil? ? "FAILURE" : res.code

def check_sitemap(target_domain,sitemap)
  results = []
  doc = REXML::Document.new(get_xml(sitemap))
  raise "Error parsing #{sitemap}" unless doc
  urls = doc.get_elements("urlset/url")

  urls.each { |url|
    target = url.elements['loc'].text
    parts = target.match(/^(https?:\/\/)?([^\/]+)?(.*)$/)
    prefix = parts[1] || "http://"
    target = "#{prefix}#{target_domain}#{parts[3]}"
    res = test_url(target)
    results << { 'url' => target, 'result' => res }
  outfile = "sitemap_check_#{target_domain}_#{Time.now.strftime('%m-%d-%Y')}.txt"
  $stderr.puts "Writing results to #{outfile}"
  File.open(outfile,'w') do |f|
    results.each { |res|
      f.puts %Q{#{res['result']}\t#{res['url']}}

if ARGV.length < 2
  puts "This script pulls an existing sitemap, remaps urls to a new domain,"
  puts "and checks to see if they exist at the new location."
  puts "The first argument should be the new domain to test on, followed by"
  puts "a filename or web url for the current sitemap (multiple allowed)."
  puts "A file titled \"sitemap_check_[yourdomain]_date.txt\" will be output"
  puts "in the current directory. I know, I should make that a CLI option. Whatever."
  puts "> #{File.basename(__FILE__)} stage.yoursite.com http://yoursite.com/yoursitemap.xml"
  target_domain = ARGV[0]
  ARGV.each { |smap|

Hope somebody finds all of this useful.