Optimizing Octopress Workflow for New Posts

Rahul Pulikkot Nath
Rahul Pulikkot Nath

Table of Contents

Over the past month I had thought of migrating this blog to Hugo, a static site generator that is faster than the current one, Octopress. Lack of workflow for creating new posts and slower build times were the main reasons. I am the kind of person when writing post want to see often, how it looks like on the real site. With the current number of posts it takes around 40-50 seconds to build the entire site and it makes me to wander off to something else while the build is happening - at times it takes a long time to get back to writing!. But migrating to a new platform has a lot of challenges and time-consuming and I did not want to invest my time in that, so though of looking out for ways to optimize the current process. A bit of googling and playing around with Ruby, solved both of the major issues and I have an improved workflow!

Draft workflow

I was lucky to find this post which handled most of the draft workflow process. Most of the code below is used from there with a very few minor additions. Newer versions of Jekyll support working with drafts and uses the '--drafts' switch to build the drafts (instead of using published flag as in thr above linked post), that are in '_drafts' folder. Drafts are posts which does not have date's, so I added in a placeholder text, 'thisIsStillADraft', in the yaml front matter of the post which will later be replaced with the post publish date. Also added in the code to open the default writer with the newly created post

# usage rake new_draft[my-new-draft] or rake new_draft['my new draft']
desc "Begin a new draft in #{source_dir}/#{drafts_dir}"
task :new_draft, :title do |t, args|
  if args.title
    title = args.title
    title = get_stdin("Enter a title for your post: ")
  raise "### You haven't set anything up yet. First run `rake install` to set up an Octopress theme." unless File.directory?(source_dir)
  mkdir_p "#{source_dir}/#{drafts_dir}"
  filename = "#{source_dir}/#{drafts_dir}/#{title.to_url}.#{new_post_ext}"
  if File.exist?(filename)
    abort("rake aborted!") if ask("#{filename} already exists. Do you want to overwrite?", ['y', 'n']) == 'n'
  puts "Creating new draft: #{filename}"
  open(filename, 'w') do |post|
    post.puts "---"
    post.puts " "
    post.puts "author: [Rahul Nath]
title: \"#{title.gsub(/&/,'&')}\""
    post.puts "  "
    post.puts "tags: "
    post.puts "tags: "
    post.puts "thisIsStillADraft:"
    post.puts "keywords: "
    post.puts "description: "
    post.puts "---"
  system %{cmd /c "start #{filename}"}

The publish draft task just asks for the post to publish and replaces the placeholder text with the current date time. Also it moves the post from the '_drafts' folder to the '_posts' folder with the file name appended with the date time. Since I run this just before deploying a post, the date on the post will be the actual publish date, and not the date I started writing the post (usually writing a post spans over multiple days).

# usage rake publish_draft
desc "Select a draft to publish from #{source_dir}/#{drafts_dir} on the current date."
task :publish_draft do
  drafts_path = "#{source_dir}/#{drafts_dir}"
  drafts = Dir.glob("#{drafts_path}/*.#{new_post_ext}")
  drafts.each_with_index do |draft, index|
      content = File.read(draft)
      if content =~ /\A(---\s*\n.*?\n?)^(---\s*$\n?)/m
        data = YAML.load($1)
    rescue => e
      puts "Error reading file #{draft}: #{e.message}"
    rescue SyntaxError => e
      puts "YAML Exception reading #{draft}: #{e.message}"
    puts "  [#{index}]  #{data['title']}"
  puts "Publish which draft? "
  answer = STDIN.gets.chomp
  if /\d+/.match(answer) and not drafts[answer.to_i].nil?
    mkdir_p "#{source_dir}/#{posts_dir}"
    source = drafts[answer.to_i]
    filename = source.gsub(/#{drafts_path}\//, '')
    dest = "#{source_dir}/#{posts_dir}/#{Time.now.strftime('%Y-%m-%d')}-#{filename}"
    puts "Publishing post to: #{dest}"
    File.open(source) { |source_file|
      contents = source_file.read
      contents.gsub!(/^thisIsStillADraft:$/, "date: #{Time.now.strftime('%Y-%m-%d %H:%M')}")
      File.open(dest, "w+") { |f| f.write(contents) }
    puts "Index not found!"

With these two new rake tasks, I can now create as many draft posts at a time and publish them once ready.

Improving the build time

Jekyll build command options provides a switch, 'configuration', that allows to pass a configuration file instead of using '_config.yml'. In the configuration file we can specify a 'exclude' option to exclude the directories and/or files from the build. I created a new task for building only the current drafts, by specifying the '--drafts' switch and a dynamically generated configuration file, _previewconfig.yml, which excludes the '_posts' folder. This dramatically increases the build time, and completes almost immediately after a making a change to a post. This fits perfectly into my workflow, as while writing new posts I do not want to see any already published posts. You can add the dynamically generated configuration file name to the .gitignore as I do not delete it in the tasks. I did not want to use the 'rake isolate' task that is already present in the rakefile, as that does not integrate with the draft workflow and unnecessarily moves all the posts to a temporary place.

desc "preview the site in a web browser with all the draft posts"
task :previewdrafts do
  raise "### You haven't set anything up yet. First run `rake install` to set up an Octopress theme." unless File.directory?(source_dir)
  puts "Starting to watch source with Jekyll and Compass. Starting Rack on port #{server_port}"
  system "compass compile --css-dir #{source_dir}/stylesheets" unless File.exist?("#{source_dir}/stylesheets/screen.css")
  File.open("_config.yml") { |source_file|
      contents = source_file.read
      File.open("_previewconfig.yml", "w+") { |f|
      f.puts("exclude: [\"#{posts_dir}\"]")

  jekyllPid = Process.spawn({"OCTOPRESS_ENV"=>"preview"}, "jekyll build --watch --drafts --config _previewconfig.yml")
  compassPid = Process.spawn("compass watch")
  rackupPid = Process.spawn("rackup --port #{server_port}")

  trap("INT") {
    [jekyllPid, compassPid, rackupPid].each { |pid| Process.kill(9, pid) rescue Errno::ESRCH }
    exit 0

  [jekyllPid, compassPid, rackupPid].each { |pid| Process.wait(pid) }

Dropbox integration

At times, I have started to draft blog posts while commuting to work from my mobile device, so I wanted to sync my draft posts to Dropbox, so that I can edit it from my mobile phone. Apps like MarkDrop/JotterPad integrates with Dropbox and supports Markdown editing. I set up a drafts folder on my Dropbox folder on laptop, which is automatically synced using the Dropbox application. I then used Mklink to create a symbolic link from the folder on Dropbox to my drafts folder in my blog repository. Whenever a new draft post is added, it gets automatically inserted into the Dropbox folder, which will then be synced to cloud and available for edit on my mobile phone too. (Part of this post is written from my mobile!)

mklink /D "C:\blog\_drafts" "C:\dropbox\_drafts"

Cmder integration

Cmder is a portable console emulator for Windows and provides a good nice looking feature rich console experience on Windows and is one of my favourite tools. Creating alias for commands is one of the features, that allows to create a short key combination for an otherwise long command. You can use this to create commands for the task in Octopress like below

alias rp=rake previewdrafts

// Below are the list of alias that I have for the rake tasks
rp=rake previewdrafts
rps=rake preview // Preview the whole site
rd=rake deploy
rnd=rake new_draft["$"] // Takes in the post name as parameter
rpd=rake publish_draft
rd=rake deploy

So my new workflow is

rnd Optimizing Octopress Workflow for New Posts

It's much faster, cleaner and easier publishing new posts. To see the latest code for the rake tasks head over to the original file on github.