Migrating a Static Site from Harp to Jekyll

So a few weeks back I blogged about how I was working on an update to CFLib. Specifically - I was looking to migrate to a new static site generator to make it easier to update content. This past weekend I made a lot of progress with my update and I think I'm ready to release the new version. I thought folks might be interested in the details of the rebuild.

The Current Version

The current version of the site is built with Harp, the first static site generator (SSG) I used and the impetus for my introduction to the technology as a whole. I've got a soft spot for Harp. It's incredibly simple compared to most SSGs and a quick way to create a simple site. As much as I appreciate Harp, it hasn't been updated in a while and I'm not sure I could recommend it anymore. If you know you're building something really simple, maybe, but even then I worry the project isn't going to be around for much longer.

When I was building CFLib, I had to find a way to support the one thousand plus UDFs in a way that would let me tweak the layout if I needed to modify something. Harp, like most SSGs, requires a physical file for each piece of content. (Jekyll actually has an interesting way around that, but that's not important right now.) Each UDF was a one line file where I simply included an EJS file to render the content. So for example:

<%- partial("_udf.ejs") %>

And here is the main template. EJS is kind of a icky templating language. It's flexible and it works, but it reminds me a lot of old ASP sites.

<%- include('../_udf.ejs') %>

<%
    //need to get udf name
    var udf = public.udfs.getUDFByName(current.source);
    title = udf.name;
%>

<h2><%- udf.name %><%- public.udfs.getArgString(udf.args)%></h2>
<h5 class="lastUpdated">Last updated <%- public.udfs.dateFormat(new Date(udf.lastUpdated)) %></h5>

<div class="author">
    <h3><span>author</span></h3>
    <p>
    <img src="<%= public.udfs.gravatar(udf.authorEmail) %>" title="<%- udf.author %>" class="img-left gravatar" />
    <span class="name"><%= udf.author %></span> <br/>
    </p> 
</div>
<p class="description">
    <strong>Version:</strong> <%- udf.version %> |  
    <strong>Requires:</strong> <%- udf.cfVersion %> | 
    <strong>Library:</strong> <a href="/library/<%- udf.library %>"><%- udf.library %></a>
</p>

<div class="udfDetails">    
<p>
    <strong>Description:</strong> <br/>
    <%= udf.description %>
</p>
<p>
    <strong>Return Values:</strong> <br/>
    <%- udf.returnValue %>                        
</p>

<a name="examples"></a><p><strong>Example:</strong></p>

<pre><code class="language-markup"><%= udf.example %></code></pre>

<p><strong>Parameters:</strong></p>

<%
    if(udf.args.length > 0) {
    
%>
    <table id="paramsTable" cellpadding="0" cellspacing="0">
        <tr>
            <th>Name</th>
            <th>Description</th>
            <th>Required</th>
        </tr>
        <% for(var i=0;i<udf.args.length;i++) { %>
            <tr>
                <td><%- udf.args[i].NAME %></td>
                <td><%= udf.args[i].DESC %></td>
                <td><%- udf.args[i].REQ? "Yes":"No" %></td>
            </tr>
        <% } %>
    </table>
<% } else { %>
<p>No arguments.</p>
<% } %>


<p><strong>Full UDF Source: </strong></p>
<pre><code class="language-<%- udf.tagBased?"markup":"javascript" %>"><%= udf.javaDoc %>
<%= udf.code %></code></pre>

<div id="disqus_thread"></div>
<script type="text/javascript">
    var disqus_shortname = 'cflib';
    var disqus_identifier = '<%- udf.oldId %>';

    /* * * DON'T EDIT BELOW THIS LINE * * */
    (function() {
        var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
        dsq.src = 'http://' + disqus_shortname + '.disqus.com/embed.js';
        (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
    })();
</script>
<noscript>Please enable JavaScript to view the <a href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
<a href="http://disqus.com" class="dsq-brlink">blog comments powered by <span class="logo-disqus">Disqus</span></a>

</div>

Not pretty, right? But it works. However, you may be asking - where's the data? Most SSGs support something called "front matter", which is basically a way to embed data on the top of a static page. This data is stripped out before rendering so it's not something the public sees, and it can be used across the site in other ways as well. So for example, your home page can show a list of titles from various blog posts where the title is embedded in front matter.

So yeah - Harp doesn't support that. Instead, you store all your data in JSON files. That's fine - I mean - JSON is easy to edit. So when I convert CFLib from dynamic to static, I simply wrote ColdFusion code to read from the database of UDFs and generate one giant JSON file of data. Giant. Like, huge.

This worked just fine until I actually needed to edit code. I had to escape code, craft a JSON block, insert it at the end of my giant JSON file, and then hope I didn't screw things up.

It got so bad that - frankly - I just stopped updating. While both traffic and submissions to CFLib has slowed considerably, I know it is still a resource for ColdFusion developers and I absolutely still want it to be available.

With that in mind - I began the conversion to Jekyll.

The New Version

The new version is built with Jekyll, my current favorite SSG. It doesn't power this site because as much as I like it, speed isn't it's best feature. (And I'll talk a bit more about speed in a bit.) Also, it uses Ruby, which I'm not a fan of, but I can get over that. The thing I like most about Jekyll is how flexible it is. Don't get me wrong - I think I can do the same stuff in Hugo that I can with Jekyll, but I swear Hugo seems to fight against me when I'm building something unique. Even the smallest thing in Hugo is awkward. (To me.) On the flip side, in Jekyll, I never worry about it. Assuming I can get past installation weirdnesses, once it's up and running I'm not concerned about being able to build what I need.

The biggest change in the Jekyll version revolves around UDFs. In Harp, a UDF is driven from:

  • one mostly empty physical file
  • a template file
  • getting data from a large JSON packet

In Jekyll, I've still got one physical file per UDF. However now the data is embedded in YAML. The file is still "empty", but the data is much easier to get to. Here's one example.

---
layout: udf
title:  acronym
date:   2013-07-18T07:48:25.000Z
library: StrLib
argString: "sTerms"
author: Jordan Clark
authorEmail: JordanClark@Telus.net
version: 1
cfVersion: CF5
shortDescription: Pass in a set of words to only display its acronym.
tagBased: false
description: |
 Takes a full string of words as input then converts it for proper display in the html &lt;acronym&gt; tag. That way you see the acronym but in most browsers you can put your mouse over the acronym to display its full meaning. I often see acronyms used in Blogs.

returnValue: Returns a string.

example: |
 <cfoutput>I love #acronym( "Hyper Text Markup Language" )#.</cfoutput>

args:
 - name: sTerms
   desc: String to modify.
   req: true


javaDoc: |
 /**
  * Pass in a set of words to only display its acronym.
  * 
  * @param sTerms      String to modify. (Required)
  * @return Returns a string. 
  * @author Jordan Clark (JordanClark@Telus.net) 
  * @version 1, July 18, 2013 
  */

code: |
 function acronym(sTerms) {
     return '<acronym title="' & sTerms & '">' & trim( reReplaceNoCase( " " & sTerms & " ", "(\w)\w+\s", "\1", "all" ) ) & '</acronym>';
 }

---

For the most part, the only issue I ran into here was figuring out various YAML aspects. So for example, the args portion is an array of structs. In the sample above the array has one element, but I had to find out exactly how it was done. While testing I ran into a few more issues (like needing to escape colons), but for the most part, it worked well. I started off with hard coded UDFs, a few, and once I was convinced my layout was ok I automated it with a Node script.

For comparison's sake, here's the layout using Jekyll's templating language, Liquid.

---
layout: default
---

<h2>{{ page.title }}({{ page.argString }}) </h2>
<h5 class="lastUpdated">Last updated {{ page.date | date: "%B %d, %Y" }}</h5>

<div class="author">
    <h3><span>author</span></h3>
    <p>
    <img src="{{ page.authorEmail | to_gravatar }}?s=43" title="{{ page.author }}" class="img-left gravatar" />
    <span class="name">{{ page.author }}</span> <br/>
    </p> 
</div>
<p class="description">
    <strong>Version:</strong> {{ page.version }} |  
    <strong>Requires:</strong> {{ page.cfVersion }} | 
    <strong>Library:</strong> <a href="/library/{{ page.library }}">{{ page.library}}</a>
</p>
<div class="udfDetails">    
<p>
    <strong>Description:</strong> <br/>
    {{ page.description }}
</p>
<p>
    <strong>Return Values:</strong> <br/>
    {{ page.returnValue }}
</p>

<a name="examples"></a><p><strong>Example:</strong></p>

<pre><code class="language-markup">{{ page.example | xml_escape }}</code></pre>

<p><strong>Parameters:</strong></p>

{% if page.args %}

<table id="paramsTable" cellpadding="0" cellspacing="0">
        <tr>
            <th>Name</th>
            <th>Description</th>
            <th>Required</th>
        </tr>
        {% for arg in page.args %}
            <tr>
                <td>{{ arg.name }}</td>
                <td>{{ arg.desc }}</td>
                <td>
                    {% if arg.req %}
                    Yes
                    {% else %}
                    No
                    {% endif %}
                </td>
            </tr>
        {% endfor %}
    </table>
{% else %}
    <p>No arguments.</p>
{% endif %}


<p><strong>Full UDF Source: </strong></p>
<pre><code class="language-{% if page.tagBased %}markup{% else %}javascript{% endif %}">{{ page.javaDoc | xml_escape }}{{ page.code | xml_escape }}</code></pre>

</div>

Personally, I like this a heck of a lot better.

There's more to it of course, but you can see the entire source code up on GitHub: https://github.com/cfjedimaster/cflib2017

There's only one feature missing now I want to add back in and that's Disqus support. All I'm missing for that is getting an ID value into the front matter that I'll need to do via my generator script. (You can see that in the repo, named generate.js.) I went ahead and got rid of the RSS feed because honestly there aren't enough updates to the site to make it worthwhile.

Now for the cool part - hosting. I've blogged about Netlify before. They host my blog and have an incredible service. While CFLib currently runs on Surge.sh, and it is a cool service, I prefer the power of Netlify.

With Netlify, the new CFLib gets - out of the box with me literally clicking a checkbox or two:

  • CDN distribution
  • Asset optimization (images, JS, CSS)
  • free https

And automatic deployment. I literally commit my code to the repo and Netlify kicks off an update. You can see the Netlify version here: https://cflib.netlify.com/.

How has performance improved? To be fair, the new site does not have Disqus yet, nor Adsense (I'm not sure I'll keep that), but using Pingdom's test at https://tools.pingdom.com, the original site had these metrics:

CFLib1

And here is the Netlify/Jekyll version:

CFLib2

Damn tootin'. What's cool is that since Netlify can build from GitHub, all someone needs to do to submit a new UDF or a fix is to make a pull request. Once I approve it, it's online about 5 minutes later. I love that.

And It Will Be Live...

Um, yeah, good question. So as I said, the Disqus integration is the only thing missing and it's probably a 5 minute fix, but I've got some intense travel coming up very soon where it will be difficult to manage any issues if things go wrong. So I either do it in the next few days or around Thanksgiving. I amy just saw screw it and push it tonight - we will see. Anyway, if you spot any problems on the Netlify version, please let me know in the comments below.

Like This?

If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can also subscribe to the email feed to get notified of new posts.

See Also