Speeding up our Hugo workflow

A tower clock

Making sure developers face as little friction as possible when working on a project is challenging. During the creation of the engineering blog, we were deliberate about choosing tools that support us in providing a seamless workflow.

We aimed to quickly add, modify and extend our content with as little work as possible. Apart from content changes, it was also crucial for us to enable quick developer feedback when adding new features or tweaking existing code.

We now want to share how we have used Hugo alongside Laravel Mix to achieve the goals outlined above.

Using Hugo as a Static Site Generator

Early on during the development, we knew that serving static pages is the most fitting solution with regards to the goals and requirements we wanted to achieve.

Settling on Hugo came from the fact that we needed a platform that is familiar to most existing developers and easy to grasp for newcomers. For example, while our Backend engineers are used to Go, they might not have experience writing React code (if we were to use, e.g., Gatsby). However, engineers not familiar with Go are likely to pick up Hugo’s templating syntax quicker than those having to learn a full-fledged framework like React from scratch.

Built-in features such as hugo server (a webserver supporting hot-reload), image processing, and advanced content structure mechanisms have convinced us to use Hugo to launch our tech blog.

Colors getting mixed together

Adding Laravel Mix

Hugo already provides built-in support for handling assets (e.g., minification, building, bundling) through the usage of Hugo Pipes. However, we wanted to decouple the asset handling process from the templates to consolidate our build process configuration in one file, enabling us to take control of cache busting, the addition of frameworks like Tailwind, and the transpiling/minification process of our JavaScript files.

Discussing the options, Grunt, Webpack, and Laravel Mix were amongst the possible candidates. Ultimately, Laravel Mix was chosen as an elegant wrapper around Webpack for the 80% use case. Using the provided API, we were able to define the process steps needed for our project neatly. Here is an example of using tailwindcss:

const mix = require('laravel-mix');
const tailwindcss = require('tailwindcss')

mix
    .setPublicPath(distDir)
    .postCss('src/css/tailwind.css', `${distCssDir}/tailwind.css`, [
        require('postcss-import'),
        tailwindcss('./tailwind.config.js'),
    ])

Beyond the build capabilities, Laravel Mix also enabled us to employ cache busting and reliable minification in production environments.

Combining the power of both

As mentioned earlier, providing quick feedback (e.g., through hot-reloading) is essential for us to ensure a smooth development workflow. It took us a bit of tinkering to make Hugo and Laravel Mix work flawlessly together, but the result was worth it.

Developers can use yarn scripts for handling everyday use cases. The current script setup is defined as follows:

{
    "scripts": {
        "watch": "hugo serve -D --port 1234 --disableFastRender & yarn run build:assets:dev --watch",
        "dev": "yarn run build:development",
        "prod": "yarn run build:production",
        "build:development": "yarn run build:assets:dev && hugo",
        "build:production": "yarn run build:assets:prod && hugo",
        "build:assets:dev": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
        "build:assets:prod": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
    }
}

A particular challenge is the implementation of file watching using both Laravel Mix and Hugo. The following diagram provides an entry point to our explanation.

A diagram outlining the build process

Here’s the process deconstructed:

  • yarn watch kicks off a Laravel Mix watch process as well as hugo server

  • Laravel Mix watch:

    • Builds assets from a source directory (e.g. src/css) to the static/* directory. Hugo’s default behavior automatically copies file contents from static to public, ultimately making these assets available in template files.
    • During the build, Laravel Mix creates a manifest file that can be used for cache-busting. The manifest file is copied to the data directory as Hugo will then automatically pick it up to make it available to use in the templates. Here is an example of what it looks like:
      {
          "/css/tailwind.css": "/css/tailwind.css?id=b85e739694315d6d0f3f",
          "/js/shared/lazyload.es5.js": "/js/shared/lazyload.es5.js?id=037e42fcaf9dc0dd59bd"
      }
    

    This file is then indexed in the templates:

    <link rel="stylesheet" type="text/css" href='{{ index .Site.Data.Assets "/css/tailwind.css" }}'>
    
    • On file changes, the process repeats.
  • Hugo Server:

    • Upon changes in the data directory (caused by a re-compilation by Laravel Mix as outlined above), Hugo will re-generate the pages again
    • Upon changes in the template files, Hugo will re-generate the pages as well

Leveraging this combination, we can now make use of cache busting, minification, hot-reloading, and advanced build steps through the usage of these two unique tools.

Closing thoughts

Separating Hugo and Laravel Mix has achieved a workflow that enables fast feedback for developers working on new content and features. Using static pages, we can gain performance and SEO benefits while keeping the tooling slim. Laravel Mix enables us to define more complex build steps in a single configuration file instead of Hugo Pipe statements littered around templating files.

Written by

Patrick Ahmetovic

May 19, 2021

Patrick is a frontend developer based in Austria, focusing on the public-facing features of the refurbed platform.

We're Hiring

  • Lead Frontend Developer / Vue.js (m/f/x)

    We are looking for a lead frontend developer with experience in Vue.js to support us in developing our interfaces and to lead a remote team of software developers.

  • Lead Backend Developer / Go (m/f/x)

    We are looking for a lead backend developer with a solid background in designing and implementing Go backend services to support us in improving our e-commerce platform and to lead a remote team of software developers.

  • Senior Data Engineer (m/f/x)

    We are looking for a senior data engineer to work in the intersection between engineering and data science. Help us improve our data processing workflows and push them to the next level.

View all positions