At Optimised Labs, we are always looking for ways to improve our processes. Recently I have turned my attention to our website. Currently, the website is built as a static HTML site and is then constructed into a Docker container using a CI pipeline.

One of the things that I have wanted to improve is how to update simple things such as the copyright notice on the footer, due to how the website is built at the moment, this requires updating individual pages, which as more is added can become a monotonous task.

This lead me to look at using other tools, but still wanting a static website, not using something overkill like WordPress or Rails. I turned my attention to using Nanoc.

Nanoc is a static site generator that uses Ruby to create and output the HTML pages. It also uses a templating and layout engine, meaning that it is simple to include a single header & footer section once and include it on every page.

There is a lot that could be written about Nanoc, but the focus of this blog post is to talk about how I have tackled combining and minimising our site's CSS and Javascript.

Before using Nanoc our build pipeline ran minimisation on our CSS & JS but it didn't combine it into a single file. Using CI we had a separate step to carry out this task, that would spin up a Docker container, install NodeJS and then run a minification task.

Using Nanoc, this can be part of its own build process using a built-in filter and some Ruby code.

Nanoc works using rules and configuration, and out of the box you get some simple rules, for example:

compile '/**/*.html' do
  layout '/default.*'
end

The above is saying for all HTML files, use the layout file called default.

For us to manage the CSS and JS we will be creating a couple of rules, but first, we need to make some changes to the config nanoc.yaml and add some helpers.

Nanoc.yaml

The Nanoc.yaml file contains configuration information that can be accessed anywhere in the project using @config we are going add a couple of additional configuration values to the file like so:

debug: false  
scripts:      # Javascript filenames without extension
  - script1
  - script2
  - script3
stylesheets:  # CSS files without extension
  - stylefile1
  - stylefile2
  - stylefile3
  - stylefile4
combinedCss: '/assets/css/combined.css'
combinedJs: '/assets/js/combined.js'

The config is just YAML so, for example, we can access the debug value using @config[:debug]

NOTE If you have css or js that needs to be loaded in a particular order, then the order in which you have them in the above file will dictate the order in which they are loaded.

Helper.rb

Nanoc will auto load and require any files in the /lib/ directory, which is a good place to create a file called helper.rb this will contain our custom filters or code that will run as part of its build process.

Inside the file add the following method:

def combine(files, target, identifier, type)
  return if @config[:debug]
  combined_file = "./content#{target}"
  combined = []
  files.each do |file|
    item = @items.find { |item| item.identifier == "#{identifier}#{file}.#{type}"}
    combined.push item.raw_content unless item.nil?
  end
  content = combined.join ' '
  File.open combined_file, 'w' do |f|
    f.write content
  end
  @items.create content, {}, target
end

The above method carries out the combining of the file, the method signature will be more understandable after the next part, but basically, it is looking for:

  • An Array of files
  • The target file (for example combinded.css)
  • The identifier is where the files can be found, for example /assets/css/
  • The type that will be combined, i.e css

The gist of this method is to read out the contents of each of the css files into an array and then write this array to a new file. It then creates a new "item" in the items array so that it can be picked up later in the build pipeline.

We also want to add an additional method which will remove the created combine file once the pipeline has finished, this is to make sure that there are no issues the next time we run it.

def remove_combined
  FileUtils.rm_rf "./content#{@config[:combinedCss]}"
  FileUtils.rm_rf "./content#{@config[:combinedJs]}"
end

Rules File

The Rules file as explained earlier contains the steps that Nanoc will do when building the site.

There are a couple of things that we need to add to this file, firstly we want to run our combination method. But we want to do this before the rest of the pipeline, as we want to add files to the items array. Therefore we can use a method called preprocess:

preprocess do
  combine @config[:stylesheets], @config[:combinedCss], '/assets/css/','css'
  combine @config[:scripts], @config[:combinedJs], '/assets/js/','js'
end

This will call out combine method and pass in details from our config.

After this has run we want to then pass our newly created combined.x file to the output.

compile '/assets/css/*' do
  if @config[:debug]
    write item.identifier.to_s
  else
    filter :rainpress
    write item.identifier.to_s if item.identifier.to_s.include? 'combined.css'
  end
end

compile '/assets/js/*' do
  if @config[:debug]
    write item.identifier.to_s
  else
    filter :uglify_js
    write item.identifier.to_s if item.identifier.to_s.include? 'combined.js'
  end
end

The above two rules do the following:

  • If the config is set to be in debug mode, then just pass our individual css & js files to the output directory
  • Otherwise run the :rainpress filter for CSS (this minifies it) and the :uglify_js filter for JS. It is worth noting that these are built-in filters that come with Nanoc.
  • Then write only the combined.x file to the output directory

Once the pipeline has completed we can run a postprocess step to clean up:

postprocess do
  remove_combined
end

Default.html

The name of the above may depend on what you have called your layout page, but usually, the default is default.html.

Now that we have a minimised and combined JS and CSS file, we need to actually use it on our page.

However we only want to use it, if it exists, otherwise, we want to include our normal non minified CSS and JS files. We can add the following to our layout page:

CSS added to the <head>

<% if @config[:debug] %>
    <% for file in @config[:stylesheets] %>
        <link rel="stylesheet" type="text/css" href="/assets/css/<%= File.basename(file, '.*') %>.css">
    <% end %>
<% else %>
    <link rel="stylesheet" type="text/css" href="/assets/css/combined.css">
<% end %>

JS added to the bottom of the <body>

<% if @config[:debug] %>
    <% for file in @config[:scripts] %>
        <script src="/assets/js/<%= File.basename(file, '.*') %>.js"></script>
    <% end %>
<% else %>
    <script src="/assets/js/combined.js"></script>
<% end %>

Hopefully, this might provide an insight into some of the processes that we are working on here, and I hope to create some more blog posts regarding Nanoc the more that I use it.