Reverse Proxy - Angular inside Astro

I launched an Elden Ring checklist app a year ago. Recently, I decided to migrate it under my new domain, devin.uk.

This created a problem. The app is built with Angular, but my main site is a blog running on Astro.

I needed to host a heavy web application alongside a static content site without breaking either of them.

Subdomains vs Subdirectories

The standard engineering solution is to use a subdomain, like app.devin.uk. It is easy to configure. You simply point the DNS records and you are done.

But this approach splits your domain authority. Search engines often treat app.devin.uk and devin.uk as separate entities. I wanted to consolidate everything. I wanted every backlink earned by the tool to boost the main domain.

To achieve this, the app needed to live at devin.uk/app/elden-ring.

The Architecture

I did not want to force Angular inside Astro. I also wanted to avoid a massive monorepo where a simple blog update triggers a complex app build. I wanted the codebases to remain separate, but the delivery to look unified.

I used Cloudflare Workers as an edge router to solve this.

  1. The Origin: The Angular app is deployed to Cloudflare Pages (devin-uk-elden-ring.pages.dev). This is the hidden source.
  2. The Gateway: My main domain (devin.uk) handles the traffic.
  3. The Logic: A Worker sits at the edge and intercepts requests.

If a user visits the blog, the Worker falls back to the Astro host. If they request /app/, the Worker acts as a reverse proxy. It fetches content from the Angular source and returns it as if it originated from the main domain.

The Proxy Logic

We are essentially writing Nginx-style logic, but in JavaScript. This offers more flexibility than static configuration files.

Here is the simplified logic of the Worker handling the routing:

export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    const UPSTREAM_ORIGIN = 'https://devin-uk-elden-ring.pages.dev';

    // 1. Intercept requests targeting the App path
    if (url.pathname.startsWith('/app/elden-ring')) {
      
      // 2. Construct upstream URL. Keep pathname intact so Angular routing works
      const targetUrl = new URL(url.pathname + url.search, UPSTREAM_ORIGIN);
      
      // 3. Clone and modify the request, 
      // We must mask the Host header, or Cloudflare will reject the request as a security violation.
      const newRequest = new Request(targetUrl, request);
      newRequest.headers.set('Host', new URL(UPSTREAM_ORIGIN).host);

      // 4. Fetch from the hidden origin
      const response = await fetch(newRequest);

      // 5. Return the response to the user transparently
      return new Response(response.body, response);
    }

    // Fallback: If not the app, let the request proceed to the Blog (Astro)
    return fetch(request);
  }
};

The Asset Issue

Choosing the subdirectory route comes with friction. The primary issue is Asset Resolution.

When you deploy an Angular app, it usually assumes it sits at the root (/). When the browser loads the HTML from devin.uk/app/elden-ring/, it attempts to fetch styles and scripts from devin.uk/styles.css.

These files do not exist there. They exist deep inside the subdirectory. This results in a functional HTML page with broken styles.

The Fix

We need to give explicit instructions to the Angular compiler. We must set the base reference in index.html so the browser knows to append the directory prefix to all relative asset requests.

In Angular, we add this to the head:

<head>
  <base href="/app/elden-ring/">
</head>

With this in place, a request for styles.css correctly resolves to devin.uk/app/elden-ring/styles.css. The Worker intercepts this, fetches it from the upstream Pages project, and delivers it correctly.

Redirect Strategy

To ensure I didn’t lose any traffic, I set up a hard 301 Redirect from the old .com domain immediately to devin.uk/app/elden-ring/.

I briefly considered adding a vanity subdomain (eldenring.devin.uk) as a short link that would be easier for users to remember. It would simply redirect to the main URL. I decided not to because it could confuse search crawlers. Also, multiple entry points create mental clutter for users. I prefer a single source of truth.

Summary

I can use Astro for high-performance content rendering and Angular for heavy interactive logic. I accepted higher initial complexity in exchange for long-term SEO benefits and a cleaner brand architecture.