Scheduling Posts and Microblogs With Jekyll
When I switched this blog to Jekyll, one of the features I lost was the ability to schedule posts for the future. Yes, Jekyll can handle future dates on posts, but you still need to run a command to build the site and then deploy it to your web server.
And I have been operating without that ability for over four years now. But this past December I decided to be done with manually posting articles and setting aside time to do the deployment by hand. So I set about building the following structure to allow me to publish new articles and microblogs to the site.
build.sh
!/bin/bash
cd /path/to/repo/
. /home/username/.bashrc
git fetch origin master --quiet
mycommit=$(git rev-parse HEAD)
origincommit=$(git rev-parse origin/master)
if [ "$mycommit" = "$origincommit" ]
then
bundle exec rake publish
else
git reset --hard origin/master
git pull
bundle install > /dev/null
bundle exec rake build
bundle exec whenever --update-cron
fi
This all starts with a build script. It sits at the top-level of the repo: my-repo/build.sh
. All this does is check for new git commits. If no new commits exist, it runs a rake task to see if there are any new posts or microblogs available. More on that later.
If it does find a new commit, there are a few steps it takes. First, it resets the local repository, pulls the new commit, and then runs a bundle install
. This allows me to update the Gemfile and add any dependencies I may need without logging into the webserver and updating it.
Then it runs the build
task. Again, more on that below. But after the build
task, it runs the whenever
command to update my cron tasks, which begs the question: what cron tasks?
Cron Job
I added gem whenever
to my Gemfile and then put this in config/schedule.rb
:
set :output, "/path/to/log/my.log"
every 5.minutes do
command "sh /path/to/repo/build.sh"
end
By using the whenever
gem, I can update the server cron job(s) by simply committing the changes to the repo. The build script will pull the changes and run the command to update the cron job.
At this point, I hope you can see the beauty of this. The build.sh
script is run via cron job every five minutes. And that script updates the cron job. They keep each other current at all times.
Rake Tasks
There are two rake tasks that build.sh
calls: build
and publish
. The build
task looks like this:
desc "Build site"
task :build do
Rake::Task["convertkit"].invoke
Rake::Task["podcast"].invoke
Rake::Task["microblog"].invoke
Rake::Task["webmentions"].invoke
sh "JEKYLL_ENV=production bundle exec jekyll b"
sh "git add --all .;git commit -m 'cron build';git push;"
sh "rm -rf /path/to/_site/"
sh "cp -pr /path/to/repo/_site/ /path/to/web/_site/"
sh "curl -X POST https://micro.blog/ping?url=https://joebuhlig.com/microblog/feed.xml"
Rake::Task["microblog_webmentions"].invoke
end
I won't get into the other rake tasks here. That's for another day. For this, the important pieces are the sh
lines. All they do is build the site, commit the changes, delete the old _site
directory, copy the newly generated _site
directory to the published web directory, and then ping the Micro.Blog service to update my Micro.Blog feed. I do this last step to make sure the post is picked up right away. Basically, I became tired of waiting for it to propagate on its own.
The publish
task has a different intent. It runs when there is no new commit found. Here's what it looks like:
desc "Publish scheduled posts"
task :publish do
update = false
today = Time.now
Dir['_posts/','_microblogs/'].each do |filename|
file = File.open("#{filename}").each_line do |line|
if line.start_with?("date: ")
post_date = Time.parse(line.split("date:")[1])
time_between = today - post_date
if time_between < 300 and time_between > 0
puts filename
update = true
end
end
end
file.close
end
last_updated_file = File.open("_data/webmentions/last-id.txt")
last_id = last_updated_file.read.strip
payload = open("https://webmention.io/api/mentions.json?token=MYTOKEN&since_id=#{last_id}")
if JSON.parse(payload.read)["links"].count > 0
update = true
end
last_updated_file = File.open("_data/microblog/last-id.txt")
last_id = last_updated_file.read.strip
payload = open('http://micro.blog/feeds/joebuhlig.json')
JSON.parse(payload.read)["items"].each do |item|
url = URI.parse(item["url"])
if url.host == "joebuhlig.com"
if item['id'] > last_id
update = true
break
end
end
end
if update
Rake::Task["build"].invoke
end
end
There's a lot more going on here, obviously. But here's the gist of it. It searches the _posts
and _microblogs
directories for files that are dated within the last five minutes. If you recall, the cron job runs every five minutes. So it only needs to determine if there is a post with a date in the last five minutes. If no, the job is done and can wait for the next run five minutes later. If yes, it tells the build
task to run.
You'll also note a webmention section here. This script also checks to see if there are any new webmentions collected for my posts and microblogs. Again, if no, be done. If yes, run the build
task.
Exclusions
In Jekyll, any file that starts with a period or an underscore is kept out of the _site
directory by default. But there are others that I find need removed as well. This is pretty easy. I have the following added to the exclude
item of my _config.yml
file.
exclude:
- Rakefile
- Gemfile
- Gemfile.lock
- node_modules
- vendor/bundle
- vendor/cache
- vendor/gems
- vendor/ruby
- config
- Capfile
- build.sh
Putting It All Together
In practice, all of this work allows very simple actions. To make an edit to my site, I merely need to commit the change. No need to build the site and copy it to the correct directory on the webserver.
This also means that to publish a new post or microblog in the future, I simply need to add a future date to the front matter. Within five minutes of the date passing, the article or microblog will post on its own. And that's refreshing.