The Client Logo That 404'd: Why We Self-Host Every Third-Party Image Now

On a Monday morning in July, one slot in the client logo wall on our homepage was empty — just the broken-image outline and a bit of alt text. The logo had been there for months. We had not touched that part of the page in just as long.

The image was hotlinked. We were loading it straight off the client's own web server, and that server no longer had the file: Tilde Loop had rebuilt their site, and the old WordPress upload path was a 404. Our page had no way of knowing. It requested a URL, got nothing back, and rendered a hole where a brand mark used to be.

The URL made it worse. It was http://tildeloop.com/wp-content/uploads/2020/09/logo-transparency-2.png — plain HTTP, on a page served over HTTPS. Browsers had been treating it as mixed content long before it broke, which meant for some visitors it had never loaded at all.

Fixing that one logo took an hour. Auditing the rest of the wall took the afternoon, and it is the reason there is not a single hotlinked image left on codigit.io.

Hotlinking Is a Dependency You Never Agreed To

Writing <img src="https://someoneelse.com/logo.png"> feels like a shortcut. No file to download, no folder to organize, always the current version. It is also a runtime dependency on infrastructure you do not own, do not monitor, and cannot fix.

Every hotlinked asset hands someone else a switch that changes your page. They redesign and the path moves — your image breaks. They add a referrer check to stop bandwidth theft — your image breaks for visitors but not for you, because yours is cached. They let a certificate lapse — your HTTPS page starts throwing warnings.

None of those events produce an error in your build, your logs, or your deploy pipeline. On a static site there is no build step to fail. The page ships fine and degrades silently in someone else's browser.

Every Single Hotlink Pointed Into a WordPress Uploads Folder

Our logo wall had nine logos. Six of them were hotlinked from external domains, and when we lined the URLs up, the pattern was impossible to miss:

https://www.medihive.com/wp-content/uploads/2024/04/logo-medihive.svg
https://www.autozubak.hr/wp-content/themes/auto-zubak/public/img/logo.svg
https://nomago.hr/themes/shuttle/assets/img/nomago.svg
https://sial.charity/wp-content/uploads/2020/08/cropped-SIALcharity_logo_01-RGB_H_pos-1.png
http://tildeloop.com/wp-content/uploads/2020/09/logo-transparency-2.png
https://www.gamelounge.com/wp-content/uploads/2024/09/gameLoungeLogo.svg

Four of the six point into /wp-content/uploads/, and two into a theme directory. Those are the two most volatile paths on any WordPress site. The uploads folder gets reorganized by media plugins, regenerated by image optimizers, and wiped on migrations. The theme folder disappears entirely the moment someone switches themes — which is exactly what a redesign is.

Look at the dates baked into those paths: 2020/08, 2020/09, 2024/04. We were depending on files uploaded to someone else's CMS up to six years ago, surviving untouched, at a URL nobody on their side knows we rely on. Tilde Loop was not an unlucky exception. It was the first one to go.

Mixed Content Is Sneakier Than It Looks

One of the six — the one that broke — was served over plain HTTP. When an HTTPS page requests an HTTP image, browsers treat it as passive mixed content. They do not block it outright the way they block mixed scripts, but they downgrade the security indicator and log a warning:

Mixed Content: The page at 'https://codigit.io/' was loaded over HTTPS,
but requested an insecure element 'http://tildeloop.com/...'.
This content should also be served over HTTPS.

Most visitors never open a console. But plenty of browsers and most corporate proxies are stricter than the default, and an image that loads for you may simply not load for the CTO evaluating your studio from inside a locked-down network. That is precisely the visitor you cannot afford to show a broken page to.

Rewriting the URL to https:// is not a fix either. It only works if the other server has a valid certificate for that hostname today, and you have no say in whether it does tomorrow.

Six Logos Meant Six Separate Connections

The failure mode is the obvious cost. The quiet one is that a hotlinked image is never just one request — it is a full connection setup to a new origin. Six hotlinked logos meant six DNS lookups and six TLS handshakes to six domains that appear nowhere else on the page, all to fetch small images sitting near the top of the viewport.

Self-hosted, those same six files total roughly 54KB in our repository and come down the connection the browser has already opened. No new origins, no handshakes, no third party in the critical path of the first thing a visitor sees.

The Fix: Download, Convert, Commit

Self-hosting is not sophisticated. It is three steps, and the only reason to skip it is that it costs ten minutes per asset instead of zero.

First, fetch the file and confirm what you actually got — a redirect chain will happily hand you an HTML error page with an image extension:

curl -L -o logos/tildeloop.png \
  http://tildeloop.com/wp-content/uploads/2020/09/logo-transparency-2.png
