Why relative style.css or script.js links 404 in a Flask-served HTML page, and how the trailing slash on the route actually causes it.
Fixing 404 Errors on CSS/JS Files Served from a Relative Path in Flask
You link a stylesheet with a plain relative path — <link rel="stylesheet" href="style.css"> — the HTML page itself loads fine, but the CSS never applies, and your browser's network tab shows a 404 for style.css. This is one of the most common flask static file 404 issues, and it almost always comes down to one specific, easy-to-miss detail: where the browser thinks "relative" is relative to.
Table of Contents
- The Symptom
- Why Relative Paths Depend on the URL, Not the File
- The Trailing Slash Problem
- How StayPresent Solves This Automatically
- Verifying the Fix
- What Still Causes a 404 After This
- Full Example
- FAQs
- Conclusion
The Symptom
<!-- dashboard.html -->
<link rel="stylesheet" href="style.css">
The page itself renders. The CSS doesn't apply. DevTools shows GET /style.css 404 — even though style.css genuinely exists right next to dashboard.html on disk.
Why Relative Paths Depend on the URL, Not the File
This is the part that trips people up: a relative path like href="style.css" is resolved by the browser against the URL of the current page, not against where the HTML file physically sits on disk. If your page is served at /dashboard (no trailing slash), the browser resolves style.css relative to /, requesting /style.css — which almost certainly doesn't exist, since your actual file lives at whatever path dashboard.html's directory maps to.
If the page were instead served at /dashboard/ (with a trailing slash), the same relative link resolves to /dashboard/style.css — which, if your server is set up to serve static files from that same directory, actually exists.
The Trailing Slash Problem
This is exactly why a bare route without a trailing slash is the root cause in most cases. If you're serving dashboard.html at a custom path like /dashboard, and a visitor (or a link elsewhere in your app) hits /dashboard without the trailing slash, every relative asset on that page resolves one directory level too high.
How StayPresent Solves This Automatically
staypresent.web.html() and web.markdown() handle this specific pitfall for you: for any registered path other than the root "/", a bare request to that path is automatically redirected (HTTP 308) to a trailing-slash version:
import staypresent
staypresent.web.html("templates/dashboard.html", path="/dashboard")
staypresent.run("bot.py")
A request to /dashboard gets a 308 redirect to /dashboard/, and then the page's relative asset links resolve correctly, since the browser is now resolving them against /dashboard/ rather than /. Static asset lookups themselves use the longest matching path prefix across every registered html()/markdown() route, so this resolves correctly even with multiple pages registered at different paths.
Verifying the Fix
If you're still seeing a 404 after confirming the trailing-slash redirect is happening (check your network tab for a 308 response before the final request), the next things to check:
-
Is the asset actually in the same directory as the HTML file?
web.html()/web.markdown()only auto-serve files sitting in the same directory as the target file — a CSS file one level up or down won't be found. -
Is the link itself actually relative?
href="/style.css"(with a leading slash) is an absolute path, not relative — it always resolves against the domain root regardless of the current page's URL, and won't benefit from the trailing-slash fix at all.
What Still Causes a 404 After This
A couple of things the trailing-slash fix doesn't cover:
-
A typo in the filename — case sensitivity matters;
Style.cssandstyle.cssare different files on most deployment environments (Linux-based hosting is case-sensitive, even if your local machine is running macOS/Windows, where it might not be). -
The asset genuinely being in a different directory. If your project structure has
templates/dashboard.htmlandstatic/style.cssas siblings rather thantemplates/style.cssalongside it, the automatic neighbor-file serving won't find it — either move the asset next to the HTML file, or use an absolute path pointing at wherever it's actually served from.
Full Example
templates/
├── dashboard.html
├── style.css
├── status.js
<!-- templates/dashboard.html -->
<!DOCTYPE html>
<html>
<head>
<title>Bot Dashboard</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Status</h1>
<script src="status.js"></script>
</body>
</html>
import staypresent
staypresent.web.html("templates/dashboard.html", path="/dashboard")
staypresent.run("bot.py")
Visiting /dashboard (or /dashboard/ directly) now correctly loads style.css and status.js, with the trailing-slash redirect handling the difference automatically.
FAQs
Does this affect pages served at the root path /?
No — the root path has no "parent" to accidentally resolve against, so this specific issue is unique to custom, non-root paths.
Does the redirect happen for web.json()/web.text() too?
No — only html()/markdown(), since only those two serve neighboring static files that a relative link could actually point at.
Is a 308 redirect bad for SEO or caching?
A 308 (Permanent Redirect) preserves the request method and is treated by browsers and crawlers as a standard permanent redirect — generally not a concern for an internal application route like a bot's own dashboard.
Conclusion
A relative CSS/JS 404 on a custom-path HTML page is almost always the trailing-slash issue — the browser resolving style.css against the wrong base URL. staypresent.web.html()/web.markdown() handle this automatically via a built-in redirect, so as long as your asset actually sits next to the HTML file on disk, relative links just work without you needing to think about the underlying URL-resolution mechanics at all.
pip install staypresent[prod]
Top comments (0)