seoSitemapXml function
Generates a sitemap.xml from the SEO route table.
Pass exactly one of routes and pages:
routesis the classic path — the table is resolved synchronously. It throws a StateError when the table contains a SeoRoute.dynamic, because a database read cannot happen without awaiting; the message names the fix.pagesis a pre-resolved snapshot fromresolveSeoPages, which works for every table. PassingadditionalPathstogether withpagesis an ArgumentError — they were already folded into the pass.seoBotMiddlewareandprerenderSitetake this path for you.
Includes every indexable URL: a route without :param segments, plus
any concrete instances from a route's enumeratePaths or from
additionalPaths. A page that resolves to a redirect, a non-200, or
opts out via includeInSitemap is dropped.
Per URL the entry carries <lastmod> (a per-record date beats the
route's static one) and <xhtml:link rel="alternate" hreflang="…"/>
for every language variant in the page's SeoMeta.alternates.
Implementation
String seoSitemapXml({
required String siteBase,
List<SeoRoute>? routes,
List<SeoResolvedPage>? pages,
List<String> additionalPaths = const [],
}) {
final resolved = pagesForGenerator(
routes: routes,
pages: pages,
additionalPaths: additionalPaths,
canonicalBase: siteBase,
);
final base = siteBase.endsWith('/')
? siteBase.substring(0, siteBase.length - 1)
: siteBase;
final entries = [
for (final page in resolved)
if (page.isIndexable) page,
];
final hasAlternates =
entries.any((e) => e.document!.meta.alternates.isNotEmpty);
final buffer = StringBuffer()
..writeln('<?xml version="1.0" encoding="UTF-8"?>')
..write('<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"')
..writeln(
hasAlternates ? ' xmlns:xhtml="http://www.w3.org/1999/xhtml">' : '>');
for (final entry in entries) {
final url = entry.path == '/' ? '$base/' : '$base${entry.path}';
final lastmod = entry.lastModified;
final alternates = entry.document!.meta.alternates;
if (lastmod == null && alternates.isEmpty) {
buffer.writeln(' <url><loc>${_xmlSafe(url)}</loc></url>');
continue;
}
buffer
..writeln(' <url>')
..writeln(' <loc>${_xmlSafe(url)}</loc>');
if (lastmod != null) {
buffer.writeln(' <lastmod>${_lastmodDate(lastmod)}</lastmod>');
}
alternates.forEach((hreflang, href) {
// The same allow list the head renderer applies: a URL SeoMeta
// refuses to emit has no business being advertised in
// sitemap.xml either. Before this, the sitemap was the one
// output path around the policy choke point — a javascript:
// alternate from CMS data shipped verbatim.
if (!isAllowedSeoAttribute('href', href)) return;
buffer
..write(' <xhtml:link rel="alternate" hreflang="')
..write(_xmlSafeAttr(hreflang))
..write('" href="')
..write(_xmlSafeAttr(href))
..writeln('"/>');
});
buffer.writeln(' </url>');
}
buffer.write('</urlset>');
return buffer.toString();
}