Last month, I participated at an internal hackathon. I picked the goal to pull data from our current solution for documenting styles via ReST and render it as static site, while everything was tracked in git. Some other developers and a designer joined me in it and in the end we even won a prize :-)

In order to not forget my learnings, I want to document them. I even redid my homepage in harp.js, to learn more of how it works. So let’s go!

Installation

First, install harp.js via npm: npm install harpjs. In order to continue, I’d recommend to create a harp.json and a directory where you put your content in.

Convention over configuration

Harp.js works with conventions. It expects you to have a harp.json in the root of a directory and a public directory with your content.

Every file or directory under public which starts with an underscore will not be served.

Harp.js can handle layouts in jade (sic!) and ejs out-of-the-box. Since Jade was renamed in Pug due to trademark issues, I will go for ejs (embedded JavaScript) here.

On the CSS side you can simply pick one of several preprocessors and harp.js will automatically compile it for you. If you don’t like JavaScript, you can use CoffeeScript, too. Inside the public directory, you can create _data.json files which hold meta data of your files.

File structure

Here’s my file structure:

.
|- harp.json
|- package-lock.json
|- package.json
|- public
  |- _data.json
  |- _layout.ejs
  |- _partials
    |- footer.ejs
    |- head.ejs
    |- header-navigation.md
    |- header.ejs
    |- matomo.ejs
    |- scripts.ejs
  |- demo
    |- index.md
  |- index.md
  |- layout.less
  |- normalize.css
  |- sitemap-website.xml.ejs
  |- sitemap.xml.ejs
  |- slides
    |- index.md
  |- typography.less

Digging deeper

Layout

Let’s start with _layout.ejs. I prefer to have a very rough one which gets refined further down. Therefore mine looks like this:

<!doctype html>
<html lang="en">
  <head>
      <%- partial("_partials/head", { "pageTitle": title }) %>
  </head>
  <body>
    <div class="wrapper">
      <header>
        <%- partial("_partials/header") %>
        <%- partial("_partials/matomo") %>
      </header>
      <main>
        <%- yield %>
      </main>
      <footer>
        <%- partial("_partials/footer") %>
      </footer>
    </div>
    <%- partial("_partials/scripts") %>
  </body>
</html>

As you can see, I am mainly making usage of partial and yield. The former allows me to load a file under _partials, whereas the markdown content gets rendered in the place of the latter.

The class="wrapper" is needed in order to use CSS grid properly, since I can’t use it on body itself (a large gap would appear at the bottom).

Partials

Let’s look at the first partial, where I am passing an additional argument:

<meta charset="utf-8" />
<title><% if (pageTitle) { %><%- pageTitle %><% } else { %><%- globals.title %><% } %></title>
<link rel="pingback" href="https://webmention.io/webmention?forward=https://jaenis.ch/webmention" />
<link rel="webmention" href="https://webmention.io/jaenis.ch/webmention" />
<link rel="stylesheet" href="/normalize.css" />
<link rel="stylesheet" href="/layout.css" />
<link rel="stylesheet" href="/typography.css" />

As you can see, the pageTitle would be rendered between <%- .. %> if present. Otherwise I am falling back to a globally defined title, which you can define in harp.json. My WebMention are already somewhat working too, but I didn’t grokked them enough to blog about it, yet.

You may have noticed, that in the file structure above, I am using .less files, but here I am using references to plain .css. HarpJS does the transformation for me. I will explain how to define the target extension later, when I am coming to the creation of a sitemap.xml.

Another interesting partial could be header.ejs, so let’s look at its content:

<a href="/">Home</a>
<nav>
  <%- partial("header-navigation") %>
</nav>

As you can see, you can use partials inside partials! Moreover, inside the directory, you are referring to other files with their relative path. This makes it possible to group them in logical directories, which can be moved around without the need to rename the references inside them.

harp.json and _data.json

Next, look at harp.json:

{
    "globals": {
        "uri": "https://jaenis.ch",
        "title": "André Jaenisch"
    }
}

Here we can define variables, which will become site-wide available. If you need locally variables, use _data.json like so:

{
    "sitemap.xml": {
        "layout": false
    },
    "sitemap-website.xml": {
        "layout": false
    }
}

