Scheduling posts 2: the Rakening
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.
Processing
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
else
file = args.filename if File.exists?(File.expand_path(args.filename))
raise "Specified file not found" unless file
end
now = Time.now
short_date = now.strftime("%Y-%m-%d")
long_date = now.strftime("%Y-%m-%d %H:%M")
# separate the YAML headers
contents = File.read(file).split(/^---\s*$/)
if contents.count < 3 # Expects the draft to be properly formatted
puts "Invalid header format on post #{File.basename(file)}"
Process.exit
end
# 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
end
#### deploy scheduling ###
# if there's a date set in the draft...
if headers.key? "date"
pub_date = Time.parse(headers['date'])
if pub_date > Time.now # 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/brettterpstra.com/atjob.sh #{pub_date.strftime('%H%M %m%d%y')}") if should[:schedule]
end
end
### 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
File.open(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]
Rake::Task[:gen_deploy].execute
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)
else
Rake::Task[:generate].execute
end
end
end
Additional functions
choose_file
# 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]
end
ask
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 !valid_options.map{|opt| opt.nil? ? '' : opt.upcase }.include?(answer.nil? ? answer : answer.upcase)
else
answer = get_stdin(message)
end
answer
end