Back to articles
engineering July 18, 2026 5 min read

Debugging a Ghost Blog Post: The Wild Goose Chase That Ended in a Wi-Fi Switch

The Symptom A Telegram bot that mirrors my Ghost admin panel suddenly started handing me a dead link. I clicked the URL it spooled…

The Symptom

A Telegram bot that mirrors my Ghost admin panel suddenly started handing me a dead link. I clicked the URL it spooled out—https://www.emrys.name.ng/blog/<slug>—and Chrome promptly displayed the generic “This site can’t be reached” screen. No 404, no Ghost‑styled “article not found”, just a network‑level timeout. Oddly, the same device could load the home page in a flash, using the exact same Wi‑Fi network.

First Theory: Bad SITE_URL

The bot builds its live URLs from an environment variable called SITE_URL. The snippet looks like this:

const liveUrl = SITE_URL
  ? `${SITE_URL.replace(/\/$/, "")}/blog/${slug}`
  : `/blog/${slug}`;

In Vercel I had entered www.emrys.name.ng without a scheme. The string therefore resolved to a relative path, which browsers interpret as something like https://myapp.vercel.app/www.emrys.name.ng/blog/slug. That would certainly break the link. I patched the variable to https://www.emrys.name.ng and redeployed. The link format looked correct, yet the same “can’t be reached” error persisted. The problem was deeper than a missing https://.

Second Theory: Missing Slug Data

Ghost renders a post via an edge function (api/post.mjs) that queries Firestore for a document whose ID matches the slug. If the query fails, the function returns a rendered “article could not be found” page – a proper HTTP response, not a network timeout. To rule this out I opened the Firebase console and inspected the document directly:

All fields were intact. The function’s notFound branch was never hit, so the slug itself was not the culprit.

Third Theory: Firestore Security Rules

Could a ruleset be silently rejecting the read? My firestore.rules file looked like this:

match /posts/{postId} {
  allow read: if true;
  allow write: if isAdmin();
}

Public reads were explicitly allowed, and any denial would surface as a 403 error that the edge function would capture and turn into a rendered page. Since I was seeing a raw connectivity failure, the rules were not the cause.

Fourth Theory: Deployment Mix‑up

I turned to Vercel’s request logs, filtering for /blog/<slug>. The log returned zero hits. The request never even reached the edge function. I verified that the rewrite rule in vercel.json was still present:

{
  "rewrites": [{"source": "/blog/:slug", "destination": "/api/post.mjs"}]
}

The rewrite was correct, so I dug into the live package.json. To my surprise it contained the configuration of an entirely different Telegram‑bot project:

The blog’s edge function actually pulls Firebase from a CDN and only uses the native fetch API, so those extra packages were irrelevant—but they also hinted that the build might be pulling the wrong entry point. I restored a clean package.json that matched the blog’s minimal needs, committed, and triggered a fresh deployment. The rewrite still fired, the logs now showed hits, yet the URL still refused to load.

Fifth Theory: Deployment Protection Settings

Vercel’s Deployment Protection can hide production URLs behind a login wall when misconfigured. I opened a preview URL (something.vercel.app) and was greeted by a sign‑in prompt. I wondered if the custom domain was being shielded as well. In the project settings I confirmed the protection mode was set to Standard Protection, which only guards preview builds. Production on www.emrys.name.ng remained public, so this wasn’t the blocker.

The Real Fix: A Simple Network Switch

Having exhausted every code‑related avenue, I tried something I hadn’t thought of: changing the phone’s network. I toggled off mobile data, connected to my home Wi‑Fi, and refreshed the link. Instantly, the Ghost post rendered perfectly.

The root cause turned out to be a flaky Wi‑Fi router that intermittently dropped outbound HTTPS connections to Vercel’s edge network. The home page, being cached on Cloudflare, survived the hiccup; the freshly‑generated post, however, required a live fetch to the edge function and hit the dead socket.

What I Learned

Closing Thoughts

The investigation turned into a mini‑audit of the entire stack: environment variables, Firestore documents, security rules, Vercel rewrites, package configuration, and finally the underlying network. Two unrelated bugs surfaced—a malformed SITE_URL and a stray package.json—both of which I cleaned up. The real blocker, however, was entirely outside the codebase.

If you ever find yourself chasing a “ghost” post that refuses to load, remember to look beyond the application layer. A quick Wi‑Fi toggle can save you hours of debugging time, and it’s a reminder that sometimes the wild goose chase ends with a simple switch of network.

Need something like this built?

I work on full-stack web apps — backend systems, APIs, and the front-ends that sit on top. If this post was useful and you've got a project that needs it, I'd like to hear about it.

Want future posts like this?

No mailing list yet — for now, email me and I'll let you know when something new goes up.

Email me