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.