Blogging with Serve
This blog was built by generating a static site with Serve. It was deployed with rsync to an Ubuntu VPS at Linode, itself built using my Babushka deps.
Selecting a static site generator
I decided to use a static site generator because I wanted something Ruby-based, very simple, and easy to extend. I considered several before settling on Serve.
Jekyll is the static site generator that powers GitHub Pages. It uses Liquid templates, which do the job of ERB but can be safely rendered without trusting their author. This is perfect - and necessary - for GitHub Pages, but as I have full control of the rendering and hosting environments, it didn’t seem like the right fit. In hindsight, lighter templates are a good thing, and I wouldn’t let Liquid put me off using Jekyll for trusted sites in the future. I’ve also heard good things about Octopress, which builds on Jekyll to get you started blogging quicker.
Nanoc looked perfect. It has clean and extensive documentation. It uses a Rules file to specify mappings from content files, via filters and layouts, to final output. It has blogging helpers baked in. Things were going okay, but after a while the Rules file got frustrating. I felt like I was having to jump through too many hoops just to get back to simple static pages.
Serve Serve has a nice introductory screencast and straightforward documentation. It works like a slimmed down version of Rails views. You don’t have routes, controllers or models, just templating languages, layouts and helpers. You can run it as a rack app, or export your site to static files as I do.
Blogging with Serve
Serve wasn’t built with blogging in mind, so I needed to handle that myself. My requirements were modest: I wanted to have a table of contents page and an Atom feed generated automatically, and to write new posts in single files without repeating myself anywhere.
Embedding metadata in ERB templates
The first challenge was gathering post metadata. Nanoc and Jekyll let you include YAML at the top of your templates. Serve doesn’t, but I found a solution using just ERB and helpers.
I have each post append a hash of metdata to the end of an array in an instance variable, which is created if it doesn’t already exist. It also includes the template file name and the captured post HTML in the hash. I wrapped this process up in a helper, which also prepares data from the last post for presentation in the blog post layout, using #content_for
.
An individual post template look like this:
<%
capture_post_data(__FILE__, {
title: "An example post",
published_on: "1970-01-01",
updated_on: "1970-01-02"
}) do
%>
<p>A blog post!</p>
<% end %>
The helper is:
def capture_post_data(file, metadata, &content)
@post_data_raw ||= []
@post_data_raw << {
file: file,
containing_dir: file.split('/')[-2],
updated_on: nil,
topless_html: capture(&content)
}.merge(metadata)
@post_data_raw.last.each { |k,v| content_for :"last_post_#{k}", v }
end
Rendering all the post templates, in any order, will build up the instance variable of all the metadata, which is used by other helpers to create the table of contents and generate the Atom feed. Another helper, #dont_capture_post_data
, does the same without leaving the metadata hash in the array, allowing me to easily toggle posts in and out of publication in the index and feed.
Multi-level layouts
Serve lets you wrap your templates in layouts, written in files named _layout.html.erb
. It will use the one in the current directory, or the first it finds as it ascends the directory tree. This makes it easy to have branches of your site use the same layout, and to let certain lower directories override a more general layout from higher up. You can also render partial templates to factor out common fragments.
Duplication emerged in two places in my templates. The first was in my individual blog post directories, each of which had a _layout.html.erb
file pointing to the blog post layout (overriding the blog index layout from the parent directory). I resolved this by putting logic into the blog root’s layout file, to decide whether to render the post layout, the blog index layout or, for the feed file also in that directory, no layout.
The next issue was that I wanted the content of blog posts to be rendered by a blog post layout, but to have it share a wrapper with the blog index page, and to have those two use the same top level wrapper as the site’s other pages. It would be nice to let every template or layout specify what it should be wrapped in. This didn’t work out of the box, but I was able to cook up something similar using the #capture
helper.
My mid-level layouts embed themselves in higher level ones by capturing their own content and explicitly rendering the desired outer layout:
<% inner = capture do %>
<div>Mid-level markup</div>
<%= yield %>
<% end %>
<% content_for :inner, inner %>
<%= render template: "/layouts/application" %>
The outer layout, of course, runs yield :inner
to wrap itself around content captured at lower levels.
On reflection
Serve is really just about templates and view helpers and I’ve shoehorned in more structured blog content and metadata, and methods to manipulate them. These things would be more at home in a model class (database backed or otherwise). It works well enough for now, and it is still quite simple and very DRY, but there are surely better ways to do this.