Hugo logo.

Introduction

I have used Drupal as the back-end of my blog ever since I started it in 2009, and it powers several of my other sites. However, the ongoing maintenance and life-support it seems to need lately is becoming disproportionate and the thought of upgrading again to version 8 was starting to fill me with dread.

Don’t get me wrong, Drupal is great and I will probably continue using in some form or other. But I have the following four major gripes:

  1. Firstly, it seems that there is a new security upgrade required and a host of module updates pretty well every time I log in. I used to ignore most of them and only apply security patches once every few months, but back in 2013 all my sites were shut down because they got contaminated by some nasty PHP injection. That took a lot of effort to rectify so now I am much more responsive with security updates. However, each one involves backing up the database, downloading a whole new copy of Drupal, closing the site, uploading the new Drupal files, running the update script and crossing your fingers that all went well and your site is still useable. To be fair, it usually is.

  2. Also, the database sizes reported by my MySQL host for many of my sites seem way out of whack with what phpMyAdmin says they are. I know that there will always be some differences between the database itself and how it is stored on disk but, in my case, if phpMyAdmin says that all the tables add up to 60MB, my MySQL host will report that as nearing 200MB. As I have a limit per database on my particular plan, I am forever getting warning emails and having to clean out access log and watchdog tables.

  3. Comment spam is also becoming pretty intolerable. Over the last 4 years I have tried most of the available spam-prevention and captcha modules, and some work well for a while but spammers always find workarounds. I moved to moderated comments, but now I am getting nearly 1000 per month. This means trudging through nearly 20 pages of Viagra, Xanax and Tramadol ads looking for something that might actually be a real comment. Sorry, but life is too short.

  4. Finally, every time I go to upgrade to a new version, my theme will invariably not be supported and some of the modules I need will be abandoned or not available for that version. This is usually because I missed a version (ie: v4 to v6), but each time I have to go though a major redesign and on one occasion had to manually recreate and copy over the content of all my posts and their comments because I simply couldn’t get the database to upgrade with the new version. Plus, I use a couple of custom modules that I wrote and each upgrade requires research and an extensive rewrite with all the new API changes. I know the Drupal community strive very hard to make upgrading as easy as possible, but a lot of these things are beyond their control.

Moving to Hugo

Hugo is a static site generator. Whereas a CMS like Drupal dynamically assembles each page from database or cached information each time it is accessed, making the assumption that different types of user will see different components on each page so it has to be generated on the fly. Hugo pre-generates your site into a directory structure that mirrors the URL addresses of each node and populates it with static HTML files. The difference here is that Hugo assumes that your site only changes when you add or remove content, and then rebuilds it.

It can still simulate dynamic content like the pagination of lists, links to related content and page summaries, but only has to do this once as it is rebuilding the site, not on every page view. This is perfect for my blog as it does only change when I add or remove content. The other benefit is that it no longer needs a database or PHP back-end, uses far fewer server resources and I can have as many full and functional copies of my site on as many machines as I like.

Tips and Tricks

It took pretty well all of the Christmas break to convert my content to the formats required by Hugo, but it wasn’t too onerous. However, I got to know Hugo pretty well in the process as this blog is not as straightforward a site as it might appear.

Using HTML Files as Content

The idea behind Hugo and most other static site generators is that you create your content within a specific folder structure with each content file prefixed with front matter containing metadata that Hugo uses to generate and inter-relate the final site pages. The Hugo website suggests that it only natively supports content written in Markdown format, though you can use Asciidoc or reStructuredText via extensions. In fact, I almost didn’t even consider Hugo as I wanted to use some content written in HTML and it appeared not to support it.

In fact it does support HTML content pages and it does so very well. You can use shortcodes and all the other features just as you can in Markdown. Just make sure you leave the .html file extension on your HTML pages and the .md file extension on your Markdown pages.

Including HTML Within Metadata

The YAML and TOML formats for front matter let you include information such as the page title and descriptions, as well as your own variables. I added a variable for defining a page abstract and wanted to use HTML within it. This is possible, but you have to do a couple of things.

The data for all front matter variables is a string enclosed in quotes. As some HTML includes quotes for tag attribute values, you need to make sure you escape them properly with a backslash, as shown below.

abstract = "Some < a href=\"https://www.w3.org/html/\">HTML</a> in a <var>string</var>."

Then, when you include them in your content or within a partial or shortcode template, make sure you pipe them through the safeHTML function, as shown below:

<p class="abstract">{{ .Params.abstract | safeHTML }}</p>

Comments and Contact Forms

Hugo won’t help you with comments on your pages, but it does have built in support for Disquss. As I don’t get a huge number of real comments on this site, and they are normally directed at me rather than as a participatory discussion, that is probably overkill. For my needs, I just created a modal dialog containing a comment web form on each page, included as a partial template. The form then submits its data to Formspree which converts it into an email directly to me.

