Photo gallery via Fastly CDN
Lately I’ve been taking more pictures, but where I stand, mainstream social media feels weird as my go-to place to share my creations. In the past I’ve shared content (mostly music covers, gym content, and videos) to YouTube, Instagram, and even TikTok.
If you’ve read this website, that doesn’t make much sense. My only argument is how practical it is, since most people I know are on these platforms. As time passes by, my thoughts on what, why, and how I want to create, have shifted. Even this blog is a display of that fight I’ve been dwelling for over a decade; since I tried blogging on platforms like Medium, Tumblr, and eventually gave up.
Unrelated to this, this week I watched A Poet, which has led me to these spiraling thoughts regarding my purpose to create beyond recognition. What would art be, if not tainted by ego? By the thirst for applause?
Back to the topic, I know I want to share what I create, but I’d like to have control over how I share my creations. For pictures, platforms like Instagram, limit us to certain ratios, quality is compromised, backing up isn’t very straightforward. These and more, are the reasons why I’ve been thinking about expanding my website to also share the media I create.
I might explore these philosophical questions more in depth later, but standing before me was a wall, one that needed technical knowledge to be crossed, so here I come.
The main challenge being ease of publishing and technical constraints of a static website hosted under a single subdomain on a small VPS.
Hopping onto the technical
Before this update, this website had been running on a small BuyVM (affiliate link) VPS with 1 vcpu, 1 GB RAM, and 20 GB disk.
Normally this would suffice, but this server also hosts a reverse proxy to other services, and has a couple services running on it. So it’s limited.
First and foremost, you can check /photos/ for my new photo gallery.
Fastly CDN for my media assets
The most obvious solution to the storage problem was to add a CDN and serve media assets from a different subdomain. For this I opted to use Fastly CDN. I won’t go into specifics of setting this up, but I might add a few snippets here and there.
For this, I set up a CDN Service under the cdn. subdomain and set an Object Storage as the origin. A similar solution can be achieved with Cloudflare and R2 Storage, or AWS Cloudfront and AWS S3. I’ve already used both of these for work, and adding a bit of a mix to the monopoly while learning a new platform looked like a win-win.
The configuration was straightforward, after setting the origin the only tricky part was to add the correct VCL snippet to the service, which Fastly already provides for S3 compatible origins.
Now my newly created bucket was accessible via my cdn. subdomain, and I could start dropping images to the photos/ path and referencing them on my Hugo site.
Adding Hugo support
If you take a look at one of my albums, images are displayed in a grid, using resizable thumbnails (thanks to Fastly’s Image Optimizer), and open to a lightbox with a higher quality variant of the image.
I’ve achieved this by using a Hugo layout for my album pages (yes, I’ve moved the project to Sourcehut) that gets populated on build.
The way this works is by generating a meta.json for each album, with the album title, date, and other metadata for the album and pictures (like extracted EXIF data). The build process fetches each photos/*/meta.json (which is available via the cdn. subdomain) from the bucket and Hugo then reads them into the template, dynamically filling the page content at build time.
photos
├── cats
│ ├── DSC00020.JPG
│ └── meta.json
...
With the meta.json being:
{
"title": "Cats",
"date": "2026-07-05",
"cover": "DSC00020.JPG",
"photos": [
{
"src": "DSC00020.JPG",
"caption": "Chimuela on chair",
"camera": "SONY ILCE-6700",
"lens": "E PZ 18-105mm F4 G OSS",
"focal": "105.0",
"aperture": "4.0",
"shutter": "1/60",
"iso": "1600"
}
]
}
Then the template gets filled, using the locally fetched meta.json:
<div class="gallery-section">
<div class="gallery">
{{- range $meta.photos -}}
{{- $imgURL := printf "%s%s%s" $cdnBase $.RelPermalink .src -}}
{{- $thumb := printf "%s?width=350&format=webp" $imgURL -}}
{{- $display := printf "%s?width=1400&quality=85" $imgURL -}}
{{- $id := .src | urlize -}}
<figure class="gallery-item">
<a href="#{{ $id }}">
<img src="{{ $thumb }}" alt="{{ .caption | default .src }}" loading="lazy">
</a>
{{- with .caption }}<figcaption>{{ . }}</figcaption>{{ end -}}
</figure>
<div id="{{ $id }}" class="lightbox">
<a href="#_" class="lightbox-bg"></a>
<figure>
<img data-src="{{ $display }}" alt="{{ .caption | default .src }}">
{{- with .caption }}<figcaption>{{ . }}</figcaption>{{ end -}}
{{- $hasExif := or .camera .lens .focal .aperture .shutter .iso -}}
{{- if $hasExif -}}
<small class="lightbox-exif">
{{- .camera -}}
{{- with .lens }} · {{ . }}{{ end -}}
{{- with .focal }} · {{ . }}mm{{ end -}}
{{- with .aperture }} · f/{{ . }}{{ end -}}
{{- with .shutter }} · {{ . }}s{{ end -}}
{{- with .iso }} · ISO {{ . }}{{ end -}}
</small>
{{- end -}}
<a href="#_" class="lightbox-close">[close]</a>
</figure>
</div>
{{- end -}}
</div>
</div>
The scripts to create the meta.json, uploading an album folder to the object storage, and fetching the meta.json can be found at the scripts/ directory.
The build & deploy process
All the publishing for this site, up until now, had been manual by running build.sh from my local copy of the repository. And since the repository had been moved, I decided to give sourcehut builds a try.
The .build.yml is fairly simple since most of the process is done via the build.sh script. The main difference here has been the deployment.
Since my VPS SSH port is behind Tailscale I opted for a pull changes model. The build script pushes the changes to a specific path in the CDN and a cron in my VPS fetches the headers to verify the etag value every 5 minutes. If this value changes, the site.tar.gz is fetched and deployed.
Given the simplicity of this build, Sourcehut Builds were perfect. I can’t say the same about Sourcehut Pages, since their limitations, more specifically the CSP directives, would block my CDN. This made me fall back to the VPS.
I even tried hosting the whole site in an object storage similar to my media, but thought it would be overcomplicating things (yes, more than I’m already doing so).
Ease of publishing
Finally, the most important part and what I think will make the difference between using this or opting for a separate service, ease of use.
My main goal here was to be able to publish new albums easily, hence the complexity of my build process.
With this setup, I can move files to the mounted object storage using my phone or my computer, then run the album-init script to create/update my meta.json and trigger the repository build.
The albums follow the bucket structure, so it’s easy to organize and navigate the files.
I also wanted to automate the album-init + build execution, but Fastly Object Storage doesn’t have events like S3 or R2, which would’ve been great. For now, I’ll stick to updating the meta.json manually for captions and letting the script do its part for the EXIF data. Maybe even a cron on the mounted storage could do this.
Overengineered? I don’t know, but I’m happy with the result so far as this could expand to videos, and even now I’m using a custom font served from the CDN.
Disclaimer: parts of this setup were made with Claude Code’s support, but they all have been tested and modified manually based on actual needs.