Blog anatomy - Site generation (upgrade 2024)

Overview

I updated Hugo from 0.88.0 to 0.127.0 which, trusting semantic versioning, was going to be backward compatible. That update bundled 3 years of change within Hugo and the theme. I mentioned the theme as it is one of the main reasons the site looks and works the way it does. Albeit the theme provided most of the features I wanted already, I've customized quite a few behaviours so that only created more chance of incompatibility.

I had little hope that the overall process of producing a complete site will succeed.

And like so, it failed, the blog appeared to be very sick styling-wise. It looked as if the styles weren't applied at all. None of them!

SASS transpiler change from libsass to dart-sass

Upon checking the detail recently, LibSass was already deprecated in 2020. The version of Hugo I rolled out with in 2021 however, defaulted to LibSass.

While scanning the Web, I kept finding varying advice on how to control the transpiling or how to install the dart-sass. I’ve gone into a few rabbit holes and wasted a few days, not understanding what the real problem was. The advice seemed to be changing.

It's because, as time and my experimentation progressed further, the actual problem stopping me was changing as well. I couldn't clearly see it as I was jumping 3 years of change, and I had no prior experience of SASS transpiling.

There was an exploration to do with:

If your experience is somewhat similar, here are my notes.

Please note I'm not saying that Hugo docs don't clarify it; it just was my journey. Also, note that the notes are for Hugo versions around 0.127.

  1. You can pick a standard or extended binary (or build it yourself with extended tag). The difference is that an extended version contains encoder to WebP format (decoding is handled by both). An extended version also has, now deprecated, LibSass SASS to CSS transpiler.
  2. Hugo's contributors recommend the extended edition due to the WebP encoder, so consider running with it.
  3. Addition ("installation") of Dart Sass is done by simply putting a couple of resources on the $PATH
  4. Running with a standard (i.e. non-extended) edition of Hugo and without Dart Sass will not transpile your SASS files. It means your site may visually be "all over" the place, unless you and your theme have used plain old CSS mainly.
  5. If you want Dart Sass, then forget about https://github.com/sass/dart-sass-embedded and get https://github.com/sass/dart-sass
  6. Running extended Hugo (LibSass) with added Dart Sass will give you the option to pick one. libsass is still the default but the theme you picked may have altered that behaviour so please check.
  7. To understand how transpiling is invoked and controlled, search your theme's directory for ToCSS or for css.Sass (EDIT: July 12, 2024), then cross-reference what you've found in its vicinity with https://gohugo.io/hugo-pipes/transpile-sass-to-css/.
  8. You may need to pass an extra option transpiler with value set as dartsass to enable Dart Sass.
  9. If you run inside Alpine Linux container (not needed for Debian derivatives but maybe applicable for other tiny distros):
    1. when selecting a binary from https://github.com/sass/dart-sass, pick one with musl in the name.
    2. when running SASS transpiling add packages libc6-compat and libstdc++ to the final container image.
    3. if you were just learning about the above, your starting point should probably be at the official Hugo Dockerfile
    4. you may also want to have a look at my Dockerfile (below), especially if you wonder how to install Dart Sass or if you're integrating pagefind with Hugo. Pagefind requires Node and npm. This image builds Hugo from sources, so it takes a few minutes to build. The build tags are defaulted to build extended version and use nodeploy as I deploy with my own tools.

Dockerfile for building extended Hugo from sources, running with Dart Sass and pagefind

My starting point was the official Hugo Dockerfile so that's why parts of it look familiar.

 1ARG BASE_IMAGE='alpine:3.20.1'
 2ARG HUGO_BUILD_TAGS='nodeploy extended'
 3ARG NODE_VERSION='22.4.0'
 4ARG GOLANG_VERSION='1.22.5'
 5ARG DARTSASS_VERSION='1.77.5'
 6ARG PAGEFIND_VERSION='1.1.0'
 7ARG TZ=Europe/London
 8
 9FROM node:${NODE_VERSION}-alpine AS node