file logos/tildeloop.png

Second, get it into the right format and size. Client logos are almost always flat-color marks, so SVG wherever you can get it and a properly sized PNG where you cannot. Four of our six came down as SVG; the two PNGs we resized to the width they actually render at rather than shipping whatever the client's CMS happened to store.

Third, commit the file and reference it locally:

<img src="logos/tildeloop.png"
     alt="Tilde Loop"
     width="120" height="32" loading="lazy" />

The width and height attributes matter more than they look. Without them the browser cannot reserve space, and the logo row shifts the moment the images land — a Cumulative Layout Shift penalty you get free with every dimensionless hotlink.

One wrinkle worth mentioning: the Tilde Loop wordmark is white, which is invisible on our light theme. Self-hosting made that solvable — we added a CSS variant that leaves the white source alone on the dark theme and inverts it on the light one. You cannot treat an asset you do not control.

The Permission Question Points the Same Way

The objection we hear most is legal: surely hotlinking is safer, since you are not copying anything? It is the reverse. Hotlinking consumes another company's bandwidth without asking, and that is the thing their terms of service typically prohibit. Copying a logo to display as a client reference is ordinary practice, and most companies publish brand assets for exactly this purpose.

If you want to be certain, ask. A short email asking for current logo files costs nothing, and it tends to surface something useful — a rebrand you did not know about, or a proper SVG to replace the compressed PNG you scraped off their homepage.

What This Generalizes To

Logos are the visible case. The same reasoning covers anything your page loads from a domain you do not control: fonts, icon sets, embedded badges, CDN-hosted libraries.

The test is simple. For each external request, ask what happens to the page if that host returns a 404, a 500, or nothing at all for thirty seconds. If the answer is "a visible part of the page breaks" or "rendering blocks", that dependency should be self-hosted or made explicitly optional.

Some are worth keeping. A major font CDN with a preconnect hint is a reasonable trade for the caching and convenience. One client's marketing server holding a 14KB PNG is not — there is no upside to weigh against the risk.

Catching It Before a Client Does

The uncomfortable part of this story is not that a logo broke. It is that it broke and stayed broken until someone looked at the page. A static site with no build step has nowhere for a check to run, which is exactly why the failure was invisible.

The cheapest fix is a pre-deploy pass over every external URL in your HTML:

