← All posts

Ons het ons beelde na 'n CDN geskuif en die oorsprong het hulle steeds bedien

Authagonal·July 30, 2026

Ons dokumentasie is swaar belaai met skermkiekies. Elke portaalfunksie, vasgevang in lig en donker, op twee wydtes, oor tien landinstellings, wat optel tot iets noord van honderd megagrepe se beelde. Dit het alles in die app se eie statiese bondel gesit, bedien vanaf die oorsprong, wat die verkeerde plek is vir honderd megagrepe se prente wat nooit verander nie. So het ons dit na Cloudflare R2 geskuif en 'n CDN voor gesit, een per omgewing: cdn.authagonal.io vir produksie en 'n ooreenstemmende CDN-gasheer vir ons dev-werf.

Die app kies die regte CDN-gasheer tydens looptyd deur na die gasheernaam te kyk waarvandaan dit bedien word. As die bladsy op authagonal.io is, kom die beelde van cdn.authagonal.io. Eenvoudig, geen konfigurasie tydens bou nie, een beeldverwysing wat korrek oplos op enige van die twee omgewings. Ons het dit uitgestuur, gekyk hoe die blaaier elke beeld skoon van die CDN aftrek, en toe na die oorsprong se verkeer gekyk. Die oorsprong het steeds die beelde bedien. Elkeen, met die eerste laai, presies soos voorheen.

Die bladsy wat 'n soekrobot sien, is nie die bladsy wat 'n blaaier bou nie

Om te verduidelik hoekom, moet ek verduidelik wat ons bemarkingsbladsye eintlik is. Dit is 'n enkelbladsy-app, maar 'n enkelbladsy-app is 'n leë dop totdat JavaScript loop, en 'n leë dop is sleg vir soekenjins en sleg vir die tyd wat dit neem vir die eerste betekenisvolle ding om te verskyn. So tydens bou render ons vooraf: ons begin die werf, maak elke bladsy in 'n koplose blaaier oop, laat dit ten volle render, en stoor die HTML wat daaruit kom. Daardie voorafgerenderde HTML is wat ons eerste bedien. Dit het reeds die inhoud daarin, die soekrobot sien 'n regte bladsy, die gebruiker sien onmiddellik teks en beelde, en dan neem die JavaScript oor.

Hier is die probleem wat in daardie beskrywing wegkruip. Die voorafrendering laat die werf op 127.0.0.1 loop, 'n loopback-adres op die boumasjien. So wanneer die app vra "op watter gasheernaam is ek, sodat ek 'n CDN kan kies," is die antwoord tydens voorafrendering "'n plaaslike IP-adres," wat nie by authagonal.io of by die dev-gasheer pas nie. Die looptyd-gasheerafleiding, die slim deel, val stilweg deur na sy enigste oorblywende opsie: die oorsprong-relatiewe pad. En daardie oorsprong-pad is wat in die voorafgerenderde HTML vasgevries word.

So elke besoeker het HTML ontvang met oorsprong-beeld-URL's ingebak, en die blaaier het die voor die hand liggende ding gedoen: dit het hulle gaan haal, vanaf die oorsprong, met die eerste verf. 'n Oomblik later het die JavaScript die bladsy weer gerender, die gasheernaam weer afgelei, hierdie keer die regte een gekry, en die beelde na die CDN oorgeskakel. Twee haaloperasies vir elke beeld, die eerste een tref presies die bediener wat ons probeer spaar het, en heeltemal onsigbaar tensy jy die oorsprong se logboeke dopgehou het, want die bladsy het perfek gelyk en gewerk.

Die oplossing wat sake vir elf minute erger gemaak het

Die eerste oplossing was die verleidelike een. As die probleem is dat 'n verkeerde URL in die voorafgerenderde HTML vasgevries word, moet daar dan glad geen URL tydens voorafrendering uitgestuur word nie. Laat die beeldbron leeg in die momentopname, en laat die JavaScript die korrekte CDN-URL invul sodra dit loop en die regte gasheernaam ken. Geen verkeerde URL nie, geen oorsprong-haaloperasie nie.

Dit is uitgestuur, en dit was verkeerd, en ons het dit omtrent elf minute later vervang. Om die beeldbron in die voorafgerenderde HTML leeg te maak, verydel die hele rede waarom ons vooraf render. Die soekrobot wat die statiese bladsy lees, sien nou geen beeld nie, en die blaaier het niks om te wys totdat JavaScript geloop het nie, wat die stadige pad is wat ons juis die voorafgerenderde HTML bou om te vermy. Ons het die dubbele haaloperasie verwyder deur die beeld te verwyder uit die enigste weergawe van die bladsy wat voor JavaScript bestaan. Dit ruil 'n verkeersprobleem vir 'n soek-en-eerste-verf-probleem, wat 'n slegter ruil is.

Die les uit daardie elf minute: die voorafgerenderde HTML moet 'n regte, werkende beeldverwysing dra. Die fout was nooit dat 'n URL teenwoordig was nie. Dit was dat die verkeerde URL teenwoordig was, en om dit leeg te maak, ruil net een gebrek vir 'n ander. Wat ons eintlik nodig gehad het, was dat die korrekte URL kenbaar moet wees tydens voorafrendering, en dit is nie, want tydens voorafrendering weet niemand watter omgewing die bladsy gaan bedien nie.

Om op die laaste moontlike oomblik te besluit

Die korrekte URL hang af van die versoek se gasheer, en die versoek se gasheer is nie bekend voordat daar 'n versoek is nie. Voorafrendering gebeur een keer, tydens bou, en lewer een lêer wat op albei omgewings bedien word. So kan die omgewing-spesifieke deel van die URL nie tydens bou besluit word nie. Dit moet per versoek besluit word, en die enigste ding in die stapel wat elke versoek en sy gasheer sien, is die webbediener by die rand.

