Personal Website Redesign

by 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.

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.