Build Script for Hugo Websites
  • Posted on January 9, 2018

Build Script for Hugo Websites

Over the past year I’ve been building many websites using Hugo. Early on in development of these sites, I noticed the need for a build script. My main goal of the build script was to store the original uncompressed images inside my website’s code repository and have the built version of the Hugo website use compressed images. In addition to compressing images, I also needed a method to compress style sheets and JavaScript files. Below is an outline of how my build script works, code snippets that may be useful if you’re creating your own build script, and at the end of the page the full version of my build script.

When creating the build script, I wanted a method to compress all files inside of the “static” directories inside of the root directory and inside of theme folders. The script works by not having Hugo’s default “static” directory and instead placing all of my static files in a folder named “assets.” I recursively loop through all files in the “asset” folders and if the file’s extension matches a rule, the file is then compressed. The compressed file is then saved to the normal “static” directory. At this point the hugo command works as normal and I can generate my website using compressed assets.

Verifying Hugo Version Numbers

I found it necessary for the build script to verify that the version of Hugo that’s installed matched the same version to which a particular website was built. During the early development of Hugo, many releases would cause websites built with previous versions to break. Luckily this doesn’t occur as often now, but this check still prevents any issues from going unnoticed.

First we setup a “build_config.sh” file that holds the version number of Hugo we are currently using. In the future when you upgrade to a different Hugo version, you will test your website with the new version of Hugo and increment this HUGO_VERSION value.

#!/bin/bash

# Hugo Version
HUGO_VERSION="0.30.2"

The version check is included in my build script. Below is the corresponding snippet of code if you’re looking to create your own build script or have another use for this function. The majority of the code below should be easy enough to understand. The source "build_config.sh" line runs the previous file we created, this is where we get the variable HUGO_VERSION.