With setting layout to false I am telling HarpJS to not render those files through the _layout.ejs we saw earlier. Instead I am treating them as is (but with the ability to use Embedded JS!

Sitemaps

Let’s have a look at the sitemap files! Since I am using another engine for my blog, I need a way to refer to different sitemap files. Therefore I will use <sitemapindex>:

<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <sitemap>
    <loc><%= uri %>/sitemap-website.xml</loc>
    <lastmod><%= (new Date()).toISOString() %></lastmod>
  </sitemap>

  <sitemap>
    <loc><%= uri %>/blog/sitemap.xml</loc>
    <lastmod><%= (new Date()).toISOString() %></lastmod>
  </sitemap>
</sitemapindex>

Pay attention on the filename /public/sitemap.xml.ejs. Here I am advising HarpJS to render this file and output it as sitemap.xml in the root directory of my website. sitemap-website.xml.ejs is controlled by HarpJS, too.

I want to express my gratitude to Ethan Dridge here, which blogged years ago how to generate a xml file in HarpJS. I took his work and refined it to make it easier to read:

<?xml version='1.0' encoding='UTF-8'?>
<urlset xmlns='http://www.sitemaps.org/schemas/sitemap/0.9'>
<%
var htmlPattern = /(\.html$)|(^index\.html$)/;
var ignoredKeys = [ '.git', '_data' ]
var ignoredFiles = [
  '404.html',
  'sitemap.xml',
  'sitemap-website.xml',
  'main.css'
]

function getDate (head, file) {
  var date = (new Date()).toISOString()
  var frontmatter = head[ '_data' ][ file ]

  if (frontmatter && frontmatter.date) {
    date = new Date(Date.parse(frontmatter.date)).toISOString()
  }
  return date;
}

function getFrequency (head, file) {
  var freq = 'daily'
  var frontmatter = head[ '_data' ][ file ]

  if (frontmatter && frontmatter.freq) {
    freq = frontmatter.freq
  }
  return freq
}

function getOptions (head, tail, file) {
  var basename = file.replace(htmlPattern, '');
  var date = getDate(head, basename)
  var freq = getFrequency(head, basename)

  if (basename !== '' && !basename.endsWith('/')) {
    basename += '/'
  }

  return {
    uri: uri,
    tail: tail,
    file: basename,
    freq: freq,
    date: date
  }
}

function renderSitemapEntry (options) {
  var entry = [
    '<url>',
    '<loc>' + options.uri + options.tail + options.file + '</loc>',
    '<changefreq>' + options.freq + '</changefreq>',
    '<lastmod>' + options.date + '</lastmod>',
    '</url>'
  ].join('')
  return entry
}

function tree (head, tail) {
  var options = {};

  for (key in head) { 
    files = head[ key ]; 
    if (!ignoredKeys.includes(key)) { 
      if (key === '_contents') { 
        for (i in files) { 
          file = files[i]
          if (ignoredFiles.includes(file)) { continue }
          options = getOptions(head, tail, file)
          %><%= renderSitemapEntry(options) %><%
        }
      }
    } else { 
      tree(files, tail + key + '/')
    }
  }
}
tree(public, '/') 
%>
</urlset> 

What’s going on here? Recursion! Read it from buttom to top. If you have questions, read it again. If there are still question marks in your head, drop me a mail. I know, that not everybody understands recursive algorithms easily :-)

Render it!

After we covered the interesting bits, let’s render it! First a word of warning: HarpJS will wipe the target directory first!

This took me about half an hour after my first try. Luckily my hoster provided a backup on a daily basis (meaning, I lost “only” 20 hours worth of analytics…).

With that being said, add some scripts to your package.json:

{
    "clean": "rimraf public/.*~",
    "compile": "harp compile",
    "harp": "harp",
    "precompile": "npm run clean",
    "server": "harp server"
}

And then run npm run compile for generating the static files to upload or npm run server for local development. The clean script removes some temporary files generated by Vim.

This article got pretty large already, so I am stopping here. You can find more information in HarpJS’ documentation, which explains a fair amount of what you can do.

If you give it a try yourself, blog about it and let me know (via e-mail). I am eager to learn new things :-)

Thanks for reading!