A practical setup of a GitHub Pages blog with custom deployment.
I’ve mentioned before that this blog is a custom deployed one. While GitHub Pages support some of the Jekyll plugins, I’d like to have more flexibility and freedom to use any other plugins, some of which I’ve made myself. As stated in the GitHub Pages documentation,
Other plugins are not supported, so the only way to incorporate them into your site is to generate your site locally and then push your site’s static files to your GitHub Pages site.
So this is exactly what I’m doing here. I think it may be useful for someone if I share my setup of this blog’s development and deployment process.
I’m using a variation of the setup suggested by David
Jacquel on Stack Overflow. The idea is to have a single Git repository with the source and built site living in two separate branches. The master
branch contains the built static site and
is used for publishing content on http://wanderwaltz.github.io
. The source
branch contains the site source, templates, Jekyll plugins and all the stuff, which is used to actually build the site.
Calling
jekyll build
in the source directory builds the site inside a _site
subdirectory, which needs to be pushed to the root of the master
branch to be presented on http://wanderwaltz.github.io
.
To achieve this, I have two local git repos, each having the corresponding branch checked out. The directory structure looks like this:
.
├── .git // source branch is checked out
├── _drafts
├── _includes
├── _layouts
├── _plugins
├── _posts
├── _sass
└── _site
├── .git // master branch is checked out
├── .nojekyll // tells GitHub not to run Jekyll for the site
└── // the built site is here
The _site
directory is added to .gitignore
of the repo, so it is not committed or pushed to the
source
branch ever.
When editing the blog, I add posts to the _posts
directory of the outer repo, then run
jekyll build
and then commit and push both of the repos in order to deploy.
Doing all this by hand would be cumbersome and error-prone, so I’m using rake to perform each step automatically.
You can find the Rakefile
I am using here. It mostly functions by making system calls to jekyll
and git
, and I’ve had to add a helper function, which allows seeing the output of these commands interactively while the Ruby process is running:
At the time of writing my Rakefile
contains the following task definitions:
Simply calling
rake build
from the command line builds the site in the _site
directory by calling jekyll build
. It
also automatically includes the --lsi
build flag to produce an index for related posts (AFAIK,
--lsi
option is not supported currently when building the site on GitHub Pages, so this is another
reason to build locally).
Implementation of the build
task is pretty much straightforward:
You may notice that I am also using Bundler to manage the gems used by the project.
Calling
rake serve
from the command line invokes jekyll serve
and automatically opens the local blog in Safari. It is used while writing new posts or editing the layout to allow checking the immediate results in browser. It also includes the --lsi
flag.
serve
task is also implemented as a helper function with a small twist of having a suffix
parameter, which is used to run another shell command while the first one is still running.
jekyll serve
will run indefinitely unless stopped and won’t allow us to open Safari right after the site is built, so I am doing it simultaneously with a fixed delay. There is a chance that the site will not yet be built and served by the time the delay ends, but it works for now.
Calling
rake commit["Message"]
adds and commits all files in the root directory to the source
branch of the repo with the commit message provided. It then builds the site and commits the output to the master
branch with the same commit message. Since the master
branch is checked out in the _site
directory, where the build results are located, we will have both the source and the built site committed to their respective branches of the main repo (remember that both directories have the same repo cloned, just with different branches checked out).
Note that all of the tasks and helper functions execute synchronously, so the commit
function will first build the site and only after Jekyll finishes working will it commit all changes in both
of the repos.
A noteworthy detail is that each call of the execute
function spawns a new shell process, so no state is preserved between the calls. Because of that, we have to make all the commands essentially
one-liners by joining them via &&
. This also allows us doing cd _site
in the second step and not worrying about returning to the current directory after git
has finished.
Calling
rake publish["Message"]
does the same thing as commit
, but also pushes both branches to the remote. There is also an option to call publish
without parameters
rake publish
which then only pushes both of the branches to remote without committing anything.
Implementation of publish
task is almost trivial:
Calling
rake init
Resets the _site
directory completely by cloning a fresh master
branch from GitHub repository.
In order to tell GitHub to leave the site alone and don’t try to build anything, an empty .nojekyll
file is added to the repo’s master
branch. Jekyll ignores hidden files by default and therefore would delete the .nojekyll
file each time the site is built (it deletes the _site
contents including the .nojekyll
, but leaves .git
alone - probably a feature of Jekyll itself).
In order to keep .nojekyll
in the _site
forever, I’ve pushed it to the source
branch and added the following line into the _config.yml
:
On the other side, Jekyll copies all files not starting with .
or _
into the build directory, so lots of internal stuff ends up there while I do not really want this to happen. Thankfully, Jekyll configuration supports file exclusion too:
Deploying a locally built Jekyll site on GitHub Pages seemed to be a cumbersome task at first, but with the right setup and the help of rake
it becomes a piece of cake. Writing a good post is a lot more challenging problem than calling
rake publish["Add a new post"]
in the command line to finish the job afterward.