About this project
esen_seo is a Flutter package that addresses a structural problem with Flutter Web: a Flutter app renders a widget tree rather than a document, so crawlers find no headings, paragraphs or links to read. The package mirrors that widget tree as semantic HTML directly in the DOM, manages meta tags, OpenGraph and Schema.org JSON-LD, and includes a shelf-based server-side rendering component that hands bots a complete HTML document in the page source. It is pure Dart and does not use Puppeteer or headless Chrome.
Usage is incremental rather than a rewrite: most widgets are mirrored automatically, `.seo()` extension calls add semantic meaning, and a set of library widgets cover cases the mirror cannot see (closed dropdowns, virtualized lists, inactive tabs, painted charts). On iOS, Android and desktop every call is a no-op and widgets render as before; the HTML exists only on the web.
Key capabilities described in the README:
- `.seo()` extensions for Text, Image, Column, Row and GestureDetector, with typed tag constants (SeoTextTag.h1, SeoContainerTag.section) and shorthands such as .h1–.h6, .p, .li, .ul, .section, .article, .nav, .tr.
- Smart defaults: pages without any .seo() calls still render, with the first text as h1, following texts as p, and images as img using semanticLabel as alt. Blocked or invalid tags fall back to safe elements.
- Custom translations via .seoNodes(), letting any widget declare its own HTML; the SEO widget library translates painted content (for example SeoBarChart mirrors as CSS bars plus a real table of its data).
- SeoRichText builds native Flutter TextSpans and nested strong, em, code and safe a elements from one declarative span tree.
- Meta tags, OpenGraph and Twitter Cards through a single EsenSeo.setMeta() call per page, with fallbacks such as og:title from title.
- Schema.org JSON-LD builders for Article, Product (including AggregateRating), Review, Event, LocalBusiness, Organization, WebSite, BreadcrumbList and FAQPage, plus a generic escape hatch.
- A bot-aware SSR server: shelf middleware detects crawlers by User-Agent and serves a real HTML document, runnable with `dart run`.
- URL routing as a single source of truth: a pure-Dart route table drives app meta tags on navigation and server-rendered route bodies for bots, and generates sitemap.xml (with lastmod and hreflang alternates), robots.txt, canonical URLs and real HTTP 404s.
- Static prerendering of the route table into the web build as static HTML files for CDN hosting without a server.
- Optional visible shell, where prerendered HTML is the first frame before the Flutter engine loads.
- Opt-in DOM-first routes that can keep a pure route body as the permanent page without loading Flutter Web, with SeoTabs, SeoCarousel and bounded SeoCollection interactions compiled from the same pure Dart source.
- llms.txt and llms-full.txt generated from the route table, plus IndexNow pings for faster indexing.
Safety policies are explicit: tags are an allow list, so script, style, iframe, form, plaintext, svg, head-only tags, custom elements and invalid names are refused at render time and fall back to span/div (with a debug warning in SeoMode.strict). Attribute handling drops event handlers and invalid names while allowing data-*, aria-*, id, lang and cite; URL attributes are restricted to relative URLs plus http, https, mailto, tel, sms and ftp, blocking javascript: and similar schemes.
The README also documents the SEO widget library: SeoBarChart, SeoPieChart (pure-CSS conic-gradient pie plus a table), SeoRating, SeoDataTable, SeoFaq (details/summary accordion), SeoBreadcrumbs, SeoFigure, SeoResponsiveImage (picture/srcset with AVIF/WebP sources), SeoTestimonial and SeoRichText. Five widgets address content Flutter never builds: SeoNavMenu (dropdown entries as data), SeoListView (all items mirrored despite lazy rendering), SeoCarousel, SeoTabs and SeoStepper. On non-web platforms all are no-ops rendering plain Flutter widgets.
Routing is defined once in a pure-Dart file without Flutter imports, shared by main.dart and the server. SeoRoute.dynamic resolves metadata and body from a single database read, with enumeratePaths for sitemap, llms.txt and prerender. A SeoRouteObserver keeps the mirror and meta tags following navigation, and works with go_router, beamer, auto_route or other Router-based packages. The README notes that the live DOM mirror is derived from the widget tree and does not require a second authored content tree, while server-rendered route bodies are separate unless both derive from a shared pure data model; auditSeoParity is provided to catch drift.
The README states the mirror is hidden by default (aria-hidden, zero size) and is explicitly not an accessibility feature, since Flutter publishes its own semantics tree.
Comments
0 Rating appears after 10 ratings
Sign in to join the discussion.