grep -rhoE '(src|href)="https?://[^"]+"' *.html blog/*.html \
  | sed -E 's/.*="([^"]+)"/\1/' | sort -u \
  | while read url; do
      code=$(curl -s -o /dev/null -w '%{http_code}' -L "$url")
      [ "$code" = "200" ] || echo "$code  $url"
    done

It runs in under a minute and turns a silent visual bug into a line of output. The broader point is not the snippet — it is that "we would have noticed" is not true for anything that fails outside your own infrastructure.

Back to the Empty Slot

The logo that started this is now a file in our repository, at a path that will not move because we are the only ones who can move it. It renders exactly as it did before. The difference is that its failure modes are now ours to cause and ours to fix.

That is the argument for self-hosting, and it has less to do with performance than with control. Every external request is a decision to let someone else change your page without telling you. Sometimes that trade is worth it. For a client logo — a file that never changes, weighs almost nothing, and sits at the top of the page a prospect judges you by — it never is.

Jednog ponedjeljka ujutro u srpnju jedno je mjesto u redu s logotipima klijenata na našoj naslovnici bilo prazno — samo obris slomljene slike i malo alt teksta. Logo je ondje stajao mjesecima. Taj dio stranice nismo dirali jednako dugo.

Slika je bila hotlinkana. Učitavali smo je izravno s klijentovog servera, a taj server tu datoteku više nije imao: Tilde Loop je iznova izgradio svoj sajt i stara WordPress upload putanja vraćala je 404. Naša stranica to nije mogla znati. Zatražila je URL, nije dobila ništa i iscrtala rupu ondje gdje je nekad bio brand.

URL je stvar činio gorom. Bio je http://tildeloop.com/wp-content/uploads/2020/09/logo-transparency-2.png — običan HTTP, na stranici koja se servira preko HTTPS-a. Preglednici su ga tretirali kao mixed content davno prije nego što je puknuo, što znači da se dijelu posjetitelja nikad nije ni učitao.

Popravak tog jednog logotipa trajao je sat vremena. Revizija ostatka zida s logotipima trajala je poslijepodne i razlog je zašto na codigit.io više nema nijedne hotlinkane slike.

Hotlinkanje je ovisnost na koju nikad niste pristali

Napisati <img src="https://netkodrugi.com/logo.png"> djeluje kao prečac. Nema datoteke za skinuti, nema mape za posložiti, uvijek aktualna verzija. To je ujedno runtime ovisnost o infrastrukturi koju ne posjedujete, ne nadzirete i ne možete popraviti.

Svaki hotlinkani asset daje nekom drugom prekidač kojim mijenja vašu stranicu. Redizajniraju sajt i putanja se pomakne — slika pukne. Dodaju referrer provjeru protiv krađe bandwidtha — slika pukne posjetiteljima, ali ne i vama, jer je vaša u cacheu. Isteče im certifikat — vaša HTTPS stranica počne bacati upozorenja.

Nijedan od tih događaja ne proizvodi grešku u vašem buildu, logovima ili deploy pipelineu. Na statičnom sajtu nema ni build koraka koji bi pao. Stranica se deploya uredno i tiho se raspada u tuđem pregledniku.

Svaki hotlink vodio je u WordPress uploads mapu

Naš zid s logotipima imao je devet logotipa. Šest ih je bilo hotlinkano s vanjskih domena, a kad smo poredali URL-ove, obrazac je bilo nemoguće promašiti:

https://www.medihive.com/wp-content/uploads/2024/04/logo-medihive.svg
https://www.autozubak.hr/wp-content/themes/auto-zubak/public/img/logo.svg
https://nomago.hr/themes/shuttle/assets/img/nomago.svg
https://sial.charity/wp-content/uploads/2020/08/cropped-SIALcharity_logo_01-RGB_H_pos-1.png
http://tildeloop.com/wp-content/uploads/2020/09/logo-transparency-2.png
https://www.gamelounge.com/wp-content/uploads/2024/09/gameLoungeLogo.svg

Četiri od šest vode u /wp-content/uploads/, a dva u theme direktorij. To su dvije najnestabilnije putanje na bilo kojem WordPress sajtu. Uploads mapu reorganiziraju media pluginovi, regeneriraju optimizatori slika i briše je svaka migracija. Theme mapa nestaje u trenutku kad netko promijeni temu — a upravo to redizajn i jest.

Pogledajte datume upisane u te putanje: 2020/08, 2020/09, 2024/04. Ovisili smo o datotekama uploadanima u tuđi CMS prije i do šest godina, o tome da ostanu nedirnute, na URL-u za koji nitko s njihove strane ne zna da nam je važan. Tilde Loop nije bio nesretna iznimka. Bio je prvi koji je otpao.

Mixed content je podmukliji nego što izgleda

Jedan od šest — onaj koji je puknuo — servirao se preko običnog HTTP-a. Kad HTTPS stranica zatraži HTTP sliku, preglednici to tretiraju kao pasivni mixed content. Ne blokiraju je odmah kao što blokiraju miješane skripte, ali spuštaju sigurnosnu oznaku i upisuju upozorenje:

Mixed Content: The page at 'https://codigit.io/' was loaded over HTTPS,
but requested an insecure element 'http://tildeloop.com/...'.
This content should also be served over HTTPS.

Većina posjetitelja nikad ne otvori konzolu. Ali dosta preglednika i većina korporativnih proxyja stroži su od zadanih postavki, pa se slika koja se vama učita jednostavno neće učitati CTO-u koji vaš studio procjenjuje iz zaključane mreže. To je točno onaj posjetitelj kojem si ne možete priuštiti pokazati slomljenu stranicu.

Prepisati URL u https:// također nije rješenje. To radi samo ako drugi server danas ima valjan certifikat za taj hostname, a nemate utjecaja na to hoće li ga imati sutra.

Šest logotipa značilo je šest zasebnih veza

Puknuće je očiti trošak. Tihi je taj da hotlinkana slika nikad nije samo jedan zahtjev — to je puna uspostava veze prema novom originu. Šest hotlinkanih logotipa značilo je šest DNS upita i šest TLS handshakeova prema šest domena koje se nigdje drugdje na stranici ne pojavljuju, i to za male slike pri samom vrhu ekrana.

Self-hostane, te iste datoteke zauzimaju otprilike 54 KB u našem repozitoriju i stižu vezom koju je preglednik ionako već otvorio. Bez novih origina, bez handshakeova, bez treće strane na kritičnoj putanji prve stvari koju posjetitelj vidi.

Popravak: skini, konvertiraj, commitaj

Self-hosting nije nikakva vještina. To su tri koraka, a jedini razlog da ih preskočite je što traju deset minuta po assetu umjesto nula.

Prvo, dohvatite datoteku i provjerite što ste zapravo dobili — lanac redirecta rado će vam podvaliti HTML stranicu s greškom pod ekstenzijom slike:

curl -L -o logos/tildeloop.png \
  http://tildeloop.com/wp-content/uploads/2020/09/logo-transparency-2.png
file logos/tildeloop.png

Drugo, dovedite je u pravi format i veličinu. Logotipi klijenata gotovo su uvijek plošni znakovi, pa SVG gdje god ga možete dobiti i primjereno dimenzioniran PNG gdje ne možete. Četiri od naših šest stigla su kao SVG; dva PNG-a smanjili smo na širinu na kojoj se stvarno prikazuju umjesto da nosimo ono što je klijentov CMS slučajno spremio.

Treće, commitajte datoteku i referencirajte je lokalno:

<img src="logos/tildeloop.png"
     alt="Tilde Loop"
     width="120" height="32" loading="lazy" />

Atributi width i height važniji su nego što izgledaju. Bez njih preglednik ne može rezervirati prostor, pa se red logotipa pomakne čim slike stignu — Cumulative Layout Shift kazna koju besplatno dobijete uz svaki hotlink bez dimenzija.

Jedan detalj vrijedan spomena: Tilde Loop wordmark je bijel, što je na našoj svijetloj temi nevidljivo. Self-hosting je to učinio rješivim — dodali smo CSS varijantu koja bijeli izvornik ostavlja na miru na tamnoj temi, a invertira ga na svijetloj. Asset koji ne kontrolirate ne možete ni tretirati.

Pitanje dopuštenja vodi na istu stranu

Prigovor koji najčešće čujemo je pravni: nije li hotlinkanje sigurnije, kad ništa ne kopirate? Zapravo je obrnuto. Hotlinkanje troši tuđi bandwidth bez pitanja, a upravo to njihovi uvjeti korištenja obično zabranjuju. Kopirati logo da biste ga prikazali kao referencu uobičajena je praksa i većina tvrtki objavljuje brand assete točno u tu svrhu.

Ako želite biti sigurni, pitajte. Kratak mail s molbom za aktualne datoteke logotipa ne košta ništa, a obično izbaci nešto korisno — rebrand za koji niste znali ili pravi SVG umjesto komprimiranog PNG-a skinutog s njihove naslovnice.

Na što se ovo šire odnosi

Logotipi su vidljiv slučaj. Ista logika pokriva sve što vaša stranica učitava s domene koju ne kontrolirate: fontove, setove ikona, ugrađene bedževe, biblioteke s CDN-a.

Test je jednostavan. Za svaki vanjski zahtjev pitajte se što se događa sa stranicom ako taj host vrati 404, 500 ili trideset sekundi ništa. Ako je odgovor „vidljivi dio stranice pukne" ili „renderiranje se blokira", ta ovisnost treba biti self-hostana ili izričito opcionalna.

Neke je vrijedno zadržati. Veliki CDN za fontove uz preconnect hint razumna je zamjena za cache i praktičnost. Marketinški server jednog klijenta koji drži PNG od 14 KB nije — nema koristi koju biste vagali protiv rizika.

Uhvatiti to prije klijenta

Neugodni dio ove priče nije to što je logo pukao. Nego to što je pukao i ostao pokvaren dok netko nije pogledao stranicu. Statični sajt bez build koraka nema gdje pokrenuti provjeru, i upravo je zato kvar bio nevidljiv.

Najjeftiniji popravak je prolaz kroz sve vanjske URL-ove u vašem HTML-u prije deploya:

grep -rhoE '(src|href)="https?://[^"]+"' *.html blog/*.html \
  | sed -E 's/.*="([^"]+)"/\1/' | sort -u \
  | while read url; do
      code=$(curl -s -o /dev/null -w '%{http_code}' -L "$url")
      [ "$code" = "200" ] || echo "$code  $url"
    done

Vrti se ispod minute i pretvara tihi vizualni bug u redak ispisa. Šira poanta nije skripta — nego to da „primijetili bismo" nije istina ni za što što pukne izvan vaše infrastrukture.

Natrag na prazno mjesto

Logo od kojeg je sve počelo danas je datoteka u našem repozitoriju, na putanji koja se neće pomaknuti jer smo mi jedini koji je možemo pomaknuti. Prikazuje se točno kao i prije. Razlika je u tome što su njegovi načini otkazivanja sada naši da ih izazovemo i naši da ih popravimo.

To je argument za self-hosting i manje ima veze s performansama nego s kontrolom. Svaki vanjski zahtjev odluka je da nekom drugom dopustite promijeniti vašu stranicu bez najave. Ponekad se ta zamjena isplati. Za logo klijenta — datoteku koja se nikad ne mijenja, ne teži gotovo ništa i stoji na vrhu stranice po kojoj vas potencijalni klijent procjenjuje — nikad se ne isplati.

← Back to blog