Behind the Scenes

Take a quick look under the hood.

By Liam Coley

Published on Feb 11, 2026 | Last Updated on Feb 20, 2026

DEVELOPMENTWEBSITE

I built the first iteration of this website in React, with Next.js and a few scripts comprising the backend. I wanted a place on the internet that was fully mine, away from the walled gardens of Instagram and the ephemeral nature of TikTok.

This first iteration served me well for about 2 years, but there were some critical issues. For one, it was incredibly slow. I had made some half-hearted attempts at optimisation, but they were hamstrung by a poor API and poor architecture decisions. Extending the site was also really difficult. Half-finished features cluttered the database, and any change I made seemed to require significant refactoring. The nail in the coffin was a React server-side component vulnerability, which required a big upgrade and a large rewrite. After a day wrestling with Vercel error messages and inconsistent build times, I decided to just rewrite the entire site.

For this task, I chose to use Svelte. I'd done some previous work with it and had really enjoyed the simplicity and control I had over reactivity, data fetching, and rendering. I augmented this with tRPC for the API layer. Two weeks of part-time work, and it was done. Page speeds were blazing fast, build times reduced by 90% (15 minutes down to 1 minute 30 seconds), and the underlying infrastructure was much more robust and extensible. Overall, a great outcome.

Behind the scenes, I have all of my photos in a private Backblaze B2 bucket. This is directly backed up from my drive, and on my local machine I also have a small Python script that generates thumbnails for all my photos, before syncing them to the cloud. To access these photos from my backend, I have a Cloudflare Worker that is given a new key each day (via a Google Cloud Script) and that forwards requests with this key to the bucket. This allows me to view all of my photos on the backend without requiring all my photos to be in a public bucket.

The Browser

The browser

I can access all of my photos in the Browser section of my backend. Each folder and photo is fetched with a sync job that is run by pressing the Refresh button. Each individual photo has metadata attached to it, which is generally pulled directly from the EXIF data (I have a script and some helpers to do this automatically). Location is filled in manually by me. I also built a basic tagging system, so I can keep track of custom information, like where I've posted my photos. This metadata is searchable and is retained when pushing the photo to the public-facing website or when generating images for socials.

Close up of a file

One of the great quality-of-life features I built out for the new version of the site is the Generate Image button. This has two operations: to create an image with a border and optional metadata, or to generate the various watermarked images that I use on the frontend.

Generate image

An image generated with the above settings would look like this:

Generated image

On the backend, I download the image, add the white border, then utilise the passed metadata to generate an SVG, which I then overlay on top of the border. Finally, the image is rendered and sent to a temporary folder in an R2 bucket, where it can be downloaded. This temporary folder is cleared periodically.

The other big button allows me to push the image directly to the website. With the way I've built V2, I can either push the image as its own photo, or I can link it with other images, creating variants of a photo[1]. This has been especially nice for displaying my black & white photos; previously, I'd have to make a difficult decision on whether to display the black & white version or the colour.

Photos

Once a photo has been pushed to the site (or created manually), I can go ahead and fill out the information. Generally, the location will come from the browser file. If I already have latitude and longitude information for that location from another photo, I can autofill it. Otherwise, I use the Geokeo API to reverse-geocode the place for my embedded maps.

The photo itself comes from the public-facing R2 bucket, and each photo is given a unique UUID. When pushing to the website, photos are generated from 64px up to 2048px (in powers of 2)[2].

On the right-hand side are the variants. Here I have two, a colour one, and a black & white one. Generally, the information for these will already be filled in via the push-to-site action, but if I need to, I can edit the data in there, or autofill it from EXIF (this is fetched automatically).

Photo edit screen

Collections

Finally, to display the photos I add them to relevant collections. I can randomise the photos, as well as manually drag them around in the grid. The cover photo can be set quickly by clicking the crown icon.

Collection edit screen

Adding photos is really quick as well. Inside this modal, I can select many photos and filter them by a query or a date. The modal automatically excludes photos that are already part of the collection.

Photo select dialog

Articles and Albums

While the focus of the site is very much on my photography, I have similar pages for my music and my writing. All articles and album blurbs are written in markdown and rendered on the frontend. Images can be uploaded and are automatically linked to the article or album.

Article edit screen

Ta-da!

That's really the gist of it. I have a few things left to finish (as of writing, the contact page hasn't been moved over), but hopefully this is somewhat illuminating. I couldn't find a solution that was cheap and flexible, so I decided to build my own.

The total cost of operation is near-nothing. I pay about CA$30 for the domain each year, Cloudflare R2 has free egress and 10GB of free tier storage that I haven't yet exceeded. The site is within the limits of the Vercel free plan. Geokeo is free. My database is on Neon and is currently within the free tier. The biggest cost is Backblaze B2, but this functions as my entire computer backup and comes to about CA$20/month (and is not really critical to the operation of the entire thing).

If you spot any bugs or are interested in a solution for your own portfolio, please don't hesitate to contact me. You can reach me via LinkedIn or, hopefully soon, via the Contact form (I'll get around to it eventually!).


Footnotes

  1. Really, all the photos you see on the site are single variants. Photos are just containers that hold common information such as photo names, descriptions, locations, and which variants are 'primary'. ↩

  2. This is along the longest edge. ↩