Ditched Wordpress for Hugo
By hagen
In the last few weeks I spent some time to replace Wordpress to maintain this page and replaced it with Hugo. Why? For the main part, just because I can.
There was a custom static site generator
I had built a static site generator on my own, for my - now derelict - homepage, about 20 years ago. Back then I used Coldfusion and later migrated the engine to php. But it never had any advanced featues. Yes, it was able to generate menus, but I lost interest in it and did not work on it any further.
Starting with blogs
When I started my first blog, I used movable type, wich at that time was a static generator too. I think it was in 2003 or 2004 when I switch to wordpress. As I did not publish a lot in the last … 10 years, I had no need to change anything, but it I’m always - at least often - interested in stuff I don’t need :)
Migrating to Hugo
I had a look at Hugo and Jekyll a few years ago, but not enough motivation to work with it. Somehow I now had, and so I installed Hugo and followed the getting started guide. That’s why I use the theme Ananke. It’s in the guide and seems to work good enough.
As I didn’t want to copy and convert all (whopping 63) articles manually. So I installed the Jekyll-Exporter plugin for Wordpress, which creates a nice Zip-Archive containing all the articles as Markdown files and the wp-content directory. The migration worked quite well, but not perfect.
It left some HTML snippets in the files, mostly for the images and galleries. So I built a quick and dirty converter in php, that creates a directory for each post, stores the post in the index.md file and creates subdirectories for the images. If there is a gallery, it creates a directory for each gallery within the post. The images are renamed for the galleries, so they are used in the same order as in the original post, as my custom gallery shortcode just uses all images in the referenced directory.
I love a nice lightbox
To provide a nice experience for viewing the images, I built some shortcodes and custom rendering templates to render single images and galleries. Clicking on any image opens a css-only-lightbox and every image is rendered with a srcset in different screen sizes.
The beauty of the render-templates in Hugo is, as soon as you built it, you don’t have to bother with them at the time of writing an article any more. By rendering static pages, you can create relative complex functions and render images in multiples sizes, without creating any rendering delay for the visistors.
Render-Image for Figures
For the images, I opted for a rendering wrapped in a figure-tag with an optional caption.
{{- $alt := "" -}}
{{- $caption := "" -}}
{{- with .Text -}}
{{- $caption = . | safeHTML -}}
{{- $alt = . | safeHTML -}}
{{- end -}}
{{- $image := .Page.Resources.Get .Destination -}}
{{ $imagePath := .Destination }}
{{- with $image -}}
{{ $pathHash := $imagePath | md5 }}
<div class="render-figure">
{{ $arguments := dict "context" . "image" $image "pathHash" $pathHash "Text" $caption }}
{{ partial "gallery-image.html" $arguments }}
</div>
{{- end -}}
Rendering the figure-tag
Within the shortcode for the image and gallery, I use a partial to handle the rendering of the image, the lightbox and creating all required image sizes for the responsive display.
{{- $fullSizeMax := 2500 -}}
{{$pathHash := .pathHash}}
{{$image := .image}}
{{ $prevHash := .prevHash | default "" }}
{{ $nextHash := .nextHash | default "" }}
{{ $inGallery := .inGallery | default false }}
{{ $ws := slice 240 480 768 1366 }}
{{ if $inGallery }}
{{ $ws = slice 240 480 768 }}
{{- end -}}
{{- $alt := "" -}}
{{- $caption := "" -}}
{{- with .Text -}}
{{- $caption = . | safeHTML -}}
{{- $alt = . | safeHTML -}}
{{- end -}}
{{- with $image -}}
{{- /* 1,2: 0; 3,4: 180; 5,6: 90 right; 7,8: 90 left (270) */ -}}
{{- with $image.Exif -}}
{{- with .Tags.Orientation -}}
{{- if (or (eq . 5 ) (eq . 6 )) -}}
{{- $image = $image.Resize (printf "%dx%d r270" $image.Height $image.Width) -}}
{{- else if (or (eq . 7 ) (eq . 8 )) -}}
{{- $image = $image.Resize (printf "%dx%d r90" $image.Height $image.Width) -}}
{{- else if (or (eq . 3 ) (eq . 4 )) -}}
{{- $image = $image.Resize (printf "%dx%d r180" $image.Width $image.Height) -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- $maxWidth := $image.Width -}}
{{- if (lt $fullSizeMax $maxWidth) -}}
{{- $maxWidth := $fullSizeMax -}}
{{- end -}}
{{- $w := printf "%dx webp" $maxWidth -}}
{{- $image = $image.Resize $w -}}
{{- $placeholder := ($image.Fill "48x48 q20") | images.Filter (images.GaussianBlur 6) -}}
{{- $lightboxPlaceholder := ($image.Resize "48x q20") | images.Filter (images.GaussianBlur 6) -}}
{{- $srcset := slice -}}
{{- range $ws -}}
{{- /* to avoid creating an image that is larger than the source */ -}}
{{- if (le . $image.Width) -}}
{{- $url := "" -}}
{{- /* crop to square image for gallery, full image in lightbox */ -}}
{{ if $inGallery }}
{{- $w := printf "%dx%d webp" . . -}}
{{- $url = ($image.Fill $w).RelPermalink | safeURL -}}
{{- else -}}
{{- $w := printf "%dx webp" . -}}
{{- $url = ($image.Resize $w).RelPermalink | safeURL -}}
{{- end -}}
{{- $fmt := printf "%s %dw" $url . -}}
{{- $srcset = $srcset | append $fmt -}}
{{- end -}}
{{- end -}}
{{- $set := delimit $srcset "," -}}
<figure class="gallery-figure img-wrapper lazy ">
<div class="cssbox">
<a href="#{{- $pathHash -}}-lightbox">
<img
class="lazyload cssbox_thumb"
width="100%"
data-sizes="(max-width: 480px) 480px, 100vw"
srcset="data:image/jpeg;base64,{{ $placeholder.Content | base64Encode }}"
data-xsrc="{{ $image.RelPermalink }}"
data-srcset="{{ $set }}"
alt="{{- if $alt -}}{{- $alt -}}{{- else if $caption -}}{{- $caption | markdownify | plainify -}}{{- else -}} {{- end -}}"
>
</a>
{{- with $caption -}}
<figcaption>{{- . | markdownify -}}</figcaption>
{{- end -}}
<a id="{{- $pathHash -}}-lightbox" href="#_">
<span class="cssbox_full">
<img
class="lazyload"
src="data:image/jpeg;base64,{{ $lightboxPlaceholder.Content | base64Encode }}"
data-src="{{- $image.RelPermalink -}}" alt="">
</span>
<a class="cssbox_close" href="#_"></a>
{{- if lt 0 (len $prevHash) -}}
<a class="cssbox_prev" href="#{{- $prevHash -}}-lightbox"> <</a>
{{- end -}}
{{- if lt 0 (len $nextHash) -}}
<a class="cssbox_next" href="#{{- $nextHash -}}-lightbox">> </a>
{{- end -}}
</a>
</div>
</figure>
{{- end -}}
Image Galleries
Galleries are a little more complex. I wanted to create a gallery with a lightbox. I used CSS-Box. The gallery-shortcode accepts a parameter for the directory that contains the images and a number of colums. The default is 2 but any number might work.
If the gallery would have incomplete rows, I use the first image or two for three columns, as header for the gallery. They are rendered either in the full width or the required number of columns, in front of the rest of the images.
<div class="gallery-container" >
{{- $imageDirectory := path.Join (.Get "dir") -}}
{{- $imageDirectoryPath := path.Join "content/" .Page.File.Dir (.Get "dir") -}}
{{- $rowSize := .Get "cols" | default 2 }}
{{- $files := readDir ($imageDirectoryPath) -}}
{{- $imageCount := len $files -}}
{{- $rest := mod $imageCount $rowSize -}}
{{- if eq 0 $rest -}}
{{- partial "columnar-gallery.html" (dict "context" . "directory" $imageDirectory "cols" $rowSize "files" $files) -}}
{{- else -}}
{{- partial "columnar-gallery.html" (dict "context" . "directory" $imageDirectory "cols" $rest "files" $files "first" $rest) -}}
{{- partial "columnar-gallery.html" (dict "context" . "directory" $imageDirectory "cols" $rowSize "files" $files "after" $rest) -}}
{{- end -}}
</div>
All the gallery-specific handling of the images is done in another shortcode-template, which by it self uses the gallery-image to render the seperate images.
{{- $imageDirectory := .directory -}}
{{- $rowSize := .cols | default 2 }}
{{- $files := .files -}}
{{- $prevHash := "" -}}
{{- $context := .context -}}
{{ $visibleFiles := $files }}
{{ with .first }}
{{ $visibleFiles = first . $files }}
{{ end }}
{{ $indexOffset := 0 }}
{{ with .after }}
{{ $visibleFiles = after . $files }}
{{ $indexOffset = . }}
{{ end }}
<div class="gallery-wrapper" style="grid-template-columns: repeat({{- $rowSize -}}, 1fr)">
{{- range $index, $file := $visibleFiles -}}
{{ $index = add $index $indexOffset}}
{{- $fileIsImage := lower .Name | findRE "\\.(gif|jpg|jpeg|tiff|png|bmp|webp|avif|jxl)" -}}<!-- is the current file an image? -->
{{- if $fileIsImage -}}
{{ $imagePath := path.Join $imageDirectory "/" .Name }}
{{ $image := $context.Page.Resources.GetMatch ($imagePath) -}}
{{- with $image -}}
{{ $pathHash := $imagePath | md5 }}
{{ $arguments := dict "context" . "image" $image "pathHash" $pathHash "prevHash" $prevHash "inGallery" true }}
{{ range first 1 (after (add $index 1) $files) }}
{{ $nextHash := (path.Join $imageDirectory "/" .Name ) | md5 }}
{{ $arguments = merge $arguments (dict "nextHash" $nextHash) }}
{{ end }}
{{ partial "gallery-image.html" $arguments }}
{{ $prevHash = $pathHash }}
{{- end -}}
{{- end -}}
{{- end -}}
</div>
Overall I’m quite happy with the result, as I managed to keep the complexity relative low and I do not have any duplicate template-code, which would suck during maintenance. There certainly is room for improvements, as this is my first contact with hugo, go and hugo templates. We will see, how this will work out in the long run and how long I do remember how it all works together.