Jekyll on Github Pages with Plugins

Jekyll runs on Ruby which means it can do lot of nifty things while still being a static site generator, even when hosted on Github Pages.

Jekyll is a sneaky blogging framework that tricks your brain into thinking you’re programming and not engaging in this new age-y practice of creative writing. This is great for devs like me, who are more comfortable producing readable code and well-structured documentation. What I’m doing here is ‘documenting’, not ‘writing’; ‘verbosely reviewing’, not ‘storytelling’. Whatever you think I’m doing on an emotional level, I’m not. You can’t let my brain know what’s really going on here.

Jekyll plugins

Because Jekyll runs on Ruby, you can create plugins which work much like helper methods in Rails. The main difference is that Rails helpers run on the server and respond in real-time, whereas Jekyll plugins are only used while compiling the static site files and not when they are served.

If you’re not using plugins or want to stop plugins from participating in site compilation, set value safe: true in your _config.yml. However, if you are deploying your Jekyll site on Github Pages, this value is hard-coded for you.

# _config.yml
# Github Pages overrides these values

safe:   true
lsi:    false
source: [your top-level directory]

Deploying via Github Pages

Github Pages provides free hosting of static sites to Github users, and because Github Pages are powered by Jekyll, you can push your entire Jekyll project to your Github Pages repository and it will get built out automatically. Win-win: your site is in a git repo and deployment is already done. Too bad all your plugin awesomeness got totally ignored.

Not a huge problem; plugins don’t run on the server anyway, so we can compile the site locally before we give them to Github Pages to serve.

There are two main options you could consider. A bad option and a good option.

  1. The bad option: Build your Jekyll site locally and simply sync your repo with your project’s compiled site folder. It will get served like any other pile of non-Jekyl-generated flat files. Who needs to keep their project source in source control anyway?
  2. The good option: Fight Github with Github. When your site is served from your Github Pages repo, it is specifically being served from the master branch. With a small bit of setup, we can maintain our project source in its own branch while the master branch gets updated with sanitized versions of the compiled Jekyll site. Additionally, pushing source commits to the new branch effectively decouples our source control and ‘production’ environments, and we now have full control over production deployments.

Fighting Github with Github

If you haven’t already, add the Jekyll destination folder (default is _site) to your .gitignore, and delete it from the repository.

# .gitignore

# Ignore Jekyll destination folder
/_site

Create the new git branch for our source control. I’ve called mine source.

$ git checkout -b source

Then push it back to the repo.

$ git push origin source

Your master branch and source branch should look identical.

Now delete all the files and folders in the root of the master branch.

$ git checkout master
$ git rm -rf *
$ git add -A
$ git commit -m "Clear root for static site deployment"
$ git push origin master
$ git checkout source

Next we add a task to our Rakefile to automate deployments.

# Rakefile

namespace :git do
  SOURCE_BRANCH = "source"
  DEPLOY_BRANCH = "master"
  DESTINATION_FOLDER = "_site"

  def git_source_branch?
    git_head = `git rev-parse --abbrev-ref HEAD`
    source = (git_head.strip == SOURCE_BRANCH)
  end

  def git_clean?
    git_state = `git status 2> /dev/null | tail -n1`
    clean = (git_state =~ /working directory clean/)
  end

  desc "Verify source branch"
  task :check_branch do
    unless git_source_branch?
      puts "Deploy not initiated from source branch. You should add `exclude: [Rakefile]` in _config.yml."
      puts "Checkout #{SOURCE_BRANCH} and deploy again."
      exit 1
    end
  end

  desc "Verify clean git state"
  task :check_git do
    unless git_clean?
      puts "Uncommitted changes. Commit or discard your changes and run deploy again."
      exit 1
    end
  end

  desc "Deploy to remote origin"
  task :deploy => [:check_branch, :check_git] do
    puts "Building Jekyll site"
    system "jekyll build"

    system "git checkout #{DEPLOY_BRANCH}"

    puts "Copying #{DESTINATION_FOLDER} to root"
    system "cp -r #{DESTINATION_FOLDER}/* . && rm -rf #{DESTINATION_FOLDER}" 

    puts "Adding .nojekyll to root"
    system "touch .nojekyll"

    unless git_clean?
      puts "Pushing to #{DEPLOY_BRANCH}."
      system "git add -A && git commit -m \"Site updated at #{Time.now.utc}\""
      system "git push origin #{DEPLOY_BRANCH}"
    else
      puts "No changes found. Deploy aborted."
    end

    system "git checkout #{SOURCE_BRANCH}"
  end
end

These are the steps git:deploy takes:

  1. Verifies that the source branch is checked out, and no changes need to be committed
  2. Builds the site locally
  3. Switches to master branch
  4. Copies the compiled site into the root and deletes the destination folder
  5. Adds an empty .nojekyll file at root to opt out of Jekyll processing
  6. Commits the new site files and pushes to master
  7. Switches back to source branch

Before you deploy, exclude unnecessary files in the build to sanitize the compiled site by setting or appending value exclude: [] in _config.yml.

# _config.yml

exclude: [Gemfile, Rakefile, README]

That’s the end of the setup. Commit your source changes and deploy.

$ rake git:deploy