Stop Hardcoding URLs in React Builds: Slot‑Safe, CDN‑Safe Patterns

React production builds often embed absolute URLs into asset-manifest.json and JavaScript chunks. This is convenient for CDNs, but risky for environments that change origin at runtime (e.g., Azure App Service deployment slot swaps). This article explains the root cause, why it breaks swaps, how to prevent it in Jenkins and local builds, and broader best practices for CI (Jenkins, GitHub Actions) and Azure.


Why This Is a Problem

Symptom: asset-manifest.json contains fully qualified URLs:
{
  "files": {
    "main.js": "https://example-staging.com/static/js/main.9b5579c6.js",
    "static/css/main.75bb81f4.css": "https://example-staging.com/static/css/main.75bb81f4.css",
    ...
  }
}
What It Should Look Like (Relative Paths):

After building with PUBLIC_URL=/ (root) or a subpath, the manifest and JS chunks point to the current origin without hardcoded domains.

asset-manifest.json (root path)

{
  "files": {
    "main.js": "/static/js/main.9b5579c6.js",
    "static/css/main.75bb81f4.css": "/static/css/main.75bb81f4.css"
  },
  "entrypoints": [
    "/static/js/runtime-main.a1b2c3.js",
    "/static/js/main.9b5579c6.js"
  ]
}

asset-manifest.json (subpath deployment)

{
  "files": {
    "main.js": "/apps/review-123/static/js/main.9b5579c6.js",
    "static/css/main.75bb81f4.css": "/apps/review-123/static/css/main.75bb81f4.css"
  },
  "entrypoints": [
    "/apps/review-123/static/js/runtime-main.a1b2c3.js",
    "/apps/review-123/static/js/main.9b5579c6.js"
  ]
}

Compiled JavaScript (Webpack runtime)

When PUBLIC_URL is relative, Webpack’s runtime sets __webpack_require__.p (the public path) to the relative base:

// Good: root
__webpack_require__.p = "/";

// Good: subpath
__webpack_require__.p = "/apps/review-123/";

// Then chunk URLs resolve relative to that base:
__webpack_require__.u = (chunkId) => "static/js/" + chunkId + ".a1b2c3.js";
const url = __webpack_require__.p + __webpack_require__.u(123); // "/static/js/123.a1b2c3.js" or "/apps/review-123/static/js/123.a1b2c3.js"
What Not To Do (Absolute Domains):

asset-manifest.json (bad)

{
  "files": {
    "main.js": "https://example-staging.com/static/js/main.9b5579c6.js",
    "static/css/main.75bb81f4.css": "https://example-staging.com/static/css/main.75bb81f4.css"
  },
  "entrypoints": [
    "https://example-staging.com/static/js/runtime-main.a1b2c3.js",
    "https://example-staging.com/static/js/main.9b5579c6.js"
  ]
}

Compiled JavaScript (bad)

// Bad: hardcoded domain
__webpack_require__.p = "https://example-staging.com/";
// Every chunk URL now points to the staging origin, breaking slot swaps.

Root Cause (with References)

More Articles on Why This Happens

Prevention in Jenkins (CI)

Use relative PUBLIC_URL

Set PUBLIC_URL to a relative path during build so assets resolve against the current origin:

// Jenkins pipeline (Groovy)
stage('Build') {
  sh 'rm -rf build'
  // Slot‑safe build
  sh 'PUBLIC_URL=/ yarn build'
}

Optional: Post‑build guard

Fail the pipeline if asset-manifest.json contains absolute URLs:

// Node script: verify-manifest.js
const fs = require('fs');
const manifest = JSON.parse(fs.readFileSync('build/asset-manifest.json', 'utf8'));
const files = Object.values(manifest.files || {});
const bad = files.filter(u => /^https?:\/\//.test(u));
if (bad.length) {
  console.error('Absolute URLs detected in asset-manifest:', bad.slice(0, 5));
  process.exit(1);
}

Run after build:

// Jenkins
sh 'node verify-manifest.js'

Handle secrets correctly


Prevention in Local/Standard React Builds

Build with relative PUBLIC_URL

// Windows
set PUBLIC_URL=/ && yarn build

// macOS/Linux
PUBLIC_URL=/ yarn build

Router base path

If deploying under a subpath, align the router’s base:

// React Router v6
<BrowserRouter basename={process.env.PUBLIC_URL || '/'}>...</BrowserRouter>
React Router: BrowserRouter

Service worker considerations


Runtime Configuration Patterns (Keep Env Out of JS)

Pattern A: Fetch a JSON at startup

// config.json (served by your app)
{
  "API_BASE_URL": "https://api.example.com",
  "ANALYTICS_ID": 123456
}
// config-loader.ts
export async function loadConfig() {
  const res = await fetch('/config/config.json'); // relative to current origin
  return res.json();
}

Pattern B: Server‑injected globals

<!-- index.html -->
<script>
  window.__APP_CONFIG__ = { API_BASE_URL: 'https://api.example.com' };
</script>
// usage
const apiBase = window.__APP_CONFIG__?.API_BASE_URL;

Why this is safer


Best Practices by Platform

Jenkins

GitHub Actions

# .github/workflows/build.yml
jobs:
  build:
    runs-on: ubuntu-latest
    env:
      PUBLIC_URL: /
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 18 }
      - run: yarn install --frozen-lockfile
      - run: yarn build
      - run: node verify-manifest.js

Azure


Scenarios: Absolute vs Relative URLs

Prefer Absolute URLs When…

Prefer Relative URLs When…


Verification Checklist


Key Takeaways