Jekyll within Rails, on Heroku

There are several HOWTO’s on the web, there’s even a gem, but all of them are slightly outdated or not fitting for my use case, so here’s how I’ve integrated Jekyll into our Rails on Heroku setup for a small project.

The goal was to use Jekyll for both the marketing homepage of the product and as a blogging engine. I also wanted Heroku to do the jekyll build process on publish and thus not having to check in the artifacts aka generated websites. There are some pitfalls that I came across, so that’s another reason for documenting it here.

So, the first thing is to include the jekyll gem in the Rails Gemfile. Theoretically, you could keep the jekyll dependencies separate, but I wanted to have all in one place. As some sort of spoiler, add the minima gem as well, which contains the new jekyll default template:

# Gemfile
gem 'jekyll', '~> 3.2'
gem 'minima'

after the usual bundle install run, you can use the jekyll command line to generate a jekyll project (I’ve used a folder within the Rails root folder for simplicity):

$ bundle exec jekyll new marketing_homepage

Now, delete the Gemfile in the jekyll folder. Make sure to move any gem that is not jekyll and minima over to your rails gem file. With the current version of jekyll I used (3.2.1), that list is supposed to be empty.

Move the _config.yml file to Rails’ config folder, as jekyll.yml. This is not strictly needed, but it tidies up things.

Now, we need to tell jekyll to use the right source and destination folders and to not interfere with the asset pipeline in production:

source: 'marketing_home'
destination: 'public'
keep_files: [assets]

What you also should do is move everything that’s currently in public/ to the jekyll folder. In theory you could use keep_files for those as well, but in practice this would mean that you would have to keep your .gitignore up to date with everything that’s generated by jekyll. (Remember: Never check in generated artifacts, right?) Now, with the public folder being empty, we can simply add the whole folder to .gitignore and be done with it. I tried both ways and this way was a lot less hassle. Also, if you accidentally run jekyll clean, which ignores the keep_files config, you won’t lose all your error pages.

Now we have everything in place to start generating into the public folder.

To do this, we’ll have to tell jekyll where to find the config file:

$ bundle exec jekyll build --config config/jekyll.yml

If you now run the rails server and load up http://localhost:3000/ (given that you haven’t defined a root route that overrides it), you should see the jekyll default page.

We now can write some rake tasks to make calling jekyll simpler:

namespace :static_pages do
desc "Build static pages"
task :build do
sh "jekyll b --config config/jekyll.yml"
end
desc "Watch and rebuild static pages"
task :watch do
sh "jekyll b --config config/jekyll.yml --watch"
end
desc "Watch and rebuild static pages with drafts"
task :watch_drafts do
sh "jekyll b --config config/jekyll.yml --drafts --watch"
end
desc "Clean up after Jekyll"
task :clean do
sh "jekyll clean --config config/jekyll.yml"
sh "mkdir public && touch public/.keep"
end
end

static_pages:watch will run jekyll in watch mode. You will have to run in in parallel to your rails server, of course, which is a little annoying, but not annoying enough for me to investigate possibilities of plugging this into rails automatically somehow.

static_pages:watch_drafts comes in handy when you’re writing blog posts, as it will make jekyll also generate blog entries that are sitting in the _drafts folder.

static_pages:clean will use jekyll’s clean command to clean up, but to not annoy git too much, I recreate the public/ folder and touch the .keep file I’ve put there, so that the state stays the same all the time.

Heroku

To build the static files on heroku, we need to call the rake task to build the jekyll site somehow during the deployment. Help comes in form of this neat “execute any rake tasks” buildpack, which you can add to your app with

heroku buildpacks:add https://github.com/gunpowderlabs/buildpack-ruby-rake-deploy-tasks

To run our build task correctly, we need to set two additional ENV variables on heroku:

heroku config:add JEKYLL_ENV=production \ DEPLOY_TASKS=static_pages:build

Now, on your next code push, heroku should correctly build the page.

Additional Tweaks

Jekyll now comes with a gem-ized default theme called minima. You can override any of the templates by simply putting a file with the same name into your jekyll file structure. For example, you may want to override _includes/head.html to add additional headers, like OpenGraph tags.

Also, to minimize the conflicts with paths in your routes, you may want to change the way jekyll constructs URLs for blog entries: By default, it uses the categories of a post as the first part. I’ve changed that so that the whole blog is namespaced unter /blog/:

collections:
posts:
permalink: /blog/:year/:month/:day/:title.html

Also, I found that the minima theme sometimes uses slightly weird constructs to construct URLs to assets and so forth.

Sharing Stylesheets or not?

We deliberately kept the stylesheets of the homepage and the rails app separate. This might not be desirable in all situations, but we use a rather heavy framework in the App and knew beforehand that this would be overkill for our static pages. Bringing the Rails asset pipeline and jekyll together would be an interesting exercise, but as I said, we saw the separation as a plus, not a minus.

Conclusion

Jekyll may not be the most advanced static site generator out there, but it gets the job done with a small set of dependencies and let’s itself tweak just enough to enable hacks like this one. We could, of course, have used Rails’ internal mechanisms to build the marketing homepage and then spent 15 minutes building a simple blog, but while static site serving is not exactly Heroku’s (and puma’s) strength, this is still better than going through the whole rails stack. Also, this may become even more beneficial when switching to a stack like nginx > puma, where nginx could serve the static pages instead.

We’ve decided early on that we wanted to keep the marketing blurb on the same domain as the app, as this has a number of SEO benefits. I think this is a rather nice, low effort solution.