Flutter Web has come a long way, but one area that trips up even experienced developers is URL structure. Get it wrong, and Google either ignores your app entirely or indexes pages it can’t read. Get it right, and your Flutter Web app stands a real shot at ranking alongside traditional websites.
This guide walks you through how to structure URLs in Flutter Web the right way, from choosing the correct URL strategy to setting up go_router, handling dynamic paths, and configuring your server so nothing breaks in production.
Why URL Structure Matters in Flutter Web
Most Flutter apps start their web life with a URL that looks something like this:
yourapp.com/#/about
That # symbol is the problem. The default URL strategy for Flutter web applications is hash-based, which is not good for SEO and degrades the user experience.
Here is why. Search engines like Google treat the fragment portion of a URL (everything after #) as a client-side instruction, not a real page path. That means Googlebot crawls yourapp.com/ and stops. It never sees /about, /products, or any of your other routes as separate pages.
Path-based URLs solve this. Instead of yourapp.com/#/about, your app serves yourapp.com/about. Google reads that as a real page, can index it, and can rank it.
This single change often produces the biggest SEO gain for any Flutter Web project.
Step 1: Switch to Path-Based URLs
Flutter web apps support two URL strategies: Hash (the default), where paths are read and written to the hash fragment, and Path, where paths are read and written without a hash.
To switch to path-based URLs, you need to call usePathUrlStrategy() before your app runs.
Add flutter_web_plugins to your pubspec.yaml:
dependencies:
flutter:
sdk: flutter
flutter_web_plugins:
sdk: flutter
Then update your main.dart:
import ‘package:flutter_web_plugins/url_strategy.dart’;
void main() {
usePathUrlStrategy();
runApp(MyApp());
}
That’s it on the Dart side. PathUrlStrategy uses the History API, which requires additional configuration for web servers. To configure your web server to support PathUrlStrategy, check your web server’s documentation to rewrite requests to index.html.
We’ll cover that server configuration in a later step.
Step 2: Set Up go_router for Clean, Readable Paths
Once you have path URLs working, the next step is defining those paths clearly. The Flutter team’s recommended tool for this is go_router.
go_router is a declarative routing package for Flutter that uses the Router API to provide a convenient, URL-based API for navigating between different screens. You can define URL patterns, navigate using a URL, handle deep links, and a number of other navigation-related scenarios.
Add it to your project:
dependencies:
go_router: ^14.3.0
Run flutter pub get and you’re ready to define routes.
Here is a simple but production-ready route setup:
import ‘package:go_router/go_router.dart’;
final GoRouter router = GoRouter(
routes: [
GoRoute(
path: ‘/’,
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: ‘/about’,
builder: (context, state) => const AboutScreen(),
),
GoRoute(
path: ‘/services’,
builder: (context, state) => const ServicesScreen(),
),
GoRoute(
path: ‘/blog/:slug’,
builder: (context, state) {
final slug = state.pathParameters[‘slug’]!;
return BlogPostScreen(slug: slug);
},
),
],
);
Then pass the router to your MaterialApp:
MaterialApp.router(
routerConfig: router,
);
Let’s break it down. Each GoRoute takes a path string and a builder. The path string is what shows in the browser. Keep these paths lowercase, descriptive, and short. Think of them as labels for your content, not variable names.
How to Structure URLs in Flutter Web for SEO
Good URL structure follows a few rules that apply to any web technology, Flutter included.
Use lowercase letters only. Mixed-case URLs cause duplicate content issues. yourapp.com/About and yourapp.com/about look like two different pages to search engines.
Separate words with hyphens, not underscores. Google treats hyphens as word separators. It treats underscores as connectors. So /flutter-web-routing is better than /flutter_web_routing for search visibility.
Keep URLs short and descriptive. A URL like /services/mobile-app-development tells both users and crawlers exactly what to expect. A URL like /page?id=47 tells them nothing.
Avoid deep nesting. More than three levels deep (like /a/b/c/d) makes crawling harder. Flatten your URL structure where you can.
Use static paths for content pages. Pages like /about, /contact, and /services should have fixed URLs. Only use dynamic path parameters for content that genuinely varies by ID or slug.
At FBIP, these same rules apply when building Flutter Web applications for clients. Clean URL structures are part of the initial planning, not an afterthought.
Step 3: Handle Dynamic URLs and Query Parameters
Real apps need dynamic routes. A blog, a product catalog, or a portfolio section all have pages that share a template but differ by content.
Here is how to handle those with go_router:
GoRoute(
path: ‘/portfolio/:projectId’,
builder: (context, state) {
final projectId = state.pathParameters[‘projectId’]!;
return ProjectScreen(projectId: projectId);
},
),
go_router also supports query parameters. You can access them with state.uri.queryParameters[‘key’].
GoRoute(
path: ‘/search’,
builder: (context, state) {
final query = state.uri.queryParameters[‘q’] ?? ”;
return SearchScreen(query: query);
},
),
For SEO, prefer path parameters over query strings for content URLs. /blog/flutter-web-routing ranks better than /blog?post=flutter-web-routing. Use query strings for filters and search terms that don’t represent standalone pages.
Step 4: Add Redirects for Auth and Legacy URLs
go_router makes redirects easy. You can add a global redirect function to handle authentication gates or forward old URLs to new ones.
You can add a redirect to the GoRouter config to redirect users to a login page if they are not authenticated.
final GoRouter router = GoRouter(
redirect: (context, state) {
final isLoggedIn = AuthService.isAuthenticated;
if (!isLoggedIn && state.uri.path.startsWith(‘/dashboard’)) {
return ‘/login’;
}
return null;
},
routes: [ /* your routes here */ ],
);
Redirects also help preserve link equity if you rename a URL. Always redirect old paths to new ones rather than letting them 404.
Step 5: Handle 404 Pages Properly
A 404 page that looks broken (or redirects to your homepage) confuses both users and crawlers. go_router has a built-in errorBuilder for this.
final GoRouter router = GoRouter(
errorBuilder: (context, state) => const NotFoundScreen(),
routes: [ /* your routes here */ ],
);
Your NotFoundScreen should return a proper HTTP 404 status code on the server side as well. Check your hosting provider’s documentation for how to configure custom error pages with the right status codes.
Step 6: Configure Your Web Server
Path-based URLs require server-side support. When a user types yourapp.com/about directly into their browser, the server needs to know to return index.html rather than look for a file called about.
For deployment on Vercel, create a vercel.json file in the root of your project. This instructs Vercel to always serve index.html for any route, which allows the client-side router to take over.
{
“rewrites”: [
{ “source”: “/(.*)”, “destination”: “/index.html” }
]
}
For Firebase Hosting, add a rewrite rule in firebase.json:
{
“hosting”: {
“rewrites”: [
{
“source”: “**”,
“destination”: “/index.html”
}
]
}
}
For Nginx, add this to your server block:
location / {
try_files $uri $uri/ /index.html;
}
Without this, direct URL access and browser refreshes will return a 404 from the server, not from your app.
Using ShellRoute for Shared Layouts Without Losing URL Clarity
One common problem in Flutter Web is keeping a persistent navigation bar or app bar while still updating the URL for each page.
With ShellRoute, a parent shell defines the shared layout. Inside this ShellRoute, you define the child routes. When a user taps a tab, only the child content changes and the shared layout stays in place.
final GoRouter router = GoRouter(
routes: [
ShellRoute(
builder: (context, state, child) => AppShell(child: child),
routes: [
GoRoute(
path: ‘/home’,
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: ‘/services’,
builder: (context, state) => const ServicesScreen(),
),
],
),
],
);
This pattern gives you a clean URL for every tab without re-rendering the entire page layout. It also means the browser’s back and forward buttons work as expected, which matters for both users and search engine crawlers.
Flutter Web SEO Limitations to Know
URL structure helps a lot, but it is one part of a broader SEO picture for Flutter Web.
Flutter’s web FAQ notes that text-rich, flow-based, static content such as blog articles benefits from the document-centric model that the web is built around. For such content, one approach is to separate your primary application experience from landing pages and marketing content created using search-engine-optimized HTML.
This matters if your Flutter Web app includes a blog or long-form content. Flutter renders to a canvas, not to DOM elements, so Googlebot may not read the text content of individual screens even if the URL is clean and crawlable. For content-heavy pages, consider a hybrid approach: serve those pages as static HTML or a separate CMS, and keep Flutter for the app-like interactive portions.
FBIP builds Flutter Web projects with this separation in mind, planning which parts of a site should be Flutter and which should be standard HTML from the start.
Quick Reference: Flutter Web URL Structure Checklist
Before you ship, run through this checklist:
- Switched from hash URLs to path URLs using usePathUrlStrategy()
- Installed and configured go_router with named path routes
- URLs use lowercase letters and hyphens only
- Dynamic content uses path parameters (/blog/:slug), not query strings
- Server configured to rewrite all paths to index.html
- Custom 404 page set up via errorBuilder
- Redirects in place for any renamed or moved routes
- ShellRoute used for shared navigation elements
Frequently Asked Questions
Why does my Flutter Web app show a blank page when I type a URL directly?
Your server returned a 404 because it looked for a file matching the URL path and found nothing. Flutter Web is a single-page app. You need to configure your server (Nginx, Firebase, Vercel, etc.) to serve index.html for every path, then let go_router take over client-side routing from there.
Does Flutter Web support SEO the same way a normal HTML website does?
Not exactly. Flutter renders to a canvas, so Googlebot may not read on-screen text the way it reads HTML. Clean URL structure helps crawling, but for text-heavy content, you may get better results pairing Flutter with an HTML-based solution for landing and content pages.
What is the difference between hash URLs and path URLs in Flutter Web?
Hash URLs look like yourapp.com/#/about. The # means the browser never sends that path to the server. Path URLs look like yourapp.com/about, which is a real HTTP request that crawlers and browsers both treat as a separate page. Path URLs are better for SEO.
Can I use go_router for both mobile and web in the same Flutter project?
Yes. go_router works on iOS, Android, and the web from a single codebase. On mobile, it handles deep linking. On the web, it also manages browser URL updates and history. You write your routes once and they behave correctly on all platforms.
How do I redirect old URLs to new ones in Flutter Web without losing rankings?
Add a redirect function to your GoRouter config that checks the incoming path and returns the new path for any old URLs. This keeps go_router routing users to the right screen. On the server side, set up a 301 redirect from the old URL to the new one so search engines transfer any link authority to the new page.





