Relative Asset Paths in index.html Break Silently When a CDN Serves the File for Deep-Link Routes
TLDR
When configuring single-page applications or multi-route web apps with CDN rewrite rules (such as/app/* -> /index.html 200), relative asset references (href="assets/css/main.css") silently break on deep links like /app/pdf/. The browser resolves relative paths against the URL currently in the browser location bar, not the physical file location on disk, resulting in cascading 404 errors for all CSS and JS assets.
| URL Route | Relative Reference Resolution (assets/main.css) | Absolute Reference Resolution (/assets/main.css) | Result |
|---|---|---|---|
https://example.com/ | https://example.com/assets/main.css | https://example.com/assets/main.css | Both Work |
https://example.com/app/pdf/ | https://example.com/app/pdf/assets/main.css | https://example.com/assets/main.css | Relative 404s; Absolute Works |
Relative path resolution yields unstyled pages and broken scripts
Everything worked on our homepage (/). But the moment we tested deep-linking directly to /app/pdf/, the page rendered as unstyled, unformatted HTML with zero interactive behavior.
Inspecting Network DevTools revealed a cascade of 404 errors across our asset dependency tree:
GET /app/pdf/assets/css/portfolio.css 404 GET /app/pdf/assets/os/contracts.js 404 GET /app/pdf/assets/components/shell.js 404
Our Cloudflare Pages _redirects rule (/app/* /index.html 200) executed as configured, returning HTTP 200 with the core index.html file. However, the document was incapable of resolving its relative stylesheets and scripts.
URL parsing dynamics append relative assets to active subdirectories
Relative URLs inside HTML documents are resolved dynamically by the browser DOM relative to window.location.href.
When the user visits /, <link rel="stylesheet" href="assets/css/main.css"> resolves to /assets/css/main.css. However, when the browser loads the identical index.html payload at /app/pdf/, the browser appends the relative asset path to the active path prefix, evaluating the target URL as /app/pdf/assets/css/main.css. Because those static assets live at root /assets/, every asset lookup fails.
+-------------------------------------------------------------+
Browser Path Evaluation Browser loads index.html at /app/pdf/ v Asset Path Type? / \ Relative Absolute (href="assets/main.css") (href="/assets/main.css") v v Appends to location.href Resolves against Origin /app/pdf/assets/main.css /assets/main.css v v Server Returns 404 (Fail) Server Returns 200 (Success)
+-------------------------------------------------------------+
Domain-root absolute path prefixing bypasses location hierarchies
The fix is straightforward: prefix every asset path in index.html with a leading slash /:
<!-- Incorrect: Relative paths fail when served from deep-link rewrite routes -->
<link rel="stylesheet" href="assets/css/main.css" />
<script src="assets/js/app.js" defer></script>
<!-- Correct: Absolute domain-root paths resolve accurately from any route --> <link rel="stylesheet" href="/assets/css/main.css" /> <script src="/assets/js/app.js" defer></script>
Absolute paths starting with / instruct the browser engine to resolve the request directly against the origin host (https://example.com/assets/css/main.css), completely bypassing the active path hierarchy in the location bar.
To catch these issues in CI/CD before deployment, we run a simple regex search across all entry HTML files:
# Grep for any src or href attribute missing a leading slash, http(s), or fragment
grep -E '(src|href)="(?!/|https?://|#)' index.html
Rule of thumb: Any HTML entry point served via CDN rewrite rules (such as SPA catch-all paths) must strictly use domain-root absolute paths (/assets/...) for all static CSS, JS, font, and media tags.