NeuroAgent

Angular SSR Subdomain Localization Guide: Fix Redirects & Broken URLs

Complete guide to configure Angular SSR with subdomain-based locale resolution. Learn to prevent redirects, fix broken asset URLs, and implement proper server-side language detection for multi-language Angular applications.

How to configure Angular SSR with subdomain-based locale resolution to prevent redirects and broken URLs

I’m setting up Angular SSR with localization where different language versions are deployed as subdomains:

My angular.json configuration is:

"i18n": {
  "sourceLocale": {
    "code": "en-GB",
    "baseHref": "/"
  },
  "locales": {
    "uk": {
      "translation": "src/locale/messages.uk-UA.xlf",
      "baseHref": "/"
    },
    "fr": {
      "translation": "src/locale/messages.fr-FR.xlf",
      "baseHref": "/"
    },

This setup works fine for non-SSR versions where I use the dist/browser/* folders as root for nginx virtual servers. However, when using SSR, the app unexpectedly redirects to /en-GB/ (based on my Accept-Language header) and fails with broken URL errors because of baseHref=“/”.

The actual script paths look like:

<script src="main-CJQXROTY.js" type="module">

But they should be:

<script src="en-GB/main-CJQXROTY.js" type="module">

I want the en-GB version to open directly on / with working hrefs to scripts and styles. Is there a way to fix or work around this? Can I override the redirect logic to resolve the language from the HTTP request via subdomain? Alternatively, should I build separate Node SSR apps for each language and host them independently? I can replace Accept-Language with nginx based on hostname to select the right language, but I can’t solve the baseHref and redirect issue.

NeuroAgent

The core issue you’re experiencing stems from Angular’s SSR redirect logic and baseHref configuration conflicts with subdomain-based localization. To resolve this, you need to implement a proper server-side language detection mechanism that overrides the default baseHref behavior and prevents unwanted redirects.


Contents


Understanding the Problem

Your current configuration creates a fundamental conflict between Angular’s SSR redirect logic and your subdomain-based architecture. The baseHref="/" setting tells Angular that all assets should be loaded from the root, but the SSR system attempts to detect language from headers and apply base redirects accordingly.

When Angular SSR encounters a request to en.example.org, it automatically:

  1. Checks the Accept-Language header
  2. Compares it with your source locale (en-GB)
  3. Redirects to /en-GB/ if there’s a mismatch
  4. Generates asset paths using the configured baseHref

This creates the broken URL pattern you’re experiencing because the server expects assets at main-CJQXROTY.js but the client-side routing expects them at en-GB/main-CJQXROTY.js.

The security vulnerability mentioned in the research results shows that SSR can be vulnerable to URL manipulation, which further complicates the redirect behavior you want to prevent.

Proper Subdomain Configuration

To fix this, you need to modify your angular.json to properly handle subdomain-based routing:

json
"i18n": {
  "sourceLocale": {
    "code": "en-GB",
    "baseHref": "/"
  },
  "locales": {
    "uk": {
      "translation": "src/locale/messages.uk-UA.xlf",
      "baseHref": "/uk/"
    },
    "fr": {
      "translation": "src/locale/messages.fr-FR.xlf", 
      "baseHref": "/fr/"
    }
  }
}

However, this alone won’t solve your SSR redirect issue. You’ll need to implement server-side middleware to:

  1. Extract the language from the subdomain
  2. Override the default SSR redirect behavior
  3. Ensure proper asset path generation

Server-Side Language Detection

The key solution is to implement server-side language detection that takes precedence over the client-side header-based detection. Here’s how to approach this:

1. Modify Server.ts

In your SSR server configuration, add middleware to detect language from subdomain:

typescript
import { Request, Response, NextFunction } from 'express';

export function subdomainLanguageDetection(req: Request, res: Response, next: NextFunction) {
  const host = req.headers.host;
  const subdomain = host?.split('.')[0];
  
  // Map subdomains to locale codes
  const subdomainToLocale: Record<string, string> = {
    'en': 'en-GB',
    'uk': 'uk-UA', 
    'fr': 'fr-FR'
  };
  
  // Set request language based on subdomain
  if (subdomain && subdomainToLocale[subdomain]) {
    req.headers['x-locale'] = subdomainToLocale[subdomain];
    // Override Accept-Language header for SSR
    req.headers['accept-language'] = subdomainToLocale[subdomain];
  }
  
  next();
}

// In your main server setup
app.use(subdomainLanguageDetection);

2. Create Custom SSR Server Route

Override the default SSR route handling to prevent redirects:

typescript
import { ServerRoute } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [
  {
    path: '**',
    renderMode: 'Server',
    getPrerenderParams: () => {
      // Prevent automatic redirects by returning no params
      return [];
    }
  }
];

Redirect Prevention Strategies

To completely prevent redirects while maintaining proper localization:

1. Nginx Configuration

Configure your nginx to handle subdomain routing and override headers before reaching Angular:

nginx
server {
    listen 80;
    server_name en.example.org;
    
    # Override Accept-Language based on subdomain
    set $locale "en-GB";
    
    location / {
        proxy_set_header X-Locale "en-GB";
        proxy_set_header Accept-Language "en-GB";
        proxy_pass http://localhost:4000;
    }
}

server {
    listen 80;
    server_name uk.example.org;
    
    set $locale "uk-UA";
    
    location / {
        proxy_set_header X-Locale "uk-UA";
        proxy_set_header Accept-Language "uk-UA";
        proxy_pass http://localhost:4000;
    }
}

server {
    listen 80;
    server_name fr.example.org;
    
    set $locale "fr-FR";
    
    location / {
        proxy_set_header X-Locale "fr-FR";
        proxy_set_header Accept-Language "fr-FR";
        proxy_pass http://localhost:4000;
    }
}

2. Custom SSR Middleware

Create middleware that intercepts and normalizes the request before Angular SSR processes it:

typescript
import { Injectable, Inject } from '@angular/core';
import { REQUEST } from '@nguniversal/express-engine/tokens';

@Injectable()
export class SubdomainLocaleService {
  constructor(@Inject(REQUEST) private request: any) {}

  getLocale(): string {
    // Check for custom header first
    if (this.request.headers['x-locale']) {
      return this.request.headers['x-locale'];
    }
    
    // Fallback to subdomain detection
    const host = this.request.headers.host;
    const subdomain = host?.split('.')[0];
    
    const subdomainToLocale: Record<string, string> = {
      'en': 'en-GB',
      'uk': 'uk-UA',
      'fr': 'fr-FR'
    };
    
    return subdomainToLocale[subdomain] || 'en-GB';
  }
}

Build and Deployment Solutions

Option 1: Single SSR Application with Runtime Detection

Deploy a single SSR application that detects language at runtime:

bash
# Build for production with SSR
ng build --configuration production --localize

# Serve SSR
ng serve --configuration production --disable-host-check

Pros:

  • Single codebase to maintain
  • Shared server resources
  • Easier deployment management

Cons:

  • Runtime language detection overhead
  • Potential caching complexities

Option 2: Separate SSR Applications per Language

Build and deploy separate SSR instances for each language:

bash
# Build English version
ng build --configuration production --localize --source-map=false --base-href="/"

# Build Ukrainian version  
ng build --configuration production --localize --source-map=false --base-href="/uk/"

# Build French version
ng build --configuration production --localize --source-map=false --base-href="/fr/"

Pros:

  • Optimal performance for each language
  • Simplified caching
  • Independent deployment scaling

Cons:

  • Multiple codebases to maintain
  • Higher deployment complexity

Alternative Approaches

1. Path-based Localization with Subdomain Proxy

Keep path-based localization internally but use subdomains as proxies:

nginx
server {
    listen 80;
    server_name en.example.org;
    
    location / {
        proxy_pass http://localhost:4000/en-GB/;
    }
}

server {
    listen 80;
    server_name uk.example.org;
    
    location / {
        proxy_pass http://localhost:4000/uk-UA/;
    }
}

2. Custom BaseHref Override

Create a custom implementation that overrides baseHref based on subdomain:

typescript
import { APP_BASE_HREF } from '@angular/common';
import { Inject, Injectable } from '@angular/core';

@Injectable()
export class CustomBaseHrefService {
  constructor(@Inject(APP_BASE_HREF) private baseHref: string) {}
  
  getBaseHref(): string {
    const host = window.location.host;
    const subdomain = host?.split('.')[0];
    
    const subdomainToBaseHref: Record<string, string> = {
      'en': '/',
      'uk': '/uk/',
      'fr': '/fr/'
    };
    
    return subdomainToBaseHref[subdomain] || '/';
  }
}

Implementation Example

Here’s a complete working implementation:

1. Update angular.json

json
{
  "projects": {
    "your-app": {
      "i18n": {
        "sourceLocale": {
          "code": "en-GB",
          "baseHref": "/"
        },
        "locales": {
          "uk": {
            "translation": "src/locale/messages.uk-UA.xlf",
            "baseHref": "/uk/"
          },
          "fr": {
            "translation": "src/locale/messages.fr-FR.xlf",
            "baseHref": "/fr/"
          }
        }
      }
    }
  }
}

2. Create SSR Middleware

typescript
// src/server/middleware/subdomain-locale.ts
import { Request, Response, NextFunction } from 'express';

export function subdomainLocaleMiddleware(req: Request, res: Response, next: NextFunction) {
  const host = req.headers.host;
  const subdomain = host?.split('.')[0];
  
  const localeMap: Record<string, string> = {
    'en': 'en-GB',
    'uk': 'uk-UA',
    'fr': 'fr-FR'
  };
  
  const locale = localeMap[subdomain] || 'en-GB';
  
  // Set custom headers for SSR
  req.headers['x-app-locale'] = locale;
  req.headers['accept-language'] = locale;
  
  next();
}

3. Configure Server.ts

typescript
// src/server.ts
import 'zone.js/node';
import { enableProdMode } from '@angular/core';
import { provideServerRendering } from '@angular/ssr';
import { ngExpressEngine } from '@nguniversal/express-engine';
import { existsSync } from 'fs';
import { join } from 'path';

// Import middleware
import { subdomainLocaleMiddleware } from './server/middleware/subdomain-locale';

enableProdMode();

const express = require('express');
const app = express();
const port = 4000;

const distFolder = join(process.cwd(), 'dist/your-app/browser');
const indexHtml = existsSync(join(distFolder, 'index.html')) 
  ? `/${join(distFolder, 'index.html')}` 
  : './src/index.html';

app.engine('html', ngExpressEngine({
  bootstrap: () => import('./src/main.server').then(m => m.app),
  providers: [provideServerRendering()]
}));

app.set('view engine', 'html');
app.set('views', distFolder);

// Apply middleware before SSR
app.use(subdomainLocaleMiddleware);

app.get('*.*', express.static(distFolder, {
  maxAge: '1y'
}));

app.get('*', (req, res) => {
  res.render(indexHtml, {
    req,
    providers: [
      { provide: 'REQUEST', useValue: req }
    ]
  });
});

app.listen(port, () => {
  console.log(`Node server listening on http://localhost:${port}`);
});

This implementation will:

  1. Detect language from subdomain in server middleware
  2. Override Accept-Language headers to prevent redirects
  3. Maintain proper asset paths based on configured baseHref
  4. Work seamlessly with your subdomain architecture

Conclusion

  1. The core issue stems from Angular SSR’s automatic redirect logic conflicting with your subdomain-based localization setup.

  2. Server-side detection is essential - override the Accept-Language header based on subdomain before Angular processes the request.

  3. Middleware implementation can prevent unwanted redirects while maintaining proper asset path generation.

  4. Nginx configuration provides an additional layer of control for header manipulation and routing.

  5. Consider deployment strategy - a single SSR application with runtime detection is simpler, while separate instances offer better performance for each language.

The key insight is that Angular SSR needs explicit guidance on which locale to use when serving from subdomains, as it defaults to header-based detection which causes the redirect behavior you want to avoid.


Sources

  1. Example multi-language Angular app with i18n and SSR (hosted on Firebase App Hosting) - Lukas Bühler

  2. Fix(@angular/ssr): prevent malicious URL from overriding host - Angular CLI

  3. CVE-2025-62427: CWE-918: Server-Side Request Forgery in angular angular-cli - OffSeq Threat Intelligence

  4. Understanding SSR in Angular - Sorus Gentrification

  5. Angular SSR: ‘getPrerenderParams’ is missing ERROR - Piyali Das