2015 update — The latest GitHub Pages stack supports some widely-used plugins, so you might not need to use the workflow described below. Post has been updated to use Jekyll 2.

GitHub Pages basically runs your project's gh-pages branch through jekyll build --safe. "Unfortunately", the --safe flag prevents from loading any plugins, when many people would want to use some not supported by GitHub Pages. There is no way to bypass this flag, but there is a workaround.

The idea is quite straightforward:

  1. do not rely on GitHub Pages to generate your content
  2. run the jekyll command locally (allowing for using any plugins)
  3. generate a clean, local gh-pages branch ready for deployment
  4. deploy by pushing to GitHub's remote gh-pages branch

This workflow is well known. Now, the challenging point might be #2: generating the cleanest gh-pages branch possible. It's by no means mandatory, but a nice optimization to perform when generating the website locally. Kind of a challenge if you will!

A clean gh-pages branch

Say you have the following brand-new Jekyll project:

$ jekyll new mywebsite
New jekyll site installed in /home/user/mywebsite/
$ cd mywebsite
$ ls
about.md  _config.yml  css  feed.xml  _includes  index.html  _layouts  _posts  _sass

Running jekyll build will generate your static content into a _site/ folder (default configuration).

Once this project is tracked with git, one could simply create a gh-pages branched off of master. It could be nice though to have the gh-pages branch contain only the latest version of the website as available in _site/, without any git history or non-production content.

The goal is to have gh-pages contain:

# While on gh-pages:
$ tree .
├── about
│   └── index.html
├── css
│   └── main.css
├── feed.xml
├── index.html
└── jekyll
    └── update
        └── 2015
            └── 10
                └── 18
                    └── welcome-to-jekyll.html

7 directories, 5 files

And it shall not have any remembrance of master's history:

# While on gh-pages:
$ git log --pretty=oneline
a05b0365bf2e91e852a0440a230957a975ed7275 New release

Such a branch would be perfect for deployment and could be considered a one-time branch, ie. one would delete it once the website has been deployed.

Note that this workflow is viable for any hosting platform, not GitHub Pages only. As long as you are able to generate _site/ and deploy it somewhere for a webserver to, well, serve, you're pretty much done. Deployment can use git to push to a remote repository, as is the case with GitHub Pages, but may also leverage (s)ftp/scp/rsync/capistrano/whatever. The core idea is always: compile the site locally then deploy. Even if you may not use git to deploy, tracking the project's history with git locally is a good idea.

Using git symbolic-ref

GitHub Pages’ documentation once advised folks to create a root gh-pages branch. Although not the case anymore, I still believe it's a great, elegant solution.

A root branch in our Jekyll context is kind of a "static", master-based, short-lived branch which will exist within the same git repository, but here with a different content structure than master's. Such a branch is thus unlike your traditional git checkout -b generated branch: our goal is for it to not have a proper "history" tracking of what happened in the base master branch, and to feature custom content which will not necessarily match the shape of the base master branch.

We can create such a strange beast leveraging git references and the symbolic-ref and clean git commands. First, let's "duplicate" master, its whole content:

# Make sure your are on master, with a clean working directory.

# Generate latest _site/.
$ jekyll build

# Let's "switch" to a gh-pages branch by overriding HEAD's semantic.
$ git symbolic-ref HEAD refs/heads/gh-pages

# Let's make all content untracked by git while on gh-pages, so it may
# be removed at will.
$ rm .git/index

Then comes the important step: remove everything except _site/:

# Let's remove all untracked content, but the _site/ folder.
$ git clean -fdx -e _site/

Be aware running git clean like that is a dangerous operation. For one thing it will wipe out any content untracked by git. Prevent this from happening by either tracking or .gitignoring any file or directory. A more advanced workflow could selectively remove lines from .git/index and feed a whitelist of files to remove to git clean. The simpler the better IMO though. Think "Spiderman" here.

Let's move all of the content at root level, otherwise Jekyll's base_url would need to include "/_site/".

$ mv _site/* .
$ rm -R _site/

You’re pretty much done with generating the "static" gh-pages branch! At this stage, it contains only the generated website, has no proper history (yet), and is basically ready for deployment… So let's deploy:

$ touch .nojekyll

# Deploying to GitHub Pages for the sake of the example.
# You could deploy to any host, using any method, really.
$ git add .
$ git ci -m "New release"
$ git push origin gh-pages

# Cleaning up.
$ git checkout master
$ git branch -D gh-pages

Note an empty .nojekyll file has been added along the way, so as to instruct GitHub not to bother trying to assess whether Jekyll should run. For it should not.

That strategy, although efficient, is a lot of git gymnastic to remember about: it should be encapsulated within a proper deployment script. A raw bash script, capistrano, a Thor task… Below is an example of a deploy.sh bash script I use to deploy this very website:


# Building release.
jekyll build
git symbolic-ref HEAD refs/heads/release
rm .git/index
git clean -fdx -e _site/
mv _site/* .
rm -R _site/

# Deploying to DigitalOcean.
git add .
git ci -m "New release"
rsync -avzhe ssh --delete --progress .

# Cleaning up.
git checkout master
git branch -D release

Works for me!