← Alle berichten

Bouwlog

Hoe deze site is gebouwd — en wat de AI-tools echt deden

Ik bouwde deze site om een vrij simpele reden: ik wilde één link om te geven aan een klant, een recruiter of een collega die vraagt wat ik nu eigenlijk doe. Door hem te bouwen kon ik ook een kleinere vraag beantwoorden — hoe ver kom je met een met de hand geschreven statische site, plus een paar AI-tools, zonder framework en zonder build-stap?

Waarom een persoonlijke site, en waarom nu

Eerlijk gezegd is het meeste van mijn werk onzichtbaar. Het staat in de repo's van anderen, achter de domeinen van anderen, en er staat zelden mijn naam op. Een persoonlijke site is het enige oppervlak dat ik volledig beheer — een plek om te vertellen wat ik doe, een paar echte projecten te tonen en een gesprek makkelijk te beginnen. Dus hield ik het bewust klein: een handvol statische pagina's en precies één stukje back-end, het contactformulier.

Eerst ontwerpen, in Open Design

Ik begon niet met code. Ik begon in Open Design, dat elke pagina als een eigen bestand behandelt en het resultaat eruit gooit als gewone HTML, CSS en JavaScript. De export komt ook met een ontwerp-manifest — een JSON-bestand dat het startpunt benoemt, de pagina's opsomt en de designtokens (kleur, afstand, radius, beweging) vastzet, zodat de code niet stilletjes terugvalt op een of ander standaardthema. Dat manifest beschouw ik als het contract. Het palet waarop je dit leest, een warme crème met één oranje accent, staat op één plek en wordt overal hergebruikt.

Statische export met Next.js

"Geen build-stap" hield geen stand zodra er echte inhoud kwam. De site is nu een Next.js-app, maar wordt nog steeds als statische bestanden uitgeleverd. De statische export van Next.js (output: "export") draait tijdens de build en schrijft gewone HTML, CSS en JavaScript naar een out/-map. Geen server en geen framework-runtime die in de browser het werk doet — dus wat er wordt uitgerold is nog altijd een statische site. Dat betaalt zich terug: de Lighthouse-scores zijn 100 voor performance, toegankelijkheid en SEO.

De enige eis die niet triviaal was, was taal. De site is tweetalig, Engels en Nederlands, en in plaats van tekst in de browser om te wisselen is elke taal nu een eigen statische route — /en/… en /nl/… — die tijdens de build uit gedeelde componenten wordt gegenereerd. De URL draagt de taal:

// next.config.mjs
const nextConfig = { output: "export", trailingSlash: true };

Omdat de taal in de URL zit, krijgen zoekmachines en schermlezers meteen de juiste pagina, met het lang-attribuut dat bij de route past — geen client-side schakelaar waarop gewacht moet worden.

Een contactformulier dat echt mail verstuurt

Een statische site kan zelf geen e-mail versturen, dus het contactformulier is het enige stukje met een back-end. Het is een kleine Azure Function, geschreven als een geïsoleerde .NET 8-worker, die luistert op POST /api/contact. Die valideert wat er binnenkomt, negeert een verborgen honeypot-veld dat bots graag invullen, en geeft het bericht door aan Azure Communication Services, dat het als echte e-mail vanaf mijn eigen domein verstuurt.

Het geïsoleerde worker-model heeft één scherpe rand die me wat tijd kostte. In die runtime zet WriteAsJsonAsync de statuscode stilletjes terug naar 200, dus een foutrespons ziet er voor de browser nog steeds uit als succes. De oplossing is om de status opnieuw te zetten nadat de body is geschreven:

var res = req.CreateResponse(code);
await res.WriteAsJsonAsync(new { error });
res.StatusCode = code; // worker 1.x WriteAsJsonAsync forces 200; re-assert after

Het is één regel, en die wordt gedekt door een unit-test — precies omdat dit het soort ding is dat maanden later stilletjes kapotgaat.

Naar Azure brengen

Het geheel draait op een Azure Static Web App. De gratis laag host de statische bestanden en de Function samen, op één domein, wat de boel klein houdt. Deployen is continu: push naar main en een GitHub Actions-workflow gaat af, bouwt niets — er valt niets te bouwen — en uploadt zowel de site als de API. Open een pull request en je krijgt een tijdelijke preview-omgeving, wat echt handig blijkt om een wijziging te bekijken voordat die live gaat.

De enige echte config is de deploy-actie vertellen waar alles staat:

app_location: "/"        # the static site
api_location: "api"      # the .NET function
output_location: ""      # nothing to build

Het eigen domein kostte de meeste tijd. Het hoofddomein wijst via een A-record naar het IP-adres van de Static Web App, www is een CNAME naar de gegenereerde hostnaam, en beide worden in Azure als custom domain geregistreerd. Met DNS bij een externe registrar moet je wachten op propagatie — en de verificatie-TXT-records laten staan tijdens het instellen.

Wat de AI-tools echt deden

Twee tools deden het AI-deel, en het is de moeite waard precies te zijn over wat elk echt deed. Open Design leverde de layout en de tokenset. OpenCode, een AI-codeeragent die in de terminal draait, schreef en herstructureerde code door de hele repo. Het koppelde de tweetalige schakelaar, de formuliervalidatie, de Azure Function en het testproject, en hield eerdere beslissingen consistent terwijl de site groeide.

Waar de tools uitblonken was het mechanische werk: een attribuut hernoemen in zeven bestanden, de inspringing eerlijk houden, een beschreven regel omzetten in een validator met tests, en opmerken dat een donkere ster op een donker vierkant gewoon een zwarte vlek wordt. Waar ik zelf nog moest beslissen: wat de site moet zeggen, welke oranje tint het wordt, en wat "klaar" nu echt betekent.

Simpel gezegd verplaatsten de AI-tools het werk van typen naar beoordelen. De meeste tijd ging naar beslissen wat er gebouwd moest worden en checken of het resultaat klopte — niet naar toetsaanslagen.

Wat ik anders zou doen

Drie dingen die ik de volgende keer anders zou doen:

  • Zet de designtokens vast voordat er één component staat. Een tint halverwege aanpassen betekent elke pagina aanraken.
  • Schrijf de tests vóór de functie, niet erna. De statuscode-bug werd gevonden door een test die er vanaf het begin had moeten zijn.
  • Houd het eerste bericht kort. Dit groeide uit; het volgende wordt kleiner en staat eerder live.