URL Strategies for Flutter Web: usePathUrlStrategy and Rewrites on Every Host
Switch Flutter web to path URLs with flutter_web_plugins, then make deep links survive refresh on Firebase Hosting, nginx, Netlify, S3/CloudFront, and GitHub Pages.
Published on • November 13, 2026
AI Assistant

flutter-url-strategies-web compares hash and path URLs and explains why you’d switch. This post is the wiring manual: the exact pubspec dependency, the one line of Dart that changes the strategy, and — the part that actually breaks production — the server rewrite each host needs so a deep link like /orders/42 survives a refresh. The configuration facts come from the official URL-strategy doc and each host’s own documentation.
Step 1: the SDK dependency and the call
flutter_web_plugins can’t be added with flutter pub add — it’s an SDK package:
dependencies:
flutter:
sdk: flutter
flutter_web_plugins:
sdk: flutter
Then, before runApp:
import 'package:flutter_web_plugins/url_strategy.dart';
void main() {
usePathUrlStrategy();
runApp(const MyApp());
}
That’s the whole client side. PathUrlStrategy uses the browser’s History API, which means the current URL is read from location.pathname — and the moment the user refreshes, the browser sends that pathname to your server as a real HTTP request for a file that doesn’t exist. Hence step 2.
Step 2: the rewrite every host needs
The Flutter doc states it directly: “check your web server’s documentation on how to rewrite requests to index.html.” The per-host recipes:
Firebase Hosting — the doc points at the “Configure as a single-page app” option, which is Firebase’s rewrites glob:
{
"rewrites": [{ "source": "**", "destination": "/index.html" }]
}
nginx — try_files falls back to the SPA entry:
location / {
try_files $uri $uri/ /index.html;
}
Netlify — a _redirects file (or netlify.toml):
/* /index.html 200
S3 + CloudFront — S3 returns the object key or errors; the standard setup routes the 404 (or a CloudFront Function on the origin request) to /index.html with a 200, so the CDN caches the shell instead of an error page. A bare S3 “index document” only helps the root — nested paths still 404 without the error mapping.
GitHub Pages — no rewrite engine exists, so ship a 404.html that loads index.html’s content (or redirects), and accept the extra hop. It’s the least clean option; that’s the cost of free hosting.
Flutter Web — nothing to configure: the doc notes flutter run -d chrome already handles any path and falls back to index.html.
One Apache flavor, since intranet deployments still use it:
FallbackResource /index.html
FallbackResource (2.4+) does the same job as nginx’s try_files; the older mod_rewrite catch-all (RewriteRule ^ index.html [L]) works too but must exclude real files explicitly, or it will happily serve HTML for your .js bundle — the misconfiguration described in the verification section below.
Step 3: sub-path hosting and --base-href
If the app doesn’t live at the domain root, both the asset base and the strategy must agree. Set the tag in web/index.html — or use the build flag:
flutter build web --base-href /flutter_app/
The docs add a subtlety that bites CDN setups: a relative base href is allowed for release builds but must be correct for every URL the page might be served from — /flutter_app/, /flutter_app/nested/route, and /flutter_app/nested/route/ need ., .., and ../.. respectively. Prefer the absolute form unless you know you need relative.
Verify like a user, not like a developer
The failure mode is invisible in dev because the dev server masks it. Post-deploy checks:
- Load the root, navigate two levels deep, then hard-refresh — the app must restore at the same URL.
- Paste the deep link into a fresh incognito window — same thing.
curl -I https://yourapp.example/orders/42— must return200 text/html, not404, or the rewrite is missing.- Check that static assets (
main.dart.js,assets/,*.vec) are not swallowed by the rewrite: they should return their real files, notindex.html. On nginx,try_files $uri …orders matter; on Firebase, rewrites only fire when no file matches.
Point 4 is the classic misconfiguration: a catch-all rewrite that works for routes but silently serves HTML with a JavaScript content type for your Dart bundle.
Takeaway: Add flutter_web_plugins as an SDK dependency, call usePathUrlStrategy() before runApp, then give every host the same contract — unknown paths return index.html with a 200 while real asset files pass through — and verify deep-link refresh plus curl on both a route and a bundle asset before calling it done.