Scheduling posts 2: the Rakening

[Tweet : nvALT]

Yesterday I covered how I’m handling scheduling with my Jekyll-based blog. The at command I mentioned there could be used in tandem with any static blogging system. Today I’m dropping in the “publish” task from my Rakefile, so you can see how I apply it specifically with Jekyll. The concepts are still portable, though.


The rake task can be run with or without an argument. If the argument is there and it’s a filename (and the file exists), it operates directly on that file. If no argument is passed, it offers a menu of available drafts to choose from.

To schedule a post, I just set the “date:” field in the YAML headers to a date in the future. That triggers all of the scheduling features. If the task is being run from the shell, it double checks with you for confirmation that you want to schedule a deploy. If confirmed (or forced), at is run and reads from a file with the necessary commands to generate and deploy the task. In my case, it bumps the site version (used to bust cache on any CSS/JS files), runs a generate task and deploys the site using Rsync.

I’ll be covering my “draft” system in more detail in a future post. I’ll mention a relevant part of it now, though. The _drafts folder is a symlink from my Dropbox writing folder. If a post shows up in there with a “publish_” prefix in the filename, Hazel triggers this system automatically. It passes a filename directly, bypassing the need for any shell interaction. The file gets published and the site gets deployed. There’s also an incomplete “preview_” mode that will generate the staging site but not deploy.

Here’s the relevant part of the Rakefile with lots of comments. For people who would be implementing something like this, they should be explanatory enough. Because it’s a work-in-progress, I’m posting its current state directly. When it’s closer to finished it will be included in a full Git repo of all of my hacks with its most current version.

The “publish” rake task

desc "Publish a draft"
task :publish, :filename do |t, args|
  # if there's a filename passed (rake publish[filename])
  # use it. Otherwise, list all available drafts in a menu
  unless args.filename
    file = choose_file(File.join(source_dir,'_drafts'))
    Process.exit unless file # no file selected
    file = args.filename if File.exists?(File.expand_path(args.filename))
    raise "Specified file not found" unless file
  now =
  short_date = now.strftime("%Y-%m-%d")
  long_date = now.strftime("%Y-%m-%d %H:%M")

  # separate the YAML headers
  contents =^---\s*$/)
  if contents.count < 3 # Expects the draft to be properly formatted
    puts "Invalid header format on post #{File.basename(file)}"
  # parse the YAML. So much better than regex search and replaces
  headers = YAML::load("---\n"+contents[1])
  content = contents[2].strip

  should = { :generate => false, :deploy => false, :schedule => false, :limit => 0 }

  # For use with a Dropbox/Hazel system. _drafts is a symlink from Dropbox,
  # posts dropped into it prefixed with "publish_" are automatically
  # published via Hazel script.
  # Checks for a "preview" argument, currently unimplemented
  if File.basename(file) =~ /^preview_/ or args.preview == "true"
    headers['published'] = false
    should[:generate] = true
    should[:limit] = 10
  elsif File.basename(file) =~ /^publish_/ and args.preview != "false"
    headers['published'] = true
    should[:generate] = true
    should[:deploy] = true

  #### deploy scheduling ###
  # if there's a date set in the draft...
  if headers.key? "date"
    pub_date = Time.parse(headers['date'])
    if pub_date > # and the date is in the future (at time of task)
      headers['date'] = pub_date.strftime("%Y-%m-%d %H:%M") # reformat date to standard
      short_date = pub_date.strftime("%Y-%m-%d") # for renaming the file to the publish date
        # offer to schedule a generate and deploy at the time of the future pub date
        # skip asking if we're creating from a scripted file (publish_*)
        should[:schedule] = should[:generate] and should[:deploy] ? true : ask("Schedule deploy for #{headers['date']}?", ['y','n']) == 'y'
        system("at -f ~/Sites/dev/ #{pub_date.strftime('%H%M %m%d%y')}") if should[:schedule]
  ### draft publishing ###
  # fall back to current date and title-based slug
  headers['date'] ||= long_date
  headers['slug'] ||= headers['title'].to_url.downcase

  # write out the modified YAML and post contents back to the original file,'w+') {|file| file.puts YAML::dump(headers) + "---\n" + content + "\n"}
  # move the file to the posts folder with a standardized filename
  target = "#{source_dir}/#{posts_dir}/#{short_date}-#{headers['slug']}.#{new_post_ext}"
  mv file, target
  puts %Q{Published "#{headers['title']}" to #{target}}
  # auto-generate[/deploy] for non-future publish_ and preview_ files
  if should[:generate] && should[:deploy]
  elsif should[:generate]
    if should[:limit] > 0
      # my generate task accepts two optional arguments: 
      # posts to limit jekyll to, and whether it's preview mode
      Rake::Task[:generate].invoke(should['limit'], true)

Additional functions

# Creates a user selection menu from directory listing
def choose_file(dir)
  puts "Choose file:"
  @files = Dir["#{dir}/*"]
  @files.each_with_index { |f,i| puts "#{i+1}: #{f}" }
  print "> "
  num = STDIN.gets
  return false if num =~ /^[a-z ]*$/i
  file = @files[num.to_i - 1]

This is borrowed from the OctoPress Rakefile.

def ask(message, valid_options)
  return true if $skipask
  if valid_options
    answer = get_stdin("#{message} #{valid_options.delete_if{|opt| opt == ''}.to_s.gsub(/"/, '').gsub(/, /,'/')} ") while !{|opt| opt.nil? ? '' : opt.upcase }.include?(answer.nil? ? answer : answer.upcase)
    answer = get_stdin(message)