Skip to content
Blog

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:

  1. Load the root, navigate two levels deep, then hard-refresh — the app must restore at the same URL.
  2. Paste the deep link into a fresh incognito window — same thing.
  3. curl -I https://yourapp.example/orders/42 — must return 200 text/html, not 404, or the rewrite is missing.
  4. Check that static assets (main.dart.js, assets/, *.vec) are not swallowed by the rewrite: they should return their real files, not index.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.

Sources