Les applications Flutter Web fonctionnent comme des SPA (Single-Page Applications) :
au lieu de renvoyer une page HTML complète à chaque navigation, elles chargent une seule fois un index.html minimal, puis mettent à jour l’interface côté navigateur avec JavaScript.
Ce index.html initial contient très peu d’informations SEO.
Mais bonne nouvelle : Google est capable d’exécuter le JavaScript, d’attendre que l’application charge, puis d’indexer le DOM final modifié.
Autrement dit :
si votre application met à jour dynamiquement coté client le <title>, la meta description ou le contenu des pages, Google peut lire ces valeurs modifiées, exactement comme vous les voyez dans le panneau “Elements” du DevTools de Chrome.
Exemple concret :
Si vous changez la meta description en JavaScript :
document.querySelector('meta[name="description"]').setAttribute('content', 'Nouvelle description SEO'); Googlebot exécutera le script, verra la meta mise à jour, et pourra l’indexer.
Cependant, pour que le référencement fonctionne réellement bien dans une application Flutter Web, il est indispensable de soigner plusieurs points :
- URLs propres (sans
#/) - Mise à jour dynamique des balises
<head>(title, description, canonical…) - sitemap.xml + robots.txt
Voici une mise en place éprouvée, simple à intégrer dans un projet Flutter Web en production, afin d’obtenir un comportement SEO comparable à une application web classique.
1. Avoir des URLs propres (sans #/)
Par défaut, Flutter Web utilise le hash (/#/page) pour gérer les routes.
Ce format fonctionne, mais il n’est pas optimal pour le SEO.
On active la URL Strategy en mode “path” (vraies URLs lisibles) :
import 'package:flutter/material.dart';
import 'package:flutter_web_plugins/url_strategy.dart';
void main() {
usePathUrlStrategy(); //enlève #/ dans les URLs
runApp(const MyApp());
} Assurez-vous aussi que dans web/index.html la balise <base> est présente :
<base href="/"> 2. Mettre à jour title et meta en Flutter
Comme Flutter gère le DOM après chargement, on doit modifier les metas depuis Dart
MetaManager (version Web)
lib/seo/meta_manager_web.dart
import 'dart:html' as html;
class MetaManager {
static void setTitle(String title) => html.document.title = title;
static void setDescription(String description) {
_upsertMeta('description', description);
}
static void setCanonical(String url) {
_upsertLink(rel: 'canonical', href: url);
}
static void _upsertMeta(String name, String content) {
final el = html.document.head!.querySelector('meta[name="$name"]');
if (el != null) {
el.setAttribute('content', content);
return;
}
html.document.head!.append(html.MetaElement()
..name = name
..content = content);
}
static void _upsertLink({required String rel, required String href}) {
final el = html.document.head!.querySelector('link[rel="$rel"]');
if (el != null) {
el.setAttribute('href', href);
return;
}
html.document.head!.append(html.LinkElement()
..rel = rel
..href = href);
}
} Version mobile (ne rien faire)
lib/seo/meta_manager_stub.dart
class MetaManager {
static void setTitle(String _) {}
static void setDescription(String _) {}
static void setCanonical(String _) {}
} Import conditionnel
lib/seo/meta_manager.dart
export 'meta_manager_stub.dart'
if (dart.library.html) 'meta_manager_web.dart'; Résultat :
- Sur Web → les balises
<head>sont modifiées - Sur iOS / Android → aucune erreur
3. Appliquer le SEO automatiquement à chaque navigation
On utilise un NavigatorObserver pour mettre à jour les balises dès que l’utilisateur change de page.
lib/seo/seo_observer.dart
import 'package:flutter/material.dart';
import 'meta_manager.dart';
class SeoData {
final String? title;
final String? description;
final String? canonicalUrl;
const SeoData({this.title, this.description, this.canonicalUrl});
}
class SeoObserver extends NavigatorObserver {
@override
void didPush(Route route, Route<dynamic>? previousRoute) {
_apply(route);
}
void _apply(Route route) {
final seo = route.settings.arguments;
if (seo is SeoData) {
if (seo.title != null) MetaManager.setTitle(seo.title!);
if (seo.description != null) MetaManager.setDescription(seo.description!);
if (seo.canonicalUrl != null) MetaManager.setCanonical(seo.canonicalUrl!);
}
}
} 4. Centraliser le SEO dans les routes (GoRouter)
Chaque route renvoie un MaterialPage avec son SEO associé :
import 'package:go_router/go_router.dart';
import 'seo/seo_observer.dart';
import 'ui/home.dart';
import 'ui/product_list.dart';
const baseUrl = 'https://monsite.com';
final router = GoRouter(
observers: [SeoObserver()],
routes: [
GoRoute(
path: '/',
pageBuilder: (context, state) => MaterialPage(
key: state.pageKey,
arguments: const SeoData(
title: 'Accueil | mon site name',
description: 'Découvrez notre catalogue, nos offres et nouveautés.',
canonicalUrl: '$baseUrl/',
),
child: const HomeScreen(),
),
),
GoRoute(
path: 'product-list/:categorie/:type',
pageBuilder: (context, state) {
final categorie = state.pathParameters['categorie']!;
final type = state.pathParameters['type']!;
return MaterialPage(
key: state.pageKey,
arguments: SeoData(
title: '$type - $categorie | Bloopa',
description: 'Tous les $type dans la catégorie $categorie.',
canonicalUrl: '$baseUrl/product-list/$categorie/$type',
),
child: ProductListScreen(
categorie: categorie,
typeproduit: type,
),
);
},
),
],
);
Avantage : pas besoin d’appeler MetaManager dans chaque page, c’est automatique.
6. Ajouter un Sitemap et un robots.txt
web/robots.txt
User-agent: *
Allow: /
Sitemap: https://monsite.com/sitemap.xml La génération du sitemap.xml peut être :
- manuelle
- générée à la compilation (article à venir)
- servie par une Cloud Function si catalogue dynamique (article à venir)
Conclusion
Avec ces quelques étapes :
- URLs propres
- Title / Description / Canonical mis à jour par page
- Google indexe le contenu réel affiché au client
- Pas de changement côté mobile
Comme Google est désormais capable d’exécuter le JavaScript et de lire le contenu final du DOM, une application Flutter Web peut être réellement SEO-friendly, à condition que les balises <head> soient mises à jour proprement.
Note importante : Les bots de Facebook, Twitter, LinkedIn, WhatsApp, Slack, etc. ne rendent PAS le JavaScript : ils ne verront donc pas les balises Open Graph ou Twitter Card modifiées côté client.
Pour que le partage social affiche correctement l’image, le titre et la description, il faut une stratégie différente, basée sur du pré-rendu (prerender) ou une page HTML statique par URL.
Un article séparé sera dédié à cette partie “Social Share” pour expliquer comment générer des pages pré-rendues compatibles avec Facebook/Twitter/LinkedIn.

I really like what you guys are usuaally up too. Thiss sort of clever work and coverage!
Keep up the fantastic worfks guys I’ve added you guy to mmy own blogroll.
Thank you, Boyarka, it’s always really nice to get a positive comment