Skip to content
โ† Back to blog

I Spent 3 Hours Trying to Proxy a Blog Subdomain. Here's My Descent Into Madness.

All I wanted was vibed-lab.com/blog. What I got was an education in why the internet is held together with duct tape.

by Jay Lee6 min readBuild Notes

Proxy 404 failure log

I just wanted one thing.

vibed-lab.com/blog. That's it. The whole blog, accessible from my main domain, URL intact. Clean. Professional. AdSense-friendly.

Three hours later I had touched Cloudflare Workers, DNS records, Vercel config, redirect rules, _redirects files, next.config.js, and my own sanity. None of it worked.

This is that story.

๐Ÿ“‹ The Plan (It Seemed So Simple)

The setup: Hashnode headless blog deployed on Vercel at vibed-lab-portal.vercel.app. Main site at vibed-lab.com on Cloudflare Pages. The goal was to proxy /blog requests from the main domain to the Vercel app โ€” URL stays the same, content comes from Vercel.

This is a completely normal thing to want. People do this all the time. There are Stack Overflow answers about it. There are blog posts about it.

Reader, those blog posts lied to me.

๐Ÿ”ง Step 1: The Cloudflare Worker (Works! Kind Of.)

First attempt: a Cloudflare Worker to intercept /blog requests and fetch from Vercel.

export default {
  async fetch(request) {
    const url = new URL(request.url);
    if (url.pathname.startsWith('/blog')) {
      const targetUrl = 'https://vibed-lab-portal.vercel.app' + url.pathname + url.search;
      return fetch(targetUrl, request);
    }
    return fetch(request);
  }
}

The blog loaded. Sort of. The layout was completely broken โ€” like a webpage from 2003 but worse, because at least 2003 websites were trying to look bad.

๐ŸŽจ Step 2: CSS Is Apparently Optional Now

Dev tools revealed the problem: vibed-lab.com/_next/static/css/b17cced2663f0266.css was returning 404. The Worker Route only covered /blog*, so /_next requests were going straight to the main Cloudflare Pages app, which had no idea what to do with them.

Okay, add more routes:

  • vibed-lab.com/_next*
  • vibed-lab.com/static*

Still broken. Turns out Cloudflare Pages has higher priority than Workers for static asset paths. The Pages app was intercepting /_next before the Worker even had a chance.

This is the part where a reasonable person would stop and reconsider the architecture. I am not always a reasonable person.

๐Ÿ“ฆ Step 3: assetPrefix to the Rescue (Not Really)

Next move: tell the Vercel blog to load its CSS/JS directly from vibed-lab-portal.vercel.app instead of the current domain.

const nextConfig = {
  assetPrefix: 'https://vibed-lab-portal.vercel.app',
}

Deployed. Refreshed. CSS loaded! Progress!

But now most of the page content was gone. Network tab showed /blog/ping/data-event returning 500. Hashnode's internal analytics endpoint was firing at a path that didn't exist on Vercel. Cool. Great. Love that for me.

๐Ÿšจ Step 4: The Redirect That Wouldn't Die

At some point I noticed blog.vibed-lab.com was redirecting to vibed-lab.com/blog. Fine, that makes sense โ€” except it was creating a loop. So I deleted the redirect rule from Cloudflare.

It kept redirecting.

Checked Page Rules. Nothing. Checked the Worker code. Nothing. Had Claude Code audit the entire project. Nothing. The redirect was a ghost. It existed nowhere and yet it persisted, like a passive-aggressive coworker who won't stop forwarding emails even after you ask them to stop.

Eventually found it: the blog.vibed-lab.com CNAME was pointing to vercel-dns.com. Vercel didn't recognize the domain, so it was silently redirecting everything on its own. Deleted the DNS record. Redirect stopped.

This is why we check DNS records.

๐Ÿ“„ Step 5: The _redirects File (Narrator: It Did Not Work)

