A photographer's website has an awkward job. It needs to have a character of its own, but not so much character that it competes with the photographs. It has to feel finished while remaining easy to change. It should invite someone to stay, then get out of the way once they do.
I rebuilt this site because the old version could show the work, but it was becoming harder to develop without repeating myself. A new project meant touching several files. English and Polish pages could drift apart. A photograph could exist in a project and still need to be added separately to the home page and archive.
The visible result is still deliberately simple. Underneath, almost everything changed.
One layout, two languages
The original site was hand-written HTML. That directness was useful at the beginning: open a file, change the markup, refresh the page. It also meant that every repeated element became another place to maintain.
I moved the site to Astro and reduced each page to a thin route around one shared component. English lives at the root, Polish under /pl/, but there are not two websites. Both versions use the same layout, navigation and behaviour. The interface copy lives in one bilingual dictionary, and internal links pass through the same locale-aware helper.
That arrangement is not exciting to look at in a screenshot. It is exciting when a design change needs to happen once instead of twice.
Side Quests follow the same rule. A post has one slug and one publication date, with an English and Polish body travelling together. The build refuses to publish it if either language is missing. I would rather have a loud failure before deployment than a quiet half-translated page afterwards.
A photograph should exist once
The more important simplification concerned the photographs themselves.
At one point the home page, archive and project index were fed by separate lists. They described overlapping pieces of the same work, which meant they could disagree. Fixing one view did not necessarily fix the others.
Now a project owns an ordered set of plates. Each plate holds its image, label, alternative text and a few editorial decisions: show it on the home page, include it in the archive, use it as the project cover. The grids are derived from those choices.
This sounds like a data-model decision because it is one. It is also an editing decision. When I attach a new photograph, I decide where it belongs beside the photograph itself. There is no separate index to remember later.
The source image goes through Astro's image pipeline, which generates appropriately sized versions rather than sending the original file everywhere. Its real proportions control the frame. Until the image is ready, the site can show a labelled placeholder or a small film-processing state instead of collapsing into an empty layout.
The website can be unfinished without looking broken.
Motion with a reason
Most of the interface is typography, spacing and photographs. DM Sans does the broad editorial work; DM Mono handles the small labels and metadata. The palette is warm black, paper white and one red accent. A faint grain sits over larger screens, just enough to keep the surface from feeling clinical.
There is motion, but it has a job.
The navigation leaves while I scroll down and returns when I reverse direction. Opening a project lets its cover expand into the first frame. Clicking a photograph opens a lightbox from its position in the grid. Images reveal only after the browser has loaded and decoded them, so an empty rectangle does not perform the animation before the photograph blinks into place.
None of this is allowed to trap the visitor. The gallery works as an accessible dialog, returns focus when it closes and leaves ordinary links in the markup as a fallback. If the operating system asks for reduced motion, the transitions and smooth scrolling step aside.
The details are small because the site is not the main event.
Static, until it cannot be
The public site is built as static files and served from Cloudflare. That keeps the common path simple: requesting a page does not wake an application server or query a database.
There is one deliberate exception. The booking form needs to send something.
A small Worker receives the brief, validates it, rejects oversized requests, slows repeated submissions and uses a hidden honeypot field to discard simple bots. It sends a plain-text email directly to me through Cloudflare's email service. It does not create an account, store the enquiry in a database or introduce a separate customer platform.
That boundary matters to me. Dynamic code exists where the interaction requires it, not because the rest of the site happened to use a framework.
A CMS that stays at home
I wanted a friendly way to edit projects and Side Quests, but I did not want the live website to depend on a CMS.
Keystatic runs only during local development. It gives me forms for uploading plates, writing bilingual posts and choosing where photographs appear. When I press save, it writes ordinary YAML and Markdoc files into the repository. Git remains the history, the backup and the source of truth. The production build contains no admin panel.
This is a useful kind of compromise: a visual editor when I need one, plain files when I need to understand what actually changed.
Built in conversation, checked in reality
Much of the rebuild happened in conversation with coding agents. I could describe a behaviour, inspect the implementation, run the site and tighten the request. That made large structural changes possible quickly, especially the move from duplicated lists to one content model.
Speed also made it easier to create convincing mistakes.
A page can compile and still hide its navigation at the wrong moment. A CMS can save a file that the content loader never sees. A reveal animation can be technically smooth while making every photograph flash twice. These were not theoretical edge cases; they were the kind of problems that appeared only when the whole system was used.
So the repository now checks content rules, bilingual publishing, the booking endpoint, accessibility contracts and production builds. Then I still open the pages, switch languages, resize the window, use the keyboard and look at the photographs. Automated checks are excellent at protecting decisions already made. They do not decide whether a page feels quiet enough.
What I was actually building
It is tempting to treat a portfolio as a finished object. Launch it, announce it and move on. A working photography website is closer to a contact sheet: it changes as the work changes, and its structure has to make the next edit easier.
The best decisions in this rebuild are therefore mostly invisible. A photograph has one home in the data. A translation cannot disappear unnoticed. The editor does not become production infrastructure. The booking form does not become a customer database. Motion waits for the image and yields when asked.
The website has a point of view, but it knows what it is framing.