How to Automatically Generate and Inline Critical CSS with Hugo Pipes

Ben Bozzay
Fullstack Digital
Published in
14 min readAug 21, 2018

--

Today I’m going to show you how to use Hugo pipes to extract critical path CSS and generate a stylesheet based on the styles that are actually in use on the website.

One of the best ways to optimize a website for load speed is to inline critical CSS and defer loading of non-critical CSS. Google PageSpeed insights refers to this optimization as “eliminating render-blocking JavaScript and CSS in above-the-fold content.

You can achieve a much faster load speed by inlining styles required to render the top area of your website in the <head> of each page’s HTML:

However, extracting critical CSS for larger websites with many unique page layouts is easier said than done.

As we work on the initial release of Pancakes Builder, a free visual website builder for Hugo, we had to solve the issue of achieving a fast page load speed for every variation of a page that a user might decide to build.

Pancakes Builder leverages a modular design where sections are stacked to construct the actual layout and content. For non-templated pages that are built with stacks of sections, we had to solve some pretty substantial optimization issues:

  • Normally, a user has to download the entire stylesheet before the page is rendered and this causes a noticeable delay in the first page load.

Solution: Inline critical CSS so that the render blocking CSS loads with the page’s HTML. Then, load non-critical CSS asynchronously.

  • Some users of Pancakes Builder will build websites that never use elements supported by the builder — accordions, sliders, etc.. We don’t want to bundle styles for unused components.

Solution: Generate a cacheable site-wide stylesheet composed only of the styles of all non-render blocking sections.

Creating a Modular Layout

Modular layouts allow a user to build a unique page by stacking sections. If we adjust our Hugo theme to use a modular layout we can do a lot of cool stuff.

To create a modular layout, we’ll adjust our template to use partial files to build pages by stacking sections.

Sections are partial files with a predefined layout.

We can then use front matter, or another preferred method, to designated what sections appear on our page and in what order.

For example, the Forestry content management system supports building flexible layouts using their blocks widget, which basically provides a simple UI for repeating front-matter fields:

Source: https://forestry.io/docs/settings/fields/blocks/

The page front-matter for these blocks would probably look like this:

blocks:
- template: "hero-section"
- template: "body-copy"
- template: "hero-section"
- template: "media-feature"
- template: "media-feature"
- template: "media-feature"
- template: "call-to-action"

Partial files with pre-built layouts are then added based on the template name in the front-matter:

{{ range .Params.page_sections }}
{{ if eq "hero-section" .template }}
{{ partial "blocks/hero" . }}
{{ end }}
{{ if eq "call-to-action" .template }}
{{ partial "blocks/cta" . }}
{{ end }}
{{ end }}

We can apply this same concept to generating the critical and non-critical styles associated with the used blocks across our website.

1. Build a flexible layout with front matter

In our page front matter, we add a range called “stacks” with a few templates:

---
title: "Home Page"
stacks:
- template: header-builder
- template: hero-builder-section
- template: contact-builder-section
- template: footer-builder

These templates will be used to specify partial files. Notice that we are even including the header and footer in our range. This is because we will use this range to extract CSS from all used sections.

We can then create a loop of sections in our main layout file:

// Check if the "stacks" key exists in our front matter{{ if .Params.stacks }}// Iterate through each stack
{{ range $index, $stacks := .Params.stacks }}
{{ if in .template "header" }}
<header>
...
</header>
{{ else if in .template "section-hero-image" }}
<section>
<div class="hero">
...
</div>
</section>
{{ else if in .template "section-form" }}
<section>
<form>
...
</form>
</section>
{{ else if in .template "footer" }}<footer>
...
</footer>
...

The display order of the sections on our page will match the order in our front-matter. For the purposes of this tutorial, I have not included many different section variations.

2. Add classes to HTML

We’ll need to append a unique class to each section in our HTML. Let’s generate a few unique classes:

  1. One class designating the template name used in our front matter. We’ll need this for our base styling (styles used for multiple sections).
  2. One class with the unique section identifier. We’ll need this for our non-critical stylesheet and to apply unique styles to a single section.

In our range we’ll add the .Scratch values to generate classes for each section.

{{ range $index, $stacks := .Params.stacks }}{{ $.Scratch.Set "section_base" .template }}{{ $section_style := print $index "-" .template }}
{{ $.Scratch.Set "section_style" $section_style }}
{{ $.Scratch.Set "page_hash" .UniqueID | urlize }}{{ $id := print "section-" $index }}
{{ $.Scratch.Set "section_id" $id}}
{{ if in .template "header" }}
<header id="{{ $.Scratch.Get "section_id" }}" class="{{ $.Scratch.Get "section_base" }} c-{{ $.Scratch.Get "section_style" }} p-{{ $.Scratch.Get "page_hash" }}">
...
</header>
{{ else if in .template "section-form" }}
<section id="{{ $.Scratch.Get "section_id" }}" class="{{ $.Scratch.Get "section_base" }} c-{{ $.Scratch.Get "section_style" }} p-{{ $.Scratch.Get "page_hash" }}">
<form>
...
</form>
</section>
...

