Building Sites With Hugo
In the process of building a new personal website, I hunted around for static website engines. My previous website used an engine called habari
, but development of the Habari Project is discontinued and I was interested in writing posts in markdown
instead of plain HTML.
So let’s get a few questions answered immediately:
- Why a static engine?
- What is markdown?
A static website engine converts an intermediate syntax into HTML. It does this as a “static” page, which means that everything the site needs to function is prebuilt. This has three main advantages:
- speed - no server-side processing is required on page load
- security - there are no scripts running on the server which are exposed externally
- portability - the statically built site should run on any web server
sometimes we want to run some scripts on top of a static site too.. more on that below
Now, we mentioned above that the site is built using an intermediate syntax, in the case of most modern static generators, this is known as Markdown. Markdown is a very simple syntax which is designed to be readable to someone without any coding experience but to also provide specific syntax for things like headers, lists, tables and images. It’s easier to work with than HTML but still powerful enough to complement writing website content. It’s also hugely advanatageous that Markdown can be written with a simple text editor.
Here’s a little example of some Markdown code
**Ranking of web servers**
Below, we can see a breakdown for Jan 2018 of current web server use on the internet.
Some interesting points:
- *Google* is disproportionatley serving the more popular sites
- *nginx* is more prevalent on these higher ranking sites
server | % use total | % in to 1000
-----------|-------------|-------------
apache | 47.7 | 16.1
nginx | 36.5 | 56.5
IIS | 10.4 | 5.1
LiteSpeed | 3.1 | 0.6
Google | 1.0 | 13.0
This then comes out as:
Ranking of web servers
Below, we can see a breakdown for Jan 2018 of current web server use on the internet.
Some interesting points:
- Google is disproportionatley serving the more popular sites
- nginx is more prevalent on these higher ranking sites
server | % use total | % in to 1000 |
---|---|---|
apache | 47.7 | 16.1 |
nginx | 36.5 | 56.5 |
IIS | 10.4 | 5.1 |
LiteSpeed | 3.1 | 0.6 |
1.0 | 13.0 |
As you can see, the intermediate syntax retains is readability and is much easier to use that HTML. It’s also a lot more forgiving, not throwing errors is there are minor syntactic problems.
Another key advantage of Markdown when used in a static website builder is that we’re not limited to just Markdown, we can also add plain HTML or create extensions which are built on the fly.
Why Hugo
Hugo has become a very popular static CMS engine, consistently being ranked in the top 5 engines. It is written in Go and has a very powerful templating engine which allows rapid customisation of sites. It’s also very fast to build sites with hugo and it supports extensions using shortcodes. At GreenAnt, Hugo has become our go-to engine for rapid prototyping and building of sites, so I thought I’d see how far it can be pushed in building this site (as well as experimenting with the most garish colour palette to inflict on readers).
Table of Contents
Customising Hugo
Styling with CSS
Hugo templates usually come with reasonable CSS defaults. We can extend these with a custom CSS file and add some new attributes. For, instance, in this site I added CSS code to reformat the sidebar and also the contact form.
Adding a contact form
I wanted to add a contact form to my site that sends me an email when a visitor wants to leave a message. As a general policy, it’s a bad idea to expose your email address on a website as there are many bots that actively scan the net looking for email addresses to target spam and intrusion attempts.
However, static HTML generators like hugo don’t include forms which process user input. That’s because they are static sites without backend processing. This is both a blessing with regards to speed and security, but also a curse with regards to more complex features.
Thankfully, it’s really easy to extend Hugo, we can do this by adding some PHP
code.
We are going to try to use PHPMailer to create a contact form.
First we get php working, which is fairly trivial in the nginx
web server.
Most of the steps that follow, came from this guide
create a custom output format
this allows hugo to build php files
in
config.toml
[mediaTypes] [mediaTypes."application/x-php"] suffixes = ["php"] [outputFormats] [outputFormats.PHP] mediaType = "application/x-php" isHTML = true baseName = "index"
- create a template for php files
copy
/themes/theme_name/layouts/_default/single.html
to/layouts/_default/single.html
and rename the copy tosingle.php
- Adjust include path in nginx
we need to make sure php can find includes, for which you should set an include path in the nginx config.
location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php7.0-fpm.sock; fastcgi_param PHP_VALUE "include_path=/var/www/SITENAME/static/php"; }
Adding PHP code
First we have to specify the shortcode:
Add a new Shortcode to
/layout/shortcodes/
in the form ofphp_code.html
containing something like:<div> {{ safeHTML .Inner }} </div>
This is best done in a div and we can include code in the
static/php
folder using the following{{< php_code >}} <?php include 'mailer.php'; ?> {{< /php_code >}}
In the page header, we need to tell hugo to render as PHP
+++ title = "A Page with PHP Code" outputs = ["PHP"] date = "2017-01-01" draft = "false" +++
- crafting the
mailer.php
code
<?php use PHPMailer\PHPMailer\PHPMailer; use PHPMailer\PHPMailer\Exception; require 'phpmailer/Exception.php'; require 'phpmailer/PHPMailer.php'; require 'phpmailer/SMTP.php'; //$mail->SMTPDebug = 2; // this should catch a lot of spam bots $honeypot = trim($_POST["email"]); if(!empty($honeypot)) { echo "BAD ROBOT!"; exit; } $msg = ''; //Don't run this unless we're handling a form submission if (array_key_exists('email', $_POST)) { date_default_timezone_set('Etc/UTC'); $mail = new PHPMailer; //Tell PHPMailer to use SMTP - requires a local mail server //Faster and safer than using mail() $mail->isSMTP(); // Set mailer to use SMTP $mail->Host = '<SMTP.server>'; // Specify main and backup SMTP servers $mail->SMTPAuth = true; // Enable SMTP authentication $mail->Username = '<mailer@username>'; // SMTP username $mail->Password = '<password>'; // SMTP password $mail->SMTPSecure = 'tls'; // Enable TLS encryption, `ssl` also accepted $mail->Port = 25; // TCP port to connect to //Use a fixed address in your own domain as the from address //**DO NOT** use the submitter's address here as it will be forgery //and will cause your messages to fail SPF checks $mail->setFrom('<send from this email>', 'Contact Form'); //Send the message to yourself, or whoever should receive contact for submissions $mail->addAddress('your@email', 'Your Name'); //Put the submitter's address in a reply-to header //This will fail if the address provided is invalid, //in which case we should ignore the whole request if ($mail->addReplyTo($_POST['email_real'], $_POST['name'])) { $mail->Subject = 'PHPMailer contact form'; //Keep it simple - don't use HTML $mail->isHTML(false); //Build a simple message body $mail->Body = <<<EOT Email: {$_POST['email_real']} Name: {$_POST['name']} Message: {$_POST['message']} EOT; //Send the message, check for errors if (!$mail->send()) { //The reason for failing to send will be in $mail->ErrorInfo //but you shouldn't display errors to users - process the error, log it on your server. $msg = 'Sorry, something went wrong. Please try again later.'; } else { $msg = 'Message sent! Thanks for getting in contact.'; } } else { $msg = 'Invalid email address, message ignored.'; } echo $msg; } ?> <form id="form" name="form" method="post"> <label for="name">Name: <input type="text" name="name" id="name"></label><br> <label for="email">Email address: <input type="text" name="email" style="display: none;"> <input type="text" name="email_real" id="email"></label><br> <label for="message">Message: <textarea name="message" id="message" rows="8" cols="20"></textarea></label><br> <input type="submit" value="Send"> </form> </body> </html>
- crafting the
Adding commenting
I wanted to include an option for visitors to add comments to the posts here. But I also wanted to have all the functions of the site self-hosted. After reading about options, I considered two different open-source commenting engines, Discourse and Isso. Discourse is a fantastic program but I only needed 3 basic features:
- anonymous commenting
- moderation
- simple maintenence
Isso fulfilled these needs really well and also supports Markdown which is helpful. Once the Isso server was running, it was relatively simple to add it to this site. I may write a full guide in the future, but here’s the main points regarding integration for the moment…
load the script in the header layouts/partials/header.html
:
<head>
...
<!--isso config-->
{{ "<!-- isso -->" | safeHTML }}
<script data-isso="{{ .Site.BaseURL }}/isso/"
src="{{ .Site.BaseURL }}/isso/js/embed.min.js"></script>
{{ "<!-- end isso -->" | safeHTML }}
</head>
put it in the custom layout layouts/_default/single.html
:
{{ partial "header.html" . }}
{{ .Content }}
{{"<!-- begin comments //-->" | safeHTML}}
<section id="isso-thread">
</section>
{{"<!-- end comments //-->" | safeHTML}}
{{ partial "footer.html" . }}
Adding an edit link
One thing that hugo doesn’t have is an online admin interface or editor. This can be considered a security feature, but also a limitation. No problem, we can add one by using an online code editor and fashioning a custom link that takes us to the proper page in the editor. Codiad is a good example of such an editor.
<a href="{{ print "https://codiad.server.net/#website_hugo/content/"
.File.Path }}" target="GA_editor">Edit</a>
Autobuilding
Hugo sites are built with a simple command: hugo
. But we still need to get to the command line to execute this command which can introduce a bit of a cumbersome step in managing the site. So I scripted a solution that monitors the /contents
directory of the site and then executes the hugo build command.
This involves the use of the excellent Linux monitoring tool, called inotifywait
.
Firstly make the inotifywait notification script
#!/bin/sh
MONITORDIR=/pathto/website/content/
while true; do inotifywait -r -e modify "$MONITORDIR" && su USER -c /pathto/buildscript/build_hugo.sh; done
this script can be started at boot by calling it from /etc/rc.local
/scripts/website.inotifywait.sh &
exit 0
script to build the site:
#!/bin/bash
WORKING_DIR=/pathto/website
cd ${WORKING_DIR}
# redirect stdout/stderr to a file
exec &> logfile.txt
# timestamp
date
# build search (optional if we are using lunr)
# exec sudo -u USER lunr-hugo -i "content/**/*.md" -o static/json/search.json -l toml
# this command generates the static files
exec sudo -u USER hugo
then just make sure the script can execute
chmod u+x build_hugo.sh
This works really well, the site builds quickly and if there are any errors I can view them through the logfile from the last build, all from within Codiad.
Conclusion
As you can see from the above examples, it’s relatively easy to extend Hugo because it builds very clean HTML. Adding scripting and extensions is also supported by a managable extension system. I’m happy with the result so far and will be refining the site over the next few months with a few more little tweaks. How long did it take to build the site? Well, it was surpisingly fast, about 20 hours I’d say, including migrating a number of articles from the previous site (which was quite trivial as HTML is still valid in a markdown document). This site is also much easier to maintain and less prone to spam comments than my previous site, so I’m happy with the time investment.
There are a lot of static site frameworks available… What are your favourites and do you have any Hugo tweaks of your own?