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
- Assets point to the wrong origin after a swap: If your build hardcodes URLs for staging, swapping to production can leave bundles referencing the staging domain. Assets 404 and service workers may refuse to register due to cross‑origin constraints.
- Coupled builds: You must rebuild for every environment change, slowing releases and complicating rollback/slot strategies.
- Security exposure: Compiled bundles can unintentionally leak endpoints or tokens if you bake them into JS.
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",
...
}
}
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"
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)
- PUBLIC_URL drives Webpack publicPath: In Create React App (CRA),
PUBLIC_URLsets the HTML base and Webpack’soutput.publicPath. When set to an absolute domain, CRA prepends that domain to all asset paths in the build output.CRA: Advanced configuration (env) PUBLIC_URL is used by the build and templates
Webpack: output.publicPath Controls the base path for all assets within the application - Service worker origin check: CRA’s service worker registers only when asset origin equals page origin; cross‑origin assets break PWA behavior.
CRA: Making a PWA Notes on service worker registration and origin
MDN: ServiceWorkerContainer.register() Service worker scope/origin rules - Deployment slot swaps change origin: Azure App Service swaps staging/production content and settings; hardcoded absolute asset URLs won’t follow.
Azure App Service: Swap deployments How slots and swaps work
- CRA Deployment: Building for relative paths
Explains usingPUBLIC_URLand thehomepagefield. - Webpack Guide: Public Path
Deep dive into how runtime chunk URLs are formed. - MDN: <base> element
How base URLs affect relative links in HTML. - CRA webpack.config.js (source)
Where CRA wiresPUBLIC_URLto WebpackpublicPath.
Prevention in Jenkins (CI)
Use relative PUBLIC_URL
Set PUBLIC_URL to a relative path during build so assets resolve against the current origin:
- Root of site:
/ - Subpath deployment (e.g., review app under
/apps/review-123):/apps/review-123
// 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
- Never bake secrets into JS: Use Jenkins Credentials and inject at runtime/server‐side only.
Jenkins Pipeline: Using credentials withCredentials, secret handling
- Runtime config instead of build‑time env: Ship a small JSON (e.g.,
/config/app-config.json) that’s served by the app and updated per environment.
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>
Service worker considerations
- Keep
PUBLIC_URLsame‑origin with the page to allow SW registration. - Disable SW if your asset hosting is cross‑origin and you don’t plan to handle advanced PWA plumbing.
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
- Slot‑safe: Config follows the active slot/origin.
- No rebuilds: You can update config without recompiling.
- Secret discipline: Front‑end only receives non‑secret values; real secrets stay server‑side.
Best Practices by Platform
Jenkins
- Set
PUBLIC_URLto relative paths in build stages. - Use Credentials for secrets; pass them to back‑end or a secure config endpoint, not to the client bundle.
- Add a post‑build guard to fail on absolute asset URLs or detected secrets in artifacts.
GitHub Actions
- Define
PUBLIC_URLat the job or step level:
# .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
- Store secrets in Actions Secrets, not source code.
Azure
- Use deployment slots for staging/production; keep builds portable with relative asset paths.
- Inject environment‑specific config via App Settings or static
/config/config.jsonper slot.Azure App Service: Deployment slots
Azure App Service: App settings Environment configuration per slot
Scenarios: Absolute vs Relative URLs
Prefer Absolute URLs When…
- You host assets on a fixed CDN domain that never changes across environments.
- You’ve addressed PWA/service worker constraints (same‑origin or custom SW strategy).
- Your routing and infra never rely on slot swaps (immutable origins).
Prefer Relative URLs When…
- You use Azure App Service swaps or rotate environments where the origin changes.
- You want a single build artifact to run across QA/Staging/Production without rebuild.
- You want SW registration to “just work” without cross‑origin asset handling.
Verification Checklist
- Build with
PUBLIC_URL=/(or the correct subpath). - Inspect
build/asset-manifest.json: ensure nohttp(s)://entries. - Open
build/index.htmland confirm assets use relative paths. - Deploy to a staging slot, then swap: assets load from the current origin.
- Confirm service worker registration (if enabled) on production origin.
Key Takeaways
- PUBLIC_URL controls asset base paths. Keep it relative unless you truly need a fixed CDN domain.
- Runtime config beats build‑time envs for portability and safety.
- CI guards catch regressions (absolute URLs, secrets) before deploy.
- Secrets never belong in client bundles. Keep them server‑side.