In het kort
- Vraag
- Mijn fotografiezaak had een platform nodig dat de hele klantreis draagt, van de eerste zoekopdracht tot de oplevering, en de processen erachter.
- Aanpak
- Ik ontwerp, bouw en beheer het zelf sinds 2009, en moderniseerde het stap voor stap.
- Opgeleverd
- Een eigen platform met site, privégalerijen, beeldbank, webshop voor cadeaubonnen, een beheeromgeving en een eigen server met een veilige uitrol.
Fotograaf en bouwer
Sinds 2009 fotografeer ik huwelijken en gezinnen, en de website bouwde ik vanaf de eerste dag zelf. Eerst één pagina in HTML en CSS, even WordPress, daarna opnieuw eigen code met wat JavaScript, en vanaf 2015 PHP. Met de kracht van PHP kon ik onderdelen hergebruiken, en dat zette mijn webontwikkeling in een stroomversnelling: de stap van HTML en CSS naar een servertaal bracht ook de server zelf mee, met alles wat daarbij komt kijken.
Daarna werd de site meer dan een portfolio. Vanaf 2021 kwamen mails, facturen en beschikbaarheid naar de site, en stap voor stap werd ook de rest van het proces digitaal. Reportages die ik vroeger op een usb-stick, een cd of zelfs in print opleverde, bekijken en downloaden klanten nu in hun eigen galerij, en een cadeaubon, vroeger een kaart op papier, bestel en betaal je nu online. Vandaag draagt het platform zowel de klantreis, van de eerste zoekopdracht tot de oplevering, als de processen die erachter draaien.
Privégalerijen, zelf gebouwd
De meeste fotografen huren voor hun klantgalerijen een abonnement bij een gespecialiseerde dienst. Ik bouwde mijn eigen oplossing, en die draagt intussen een groot archief van reportages.
Een klant krijgt één link. Daarachter staat de hele reportage, ingedeeld in de momenten van de dag, met een balk die meeloopt terwijl je scrolt: een IntersectionObserver houdt bij welk moment in beeld is. Je kiest zelf hoe je de foto’s bekijkt: in rijen, in kolommen of compact. De rijen komen uit een eigen algoritme. Het combineert staande en liggende foto’s per twee of drie, met voor elke combinatie een eigen verdeling van de breedte, zodat elke rij even hoog is zonder iets af te snijden. De kolommen berekent de server vooraf voor twee, drie en vier kolommen, en CSS kiest welke bij je scherm past. Verder: een diavoorstelling op volledig scherm, downloads per foto of voor de hele reportage, en elke reportage in kleur en in zwart-wit.
Dat het vlot aanvoelt, komt doordat er weinig tussen zit. De pagina wordt op de server gebouwd, zonder framework. Elke foto bestaat in vier breedtes, van 320 tot 2560 pixels, en met srcset en sizes kiest de browser zelf de juiste: een telefoon laadt een kleinere versie dan een scherm van 27 inch, en op allebei is de foto scherp. Foto’s laden pas wanneer je ernaartoe scrolt, en er verspringt niets: de afmetingen van elke foto staan al in de pagina voor ze binnenkomt.
Het verhaal naast de foto’s
Een reportage is meer dan een stapel foto’s, dus bouwde ik er het verhaal van de dag bij. Op dezelfde link staat dan links een geschreven verhaal, hoofdstuk per moment, en rechts alle foto’s (het beeld hierboven). Scrol je in het verhaal, dan schuiven de foto’s mee naar dat moment, en omgekeerd: de kant waar je muis is, leidt. De hoofdstukken komen uit dezelfde indeling in momenten als de galerij, dus er is niets dubbel bij te houden. Er is een leesstand, en op de gsm een knop om te wisselen.
Koppels kunnen ook zelf meeschrijven. Ze krijgen een eigen link met vragen bij hun foto’s, hun antwoorden bewaren zich terwijl ze typen, en ze kunnen opmerkingen en bijlagen toevoegen. Elke bijlage wordt gecontroleerd op wat het bestand echt is en niet op de extensie (finfo), krijgt een willekeurige naam en komt op een plek die het web niet kan bereiken.
Eén link als sleutel
Een galerij heeft geen wachtwoord: de link zelf is de sleutel, 32 willekeurige tekens (128 bit) die niemand kan raden. Alle galerijlinks komen bij één router binnen. De server laat enkel een sleutel van de juiste vorm door en zoekt de galerij op met een vergelijking in constante tijd (hash_equals). Wie ernaast zit, krijgt dezelfde 404-pagina als voor een adres dat niet bestaat, zodat geen enkel antwoord verraadt of een galerij bestaat. De link reist niet mee naar andere sites (Referrer-Policy), en namen en e-mailadressen van klanten staan versleuteld (AES) in de databank.
Een eigen beeldbank
Achter de galerijen zit een eigen systeem voor digital asset management: een beeldbank waarin elke reportage, elke foto en alles wat erbij hoort beheerd wordt. Het werkt als professionele software: selecteren zoals in de bestandsverkenner van je computer, sneltoetsen voor alles, ongedaan maken over bulkbewerkingen heen, en automatisch bewaren, waarbij enkel de gewijzigde reportage vertrekt in plaats van de hele databank. Een opslag die het aantal foto’s zou doen dalen, wordt geweigerd.
Foto’s komen erin via een eigen beeldstraat. Ik exporteer één keer op volle resolutie, en libvips maakt daaruit alle maten, met een recept dat kleinere bestanden geeft zonder zichtbaar verschil. Daarnaast komt er van elke foto een AVIF-versie. Het werk loopt in porties van een achttal seconden, zodat ook een huwelijk van honderden foto’s nooit vastloopt, en een origineel verdwijnt pas als elke maat aantoonbaar bestaat.
Eén commando zet daarna alles online. Het rekent eerst in een droge run uit of alles op de server past (het volledige archief is groter dan de schijf) en verstuurt anders niets. Een vinkje in de beeldbank beslist welke galerijen online staan.
Brandpunten met gezichtsdetectie
Dezelfde foto verschijnt op de site in verschillende uitsneden: vierkant, smal staand, breed liggend. Waar het belangrijke deel van een foto zit, bepaalt een brandpunt, en elke uitsnede schikt zich daarnaar (object-position). Brandpunten zet ik in de beeldbank, en een script in Swift zoekt er met Apple Vision zelf gezichten bij. Ik ijkte het op mijn eigen keuzes: bij een duidelijk gezicht doet het beter dan het midden van de foto, zonder gezicht is het toeval. Het script zet dus enkel een brandpunt waar het een gezicht vindt.
Een site die licht blijft
De publieke site draait op een eigen componentensysteem in gewone PHP, met tientallen componenten en paginasjablonen, en zonder bouwstap: wat ik schrijf, is wat de server uitvoert. Er is geen compiler of bundler die er eerst iets anders van maakt. Een gewijzigde stylesheet krijgt vanzelf een nieuw adres, met de wijzigingsdatum van het bestand erin, zodat browsers alles lang mogen bewaren. Er laadt niets van derden: geen lettertypes van Google, geen statistieken, geen bibliotheken van een CDN. Er zit heel weinig JavaScript in, enkel waar het iets toevoegt, en een eigen service worker houdt de site bruikbaar op een zwakke verbinding.
Elke foto bestaat in negen breedtes, van 320 tot 5120 pixels, en de browser kiest met srcset de kleinste maat die scherp genoeg is: een telefoon heeft genoeg aan een kleine versie, een groot scherm met hoge resolutie vraagt een grote. De grootste maten zijn er voor de foto’s die het hele scherm vullen. Daarnaast levert dezelfde link twee formaten. De pagina verwijst altijd naar een .jpg, maar een browser die laat weten dat hij AVIF aankan, krijgt van de server het AVIF-bestand, gemiddeld de helft lichter. Vary: Accept zorgt ervoor dat een cache onderweg nooit het verkeerde formaat aan iemand anders geeft.
Verhalen als eigen CMS
De publieke verhalen van huwelijken en gezinnen hebben een dubbel doel. Koppels en gezinnen vinden het fijn om hun dag als verhaal te lezen en te delen, en tegelijk zijn die verhalen goed voor de organische vindbaarheid: echte tekst over echte trouwdagen is precies wat mensen in een zoekmachine zoeken. Daarom ben ik ermee begonnen.
Ze lezen als een tijdschrift en zijn opgebouwd uit blokken (tekst, foto’s, een hoofdstuk, een citaat) in een JSON-bestand, en elke bloksoort heeft een eigen component die ze toont. Waarom JSON, en geen databank of CMS? De inhoud staat mee in git, dus elke wijziging is een versie die je kan vergelijken en terugdraaien, en er is geen extra systeem om te beveiligen en bij te werken. Foto’s van verschillende verhoudingen legt de pagina vanzelf op één hoogte, en bij een lang verhaal bouwt ze zelf een inhoudstafel.
Een verhaal gaat pas online als ik het goedkeur én de mensen op de foto’s hun toestemming gaven. Dat zit in het systeem zelf, in één functie die de regels kent: wat niet publiek is, krijgt vanzelf noindex en staat niet in de sitemap.
Van aanvraag tot factuur
Achter de site draait de zaak: aan de ene kant de klantreis, aan de andere kant de processen die ze mogelijk maken.
Beschikbaarheid toon ik als vrije plaatsen per maand, niet als agenda met datums. Zo staan de datums van andere koppels nergens, en beslis ik zelf hoeveel reportages ik per maand doe. Aanvragen komen binnen via een formulier dat spam weert zonder captcha en zonder JavaScript: met een ondertekend tijdstempel (HMAC), verborgen lokvelden en een filter op wartaal. Een bezoeker merkt er niets van.
Contracten, facturen en mails maak ik in een eigen beheeromgeving. Die staat bewust niet online: ze wordt uit de productieversie geknipt en opent lokaal enkel onder twee voorwaarden. Elke mail kan ik daar bekijken met voorbeeldgegevens en eerst als proefmail naar mezelf sturen. Er komt automatisch een platte-tekstversie bij, en de mails zijn met tabellen opgebouwd, zodat ze ook in Outlook goed staan. Dat ze aankomen en niet bij spam belanden, regelen de DNS-records van het domein: SPF zegt welke servers namens het domein mogen versturen, DKIM ondertekent elke mail, en DMARC staat op p=reject, zodat mail die zich voor het domein uitgeeft maar niet klopt, geweigerd wordt.
Cadeaubonnen met Mollie
Cadeaubonnen bestel en betaal je zelf, ook om elf uur ’s avonds. De webshop praat rechtstreeks met de API van Mollie, zonder bibliotheek. Het bedrag komt altijd van de server, nooit uit het formulier. Dat de koper terugkeert naar de site, bewijst niets: de melding van Mollie (de webhook) is de enige bron van waarheid, en ook dan vraagt de server de status zelf na. Dezelfde betaling twee keer verwerken heeft geen gevolgen, en een betaling waarvan het bedrag niet klopt, komt bij mij ter controle in plaats van een bon op te leveren. De bon zelf is een drukklaar document met afloop en snijtekens, opgemaakt in millimeters met gewone CSS.
Een eigen server, een veilige uitrol
Alles draait op een eigen server bij Hetzner in Duitsland, in Docker, achter Caddy (automatische HTTPS en HTTP/3). Eén Dockerfile levert een versie om te ontwikkelen en een productieversie, waaruit de beheeromgeving en alles wat privé is fysiek wegvalt. Een uitrol start de nieuwe versie eerst in een aparte container en keurt ze met een uitgebreide rookproef. Pas dan schakelt hij om, test opnieuw, en valt bij een fout zelf terug op de vorige versie. Een dagelijkse controle mailt me enkel als er iets mis is.
Vindbaarheid
Vindbaarheid zit in de code, niet in een plug-in. Gestructureerde gegevens in JSON-LD vertellen zoekmachines wat de zaak is en waar ze werkt (LocalBusiness), hoe de site heet (WebSite) en waar een pagina in de site staat (BreadcrumbList). Ze verwijzen naar dezelfde persoon als mijn andere sites, zodat een zoekmachine weet dat het om één en dezelfde gaat.
De sitemap bouwt zichzelf, met elke pagina en elk publiek verhaal. Als datum van de laatste wijziging staat er de datum van de uitgerolde versie, en niet elke dag een nieuwe datum, die zoekmachines leren negeren. Wat privé is, zoals galerijen, bestellingen en verhalen zonder toestemming, krijgt noindex. Elke pagina heeft één vast adres (canonical), en oude adressen en domeinen sturen met een 301 door. Een gedeelde link toont altijd een deelbeeld dat klein genoeg is voor chatapps: de rookproef weegt het bij elke uitrol.
Hoe ik bouw
Tot 2026 schreef ik alles zelf. Voor dit project koos ik bewust voor zo puur mogelijke technologie: HTML, CSS en gewone JavaScript (heel even jQuery, toen de gangbare manier om met JavaScript te werken), daarna PHP, en zo weinig mogelijk externe bibliotheken. Ik ken frameworks als Laravel en React, en voor klanten grijp ik er sneller naar. Maar hier wilde ik de patronen zelf doorlopen, op een lager niveau, zodat ik ze echt begrijp en het mentale model zelf in mijn hoofd heb.
De opkomst van AI-codeerassistenten was in 2026 het kantelpunt om de hele site te moderniseren, stap voor stap in plaats van alles opnieuw te schrijven: elke stap laat de site werkend achter, en de volgorde volgt risico en opbrengst. Met een AI-codeerassistent als bouwpartner kwamen onder meer het verhaal naast de foto’s erbij (de assistent helpt ook bij het schrijven van de verhalen), de automatische brandpunten, een veiligere sleutel voor elke galerij, de webshop, de verhuis naar een eigen server en de veilige uitrol. Ik bepaal wat er gebouwd wordt en hoe, schrijf de analyse en de regels uit, en review en test elke wijziging. Elke beslissing staat met haar waarom in de code en de documentatie.
Waarom het hier staat
Een eigen zaak hebben en er zelf de IT voor bouwen, is al jaren mijn passie. Ik fotografeer graag, maar dat is een verhaal apart. Wat ik hier het liefst doe: een echt proces nemen, van de klantreis tot wat erachter gebeurt, en het met eigen software lichter maken. Voor de klant, en voor mezelf.