We use .Scratch instead of normal variables so we can easily access these variables in our partial files and change their values from within our partials.

Since we’ve attached an index to our range (think of this as a “counter”), we can generate a section_id based on the order of our section within the range:

{{ $id := print "section-" $index }}
{{ $.Scratch.Set "section_id" $id}}

The output will look like this: section-1.

We can also create a general class based on our section-template used for our base CSS:

{{ $.Scratch.Set "section_base" .template }}

The output will look like this: section-form.

For specific section styling, we’ll create another scratch called section_base that prepends “c” with the index number and template name:

{{ $section_style := print $index "-" .template }}
{{ $.Scratch.Set "section_style" $section_style }}

The output will look like this: c-2-section-form.

We’ll create a scratch called page_hash with the page UniqueID and add this unique identifier to each section in our range:

{{ $.Scratch.Set "page_hash" .UniqueID | urlize }}

This is necessary so we can target unique sections on other pages.

Example output in the HTML:

<header id="section-0" class="c-header-builder p-123sdf6521341dsfa">
...
</header>
<section id ="section-1" class="form-builder-section c-1-form-builder-section p-123sdf6521341dsfa">
...
</section>

In our stylesheet, we can use .form-builder section {…} to add the base styles for our form. We can then use .p-123sdf6521341dsfa.c-1-form-builder-section {…} in our stylesheet to target the unique styles for a specific form-builder section on a specific page.

We can uniquely target sections in our stylesheet while applying base styles if needed:

/*base header style*/
header {
background: #ffffff;
height: 80px;
}
/*override the base color for this particular page and section*/
.c-0-header-builder.p-123sdf6521341dsfa {
background: #efefef;
}

Later in this tutorial, we will use template variables to specify these classes automatically:

header {
background: #efefef;
height: 80px;
}
/*header style for this particular page and section*/
.c-{{ $.Scratch.Get "section_class" }}.p-{{ $.Scratch.Get "page_hash" }} {
background: #efefef;
}

Now that our HTML is generated, we can focus on generating styles.

Generating our critical inline style

We create partial files for a typical section and unique elements. The folder structure looks something like this:

partials
├── sections
│ ├── section
│ │ ├── style.html
│ │ ├── base.html
  • Style.html: contains the unique style for a specific section and overrides the base style.
  • Base.html: the standardized base style of our section. The default color, size, etc.

Our overall strategy is to iterate through each section on each page. We’ll then grab the individualized section style for each section. Then, we will include the base.html only once. and the base style for whatever elements are in use in that section.

1. Create css_gen.html with section styles

Let’s start by creating a partial layout file that contains our generated CSS. We’ll use Hugo pipes to generate a resource from this file later on.

We’ll include this partial file in our inline header style (for critical css) and our non-critical stylesheet that we load asynchronously.

Add this partial between <style> tags in the head and footer of your site:

<style>{{ partial “site/css_gen.html” (dict “page” . “global” $ ) }}</style>

We’ll then pass the current context using dict so that we can use page-level and global variables in our partial file. We can now access page context (“the dot”) using .page and global context ($) using .global.

Normally, outside of a partial file we can access page params with {{ .Params }}. In our css_gen partial file, however, we will access page params with {{ .page.Params }}.

Our css_gen.html file will start by looking similar to the main layout file with our section loop:

// get each page
{{ range $index, $stacks := .page.Site.Pages }}
// check if the "stacks" key exists in our front matter{{ if .Params.stacks }}// iterate through each stack
{{ range $index, $stacks := .Params.stacks }}
// set a section ID based on the index
{{ $.page.Scratch.Set "section_index" $index }}
// set one class based on the front matter template value
{{ $.page.Scratch.Set "section_base" .template }}
// set a class with index prepended
{{ $section_style := print "c-" $index "-" .template }}
{{ $.page.Scratch.Set "section_style" $section_style }}
// set a class with page uniqueID
{{ $.Scratch.Set "page_hash" .UniqueID | urlize }}
{{ if in .template "header" }}
{{ partial "sections/header/style.html" (dict "page" . "global" $ ) | safeCSS }}
{{ else if in .template "section-form" }}
{{ partial "sections/section-form/style.html" (dict "page" . "global" $ ) | safeCSS }}
{{ else if in .template "footer" }}
{{ partial "sections/footer/style.html" (dict "page" . "global" $ ) | safeCSS }}
...

