Personal Website Redesign
by Michael Brundage Created: 15 Apr 2014 Updated: 20 May 2014
After 8 years of neglect, I recently recreated my personal website. This note describes the new site build, deployment, and design.
Contents |
Build
I wanted statically-generated site content using a mix of Markdown and HTML.
After surveying the available template languages, I actually set out to create my own. I may eventually release that software, once it’s mature enough to support this site. However, that project was taking longer than I was willing to spend on it; I wanted results faster.
So, after taking another look at available technologies, I settled on Jekyll, which uses Liquid for templates. Jekyll offered most of the features I needed, including LaTeX (using MathJax) and code syntax highlighting (using Pygments) out of the box.
Jekyll Setup
Ruby is a bit of a pain to install on Dreamhost. I followed these instructions but ran into compiler errors, apparently because GCC was running out of memory. What worked for me was to then cd into the build tree (see the log for its location) and manually make
and make install
.
I also needed to install some dependencies, for Jekyll and the plugins and config options I use:
pip install pygments
gem install psych -- --enable-bundled-libyaml
gem install kramdown
gem install nokogiri
gem install jekyll-assets
gem install uglifier
gem install sass
gem install jekyll
Jekyll itself was pretty straightforward to setup and use, except that I have multiple collections. Support for multiple collections is under development in Jekyll but not yet released. I tried using the proto version of it, and found it didn’t do half the things I needed, so I used the jekyll-page-collections plugin to handle the ‘Projects’ and ‘Notes’ parts of this site.
Jekyll Plugins
I ended up needing quite a few plugins. I’ve written 11 so far and counting. It’s great that Jekyll is flexible enough to enable this, but I have run up against some limitations.
I didn’t see a plugin for generating Amazon affiliate links, but it’s trivial to do this in Jekyll; here it is in its entirety:
# Implement Amazon Affiliate program links.
# By Michael Brundage (michael@michaelbrundage.com)
#
# Add your affiliate ID to _config.yml for the amazon key, like:
#
# amazon: michaelbrundage
#
# And then use any product ID with this filter, like:
#
# {{ '0321165810' | amazon }}
#
# To get the URL
# https://www.amazon.com/gp/product/0321165810?tag=michaelbrundage
module AmazonFilter
def amazon(input)
affiliate_id = @context.registers[:site].config['amazon']
if (affiliate_id.nil?)
raise FatalException.new("Missing required 'amazon' affiliate id in _config.yml.")
end
"https://www.amazon.com/gp/product/#{input}/?tag=#{affiliate_id}"
end
end
Liquid::Template.register_filter(AmazonFilter)
Save that as amazon.rb
in your _plugins
folder and use it in templates as {{ ASIN | amazon }}
. For example, {{ '0321165810' | amazon }}
.
I also wanted to change the highlighter markup so that it includes the language canonical name nicely formatted. My solution to that was to override an internal method of the builtin HighlightBlock, which is fragile but works for now. While I was at it, I also tracked whether a page uses any syntax highlighting, so that I could conditionally include the syntax.css file only on those pages that need it, for performance.
module Jekyll
class MyHighlightBlock < Jekyll::Tags::HighlightBlock
def initialize(tag_name, markup, tokens)
super
end
def render(context)
context.environments.first['page']['highlight'] = 1
result = super
result.gsub(/\n\n/, "<br/>\n")
end
def add_code_tag(code)
lang = @lang.to_s.gsub("+", "-")
lang = Pygments::Lexer.find_by_alias(lang).name
code = code.sub(/<pre>\n*/,"<pre rel='#{lang}'><code>")
code = code.sub(/\n*<\/pre>/,"</code></pre>")
code.strip
end
end
end
Liquid::Template.register_tag('highlight', Jekyll::MyHighlightBlock)
Normally, Jekyll can include files only from the _includes
directory. To show source code here from the _plugins
directory (or any other source code of the site), I wrote a plugin to include files from arbitrary locations. For example, {% includeraw _plugins/includeraw.rb %}
displays this plugin itself:
module Jekyll
# Include any file, not just things in the _includes directory.
# Also allows dynamic file names.
# Adapted from https://github.com/jekyll/jekyll/issues/176#issuecomment-6757780
class IncludeRawTag < Jekyll::Tags::IncludeTag
def render(context)
file = context[@file] || @file
includes_dir = context.registers[:site].source
if File.symlink?(includes_dir)
raise FatalException.new("Includes directory '#{includes_dir}' cannot be a symlink")
end
if file !~ /^[a-zA-Z0-9_\/\.-]+$/ || file =~ /\.\// || file =~ /\/\./
return "Include file '#{file}' contains invalid characters or sequences"
end
Dir.chdir(includes_dir) do
choices = Dir['**/*'].reject { |x| File.symlink?(x) }
if choices.include?(file)
source = File.read(file)
else
raise FatalException.new("Included file '#{file}' not found")
end
end
end
end
end
Liquid::Template.register_tag('includeraw', Jekyll::IncludeRawTag)
I also wrote a plugin to generate a sitemap that handles all page-collections and static files, another plugin to generate TOC (there are several out there, but they all had problems with my site design), and others. Eventually, I’ll put these up on GitHub.
I also use some third-party plugins. See the license page for more information.
Deploy
There are many ways to deploy Jekyll-generated content. I’ve used git for deployments on many projects now, and it works well for my purposes.
When using git on DreamHost, it’s necessary to configure headless git to play nicely in their environment by creating a ~/.gitconfig
file. I use these settings:
[pack]
windowMemory = 60m
packSizeLimit = 60m
deltacachesize = 50m
threads = 1
[core]
packedgitwindowsize = 50m
packedgitlimit = 50m
deltacachesize = 50m
threads = 1
excludesfile = /home/USERNAME/.gitignore
[user]
name = USERNAME, on the server
email = USER@DOMAIN.COM
replacing USERNAME with your username and USER@DOMAIN.COM with your email address.
I also use rbenv there (installed into ~/local/
, and added to ~/.bash_profile
and ~/.bashrc
).
There are many ways to use Git for deployment, but they’re all similar in spirit to this. You set up a bare repo to use as the remote for local deployment and a separate repo to use for the deployment source tree, and then finally configure a post-receive (or post-update) hook in the bare repo that causes the other repo to pull.
Those instructions build locally and push the build products to the remote server. I like to also build remotely, which involves adding jekyll build
to the hook, with either the _config.yml
file set to build to the right place, or else passing --destination
to the jekyll build command.
Note that, whether you build locally or remotely, this deployment method is non-atomic. I recommend deploying atomically, by combining this with Capistrano or an equivalent method. Otherwise, you risk an outage if any errors occur.
Development
I use Pow and jekyll build --watch
(instead of jekyll serve --watch
). Pow makes vhost setup painless, and also enables testing on other devices on your LAN, which is very useful for testing mobile and other browsers.