René's Blog

TwicPics for Seamless Image Delivery

Continuing the works of setting up this Hugo blog, after initial hosting setup and some styling and theming updates (for which all updates are shared in the publicly forked repo). This time, I was looking at image processing and delivery.

It seems kind of obvious, but with a static site generator, easy image processing can be a bit tricky. Hugo offers several built in image processing capabilities, like resizing, crop, fit, fill, and other manipulators like filtering, colorizing, and blurring. However, they all have to be used knowingly, like so:

{{ $image.Resize "600x webp picture" }}

$image is processed to be resized (using the Resize function with the arguments 600px width, webp as format, and picture as WebP preset used in cwebp). This is all nice and good, but way too complicated. As a user, I don’t really want to think about that.

A solution is to outsource this and other image processing logic to a third-party image processing service like TwicPics, Cloudimage, or imagekit (full disclosure: my employer acquired TwicPics recently). Let’s integrate TwicPics and see how it behaves.

What TwicPics does and how it works

The TwicPics landing page

TwicPics doesn’t need much configuration. The setup guide is pretty straightforward. You have to set up a custom subdomain, for me this was https://rebeblog.twic.pics. This domain is then configured to a source path, which is https://rebe.blog/ for me as it want to allow all images served on my blog to be optimized and delivered via TwicPics.

TwicPics workspace panel

The TwicPics URL can now be used to access my publicly served images, like so:

https://rebeblog.twic.pics/uploads/Screenshot_2024-06-07_at_22.44.08.png?twic=v1/max=1400

Note the query parameter twic=v1/max=1400. This is part of the transformations API, where you can pipe transformations applied to an image. Besides standard things like cropping, rotation, resizing, etc., we can also do things like correct colors for color blind people by just adding achromatopsia=0.5 or deuteranopia=... respectively. TwicPics also allows for background removal or replacement (ML detecting subjects) and it works with focal points, again recognizing spots of attention via AI/ML.

When the image is processed, it gets delivered via their CDN behind twic.pics.

Integrating TwicPics into Hugo

The integration into a Hugo site is pretty easy.

First, we need to add the following into our head tag of the website:

<script async defer src="https://rebeblog.twic.pics/?v1"></script>

Second, we need to render the image tags as follows:

{{ $image := .Destination | safeURL }}

{{ $path := "" }}
{{ if eq .Page.BundleType "leaf" }}
    {{ $path = .Page.RelPermalink }}
{{ end }}

{{ if and hugo.IsProduction site.Params.twicPicsUrls }}
    <img loading="lazy" alt="{{ .Text }}" data-twic-src="image:{{ $path }}{{ $image }}" />
{{ else }}
    <img loading="lazy" alt="{{ .Text }}" src="{{ $path }}{{ $image }}" />
{{ end }}

For leaf bundles we need to add the relative permalink of the page so the files are resolved correctly when added relative to the bundle.

E.g., sunrise.png is placed in /content/posts/good-morning/ and used in the index.html with ![The sun rises](sunrise.png). The src or data-twic-src attribute will be rendered as /posts/good-morning/sunrise.png.

However, pages (markdown files without a separate directory) will render the file relative to the static directory.

The src attribute is replaced by data-twic-src and the URL is prefixed by image:. And .Text is the image description.

I’ve hacked that together in the typo template (and fixed it afterward), feel free to have a look there. Additionally, I bound it to Hugo’s IsProduction state as local files aren’t accessible by TwicPics and the parameter twicPicsUrls to configure this easily or turn it off if needed.

Thus, my hugo.toml looks as such:

[params]
# TwicPics
twicPicsUrls = "https://rebeblog.twic.pics/?v1"
# ...other parameters

That’s it, actually.

Impressive Improvements

TwicPics recognizes the device, viewport size, heck, even the bandwidth that is available, and renders the images in the backend accordingly to produce an optimal user experience.

Let’s have a look at what it does with Brave/Chrome and the picture from a previous post:

Image served via TwicPics

Noteworthy:

  • We see that the image is served via the TwicPics domain and not mine.
  • Although the original is a PNG, we get the content type image/webp, thus it’s a WebP.
  • The width of the column in the template is 700px, so TwicPics renders the image at the max size of 1400px.
  • The size of the delivered file is 94 KB. The original is 1.6 MB. That’s 17 times smaller!

Needless to say that this is quite an improvement!

And best, it’s free for small use cases like this blog, or whatever fits into 3 GB of CDN traffic.

Have fun! 🏞️