As for the string manipulation of the variable version_string, it can be a little difficult to understand what’s happening at first glance. First we get the Hugo version output and store it in the variable version_string. We then perform two substring removal actions by using Bash Parameter Expansion. The first modification of ${version_string#* v} removes characters from the start of the string up until the sequence of [space]v is matched. The second expression removes the longest section of characters from the end of the string that starts with a [space]. In the end, the version_string variable will hold the Hugo version number.

#!/bin/bash

source "build_config.sh"

function check_hugo_version {
	version_string=$(hugo version)
	version_string=${version_string#* v}
	version_string=${version_string%% *}

	if [ "${version_string}" != "${HUGO_VERSION}" ]; then
		echo "Hugo is v${version_string} but we expected v${HUGO_VERSION}."
		return  1
	fi

	return 0
}

# Check Hugo Version
if ! check_hugo_version; then
	exit 1
fi

Recursively Building and Compressing Assets

There is the possibility that files will be stored in many different folders that needs to be processed. To accomplish this goal, we recursively loop through the root “assets” folder along with looping through each theme folder and their “assets” folders. Once the script reaches an “assets” folder, it then loops through all files and directories. On each file it then passes the file into a function to handle the compression of that individual file.

To recursivly loop through files, we have the following function. The function stores three local variables, which is important. The variable current_dir stores the location of the folder we are currently in. The input_dir variable is the starting directory and never changes; this makes it easy to get the destination path for a file by replacing the input_dir with the output_dir variable. Finally, the output_dir variable stores the root of the destination folder and doesn’t change as we go into deeper folders.

#!/bin/bash

function process_file {
	#
	# INCOMPLETE FUNCTION
	#
	input_file="${1}"
	input_dir="${2}"
	output_dir="${3}"

	ext="${input_file##*.}"

	output_file=${input_file/$input_dir/$output_dir}
	output_file_noext="${output_file%.*}"

	input_file_short=${input_file/$input_dir/}
	output_file_short=${output_file/$output_dir/}

	input_file_dir=$(dirname "${input_file}")
	output_file_dir=$(dirname "${output_file}")

	# Here is where you would have a switch with the `ext` variable. Using the
	# variables above, you have the many input and output file strings to use.
	echo "Input file ${input_file} to ${output_file}"
}

function process_folder {
	local current_dir="${1}"
	local input_dir="${2}"
	local output_dir="${3}"

	for file in $current_dir/* $current_dir/.*; do
		if [ "${file}" != "${current_dir}/." ] && [ "${file}" != "${current_dir}/.." ]; then
			if [ -d "${file}" ]; then
				process_folder "${file}" "${input_dir}" "${output_dir}"
			elif [ -f "${file}" ]; then
				process_file "${file}" "${input_dir}" "${output_dir}"
			fi
		fi
	done
}

A lot of code from the entire build script would have to be present in the above snippet for it to work properly, so take a look below at the entire script. If we are working to process the root “assets” folder and save all future files to the “static” directory, we would have a line of code similar to process_folder "./assets" "./assets" "./static".

Verifying Required Programs and Scripts Exist

When Hugo was still using Pygments for syntax highlighting, I would sometimes forget to verify that it was actually installed before running Hugo, and in return had a website that wasn’t rendered correctly. Pygments isn’t used anymore, but many other programs are used by the build script and I wanted to ensure that they were all installed before trying to run the build script and show an error if any one program is missing. In the bash script, the variable REQUIRED_COMMANDS is an array of every command that is required for the build script. Later on we loop through the REQUIRED_COMMANDS variable and verify each entry is actually on the system.

#!/bin/bash

REQUIRED_COMMANDS=(
	"hugo"
	"cjpeg"
	"uglifycss"
	"uglifyjs"
	"pngout" )

function command_exists {
	cmd="${1}"
	if type "${cmd}" > /dev/null 2>&1; then
		return 0
	else
		return 1
	fi
}

# Check for Required Functions
for cmd in "${REQUIRED_COMMANDS[@]}"; do
	if ! command_exists $cmd; then
		echo "Missing command ${cmd}. Terminating job."
		exit 1
	fi
done

Entire Build Script

Here is the entire build script. This version of the script compresses BMP to JPEG, PNGs, CSS, and JavaScript files. The reason that we compress BMP to JPEG is that JPEG is always lossy and I didn’t want any lossy images in the asset folder. It’s possible to add further rules for compression; in other versions of this script for my other sites, I also use LESSC to build CSS files before compressing them. The first code block is the “build_config.sh” file that holds the version number for Hugo.

#!/bin/bash

# Hugo Version
HUGO_VERSION="0.30.2"

When running the build script below, any passed parameters will be passed onto Hugo. You can run `./build.sh –buildDrafts –buildExpired” to build the expired and draft pages. Also keep in mind that this script compresses files from the “assets” folder and not the “static” folder. The “static” folder is treated as a build folder and should be ignored by your version control system.

#!/bin/bash

ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
ASSET_DIRNAME="assets"
STATIC_DIRNAME="static"
THEMES_DIRNAME="themes"

source "${ROOT_DIR}/build_config.sh"

REQUIRED_COMMANDS=(
	"hugo"
	"cjpeg"
	"uglifycss"
	"uglifyjs"
	"pngout" )

function check_hugo_version {
	version_string=$(hugo version)
	version_string=${version_string#* v}
	version_string=${version_string%% *}

	if [ "${version_string}" != "${HUGO_VERSION}" ]; then
		echo "Hugo is v${version_string} but we expected v${HUGO_VERSION}."
		return  1
	fi

	return 0
}

function build_themes {
	theme_dir="${1}"

	for theme in $theme_dir/*; do
		if [ -d "${theme}" ]; then
			build_folder "${theme}/${ASSET_DIRNAME}" "${theme}/${STATIC_DIRNAME}"
		fi
	done
}

function build_folder {
	input_dir="${1}"
	output_dir="${2}"

	rm -rf "${output_dir}"
	mkdir "${output_dir}"

	process_folder "${input_dir}" "${input_dir}" "${output_dir}"
}

function process_folder {
	local current_dir="${1}"
	local input_dir="${2}"
	local output_dir="${3}"

	for file in $current_dir/* $current_dir/.*; do
		if [ "${file}" != "${current_dir}/." ] && [ "${file}" != "${current_dir}/.." ]; then
			if [ -d "${file}" ]; then
				process_folder "${file}" "${input_dir}" "${output_dir}"
			elif [ -f "${file}" ]; then
				process_file "${file}" "${input_dir}" "${output_dir}"
			fi
		fi
	done
}

function process_file {
	input_file="${1}"
	input_dir="${2}"
	output_dir="${3}"

	ext="${input_file##*.}"

	output_file=${input_file/$input_dir/$output_dir}
	output_file_noext="${output_file%.*}"

	input_file_short=${input_file/$input_dir/}
	output_file_short=${output_file/$output_dir/}

	input_file_dir=$(dirname "${input_file}")
	output_file_dir=$(dirname "${output_file}")

	# Create Directory
	mkdir -p "${output_file_dir}"

	# Process Files
	case "${ext}" in
		bmp)
			echo "Compressing \"${input_file_short}\""
			cjpeg -quality 85 -progressive -optimize -outfile "${output_file_noext}.jpg" "${input_file}"
			;;
		css)
			echo "Compressing \"${input_file_short}\""
			uglifycss "${input_file}" > "${output_file}"
			;;
		js)
			echo "Compressing \"${input_file_short}\""
			uglifyjs "${input_file}" > "${output_file}"
			;;
		png)
			echo "Compressing \"${input_file_short}\""
			pngout "${input_file}" "${output_file}" -q
			;;
		*)
			cp "${input_file}" "${output_file}"
			;;
	esac
}

function command_exists {
	cmd="${1}"
	if type "${cmd}" > /dev/null 2>&1; then
		return 0
	else
		return 1
	fi
}

# Check for Required Functions
for cmd in "${REQUIRED_COMMANDS[@]}"; do
	if ! command_exists $cmd; then
		echo "Missing command ${cmd}. Terminating job."
		exit 1
	fi
done

# Check Hugo Version
if ! check_hugo_version; then
	exit 1
fi

# Build Site
build_folder "${ROOT_DIR}/${ASSET_DIRNAME}" "${ROOT_DIR}/${STATIC_DIRNAME}"
build_themes "${ROOT_DIR}/${THEMES_DIRNAME}"

hugo "$@"