I'm paid to be back-end developer. I'm a lot better at that than at front-enddevelopment. Recently, I built a mostly front-end website – a photo album. Idid this using front-end technologies I knew and actually understand andmostly enjoy using. This post explains how and why I combined Hugo, Tailwindand 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, orwant to learn more about this, feel free to contact me, preferably on Twitterfor shorter things, e-mail for longer stuff (links at the bottom of the page).
The first part of this puzzle is the static site generator I've been a fan offor a while now. Back when I (re)made this site it was great, and it'scontinuously been improved since.
The site I was making is a photo album, and ImageProcessing was afeature I'd seen added to Hugo but hadn't used yet. It turned out to be one ofthe simpler parts of building the site.
Overview pages
I gave the site a home page, and four page bundles for the four categories ofphoto's I had. The index.md
of the bundles at first didn't contain anythingother 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 showside-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 highpixel density, because I didn't have a display to test that on. I'll probablyadd 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 feedthese to the viewer application I wanted to make. So I added a JSON OutputFormat for the album pages, inwhich I built a mapping of files to formats. The JSON would have the followingshape:
{ "#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 asimple {{ (.Fit "1200x800 q95").RelPermalink }}
for each resolution.
To be able to open the viewer, I added links to a hash identifier that matchedthe 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 URLfor my photo viewer data.
Before I go on about the viewer, first let's look at Tailwind.
Layout is an art in and of itself, and one I don't do well in. Tailwind hashelped me through this by simplifying it a lot for me. Additionally, I wasn'tthe first to use Tailwind combined with Hugo, so I could copy from myneighbors and get a passing grade without much studying.
To combine the two, I used Hugo'sPipes, 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
. Thatwas all I needed to be able to process assets/main.css
into the stylesheetthe site uses. To reduce the size of the stylesheet, I added PurgeCSS too.
On to the hardest part: JavaScript!
I've always had trouble working with JavaScript, but no single thing hassimplified it as much for me as Svelte has. It's not made it easy, but atleast I can now manage to build something useful out of it.
The goal was, relatively, simple: intercept some links in a document to open aphoto viewer instead of the link, and display an image with some invisibleoverlays to click on to navigate to the next or previous images, and somekeybindings for that same purpose.
For that I made two components: <Viewer>
to manage the data andinteractions, and <Image>
to display the right sized image. Thanks to thesrcset
and sizes
attributes, the latter is really quite simple. With a fewArray.map
invocations the input data is manipulated into what in the endis beautifully just <img {alt} {sizes} {srcset} />
.
Still, even though <Viewer>
is the more complex one, it's still far fromcomplicated. I had more trouble with the styles than with any of the script.
The code can be foundherein its own repository, because there's no clean integration in Hugo forsomething that can compile and bundle a Svelte app for you. This brings us tothe final section of this post.
This is the part I'd love to see some improvements. To use the Svelte-basedphoto viewer in my Hugo-based site, I made some decisions I wish I didn't haveto make.
I could have built everything in one repository, but that would mean addingmore 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 andbundle.css instead of only the sources, otherwise I would have still neededthe tooling for Svelte in the Hugo repository.With the relatively new Hugo Modules Icould 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 toZEIT the modules wouldn't work in absence of a Gotoolchain. This was quickly fixed by vendoring the modules, but I'd prefernot to have to do that.
Having both Hugo and Svelte use Tailwind was too much of a challenge forme, so I used Tailwind just for the Hugo part. The CSS in de Sveltecomponents was simple enough to just use plain CSS for, but if I ever buildsomething more complex or integrated, I'd love to combine this.
To improve on this setup, I'd prefer to see some way to transform Sveltecomponents in a way similar to how PostCSS can be invoked to transform andbundle assets. I have no idea how exactly, but I think I'll start aconversation about that in Hugo's communitysomewhere soon.