Yesterday I blogged about the new static-site hosting service, Surge. In order to test it, I decided to rebuild JavaScript Cookbook as a static site. (Which, to be honest, was a silly decision. Surge takes about five minutes to use. My rewrite took about five hours. ;) I decided to give Jekyll a try and I thought I'd share my thoughts about the platform. Obviously I've just built one site with it so take what I say with a grain of salt, but if you're considering setting up a static site, maybe this post will be helpful.

jekyll

Jekyll, like HarpJS, is run via a command line tool. Unlike Harp, Jekyll is a Ruby-based tool but you don't need to know Ruby in order to use it. I had kind of a crash course in Ruby while I worked with it, but that's only because of some of the requirements I had while building out my site. The full requirements are documented with the big red flag being that there is no Windows support. There's unofficial support, but I'd be wary of committing to Jekyll if you need to support developers on the Windows platform.

Once installed, you can fire up the Jekyll server from the command line and begin working. Jekyll will automatically refresh while you work so it is quick to get up and running. Speaking of testing, the command line includes an option to create a default site, simply do jekyll new directoryname.

At this point you can start typing away and testing the results in the browser. I'm assuming most of my readers are already familiar with why tools like this are cool, but in case you aren't, the point of a static site generator is to let you build sites in a similar fashion to dynamic server-side apps but with a flat, static file as the output. So as a practical matter that means I can build a template and simply use a token, like {{body}}, that will be replaced with a page's content. I can write a page and just include the relevant data for that page and when viewed in the browser it will automatically be wrapped in the template. This isn't necessarily that special - it's 101-level PHP/ColdFusion/Node stuff - but the generator tool will spit out flat HTML files that can then be hosted on things like S3, Google Cloud, or, of course, Surge.

For its templates, Jekyll allows for Markdown and Liquid. It does not support Jade, because Jade is evil and smelly and shouldn't be supported anywhere. I found Liquid to be very nice. You've got your basics (variable outputting, looping, conditionals) as well as some powerful filters too. For example, this will title case a string: {{ title | capitalize}}. This will do truncation: {{ content | truncate: 200, '...' }}. You can do this with EJS templates in HarpJS as well (but I didn't know that till today!).

The other big change in Jekyll is how it handles data for content. In Harp, this is separated into a file unique to a folder. In Jekyll, this is done via "front matter", basically formatted content on top of a page. Initially I preferred Harp's way, but the more I played with Jekyll the more it seemed natural to include it with the content itself.

You can, if you want, also include random data files, which is cool. If you need something that isn't related to content you could abstract it out into a JSON or YAML file and make use of it in your site. Hell, you can even use CSV.

As a trivial example of a Liquid file, here's a super simple page I use for thanking people after they submit content. It doesn't have anything dynamic in it at all, but the content on top tells Jekyll what template to use and passes on a title value.

---
layout: default
title: Thank You!
---

<p>
Thanks for sending your content submission. I'll try to respond as soon as possible. If
for some reason I don't get back to you, please feel free to drop me a line via email (raymondcamden at gmail dot com).
</p>

Here is a slightly more complex example, the default layout for the site. Note the use of variables and conditions for determining which tab to highlight.

<!doctype html>
<html lang="en">
    <head>
        <title>{{page.title}}</title>
    <link rel="stylesheet" href="/css/bootstrap.min.css" type="text/css" />
    <link rel="stylesheet" href="/css/app.css" type="text/css" />
    <script src="/js/jquery-2.0.2.min.js"></script>
    <script src="/js/bootstrap.min.js"></script>

    <script src="/js/prism.js"></script>
    <link rel="stylesheet" href="/css/prism.css" />
    <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.javascriptcookbook.com/rss" />
    </head>
    
    <body>
    <div class="container">
      
      <div class="navbar navbar-inverse">
        <div class="navbar-inner">
          <div class="container" style="width: auto;">
          <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
          </a>
          <a class="brand" href="/">JavaScript Cookbook</a>
          <div class="nav-collapse">
            <ul class="nav">
            <li {% if page.url == '/index.html' %}class="active"{% endif %}><a href="/">Home</a></li>
            <li {% if page.url == '/submit.html' %}class="active"{% endif %}}><a href="/submit.html">Submit</a></li>
            <li {% if page.url == '/about.html' %}class="active"{% endif %}><a href="/about.html">About</a></li>
            
            </ul>
            <form class="navbar-search pull-right" action="/search.html" method="get">
            <input type="search" class="search-query span2" placeholder="Search" name="search">
            </form>
          </div><!-- /.nav-collapse -->
          </div>
        </div><!-- /navbar-inner -->
        </div><!-- /navbar -->

        {{content}} 
    
    </div>

<script>
  (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
  (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
  m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
  })(window,document,'script','//www.google-analytics.com/analytics.js','ga');

  ga('create', 'UA-70863-21', 'javascriptcookbook.com');
  ga('send', 'pageview');

</script>
    
    </body>
</html>

One of the cooler aspects of Liquid is the assign operator. Given that you have access to data about your site, a list of articles for example, you can quickly slice and dice it within your template. While Jekyll makes it easy to work with blog posts, my content was a bit different. I needed a quick way to get all my article content and sort it by the last date published. Here's how the "Latest Articles" gets generated.