So die voorafrendering skryf nie meer 'n URL of 'n leë waarde nie. Dit skryf 'n plekhouer, 'n letterlike sentinel-string, https://cdn.authagonal.io, in die plek van die CDN-gasheer. Die voorafgerenderde HTML wat die bou verlaat, sê https://cdn.authagonal.io/screenshots/whatever.webp, wat nie 'n oorsprong-URL of 'n leë bron is nie. Dit is 'n URL met 'n gat daarin.

nginx vul die gat op pad uit. Dit karteer die versoek se gasheer na die regte CDN-basis, die nie-produksie-gasheer na sy eie dev-CDN, authagonal.io na https://cdn.authagonal.io, enigiets anders na leeg sodat die URL terugval na oorsprong-relatief as 'n veilige plaaslike terugval. Dan herskryf 'n enkele riglyn die sentinel na daardie waarde soos die HTML na die kliënt stroom:

map $host $asset_cdn_base {
    default              "";
    "~*example\.dev$"    "https://cdn.example.dev";
    "~*authagonal\.io$"  "https://cdn.authagonal.io";
}

sub_filter 'https://cdn.authagonal.io' $asset_cdn_base;

Die statiese HTML verlaat nou die oorsprong terwyl dit reeds regte CDN-URL's dra, korrek vir watter gasheer ook al gevra het. Die blaaier haal reguit vanaf die CDN met die eerste verf. Daar is geen verkeerde eerste haaloperasie om ongedaan te maak nie, want daar is geen verkeerde URL nie, en daar was ook nooit 'n leë een nie. Die soekrobot sien 'n volledige bladsy met regte beeldskakels. Een lêer bedien albei omgewings, en nie een van hulle het 'n aparte bou nodig nie.

Daar is 'n klein elegansie in watter dele van die respons herskryf word. Die herskrywing is beperk tot slegs HTML, wat nginx se verstek is. Dit maak saak omdat dieselfde sentinel-string ook in die JavaScript-bondel verskyn, as die terugval-tak van die gasheerafleidingskode. Op die kliënt is daardie tak dooie kode, aangesien die blaaier 'n regte gasheernaam het en nooit die sentinel uitstuur nie, so ons wil spesifiek nie hê nginx moet aan die bondel raak nie. Om die herskrywing by sy slegs-HTML-verstek te laat, kry dit gratis. Ons het terloops geleer dat om die verstek uitdruklik uit te skryf, slegter is as om dit weg te laat, want 'n oortollige verklaring laat nginx waarsku oor 'n duplikaat.

Hoekom nie net die lêer een keer met opstart herskryf nie

Die voor die hand liggende beswaar is dat dit baie werk per versoek is vir 'n waarde wat net twee moontlikhede het. Hoekom nie die HTML een keer herskryf wanneer die houer opstart, die regte gasheer inbak, en 'n statiese lêer sonder filter bedien nie?

Want die houer kan nie na homself skryf nie. Die statiese werf loop in nginx as 'n nie-root-gebruiker met 'n leesalleen-wortellêerstelsel en elke Linux-vermoë laat vaar, met 'n enkele klein skryfbare krabbelgids vir nginx se eie tydelike lêers. Dit is 'n doelbewuste verhardingshouding: 'n webbediener wat onbetroubare verkeer bedien, behoort nie die lêers wat dit bedien te kan wysig nie, sodat 'n aanvaller wat 'n vastrapplek kry, niks vind om te herskryf nie. 'n Herskrywing van die HTML met opstart sou presies die skryftoegang benodig wat ons doelbewus weggeneem het. Die leesalleen-lêerstelsel is nie 'n hindernis om omheen te werk nie, dit is 'n eienskap wat ons wil hê, en dit sluit die opstart-herskrywing skoon uit. Die herskrywing per versoek raak glad geen lêers nie, en dit is hoekom dit versoenbaar is met 'n bediener wat niks besit wat dit kan verander nie.

Ons wou ook nie die CDN-gasheer tydens bou inbak en twee verskillende beeldbondels lewer, een per omgewing, nie, want dit herintroduseer die omgewing-spesifieke bou waarvan ons pas ontslae geraak het. Die punt van gasheerafleiding was een artefak vir albei omgewings. Die sentinel hou daardie belofte: een bou, een lêer, die omgewing opgelos by die rand op die laaste moontlike oomblik.

Voorafrendering vries omgewingswaardes, so los hulle op by die rand

Voorafrendering vries 'n oomblik vas. Enigiets wat jou app besluit deur na sy omgewing te kyk, word besluit, tydens voorafrendering, teen die bou se omgewing, wat 'n loopback-adres is en niemand se regte gasheer nie. So word 'n waarde wat korrek is tydens looptyd verkeerd die oomblik wat dit as momentopname geneem word, en dit misluk stilweg, want die voorafgerenderde bladsy render steeds, dit wys net na die verkeerde plek.

Die algemene oplossing is om glad nie omgewing-spesifieke waardes tydens voorafrendering op te los nie. Stuur 'n plekhouer uit, dra dit ongeskonde deur die statiese artefak, en los dit op by die laag wat werklik die omgewing sien, wat vir 'n waarde per versoek die rand is. Dit is laat binding, toegepas op 'n string in 'n HTML-lêer: laat die gat oop deur elke stadium wat nie die antwoord ken nie, en vul dit by die een stadium wat dit wel ken.

As jy verkies dat jou identiteitsverskaffer se eie dokumentasie en bates reeds korrek vanaf die rand bedien word, is dit een van die vele klein dinge wat Authagonal reeds uitgeredeneer het, sodat jy die argument eerder aan jou eie produk kan bestee.