10ARG NODE_VERSION
11
12FROM golang:${GOLANG_VERSION}-alpine AS go
13ARG GOLANG_VERSION
14
15FROM go AS hugo
16ARG GOLANG_TAG
17ARG DARTSASS_VERSION
18
19ARG HUGO_BUILD_TAGS
20
21ARG CGO=1
22ENV CGO_ENABLED=${CGO}
23ENV GOOS=linux
24ENV GO111MODULE=on
25ENV HUGO_BUILD_TAGS=${HUGO_BUILD_TAGS}
26
27WORKDIR /go/src/github.com/gohugoio/hugo
28
29COPY ./src /go/src/github.com/gohugoio/hugo/
30
31# gcc/g++ are required to build SASS libraries for extended version
32RUN apk update && \
33    apk add --no-cache gcc g++ musl-dev git \
34      curl && \
35    go install github.com/magefile/mage
36
37RUN mage hugo && mage install
38
39RUN apk add --no-cache  \
40      curl && \
41    curl -sL "https://github.com/sass/dart-sass/releases/download/${DARTSASS_VERSION}/dart-sass-${DARTSASS_VERSION}-linux-x64.tar.gz" -o dart-sass.tar.gz && \
42    tar -xf dart-sass.tar.gz && \
43    cp -r ./dart-sass/* /usr/local/bin/ && \
44    apk --no-cache del \
45      curl && \
46    rm -rf /var/cache/apk/*
47
48# ---
49
50FROM ${BASE_IMAGE}
51
52ARG BASE_IMAGE
53ARG PAGEFIND_VERSION
54ARG TZ
55ENV TZ=${TZ}
56ENV GOTOOLCHAIN=local
57ENV GOPATH=/go
58ENV PATH=/go/bin:/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
59
60VOLUME /site
61WORKDIR /site
62
63# node/npm
64COPY --from=node /usr/lib /usr/lib
65COPY --from=node /usr/local/share /usr/local/share
66COPY --from=node /usr/local/lib /usr/local/lib
67COPY --from=node /usr/local/include /usr/local/include
68COPY --from=node /usr/local/bin /usr/local/bin
69## go (optional)
70#COPY --from=go /usr/local/go /usr/local/go
71#COPY --from=go /go /go
72# hugo
73COPY --from=hugo /go/bin/hugo /usr/bin/hugo
74# dart-sass
75COPY --from=hugo /usr/local/bin/sass /usr/local/bin/
76COPY --from=hugo /usr/local/bin/src /usr/local/bin/src
77
78# libc6-compat & libstdc++ are required for extended SASS libraries
79# ca-certificates are required to fetch outside resources (like Twitter oEmbeds)
80RUN apk update && \
81    apk add --no-cache ca-certificates libc6-compat libstdc++ git && \
82    apk add --no-cache tzdata  && \
83    npm install -y pagefind@${PAGEFIND_VERSION} && \
84    rm -rf /var/cache/apk/*
85
86# Expose port for live server
87EXPOSE 80
88
89CMD ["hugo", "env"]

./src directory is expected to contain Hugo source code (https://github.com/gohugoio/hugo.git) as we will build it here.

Once you git clone the source directly into "src" directory (./src/magefile.go has to exist) you can build it with:

1docker build --tag caged-hugo:1 .

For orientation this image was recently built and was 375MB large (without go).

Run the image with default entrypoint/command to see the output of hugo env:

 1docker run --rm caged-hugo:1
 2hugo v0.129.0-DEV+extended linux/amd64 BuildDate=unknown VendorInfo=mage
 3GOOS="linux"
 4GOARCH="amd64"
 5GOVERSION="go1.22.5"
 6github.com/sass/libsass="3.6.5"
 7github.com/webmproject/libwebp="v1.3.2"
 8github.com/sass/dart-sass/protocol="2.7.1"
 9github.com/sass/dart-sass/compiler="1.77.5"
10github.com/sass/dart-sass/implementation="1.77.5"

This will help you verify a few things.

The top-left corner, in the Hugo version string, tells you it is an extended build.
On the list you can see LibSass and WebP encoder (included in the extended build) and Dart Sass (added inside the Dockerfile).

An example of how to use this container to serve Hugo contents:

 1docker run \
 2    -p 80:80 \
 3    --rm \
 4    -it \
 5    -v /hugo/site/dir/on/host/src:/app/src \
 6    -v /hugo/site/dir/on/host/config:/app/config \
 7    -v /hugo/site/dir/on/host/out:/app/out \
 8    -v /hugo/site/dir/on/host/themes:/app/themes \
 9    --user 1000:1000 \
10    caged-hugo:1 \
11        hugo serve \
12            --bind 0.0.0.0 \
13            --port 80 \
14            --liveReloadPort 80 \
15            --environment development \
16            --source /app/src \
17            --configDir /app/config \
18            --themesDir /app/themes \
19            --destination /app/out \
20            --logLevel debug \
21            --cleanDestinationDir \
22            --disableFastRender

The above is how I use it, you are certainly going to have a different directory structure on your host machine (/hugo/site/dir/on/host). You may prefer to change port 80 to avoid collision. Keep in mind the server binds to all network interfaces (but is inside the container).

The pagefind ingests the output of Hugo build, and I place it in the static part of the website source. If you like the sound of that, please run:

 1# clear previously generated pagefind contents
 2rm -rf /hugo/site/dir/on/host/src/static/pagefind
 3
 4docker run \
 5    --rm \
 6    -it \
 7    -v /hugo/site/dir/on/host/src:/app/src \
 8    -v /hugo/site/dir/on/host/out:/app/out \
 9    --user 1000:1000 \
10    caged-hugo:1 \
11        npx pagefind \
12            --site /app/out \
13            --output-subdir /app/src/static/pagefind

If you included Go in the Dockerfile then also do:

1docker run \
2    --rm \
3    caged-hugo:1 \
4        go version

Markdown renderer changes from Blackfriday to Goldmark

I think that was a quiet failure and not as dramatic as SASS transpilation to fix. The majority of the blog contents were rendered well enough to be readable.

Until now, I've been styling markdown's code blocks and other constructs by attaching CSS classes to them (appending {: class="whatever-magic"}). Something has changed around that mechanism as instead of attaching the class, it would just print the entire curly statement. I've tried:

1[markup.goldmark.renderer]
2    unsafe = true

without success, but it was easy to work around it by creating a shortcode which wraps inner-markdown in a div with a class.

There was also an issue with links not opening in a blank page, but since I can embed plain HTML, I just do that now.

I think there were a few other tiny things that behaved a bit differently, but it wouldn't be worth digging it out.


Other posts in blog-anatomy series