<h3>Latest Articles</h3>

{% assign sorted = (site.pages | where:"layout","article" | sort: 'published' | reverse) %}

{% for page in sorted limit:5 %}
    <p>
    <a href="{{page.dir}}">{{page.title}}</a> - {{page.published | date: "%-m/%-d/%y at %I:%M" }}
    </p>
{% endfor %}

Like I said, that assign command just makes me happy all over.

So this is all well and good - but there is one killer feature of Jekyll that makes me think this may be the best tool for the job I've seen yet - plugins. Jekyll lets you create multiple additions to the server to do things like:

  • Create generators - code that will create new files for you
  • Add tags to the Liquid template system
  • Add filters that can be used in assign calls

These plugins must be written in Ruby, but even with my absolute lack of knowledge in the language I was able to create two plugins to complete my site. Let me be clear - without these plugins I would not have been able to complete the conversion. (Well, I would have had to do a lot more work.) Let me give you a concrete example of where this helps.

One of the issues you run into with static-site generators is that they require one file per URL. What I mean is - for every page of my site, from the home page, to the "About" page, to each piece of blog content, you will have one physical file. That's certainly OK. I just add a file, write my content, and I know I get the benefits of automatic layout, variable substitution, etc. But there are some cases where this requirement is a hinderance.

Imagine you have N articles. Each article has a set of assigned tags. In Harp this would be defined in your data file, in Jekyll this would just be front matter. Here's a sample from one of the JSCookbook articles:

---
layout: article
title: Check if a value is an array
published: 2014-10-23T21:18:46.858Z
author: Maciek
sourceurl: 
tags: [array]
id: 544970b682f286f555000001
sesURL: Check-if-a-value-is-an-array
moreinfo: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray
---

Now imagine I want to make one page for each tag. Normally I'd have to:

  • Figure out all my tags. That's not necessarily a bad thing - you may only have 5-10 static tags.
  • Make a file each for tag, called sometag.html.
  • Write the code that slurps content and displays items that match that tag.
  • Include that code in every page. Both Harp and Jekyll support template languages that make this easy.

At the end I have N pages, one for each tag. If I remove a tag, or add one, I have to remember to create a new flat file. Not the end of the world, but something you could forget.

With Jekyll, I can use a plugin to create a generator. This will run on server startup and when things change, and can add new pages to the system dynamically. Here is a plugin I wrote to handle my tag issue. Keep in my mind I'm probably better at ballet dancing then Ruby.

module Jekyll

class TagPage < Page def initialize(site, base, dir, tag, pages) @site = site @base = base @dir = dir @name = 'index.html'

#print "Running Tag page for "+tag+" "+pages.length.to_s+"\n"

self.process(@name) self.read_yaml(File.join(base, '_layouts'), "tag.html") self.data['tag'] = tag self.data['title'] = tag self.data['pages'] = pages end end

class TagPageGenerator < Generator safe true

def generate(site)

dir = "tag/"

#create unique array of tags unique_tags = {} site.pages.each do |page| if page.data.key? 'layout' and page.data["layout"] == 'article' #print page.data #print "\n" page.data["tags"].each do |tag| if !unique_tags.include?(tag) unique_tags[tag] = [] end unique_tags[tag].push(page) end end

end

#print "unique tags: "+unique_tags.keys.join(",") + "\n"

#create page for each unique_tags.keys.each do |tag| site.pages << TagPage.new(site, site.source, File.join(dir, tag), tag, unique_tags[tag]) end

end end

end

And that's it! (A big thank you to Ryan Morrissey for his blog post about this - I ripped my initial code from it.)

Another example of plugin support is adding your own tags. I needed a way to generate a unique list of tags for the home page. I wrote this plugin, which adds taglist to Liquid for my site.

module Jekyll
  class TagListTag < Liquid::Tag
    def initialize(tag_name, text, tokens)
      super
    end

    def render(context)
      tags = []
      context.registers[:site].pages.each do |page| 
        if page.data.key?'layout' and page.data["layout"] == 'article'
          if page.data.key?'tags'
            page.data["tags"].each do |tag|
              if !tags.include?tag
                tags.push(tag)
              end
            end
          end
        end
      end
      tags = tags.sort
      #now output list
      s = ""
      tags.each do |tag|
        s += "<li><a href='/tag/" + tag + "'>" + tag + "</a></li>"
      end
      return s
    end

  end
end

Liquid::Template.register_tag('taglist', Jekyll::TagListTag)

Again - I'm probably writing pretty crappy Ruby - but I love that I was able to extend Jekyll this way. If Harp could add this in - and let me use JavaScript - that would be killer.

And that was really it. I converted my form to use FormKeep and converted the search to use a Google Custom Search Engine. You can see the final result here: http://aloof-zephyr.surge.sh/.

As JavaScript Cookbook has not had the traffic I'd like (hint hint - I'm still looking for content!), I'll be pointing the domain to the static version so I can have a bit less Node out there. Once I add in Grunt support and add in Surge, I can write a post and push live in 30 seconds. I can't wait.

p.s. I didn't include a zip of the Jekyll version, but if anyone would like it, just ask.