The main difference is the included style partial files, which use dictto pass the context and safeCSS to designate safe output.

In our page front matter, we can designate some styles for our specific section:

stacks:
- template: "header"
- template: "section-form"
background-color: "#efefef"
- template: "footer"

Our style.html for section-form looks like this:

{{ if .page.background_color }}.{{ .global.page.Scratch.Get "section_class" }}.p-{{ .global.page.Scratch.Get "page_uniqueid" }} {
background-color: {{ .page.background_color | default "#ffffff" }};
}
{{ end }}

You can use default values, isset, with, and other Hugo functions to control default and conditional stylesheet content.

Output:

.c-1-section-form.p-123sdf6521341dsfa {
background-color: #efefef;
}

2. Add base styles to css_gen.html

Base styles are only included once, while section styles are included each time a style is used for a particular section. We can use .Scratch to “activate” a base style and include the style outside of our range:

{{ range $index, $stacks := .page.Site.Pages }}
{{ if .Params.stacks }}
{{ range $index, $stacks := .Params.stacks }}
...
{{ else if in .template "section-form" }}
{{ partial "sections/section-form/style.html" (dict "page" . "global" $ ) | safeCSS }}
{{ $.page.Scratch.Set "form_base_css" true }}
...
// End of stacks range
{{ end }}
// End stacks conditional
{{ end }}
// End of page range
{{ end }}
// Include the base CSS for the form section only once
{{ if eq ( $.page.Scratch.Get "section_base_css" ) true }}
{{ partial "sections/section-form/base.html" (dict "page" . "global" $ ) }}
{{ end }}
...

Using .Scratch, we can choose to include the base CSS if the section exists. Scratch allows us to specify a variable within our range and pass that value outside of the range.

3. Specify critical and non-critical CSS

Our current css_gen file doesn’t specify critical or non-critical CSS. To do this, we need to limit our range to the first 2 sections for our critical CSS. Our non-critical CSS range will need to a range greater than 2. Since we want to use the same css_gen file and avoid duplicating work, we’ll need to adjust our current stacks range.

Don’t Repeat Yourself is the perennial mantra of the software developer. It doesn’t mean you should never do the same thing twice, but instead refers to having a single, authoritative source of truth for every piece of information used in your software. — DJ Walker

Let’s adjust where we include our partial file. In our head where we specify inline critical CSS:

{{ $.Scratch.Set "is_critical" true }}{{ $env := getenv "HUGO_ENV" }}{{ if or (.Site.IsServer) (eq $env "") }}<style>{{ partial "site/css_gen.html" (dict "page" . "global" $ ) | safeCSS }}</style>{{ end }}

We add a new scratch allowing us to pass a true/false value to our partial file.

We include an environment variable so that we can check if we’re in production or staging and then output different content. We use .IsServer to check if we’re using hugo server locally. We’ll use this more later on, but for now just know that there are certain functions we only want to use in our staging and local environments.

In our footer file where we include non-critical CSS:

{{ $.Scratch.Set "is_critical" false }}{{ partial "site/css_gen.html" (dict "page" . "global" $ ) }}// For IsServer just inline the style{{ $env := getenv "HUGO_ENV" }}{{ if or (.Site.IsServer) (eq $env "") }}<style>{{ partial "site/css_gen.html" (dict "page" . "global" $ ) | safeCSS }}</style>{{ end }}

In our footer, we set is_critical to false. We’ll add a conditional statement within our range:

// Get this current page UniqueID
{{ $this_uniqueid := .page.UniqueID | urlize }}
{{ range $index, $stacks := .page.Site.Pages }}
// Get the unique ID for each page
{{ $page_uniqueid:= .UniqueID | urlize }}
{{ if .Params.stacks }}
{{ range $index, $stacks := .Params.stacks }}
// Check for true/false value in outer template
{{ if eq ($.page.Scratch.Get "is_critical") true }}
// Get the styles for this page with an index of 0 and 1
{{ $.page.Scratch.Set "style_index" (and (eq $this_uniqueid $page_uniqueid) (le $index 1)) }}
// Non-critical CSS
{{ else }}
// Get the styles for all sections on all pages with an index greater than 1
{{ $.page.Scratch.Set "style_index" (gt $index 1) }}
{{ end }}// Our condition is now based on a scratch value
{{ if $.page.Scratch.Get "style_index" }}
// Include partial files
...

Two UniqueIDs are added so that we can get the inlined critical CSS. Then, we use the is_critical scratch value to specify our style_index scratch value.

The conditional for our critical CSS looks like this:

...
{{ if and (eq $this_uniqueid $page_uniqueid) (le $index 1) }}
...
{{ else if in .template "section-form" }}
{{ partial "sections/section-form/style.html" (dict "page" . "global" $ ) | safeCSS }}
{{ $.page.Scratch.Set "form_base_css" true }}
...

