Hugo, Tailwind, Svelte

I'm paid to be back-end developer. I'm a lot better at that than at front-end development. Recently, I built a mostly front-end website – a photo album. I did this using front-end technologies I knew and actually understand and mostly enjoy using. This post explains how and why I combined Hugo, Tailwind and Svelte to build the best front-end I've made so far.

This is a very quickly made write-up of this. If you have any questions, or want to learn more about this, feel free to contact me, preferably on Twitter for shorter things, e-mail for longer stuff (links at the bottom of the page).

Hugo

The first part of this puzzle is the static site generator I've been a fan of for a while now. Back when I (re)made this site it was great, and it's continuously been improved since.

The site I was making is a photo album, and Image Processing was a feature I'd seen added to Hugo but hadn't used yet. It turned out to be one of the simpler parts of building the site.

Overview pages

I gave the site a home page, and four page bundles for the four categories of photo's I had. The index.md of the bundles at first didn't contain anything other than the layout = "album"

{{ define "main" -}}
<div class="flex flex-wrap justify-center min-w-full">
  {{- range .Resources.ByType "image" }}
  <img sizes="(min-width: 1264px) 400px, (min-width: 964px) 300px, 200px"
    srcset='{{ (.Fill "400x400 Center q90").RelPermalink }} 400w, {{ (.Fill "300x300 Center q90").RelPermalink }} 300w, {{ (.Fill "200x200 Center q90").RelPermalink }} 200w'>
  {{- end }}
</div>
{{ end }}

With the srcset and sizes, I make sure at least 3 thumbnails show side-by-side, and they scale from 200 to 300 and finally 400 pixels square. Note: this configuration apparently isn't the prettiest on displays with high pixel density, because I didn't have a display to test that on. I'll probably add a few more sizes for 1.5x and 2x scaling.

Photo viewer

Next I needed to get Hugo to produce a few more sizes of the images to feed these to the viewer application I wanted to make. So I added a JSON Output Format for the album pages, in which I built a mapping of files to formats. The JSON would have the following shape:

{
    "#images/001.jpg": {
        "600x400": "path/to/resized/001_600x400.jpg",
        "960x640": "path/to/resized/001_960x640.jpg",
        "1200x800": "path/to/resized/001_1200x800.jpg",
        // more sizes
    },
    "#images/002.jpg": {
        // sizes
    },
    // ... many more images
}

Generating these links is done by Hugo's image pipelines as well, with a simple {{ (.Fit "1200x800 q95").RelPermalink }} for each resolution.

To be able to open the viewer, I added links to a hash identifier that matched the identifiers in the JSON.

{{ define "main" -}}
<div class="flex flex-wrap justify-center min-w-full">
  {{- range .Resources.ByType "image" }}
  <a class="block px-2 py-2" href='#{{ .Name | safeURL }}'>
    <img sizes="(min-width: 1264px) 400px, (min-width: 964px) 300px, 200px"
      srcset='{{ (.Fill "400x400 Center q90").RelPermalink }} 400w, {{ (.Fill "300x300 Center q90").RelPermalink }} 300w, {{ (.Fill "200x200 Center q90").RelPermalink }} 200w'>
  </a>
  {{- end }}
</div>
{{ end }}

And finally, added the script and CSS of my viewer built using Svelte:

{{ with .OutputFormats.Get "json" -}}
<link rel='stylesheet' href='{{ (resources.Get "photo-viewer/bundle.css").RelPermalink }}'>
<script defer src='{{ (resources.Get "photo-viewer/bundle.js").RelPermalink }}'
  data-photo-viewer='{{ .RelPermalink }}'></script>
{{ end -}}

With this, I can use the script[data-photo-viewer] selector to find the URL for my photo viewer data.

Before I go on about the viewer, first let's look at Tailwind.

Tailwind

Layout is an art in and of itself, and one I don't do well in. Tailwind has helped me through this by simplifying it a lot for me. Additionally, I wasn't the first to use Tailwind combined with Hugo, so I could copy from my neighbors and get a passing grade without much studying.

To combine the two, I used Hugo's Pipes , and more specifically: Hugo's PostCSS processing capability. My root folder contains a package.json with postcss-cli and tailwindcss dependencies, a simple postcss.config.js and a tailwind.config.js. That was all I needed to be able to process assets/main.css into the stylesheet the site uses. To reduce the size of the stylesheet, I added PurgeCSS too.

On to the hardest part: JavaScript!

Svelte

I've always had trouble working with JavaScript, but no single thing has simplified it as much for me as Svelte has. It's not made it easy, but at least I can now manage to build something useful out of it.

The goal was, relatively, simple: intercept some links in a document to open a photo viewer instead of the link, and display an image with some invisible overlays to click on to navigate to the next or previous images, and some keybindings for that same purpose.

For that I made two components: <Viewer> to manage the data and interactions, and <Image> to display the right sized image. Thanks to the srcset and sizes attributes, the latter is really quite simple. With a few Array.map invocations the input data is manipulated into what in the end is beautifully just <img {alt} {sizes} {srcset} />.

Still, even though <Viewer> is the more complex one, it's still far from complicated. I had more trouble with the styles than with any of the script.

The code can be found here in its own repository, because there's no clean integration in Hugo for something that can compile and bundle a Svelte app for you. This brings us to the final section of this post.

Svelte with Hugo

This is the part I'd love to see some improvements. To use the Svelte-based photo viewer in my Hugo-based site, I made some decisions I wish I didn't have to make.

  1. I could have built everything in one repository, but that would mean adding more tooling around Hugo which would have to run in parallel with hugo serve for development, and in order when building the site for publishing. Because of this, I also had to commit the generated bundle.js and bundle.css instead of only the sources, otherwise I would have still needed the tooling for Svelte in the Hugo repository.

  2. With the relatively new Hugo Modules I could add the photo viewer without having to struggle with git submodules, so I quickly decided to use that. Sadly, when I got to deploy the site to ZEIT the modules wouldn't work in absence of a Go toolchain. This was quickly fixed by vendoring the modules, but I'd prefer not to have to do that.

  3. Having both Hugo and Svelte use Tailwind was too much of a challenge for me, so I used Tailwind just for the Hugo part. The CSS in de Svelte components was simple enough to just use plain CSS for, but if I ever build something more complex or integrated, I'd love to combine this.

To improve on this setup, I'd prefer to see some way to transform Svelte components in a way similar to how PostCSS can be invoked to transform and bundle assets. I have no idea how exactly, but I think I'll start a conversation about that in Hugo's community somewhere soon.