In short
- The question
- My photography practice needed a platform that carries the whole client journey, from the first search to delivery, and the processes behind it.
- Approach
- I have designed, built and run it myself since 2009, modernising it step by step.
- Delivered
- A platform of my own with a website, private galleries, an asset library, a gift card web shop, an admin and a server of my own with safe deployments.
Photographer and builder
I have photographed weddings and families since 2009, and I built the website myself from day one. First a single page in HTML and CSS, briefly WordPress, then my own code again with some JavaScript, and PHP from 2015. The power of PHP let me reuse parts, and that put my web development into a higher gear: the step from HTML and CSS to a server language brought the server itself along, with everything that comes with it.
After that the site became more than a portfolio. From 2021 emails, invoices and availability moved onto the site, and step by step the rest of the process went digital too. Shoots I used to deliver on a USB stick, a CD or even in print are now viewed and downloaded by clients in their own gallery, and a gift card, once a paper card, is now ordered and paid for online. Today the platform carries both the customer journey, from the first search to the final delivery, and the processes that run behind it.
Private galleries, built in-house
Most photographers rent their client galleries from a specialised subscription service. I built my own, and it now holds a large archive of shoots.
A client gets one link. Behind it is the whole shoot, divided into the moments of the day, with a bar that follows along as you scroll: an IntersectionObserver keeps track of which moment is in view. You choose how to view the photos: in rows, in columns or compact. The rows come from an algorithm of my own. It combines portrait and landscape photos in twos or threes, with its own split of the width for each combination, so every row is the same height without cropping anything. The server calculates the columns in advance for two, three and four columns, and CSS picks the one that suits your screen. There is also a full-screen slideshow, downloads per photo or for the whole shoot, and every shoot in colour and in black and white.
It feels fast because there is little in between. Pages are built on the server, without a framework. Every photo exists in four widths, from 320 to 2560 pixels, and with srcset and sizes the browser picks the right one itself: a phone loads a smaller version than a 27-inch screen, and the photo is sharp on both. Photos only load once you scroll towards them, and nothing jumps: each photo’s dimensions are in the page before it arrives.
The story beside the photos
A shoot is more than a pile of photos, so I added the story of the day. On the same link, a written story then sits on the left, a chapter per moment, with all the photos on the right (the image above). Scroll the story and the photos move along to that moment, and the other way round: whichever side your mouse is on leads. The chapters come from the same division into moments as the gallery, so nothing has to be kept up twice. There is a reading mode, and on a phone a button to switch.
Couples can also write along. They get a link of their own with questions about their photos, their answers save as they type, and they can add comments and attachments. Every attachment is checked for what the file really is rather than its extension (finfo), gets a random name and is stored where the web cannot reach it.
One link as the key
A gallery has no password: the link itself is the key, 32 random characters (128 bits) that no one can guess. All gallery links arrive at a single router. The server only lets through a key of the right shape and looks up the gallery with a constant-time comparison (hash_equals). A wrong guess gets the same 404 page as an address that does not exist, so no response gives away whether a gallery exists. The link never travels on to other sites (Referrer-Policy), and clients’ names and email addresses are encrypted (AES) in the database.
An asset library of my own
Behind the galleries sits my own digital asset management system: an asset library that holds every shoot, every photo and everything that belongs to them. It works like professional software: select as in your computer’s file manager, keyboard shortcuts for everything, undo across bulk edits, and autosave that sends only the shoot that changed instead of the whole database. A save that would reduce the number of photos is refused.
Photos get in through an image pipeline of my own. I export once at full resolution, and libvips makes all the sizes from it, with a recipe that gives smaller files with no visible difference. Every photo also gets an AVIF version. The work runs in batches of about eight seconds, so even a wedding of hundreds of photos never stalls, and an original is only removed once every size demonstrably exists.
One command then puts everything online. It first does a dry run to work out whether it all fits on the server (the full archive is larger than the disk) and sends nothing if it does not. A checkbox in the asset library decides which galleries are online.
Focal points with face detection
The same photo appears on the site in different crops: square, narrow portrait, wide landscape. A focal point decides where the important part of a photo is, and every crop arranges itself around it (object-position). I set focal points in the asset library, and a script in Swift uses Apple Vision to find faces by itself. I calibrated it against my own choices: with a clear face it beats the centre of the photo, without a face it is chance. So the script only sets a focal point where it finds a face.
A site that stays light
The public site runs on a component system of my own in plain PHP, with dozens of components and page templates, and no build step: what I write is what the server runs. There is no compiler or bundler that turns it into something else first. A changed stylesheet automatically gets a new address with the file’s modification date in it, so browsers may keep everything for a long time. Nothing loads from third parties: no Google fonts, no analytics, no libraries from a CDN. There is very little JavaScript, only where it adds something, and a service worker of my own keeps the site usable on a weak connection.
Every photo exists in nine widths, from 320 to 5120 pixels, and with srcset the browser picks the smallest size that is still sharp: a phone makes do with a small version, a large high-resolution screen asks for a big one. The largest sizes are there for the photos that fill the whole screen. On top of that, the same link serves two formats. The page always points to a .jpg, but a browser that says it can handle AVIF gets the AVIF file from the server, on average half the weight. Vary: Accept makes sure a cache along the way never hands the wrong format to someone else.
Stories as a CMS of my own
The public stories of weddings and families serve two purposes. Couples and families enjoy reading and sharing their day as a story, and at the same time those stories are good for organic findability: real writing about real wedding days is exactly what people look for in a search engine. That is why I started them.
They read like a magazine and are built from blocks (text, photos, a chapter, a quote) in a JSON file, and each kind of block has its own component that renders it. Why JSON, and not a database or a CMS? The content lives in git, so every change is a version you can compare and roll back, and there is no extra system to secure and keep up to date. The page lays out photos of different proportions at the same height by itself, and builds its own table of contents for a long story.
A story only goes online once I approve it and the people in the photos have given their consent. That is built into the system, in a single function that knows the rules: anything that is not public automatically gets noindex and stays out of the sitemap.
From enquiry to invoice
Behind the site runs the business: the customer journey on one side, and the processes that make it possible on the other.
I show availability as free places per month, not as a calendar with dates. That way other couples’ dates appear nowhere, and I decide how many shoots I take on each month. Enquiries come in through a form that blocks spam without a captcha and without JavaScript: with a signed timestamp (HMAC), hidden decoy fields and a filter for gibberish. A visitor notices none of it.
Contracts, invoices and emails come from an admin of my own. It is deliberately not online: it is cut out of the production build and only opens locally under two conditions. There I can view every email with sample data and first send it to myself as a test. A plain-text version is added automatically, and the emails are built with tables so they also look right in Outlook. Whether they arrive, and not in spam, is down to the domain’s DNS records: SPF says which servers may send on the domain’s behalf, DKIM signs every email, and DMARC is set to p=reject, so mail that claims to come from the domain but does not check out is refused.
Gift cards with Mollie
Gift cards can be ordered and paid for without me, even at eleven at night. The web shop talks directly to the Mollie API, without a library. The amount always comes from the server, never from the form. A buyer returning to the site proves nothing: Mollie’s notification (the webhook) is the single source of truth, and even then the server checks the status itself. Processing the same payment twice has no effect, and a payment whose amount does not match comes to me for review instead of producing a card. The card itself is a print-ready document with bleed and crop marks, laid out in millimetres with plain CSS.
A server of my own, a safe deployment
Everything runs on a server of my own at Hetzner in Germany, in Docker, behind Caddy (automatic HTTPS and HTTP/3). A single Dockerfile produces a development build and a production build, from which the admin and everything private is physically removed. A deployment first starts the new version in a separate container and checks it with an extensive smoke test. Only then does it switch over, test again, and roll back to the previous version on its own if anything fails. A daily check emails me only when something is wrong.
Findability
Findability lives in the code, not in a plug-in. Structured data in JSON-LD tells search engines what the business is and where it works (LocalBusiness), what the site is called (WebSite) and where a page sits in the site (BreadcrumbList). It points to the same person as my other sites, so a search engine knows they are one and the same.
The sitemap builds itself, with every page and every public story. As the date of the last change it carries the date of the deployed version, not a new date every day, which search engines learn to ignore. Anything private, such as galleries, orders and stories without consent, gets noindex. Every page has one fixed address (canonical), and old addresses and domains redirect with a 301. A shared link always shows a share image small enough for chat apps: the smoke test weighs it on every deployment.
How I build
Until 2026 I wrote everything myself. For this project I deliberately chose technology that is as plain as possible: HTML, CSS and plain JavaScript (briefly jQuery, the usual way to work with JavaScript at the time), then PHP, and as few external libraries as possible. I know frameworks such as Laravel and React, and for clients I reach for them sooner. But here I wanted to work through the patterns myself, at a lower level, so that I really understand them and carry the mental model in my own head.
The rise of AI coding assistants was the turning point in 2026 to modernise the whole site, step by step instead of rewriting it: every step leaves the site working, and the order follows risk and return. With an AI coding assistant as a build partner, the additions included the story beside the photos (the assistant also helps write the stories), automatic focal points, a safer key for every gallery, the web shop, the move to a server of my own and the safe deployment. I decide what gets built and how, write out the analysis and the rules, and review and test every change. Every decision is recorded, with its reasons, in the code and the documentation.
Why it is here
Running a business of my own and building its IT myself has been my passion for years. I love taking photos, but that is a story of its own. What I enjoy most here is taking a real process, from the customer journey to everything behind it, and making it lighter with software of my own. For the client, and for myself.


