Maskinrummet

Maskinrummet

Sådan er wi24rd.com bygget. Kolofon, ikke markedsføring.

Sådan er wi24rd.com bygget

For de IT-kyndige: wi24rd.com kører på Cloudflares platform fra DNS til database, uden en eneste server, som nogen skal holde øje med. Sitet er samtidig en åben demo af, hvad man kan bygge på Cloudflare Workers og resten af platformen, med motoren frit tilgængelig i tingsvad.dk-projektet.

Alt herunder er, hvad sitet faktisk bruger. Motoren er open source i tingsvad.dk-projektet på GitHub; sitets eget indhold, design og kode er privat. Undersiderne måler, mens du ser på dem.

Kanten

Navn, værn og adgang foran alt andet
DNS og domæne

Navnet bor på Cloudflares navneservere

Zonen wi24rd.com svarer fra Cloudflares anycast-DNS, så opslaget sker i den by, der ligger nærmest den, der spørger. Trafikken proxies gennem Cloudflare: Workerne har ingen egen offentlig IP-adresse, der kan angribes udenom. DNSSEC signeres i samme zone, og DS-posten lægges hos registraren, så en validerende resolver kan afvise forfalskede svar.

Netværk og sikkerhed

HTTP/3, HSTS og CSP

Alt serveres over HTTP/3 fra Cloudflares net med HSTS, så en browser aldrig prøver ukrypteret. Content-Security-Policy tillader kun sitets egne scripts; skrifterne ligger på sitet selv, og den eneste fremmede adresse er Cloudflares egen analyse-beacon. Siderne er statiske filer, så CSP og HSTS sættes af den middleware, der streamer HTML (_headers gælder kun for svar direkte fra filerne). Ingen cookies til gæster.

DDoS, WAF og analyse

Et værn, der er tændt, før nogen beder om det

Cloudflares DDoS-beskyttelse står foran al trafik uden opsætning, og det forvaltede WAF-regelsæt stopper kendte angrebsmønstre, før de når Workerne. Værnet viser hver time, hvad det har stoppet, og Crawlerne, hvilke bots der kom forbi. Web Analytics tæller besøg med et lille script fra Cloudflares eget domæne, uden cookies eller fingerprinting. Sitets egen besøgstæller nederst på siden er en saltet daghash i KV: saltet skiftes hver dag og kasseres, så tallet ikke kan regnes tilbage til en IP-adresse.

request.cf

Kanten ved, hvor den står

Hver forespørgsel får et cf-objekt med fra Cloudflare: datacenter, protokol, TLS-version, målt tur-retur-tid og meget mere. En lille rute i site-Workeren, /api/kant, læser fire af felterne og sender dem tilbage til dig, så du kan se, hvilket af Cloudflares datacentre der svarede (din vej hertil). IP, by, land og udbyder står også i objektet; dem læser sitet ikke, og intet gemmes.

Workers Cache

Tallene laves én gang og læses mange

Tallene bag Værnet, Crawlerne, Hastigheden, Danmark lige nu og revisionen er ens for alle, så de caches foran Workeren med Cache-Control: public, et Cache-Tag og en ETag ud fra indholdet. Næste gæst får svaret fra kantens cache, uden at Workeren kører; bagefter genvalideres med If-None-Match og et tomt 304. Når Cron har samlet nye tal, kalder Workeren ctx.cache.invalidate({tags}). Headeren x-wi24rd-kantcache fortæller, om svaret kom fra cachen.

Secrets Store

Nøglerne i ét skab

Sitets nøgler er hemmeligheder sat med wrangler secret put og står aldrig i repoet; wrangler.toml har kun tydeligt mærkede pladsholdere for konto- og zone-id. Kontoens fælles Secrets Store kan tage over nøgle for nøgle: koden spørger først skabet og falder tilbage til Worker-hemmeligheden.

Levering

Sitet selv: statiske filer og en Worker, der kun vågner, når der er brug for den
Workers Static Assets

Statisk site uden byggetrin ved deploy

Hele sitet er én Worker med filerne som Static Assets. Hvert push til main bliver til en ny udgave på det globale net via Workers Builds. HTML, CSS og JavaScript ligger i repoet, som de serveres; siderne genereres af små Python-scripts, der også ligger der. Billeder, scripts og stylesheets sendes direkte fra kanten uden at vække Workeren, og de koster ingenting.

