← Back to blog
CloudflareNext.jsDevLogInfrastructure

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··5 min read·VIBED LAB B.LOG

Series: VIBED LAB B.LOG

  1. 1. Hello World - A Non-Developer Enters the Chat
  2. 2. I Spent 3 Hours Trying to Proxy a Blog Subdomain. Here's My Descent Into Madness. ← you are here

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.

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.