Simplified Development Workflow
The web platform has grown in complexity over the years and with it the complexity in the tools we use to build for the web. Tools like Gulp and Webpack are intended to make our life easier but also introduce a significant amount of tech debt that must be managed and maintained over the lifecycle of a project. Don’t get me wrong: I’ve always enjoyed the conveniences that task runners and module bundlers have to offer, but I also feel a bit overwhelmed by the configuration they require, the diligence needed to maintain it, and the mental energy it takes to be productive during each development session.
As a result, I’ve found myself valuing simplicity in my development workflow more and more over the years. This article explores how I’ve managed to significantly reduce the decisions I need to make in order to spin up a working environment and be productive by embracing tools that have made my developer experience simpler without the complexity debt.
This article details vital parts of a starter project I’ve made called Hugo Starter Project, which is available via Github. It’s a fork of the Hugo starter project Atlas by Indigo Tree with the addition of tooling and conveniences I use often.
Static Site Generator
Static Site Generators (SSGs) offer a host of advantages when it comes to a simpler development experience. There’s no monolithic architecture or database to manage — the HTML is build from Markdown files with reusable templates files providing structure. Additionally, authoring Markdown content files feels like a breath of fresh air in comparison to writing verbose HTML or building out custom fields in Wordpress.
I’ve experimented with a few SSGs but Hugo has remained by go-to for a few reasons. Firstly, it’s blazing fast. It clocks in at around <1ms per page, so the average site builds in less than a second. This means no more waiting for your pages to build each time you save. Secondly, it comes with an asset pipeline baked right in (more on this later). When it comes to simplicity, there’s something really nice about not needing to configure a build process for each project that leans heavily on a build tool like Grunt or Gulp. Lastly, it comes with the ability to spin up a local server and live-reload that server when changes are detected. This is typically something I’d have to setup with a task runner, but in Hugo it comes baked in.
There are lots of options when it comes to static site generators. If you want to start using one but aren’t sure which is the best for your needs, StaticGen is a great resource for seeing a high-level comparison between the most popular SSGs.
Plain Old CSS
CSS has evolved quite a bit over the last couple of years. Features such CSS custom properties, flexbox, grid, calc() and it’s ability to calculate multiple units, and functions like min(), max(), and clamp() all have dramatically enhanced the capabilities of native CSS. For me, this has slowly eliminated the need for CSS pre-processors like Sass. Authoring styles in CSS after using Sass for a long time feels lighter and simpler. I can feel confident that these styles are future-proof and don’t require tools to process it into CSS. For me, the less that can break in the future is a win because many of my projects are continuously maintained over serveral years.
Sass was the hare. CSS is the tortoise. Sass blazed the trail, but now native CSS can achieve much the same result.
That being said, there’s a few conveniences I’ve come accustomed to that Sass supports natively and CSS does not. Features such as the ability to break styles into separate files and concatenate them on build, nesting styles, and mixins have helped me become more efficient when writing styles. Luckily, Hugo is once again here to help. As of version 0.43 Hugo comes with Hugo Pipes, a built-in set of asset processing methods. Support for PostCSS and Sass comes baked-in, as well as file concatenation on build via the @import
statement in your main file.
I use Hugo Pipes to run the PostCSS plugin Autoprefixer, which parses CSS and add vendor prefixes to CSS rules using values from Can I Use. I also leverage the built-in ability to minify the output file and fingerprint it to allow for cache busting. This covers a significant amount of what I would have used a build tool like Gulp for in the past, amounting to additional complexity and dependencies to configure and manage. With a built-in asset pipeline, it’s trivial to chain tasks as needed.
There are many more PostCSS plugins available to help augment your CSS workflow. My favorite place to search for them is the searchable catalog PostCSS.parts.
Styles
To process CSS with Hugo Pipes, you’ll need to call resources.Get
on the CSS file that’s saved in a variable. The next step is to pass that variable into the href
attribute along with .Permalink
appended to the end. Also note that I’m chaining a series of actions within the variable to (from left to right) process CSS/Sass and combine the various files into one, run PostCSS plugins, then minify and fingerprint the file. The templating required to transform and manipulate CSS or Sass is simple, configurable, and happens where you’d normally load your stylesheet in the <head>
of the page — no more dealing with asset concatenation and transformation in a separate configuration file (PostCSS plugins being an exception).
{{ $s := resources.Get "css/app.css" | toCSS | postCSS (dict "config" "postcss.config.js") | minify | fingerprint }}
<link rel="stylesheet" href="{{ $s.Permalink }}" />
Postcss Config
In order to use PostCSS plugins (e.g. Autoprefixer), you’ll need to have a config file in the root directory. This configuration file gets called by referencing it in our resources.Get
statement above (postCSS (dict "config" "postcss.config.js"
)). Here’s a closer look at what my typical configuration looks like:
module.exports = {
plugins: [
require('autoprefixer')({
browsers: ['ie >= 8', 'last 3 versions']
})
]
};
Image Processing
Another task that can quickly complicate a build process is image processing. Whether it’s creating an array of sizes, optimizing each image size, or configuring a template to include them on a page can take quite a bit of time. Once again Hugo comes to the rescue with baked-in image processing. With Hugo pipes, creating different image sizes, cropping images, automatically optimizing file size and referencing images with paritials or shortcodes becomes a trivial task. Hugo Pipes gives us the ability to easily do tasks we’d usually have to configure through a build process, which can quickly get lengthy if you’re working with different image sizes, etc.
Image Shortcode
I usually set srcset
and sizes
attributes on images in order to ensure an appropriately sized images are served. In order to do this, I leverage a shortcode that takes a name and ‘alt’ value, then creates the various sizes I need. Hugo also optimizes the images it generates and can crop them if you’d like (handy for the art direction responsive image use case). Check out the Hugo docs to learn more about Hugo’s image processing capabilities.
{{ $src := $.Page.Resources.GetMatch (print (.Get "src")) }}
<img
sizes='(min-width: 35em) 1200px, 100vw'
srcset='
{{ ($src.Resize "800x").RelPermalink }} 800w,
{{ ($src.Resize "1200x").RelPermalink }} 1200w,
{{ ($src.Resize "1920x").RelPermalink }} 1920w'
src='{{ ($src.Resize "1200x").RelPermalink }}'
loading='lazy'
alt='{{ with .Get `alt`}}{{ . }}{{ end }}'>
Once your shortcode is setup, you can call it from within your content files by passing in the name and alt text for the image you’d like to generate like so:
{{< image src="detail.jpg" alt="Laws of UX book detail" >}}
An important note to make here is that Hugo requires images to live in the same directory as your content markdown file when referencing images via the method shown above. I’ve found the ability to group content and assets together quite nice because it eliminates the separation across directories. If you’re interested in learning more about Hugo page bundles I recommend checking out the docs.
Javascript
Much like CSS, Javascript can grow to the point that breaking it up into smaller chucks make sense due to size and complexity. Hugo pipes not only enables me to concatenate JS files, but also to minify, fingerprint and create sourcemaps. If I need external plugins, I can install them via NPM and include them in my JS bundle. This allows me to manage dependencies in one place, while also allowing for items in my node_modules folder to be available in my project.
Scripts
Processing Javascript files with Hugo Pipes is as simple as assigning each file to a variable, adding them to an array and then concatenating them into a bundled file.
{{ $pluginjs := resources.Get "js/plugin.js/dist/plugin.js" }}
{{ $mainjs := resources.Get "js/app.js" }}
{{ $scripts := slice $pluginjs $mainjs | resources.Concat "js/bundle.js" | minify | fingerprint }}
<script src="{{ $scripts.Permalink }}" integrity="{{ $scripts.Data.Integrity }}" media="screen"></script>
Hugo also enables you to split your JS into bundles if you’d have conditions which require some pages to get different scripts. For more information, check out the Hugo javascript bundling docs.
Hugo Config
Hugo Modules allows you to import and configure almost anything into your project. In order to include NPM scripts into your bundle, you’ll need to first copy over the script via module.mounts. Once this is set, you can include the file in your JS bundle like any other file.
[module]
[[module.mounts]]
source = "./node_modules/plugin.js"
target = "assets/js/plugin.js"
[[module.mounts]]
source = "assets"
target = "assets"
Here’s a tip that might save you some time: when adding a mounts
section, I’ve found it necessary to also declare the assets directory. Otherwise, you’ll receive an error that will prevent your site from building.
Extending the Tools
Hugo Pipes handles most of my use cases when it comes to build tooling but there are often times when I need to extend it. For example, one task I commonly include in all of my projects is to take a collection of SVG files, and combine them in order to build an SVG sprite. To achieve this, I install svgstore-cli, specify where it can find my individual SVGs, where it should output the combined SVG sprite, and when it will run in relation to the overall build process. This can be repeated for any number of tasks you’d like to add that isn’t covered by Hugo Pipes.
package.json
In order to use NPM as a build tool, you’ll need to install the dependency as normal via NPM, then call it in the scripts
section of your package.json file. As you can see below, I’ve installed the svgstore-cli tool, then created a build:icons
task that will run the tool along with any options I pass it. The next step is to run the build:icons
task, which I do by leveraging the pre
hook to build my SVG sprite file before the start
and build
tasks runs.
{
"scripts": {
"prestart": "npm run build:icons",
"start": "hugo server",
"prebuild": "npm run build:icons",
"build": "hugo && npm run build:functions && hugo --minify",
"build:icons": "svgstore assets/svg/**/*.svg > static/sprite.svg --inline",
"build:functions": "netlify-lambda build assets/lambda",
"server": "hugo server"
},
"dependencies": {
"svgstore-cli": "^2.0.0"
}
}
Netlify
The final piece of my workflow is deployment, which is made incredibly easy thanks to Netlify. By connecting your Github repo with your Netlify account, you’ll get automatic deployments each time you commit changes. Not only are your deployments configurable, but you extend the functionality of the Netlify Build process with Build Plugins.
Build Plugins are great because they enable you to perform additional tasks on your project after each build. Tasks such as running accessibility checks, running performance testing, optimizing images and SVGs, building and inserting critial CSS, and even visual diffing are all one button press away. Not only that, but you can write your own, save them locally in your repository, or share them with others via npm and the Netlify plugins directory. I’ve found that extending by build process with Build Plugins helps me to augment my development workflow without the overhead of configuring additional tooling.
As much as I like exploring new tools, I’ve come to really appreciate simplicity when it comes to my development workflow. By embracing static site generators, dropping Sass in favor of CSS, leveraging Hugo’s asset pipeline features, deploying with Netlify and augmenting my builds with Netlify Build Plugins, I’ve managed to significantly reduce the decisions needed to spin up a working environment and be productive in each development session. This gives me the ability to focus on delivering the best possible user experience by removing the complexity debt that often times becomes a focus.
To further reduce the friction that comes with starting a product and configuring the build tooling, I’ve created a Hugo starter project that’s available via Github. It’s a fork of the Hugo starter project Atlas by Indigo Tree with the addition of tooling and conveniences I use often. Feel free to clone it and use for your own Hugo projects and enjoy the simplified development workflow without the complexity debt.