By this point I had abandoned the Worker approach entirely and pivoted to Cloudflare Pages native _redirects:

/blog/* https://vibed-lab-portal.vercel.app/:splat 200
/blog   https://vibed-lab-portal.vercel.app 200

Status code 200 means proxy โ€” URL stays the same, content comes from the target. Deployed. Opened vibed-lab.com/blog.

"This site can't be reached."

Turns out Cloudflare Pages _redirects with status 200 does not support external domains. This is technically documented somewhere. I did not read that part of the documentation. Switched to 302.

Now the root domain (vibed-lab.com) was serving the blog. Because the Worker Route vibed-lab.com/* was still catching everything. Of course it was.

Why This Architecture Is Structurally Hard

Looking back, the failures weren't bad luck โ€” they were predictable consequences of the architecture I tried to force. Three reasons subpath proxying between separate origins (Cloudflare Pages + Vercel) doesn't work cleanly:

1. Same-origin policy bites at every layer. Cookies, localStorage, service workers, postMessage โ€” all of them care about the origin, not the path. When vibed-lab.com/blog/* is served from a different origin than vibed-lab.com/*, you fight CORS, cookie scoping, and CSP at every step. The fixes work in isolation but accumulate friction.

2. Cloudflare Pages routing prefers static assets first. Cloudflare Pages resolves a request by checking the deployed static asset tree before applying redirects or workers. If your blog's HTML/JS happens to share a path prefix with anything in the static tree, Pages serves the wrong file silently. Debugging means understanding the resolution order, which isn't well documented.

3. assetPrefix and basePath solve different problems. Next.js basePath rewrites internal links and routes, but doesn't change where the build is served from. assetPrefix only applies to static asset URLs. Neither helps when a CDN intercepts before your app even sees the request. The two configs look related but compose poorly.

The takeaway isn't "subpath proxying is impossible" โ€” it's that subpath proxying between two origins on a single domain requires you to own the routing layer. Either both apps live on the same Pages project (single origin), or you put both behind a Worker that routes deliberately. Mixing two managed platforms with overlapping path responsibilities is an architecture that has no clean exit.

If you find yourself reaching for this pattern, the cheaper alternative is almost always subdomain split (blog.example.com and app.example.com) with shared auth via cookies on the parent domain. You give up the URL aesthetic but keep the architecture sane.

๐Ÿ’ฅ The Ending Nobody Asked For

After three hours, the final tally:

  • Cloudflare Workers: touched โœ“
  • DNS records: deleted and recreated โœ“
  • Vercel config: modified โœ“
  • Redirect Rules: deleted โœ“
  • Page Rules: checked and found innocent โœ“
  • _redirects: created, deployed, failed โœ“
  • assetPrefix: configured, partially helpful โœ“
  • My will to live: intact, but diminished โœ“

The actual solution? Scrap Hashnode entirely. Build a real Next.js blog inside the main project. /blog is just a route now.

No proxies. No Workers. No _redirects. No external domains. No ghosts.

In retrospect, the subpath proxy approach has a fundamental problem: every CSS file, every JS bundle, every analytics ping, every internal API call from the external app needs to either be rewritten or proxied separately. It's not impossible โ€” but it's fighting against how the web works, and the web usually wins.

The three hours weren't wasted though. I now understand Cloudflare Worker priority, Pages routing behavior, DNS propagation quirks, and exactly why every "just use a reverse proxy" tutorial glosses over the implementation details.

Turns out "just proxy it" is the "just add a login page" of infrastructure advice.

Written by

Jay Lee

Korea-Licensed Pharmacist (#68652) ยท Senior Researcher

Korea University, College of Pharmacy (B.S. + M.S., drug delivery systems & industrial pharmacy). Building production-grade AI tools across medicine, finance, and productivity โ€” without a CS degree. Domain expertise first, code second.

About the author โ†’
ShareX / TwitterLinkedIn