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.
DNS og domæneNavnet 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 sikkerhedHTTP/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 analyseEt 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.cfKanten 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 CacheTallene 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 StoreNø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.
Workers Static AssetsStatisk 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_firstServerlogik 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 negotiationMarkdown 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.
FejltavlenFejl, 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.
Workers + Service BindingMaskin-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 RunRevisionen, 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 TriggersHver 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.
Workers KVDagens 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.
D1SQLite 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.
R2Skæ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 SQLTæ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 · SQLSeglene 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.
GitHub ActionsEnhedstests 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 PreviewsHver 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 + TracesFejl 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 AnalyticsMå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.shCache-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.