Rebuilding my photography site
So to go along with my 10 year anniversary, I launched the latest version of my website.
To the casual observer, not much has changed. Very similar layout, but underneath, it's totally brand new. So why did I change it then?
I used to be a HUGE fan of Koken, and that was what powered my site. Unfortunately the original developers sold it off to NetObjects. Their goal was to sell to a company that would continue development. Seems like that didn't happen. They did one release, then just let it die. It hasn't been updated in years. Given that it's generated server side, I wasn't happy that there weren't any security issues coming up.
So what did I end up going with and how did I develop it? Given I'm a programmer in my day to day work, generally favouring frontend web work, I decided to write it myself.
I have done a bit of work with headless CMS's lately and knew I wanted to go with that, so it was a matter of picking which one. I've used Prismic before and found it great to use, but being a hosted solution, and the way social media is going with artistic nudity, I wanted to self host, so that was out.
I ended up going with Strapi in a self hosted solution. However that is only part of the story. I was keen to go with a JAM Stack so that I didn't need to generate things on the server (which is great for caching). React.js is my favourite frontend framework (I know, it's not an actual framework, but it's easier to say) so I wanted something along those lines, and to avoid server requests, I used Next.js to generate everything needed ahead of time.
Working with Strapi was super easy, but I did add a couple of extra plugins. GraphQL which is an official one, along with a point list plugin (more on this later), which made deploying in Docker a little trickier (I'll cover build and deployment later on).
Using Next.js builds your page ahead of time and you have to follow some specific rules to prevent server side rendering, but beyond that, it just follows React.js, meaning all React.js addons are available for frontend use. I used:-
- Apollo (given we're pre-rendering, GraphQL isn't really that much of an advantage, but I just love working with it)
- Hamburger React (A simple hamburger menu control for on mobile)
- React Loader Spinner (For loading of the images on the home page)
- React Markdown (Strapi uses markdown for its text, so this is a no brainer)
- React Stack Grid (Pinterest style thumbnail grid)
- Simple React Lightbox (Lightbox for viewing larger images)
The rest is written by me.
For my homepage, I wanted to display a selection of my styles. Due to filling the screen, I had to choose what I was going to do when the aspect ratio didn't match (eg. Desktop vs Mobile). At one stage, I was scrolling the images to show it, but something really didn't sit right with me on that. So what I ended up doing is having two "feature" galleries on my backend, one for landscape and one for portrait orientations.
This still wasn't quite enough, yep, we'd stretch out (keeping aspect ratio) but that often meant that one side would end up off the screen. Often this would be fine, but in some cases, it would hide key elements of a photo or just look strange in cropping. This is where Strapi's point list plugin came in. It allows me to select a focus region of an image and send that data through to the client. Client side, I work out the percentage of X and Y that is, and move the image accordingly. Not perfect, but 90%, good enough for what I want, I can tweak the point location on the image if need be.
The server side setup was fun (for me). I'm a massive user of Docker to split everything into their own nice little packages.
The client was a simple two stage Docker build, first stage pulling in Next.js, adding my code, and building, the second stage, pulling that output into an NGINX container. The only tricky bit here is that being React.js based, the page is a Single Page Application, so designed to just come in on index.html
, to get around this, I found adding a try_files
section to my config to point at the root page worked well.
Strapi was a little tricker, the way it works is that it generates a heap of files for your collections (templates), so what I ended up doing was creating those manually on my machine, then using the Strapi Base Docker image, installing my required plugins and copying the collections in.
Strapi runs on my server, however whenever you hit my page, it doesn't connect to it. At one stage, it was hitting that server for the images that had been uploaded into Strapi, but using Docker volumes I was able to get around this. Strapi will send the URL in the form /uploads/20151009_SD_1_7541_5899e82fdc.jpg
. But given these just go into a directory on the server, I was able to share a volume between Strapi and my frontend (setting the frontend to be read-only) meaning that it's NGINX serving the files directly from the path that Strapi gives us. Perfect!
Now that everything is just static files, it means everything can be cached nicely. I set some reasonable lifetimes in my NGINX config for HTML, CSS and JS, and very long lifetimes for the images, as they won't change over time.
There are two downsides to the new site however. Given that I serve up large images, on slower connections, it can take a little while to load the first time (I lost points in the Lighthouse tests for this), unfortunately given the nature of the site, this isn't something that can be optimised further than it already is. So you may see a spinner while it loads.
The other downside means that whenever I upload new images, I have to rebuild and release the site. Not too bad given the images have already been uploaded to my server (into Strapi). The site only takes about 30 seconds or so to build, and a minute to upload. I have scripted this into a convenient Makefile
. If I didn't mind running the build on my server, I could set up a webhook within Strapi to do this for me, but I'd like to keep the build on my machine for now.
My biggest issue I had to find a workaround for is that using Next.js, requesting data can only be done on a page by page basis. This makes dynamic menus trickier (I use them for the different galleries, featured images and social media buttons). A bit of a hacky solution that I did was that before doing the site build, I use curl
to download those from Strapi (a simple GraphQL request) and store into .json
files (this is also scripted in my Makefile
). I then read those into my frontend code.
The final result is a MUCH faster site than my Koken based one, taking up a lot less space on the server too as a bonus. It's also a lot more secure not needing a backend component and all services and dependencies are easily updated.