Formspree has its own anti-spam controls and I simply added my own random math equation as an extra input. So far so good, absolutely no spam comments since its been up and I get the email as soon as the comment is sent. Obviously I then have to manually edit the comment into the right page and then re-upload it to respond, but I’d rather that than the old way.

I did the same for the site Contact form, though without the modal dialog.

Page-Specific CSS and JS Files

In Hugo, you normally include all your CSS files in a partial template and include that at the start of each content type template. You do the same with any JavaScript libraries you need and include that at the bottom of each content type template, just before the </body> tag. However, if you need page-specific CSS or Javascript files, you either have to make those pages a different content type so you can customise their specific template, or you can add some logic to your existing partial templates.

For example, to support page-specific CSS files, I created a page-start.html partial template that set up the page and included towards its end the line shown below.

{{ range $.Params.css }}<link rel="stylesheet" href="{{ . }}" />{{ end }}

Now, in any page I can add the following line to its front matter and both those files will be included in that page. If the css variable is not defined or is an empty list, nothing will be displayed.

css = [ "/css/custom/file01.css", "/css/custom/file02.css" ]

Similarly, for JavaScript files, I have a page-end.html partial template that includes the following line:

{{ range $.Params.scripts }}<script src="{{ . }}"></script>{{ end }}

Now, in any page I can add the following line to its front matter and the script will be included. Again, if the scripts variable is not defined or is an empty list, nothing will be displayed.

scripts = [ "/js/vendor/MathJax.js?config=TeX-MML-AM_CHTML" ]

Getting More Complex

I also wanted to include the Bootstrap Image Gallery on some pages, but that requires a bit more than just scripts. In this case, you can simply create a partial template with all the required code and then conditionally include it using a boolean variable in the front matter.

The first step is to download all the required image gallery CSS and Javascript files and place them in the right places within your static folder so they get copied into your site when it is built.

The next step is to add the following line at the end of page-start.html to include the required CSS.

{{ if $.Params.lightbox }}<link rel="stylesheet" href="/css/blueimp-gallery.css" />{{ end }}

Then add the following line at the end of page-end.html to include the partial template.

{{ if $.Params.lightbox }}{{ partial "lightbox.html" . }}{{ end }}

Then create a new partial template called lightbox.html with the following code in it.

<!-- Create a hidden Bootstrap modal dialog for the images -->
<div id="blueimp-gallery" class="blueimp-gallery blueimp-gallery-controls">
    <div class="slides"></div>
    <h3 class="title"></h3>
    <a class="prev">‹</a>
    <a class="next">›</a>
    <a class="close">×</a>
    <a class="play-pause"></a>
    <ol class="indicator"></ol>
    <div class="modal fade">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <button type="button" class="close" aria-hidden="true">&times;</button>
                    <h4 class="modal-title"></h4>
                </div>
                <div class="modal-body next"></div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-default pull-left prev">
                        <i class="glyphicon glyphicon-chevron-left"></i> Previous
                    </button>
                    <button type="button" class="btn btn-primary next">
                        Next
                        <i class="glyphicon glyphicon-chevron-right"></i>
                    </button>
                </div>
            </div>
        </div>
    </div>
</div>

<script src="/js/vendor/blueimp-gallery.js"></script>
<script src="/js/vendor/jquery.blueimp-gallery.js"></script>

Now, on any page that you need an image gallery, add the following line to its front matter:

lightbox = true

Working Offline With CDN Files

Using a content delivery network (CDN) when including standard libraries helps speed up your page load times and reduces the bandwidth on your web server. On this site I use a CDN for jQuery, Bootstrap and MathJax. However, if I want to develop whilst offline, this can be a problem without a local fallback. Thankfully, this is relatively easy to do.

The first step is to download local copies of all the CSS and JS files that you use via CDN. Then all you need to do is write something similar to the following within your page-end.html partial template. The key is testing for something unique to each library. jQuery and MathJax are easy as they add a variable to the top level window variable. Bootstrap on the other hand simply extends jQuery so you have to look for a method that is likely unique to Bootstrap.

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script> // Use CDN for jQuery - with local fallback.
    window.jQuery || document.write('<script src="/js/vendor/jquery-1.11.3.min.js"><\/script>')
</script>

<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
<script> // Use CDN for Bootstrap - with local fallback.
    if (typeof($.fn.popover) == 'undefined') document.write('<script src="/js/vendor/bootstrap-3.3.6.min.js"><\/script>')
</script>

{{ if $.Params.mathjax }}<script src="//cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-MML-AM_CHTML"></script>
<script> // Use CDN for MathJax - with local fallback.
    window.MathJax || document.write('<script src="/js/vendor/MathJax.js?config=TeX-MML-AM_CHTML"><\/script>')
</script>{{ end }}


Click here to comment on this page.