Migrating from Jekyll to Wordpress

Published on

Migrating!

After a few years maintaining a blog in Jekyll (not this one, this one is in Hugo), I started wanting to migrate to WordPress on a VPS. The reasons were many:

  • the blog started growing in the number of posts, which made writing new ones more sluggish, due to Jekyll’s slowness and the lack of motivation that this slowness causes

  • a feeling of “immobility” from Jekyll, almost as if it isn’t evolving anymore

  • easier writing experience: sure, markdown works well, but it’s much more practical and quicker to open the admin panel in a browser and start writing the post directly in the format that will be seen in the end. Media handling and having everything centralized makes the act of writing easier and more straightforward

  • the reason above all: because I wanted to. It had been a while since I properly maintained a VPS running live, and I had grown too comfortable with the static site hosted on Netlify. I wanted to shake things up. Simple as that

The Plan

There are many plugins and tools going the opposite way, but few that help you migrate to WordPress, and most of them use the feed to generate content in the end (as described here). That works, but gives me less control than I like.

I was thinking about writing a script to insert directly into the WordPress database when I came across the wp utility. That made life a lot easier.

Relying on wp-cli, I put together the Ruby script below to generate the commands and files needed to import everything into WordPress.

require "time"
require "set"

if ARGV.size != 2
  puts "Usage: ruby migrate_posts_from_jekyll.rb posts_folder output_folder"
  exit 1
end

POSTS_DIR = ARGV[0]
OUTPUT_DIR = ARGV[1]

def starts_with_label?(line, label)
  line.downcase.strip.start_with?(label)
end

def clean(line, label)
  replacements = {
    "#{label}: " => "",
    "'"          => "",
    "\""         => "",
    "–"    => "-",
    "“"    => "\"",
    "”"    => "\"",
    "\n"         => "",
    "/"          => ""
  }

  line.strip.gsub(Regexp.union(replacements.keys)) { |match| replacements[match] }
end

commands = "#!/usr/bin/env bash \n"
categories_to_create = Set.new

Dir.children(POSTS_DIR).each do |filename|
  full_path = File.join(POSTS_DIR, filename)
  next if File.directory?(full_path) or filename.start_with?(".")

  title = ""
  date = ""
  permalink = ""
  categories = []
  tags = []
  content = ""

  in_categories = false
  in_tags = false
  header_count = 0

  File.foreach(full_path) do |line|
    unless line.start_with?(" ")
      in_categories = false
      in_tags = false
    end

    case
    when starts_with_label?(line, "title")
      title = clean(line, "title")
    when starts_with_label?(line, "date")
      date = clean(line, "date")
    when starts_with_label?(line, "permalink")
      permalink = clean(line, "permalink")
    when starts_with_label?(line, "categories")
      in_categories = true
    when starts_with_label?(line, "tags")
      in_tags = true
    when in_categories
      value = line.sub("- ", "").delete("\"").gsub("'", "").strip
      categories << value
      categories_to_create << value
    when in_tags
      tags << line.sub("- ", "").delete("\"").gsub("'", "").strip
    when starts_with_label?(line, "---") 
      header_count += 1
    when header_count == 2
      content += line
    end
  end
  output_file_name = permalink + ".txt"
  parsed_date = Time.parse(date).strftime("%Y-%m-%d %H:%M:%S")
  puts "#{title} - #{parsed_date} - #{permalink} - #{categories.to_s} - #{tags.to_s}"
  commands += "wp post create ./#{output_file_name} --post_author=1 --post_title='#{title}' --post_name='#{permalink}' --post_date='#{parsed_date}' --post_category='#{categories.join(', ')}' --tags_input='#{tags.join(', ')}' --post_status=publish --post_type=post \n"
  File.write(OUTPUT_DIR + output_file_name, content)
end

categories_commands = "#!/usr/bin/env bash \n"

categories_to_create.each do |cat|
  categories_commands += "wp term create category '#{cat}' \n"
end

File.write(OUTPUT_DIR + "categories.sh", categories_commands)
File.write(OUTPUT_DIR + "posts.sh", commands)

Know that the metadata collection expects the Jekyll posts header to be in the following format:

---
id: 124
title: The Title
date: 2018-04-10T10:19:55-03:00
category: blog
author: Name SecondName
layout: post
permalink: /the-permalink/
categories:
  - Category1
  - Category2
tags:
  - tag1
  - tag2
---
TEXT

And then you can run the script as following:

ruby migrate_posts_from_jekyll.rb content/_posts/ ../output/

The command will (hopefully) generate the files categories.sh, posts.sh, and the individual posts .txt in the output folder. Notice the post creation line with the parameter –post_author=1. This must match the ID of the post author in WordPress.

With that in hand, just copy this folder into your WordPress installation and run categories.sh and posts.sh using “./” as is.

The only thing not covered here was the media part, because it depends a lot on how media was used throughout your blog’s lifetime. If there’s a pattern, just modify the content variable in the script to handle the way you insert images into your posts and convert them to the format WordPress expects. In my case, it was just a matter of copying the Jekyll media folder into the WordPress media folder and running the WP Media Sync plugin. Afterwards, I uninstalled the plugin and that was it.

And with that, the blog was migrated to WordPress.

(just kidding, I had several issues with DNS configuration, but that was my own fault and a topic for another post)

Reminder: before doing this kind of operation, make sure you have backups and keep your Jekyll repository data saved, as it might still be useful in the future.

Comments

I feel that comments on specific blogs have been dying down as the times goes. If you have any questions or want to talk about the post, contact me through the below links.