If the Unique ID of this page equals the Unique ID in our current range, the resulting style.html and base.html for sections 0 and 1 will be inlined in the <head> of our template.

The condition for our non-critical CSS looks like this:

...
{{ if gt $index 1 }}
...
{{ else if in .template "section-form" }}
{{ partial "sections/section-form/style.html" (dict "page" . "global" $ ) | safeCSS }}
{{ $.page.Scratch.Set "form_base_css" true }}
...

style.html and base.html for all non-critical sections on all pages are included.

Using PostCSS and asset pipelines in production

Now that we’ve generated our HTML and set up our critical and non-critical CSS generation, we can focus on processing our CSS using Hugo’s resource functions.

1. Create critical and non-critical stylesheets

In assets/scss/ we’ll create a critical and non-critical stylesheet with the same partial file included:

// in assets/scss/critical.scss and assets/scss/non-critical.scss{{ partial “site/css_gen.html” (dict “page” . “global” $ ) }}

Note: make sure you place your files in the assets directory since we are using Resource functions. Also, Resource functions respect Hugo’s lookup order, so you can place the associated assets in your theme assets folder or the top-level assets folder.

2. Set up Hugo Pipes

Let’s go back to our head where we include our critical CSS:

{{ $.Scratch.Set "is_critical" true }}
{{ $env := getenv "HUGO_ENV" }}
// Inline uncompressed styles if we're in a dev environment
{{ if or (.Site.IsServer) (eq $env "") }}
<style>{{ partial "site/css_gen.html" (dict "page" . "global" $ ) | safeCSS }}</style>
{{ end }}
// Inline minified and PostCSS styles if we're in production or have set a testing param
{{ if or (eq $env "production") (eq .Site.Params.postcss_testing true) }}
// Generate a resource and inline the resulting processed CSS
{{ $critical := resources.Get "scss/critical.scss" | resources.ExecuteAsTemplate "style.critical.scss" . | toCSS |postCSS | minify }}
// Display the content from our resource
<style class="critical">{{ (slice $critical | resources.Concat "style.scss").Content | safeCSS }}</style>
{{ end }}

We can use Hugo pipelines to generate a CSS resource from our HTML so we can minify our output and add vendor prefixes. Otherwise our inlined CSS will have significant whitespace and won’t be minified. If we’re using hugo server we’ll just inline the HTML instead of processing a resource each time we save the site.

I recommend adding a postcss_testing parameter so you can test the critical and non-critical CSS processing in your dev environment.

If you don’t care about vendor prefixes, you can probably just avoid this step entirely (only for critical CSS) and use the new (0.47) minify flag to compress your HTML.

Let’s go back to our footer where we include our non-critical CSS:

{{ $.Scratch.Set "is_critical" false }}
{{ partial "site/css_gen.html" (dict "page" . "global" $ ) }}
{{ $env := getenv "HUGO_ENV" }}
// Inline the style if we're in a dev environment
{{ if or (.Site.IsServer) (eq $env "") }}
<style>{{ partial "site/css_gen.html" (dict "page" . "global" $ ) | safeCSS }}</style>
{{ end }}
// Include a link to the non-critical stylesheet in production
{{ if or (eq $env "production") (eq .Site.Params.postcss_testing true) }}
{{ $noncritical := resources.Get "scss/noncritical.scss" | resources.ExecuteAsTemplate "noncritical.scss" . | toCSS | postCSS | minify | fingerprint }}<link rel="stylesheet" href="{{ $noncritical.Permalink }}" media="screen">{{ end }}

Our non-critical stylesheet reference is similar, but we include a link to the stylesheet instead of inlining it. Additionally, we use fingerprint for cache-busting. For a more comprehensive tutorial on Hugo pipes, read this tutorial from Regis.

Sample critical output with PostCSS:

Notice that sections 0 and 1 are included and have the same pageID.

Non-critical stylesheet with PostCSS, fingerprint, and minify:

Notice that sections with an index greater than 2 are included and there are many different page IDs referenced in the stylesheet.

Wrapping up

In this tutorial I’ve included a more simplified version of building sections with stacks of sections (blocks, partial files etc.).

As I’m developing Pancakes Builder, I actually include only one partial section layout instead of multiple pre-built section layouts (like section-form). Then, I use front-matter to make each section completely unique. I pull out base and individual styles for each element included in each section:

partials
├── section
│ ├── style.html
│ ├── base.html
├── elements
│ ├── element
│ │ ├── style.html
│ │ ├── base.html

This method allows for some pretty cool stuff, such as overlaying a visual builder that generates the front-matter required to build each partial layout.

Follow me on Twitter: https://twitter.com/BenBozzay

--

--