run_worker_first

Serverlogik ved siden af filerne

En liste af mønstre bestemmer, hvornår Workeren skal svare før filerne: HTML-siderne, /api/* og /admin/*, men ikke /assets/*. En lille router uden afhængigheder svarer selv på /api/kant og /api/besoeg, sender /api/maskine/* videre til maskin-Workeren og henter resten fra filerne gennem env.ASSETS. Det hele sker i samme request, i det datacenter der er tættest på dig.

HTMLRewriter

Én navigation, ingen byggetrin

Siderne er almindelige HTML-filer med pladsholdere (data-tv-del). En middleware streamer hver side gennem HTMLRewriter og sætter topbar, navigation og footer ind fra /partials/, udfylder sprogskiftet ud fra sidens hreflang, sætter brødkrummerne ud fra /assets/sider.json og markerer den side, du står på. En svarheader fortæller, hvilke dele der blev sat ind; revisionen tjekker den hver morgen.

Content negotiation

Markdown til maskinerne

Beder en klient om Accept: text/markdown, eller føjer den index.md til en sides adresse, svarer middlewaren med sidens indhold som Markdown: titel, beskrivelse, kanonisk adresse og sprogversioner som YAML-frontmatter, derefter teksten uden navigation, scripts og pynt, og headeren x-markdown-tokens med et skøn over længden. Det sker med en lille parser uden afhængigheder. Browsere sender aldrig text/markdown og får HTML som altid; svaret bærer Vary: Accept, så ingen cache blander de to sammen.

Fejltavlen

Fejl, som begge slags gæster kan læse

En side, der ikke findes, giver mennesker en 404-side på det rigtige sprog med links til sitets sider. Maskiner, der beder om application/problem+json, får et RFC 9457-svar med type, title, status, detail, instance, retryable og forslag; application/json giver sitets egen fejlkontrakt, og text/markdown giver frontmatter med http_status og retryable. Alt under /api/ svarer altid JSON. Fejler noget i middlewaren, svarer den 503 med Retry-After, aldrig 502.

Beregning

Målingerne i en Worker uden egen adresse
Workers + Service Binding

Maskin-Workeren bag /api/maskine/

Alt, der kræver mere end at sende en fil, bor i en selvstændig Worker. Site-Workeren kalder den gennem en service binding, altså et direkte kald mellem to Workers uden at røre det offentlige net. Maskin-Workeren har ingen egen adresse, og den offentlige proxy rammer kun dens fetch(). Health, topologi, tælleværk og alle målingerne herunder kommer derfra.

Durable Objects

Ét objekt, låst til EU

Det europæiske arkivskab er ét Durable Object med eget SQLite-lager, hentet med EU-jurisdiktion, så det kun kan bo i et datacenter i EU. Ved siden af står samme klasse uden lås, så man kan se forskellen. Hvert segl, en gæst lægger i skabet, bliver en række i objektets egen tabel (ctx.storage.sql) med tidspunkt og datacenter, intet andet.

Workflows + Browser Run

Revisionen, trin for trin

Revisionen er en Workflow, der hver morgen tager én side ad gangen med pause imellem: Workeren henter siden og tjekker HTTP-status og de indsatte dele, og en headless browser (Browser Run) åbner den, tæller nøgleelementerne, læser JavaScript-fejl og tager et skærmbillede til R2. Hvert trin er idempotent og genforsøges. Fejler sider, åbner sidste trin ét issue på GitHub og lukker det selv, når alt består igen.

Cron Triggers

Hver time og hver morgen

En timeplan i maskin-Workeren samler tallene til Værnet, Crawlerne og Døren (GraphQL Analytics API), Hastigheden (Web Analytics) og Danmark lige nu (Radar) én gang i timen og lægger oversigterne i KV. Én gang i døgnet starter den revisionen. Sidevisninger sætter aldrig noget i gang; gæsterne læser kun det, der allerede er samlet.

Data

Hvor sitet husker, så lidt som muligt
Workers KV

Dagens besøgende og oversigterne

Et nøgle-værdi-lager med global replikering holder dagens besøgende som saltede daghashes (saltet skiftes dagligt og kasseres), der udløber efter to døgn, så ingen cookie er nødvendig. Samme navnerum holder de færdige oversigter til Værnet, Crawlerne, Hastigheden og Danmark lige nu.

D1

SQLite på kanten

Rækkerne bag Værnet og Crawlerne (ét døgn ad gangen, gemt i 95 dage), revisionens resultater pr. side og dagskvoter for Browser Run og seglene ligger i en D1-database. Kortet viser, hvilken region og hvilket datacenter databasen svarede fra, og om det var den primære kopi.

R2

Skærmbilleder uden udgående gebyr

Revisionens skærmbilleder gemmes i en R2-bucket og hentes gennem maskin-Workeren på /api/maskine/revision/billede/*, så bucketen aldrig er offentlig. R2 tager ikke betaling for udgående trafik, så billederne kan vises, så tit man vil.

Analytics Engine + Analytics SQL

Tælleværket

Middlewaren skriver ét datapunkt pr. side og maskin-Workeren ét pr. kald til et Analytics Engine-datasæt. Et datapunkt er en art, et navn, et sprog, et datacenter og to tal: ingen IP, ingen cookie, ingen session. Skrivningen blokerer ikke svaret. Workeren læser tallene tilbage med Analytics SQL-bindingen uden API-token (/api/maskine/taelle) og gemmer svaret i fem minutter. Infrastrukturkortet viser, hvilke datacentre der har betjent gæster i dag.

Durable Objects · SQL

Seglene i skabet

Arkivskabets Durable Object bruger sit SQLite-lager: hvert segl er en række med tidspunkt, dato og datacenter, og tallene på siden er en GROUP BY i objektet selv. Kun tidspunkt og sted, aldrig hvem.

Værktøj og kvalitet

Det, der holder resten ærligt
GitHub Actions

Enhedstests ved hvert push

Workflows i .github/ kører enhedstestene i tests/ ved hvert push og hver pull request: routeren, Markdown-parseren, fejltavlen og maskin-Workerens små moduler. Testene læser kun; de sender intet. Motorens testkørsler er offentlige i tingsvad.dk-projektet.

Workers Builds + Worker Previews

Hver branch sin egen udgave

Workers Builds bygger og udgiver begge Workers direkte fra GitHub. Hver branch og pull request får en Worker Preview på sin egen adresse med egne bindinger, egen KV og egen maskin-Worker, så en test aldrig skriver i sitets rigtige tal. Først når ændringen er på main, rammer den wi24rd.com.

Workers Issues + Traces

Fejl i drift samles ét sted

Begge Workers har Workers Issues og Traces slået til: kørselsfejl samles og grupperes efter stak, og en del af kaldene spores hele vejen gennem bindingerne, så man kan se, om tiden gik i D1, R2 eller browseren. Hvert svar bærer desuden en Server-Timing-header, som »Vis maskineriet« på undersiderne læser.

Web Analytics

Målt hos rigtige gæster

Cloudflare Web Analytics måler Core Web Vitals hos gæsterne selv med et lille script uden cookies. Hastigheden henter tallene hver time og viser dem side for side og land for land.

bump.sh

Cache-busting uden byggetrin

Alle stylesheets og scripts hentes med ?v=<version>. Et lille shell-script, bump.sh, skifter versionen i alle HTML-filer før et push, så gæsternes browsere og Cloudflares cache henter de nye filer med det samme, mens de uændrede bliver liggende i cachen.

Din vej hertil

En lille måling, der er ægte: hvor på Cloudflares net siden kom fra. Den ved ikke, hvem du er.

Til maskinerne

Hver side findes også som Markdown, og sitets fejl svarer i et format, en maskine kan læse. Prøv selv:

curl -H "Accept: text/markdown" https://wi24rd.com/maskinrummet/
curl https://wi24rd.com/om/index.md
curl -H "Accept: application/problem+json" https://wi24rd.com/findes-ikke/

Metoden står på kortene Markdown til maskinerne og Fejltavlen ovenfor. En agent kan også begynde med llms.txt.

Revisionen

Hver morgen gennemgås alle sider på begge sprog. Her står kun resultatet; tabellen, billederne og metoden står på revisionens egen side.

Motoren på GitHub (tingsvad.dk) · MIT

Har du en god idé?

Ønsker til nye funktioner, fejlrapporter og spørgsmål er velkomne. Hver henvendelse får et personligt svar.