seoLlmsFullTxt function

Future<String> seoLlmsFullTxt({
  1. required String siteBase,
  2. List<SeoRoute>? routes,
  3. List<SeoResolvedPage>? pages,
  4. String? title,
  5. String? description,
  6. List<String> additionalPaths = const [],
  7. int concurrency = 8,
})

Generates an llms-full.txt: like seoLlmsTxt, but with the complete page content inlined as markdown — AI assistants get the whole site in one request instead of crawling page by page.

The content comes from each page's resolved body (the same SeoNode trees the SSR server and prerenderer use), converted to markdown: headings, paragraphs, lists, links, images and blockquotes. Pages with no body list their metadata only.

Pass exactly one of routes and pages. Unlike the sync generators this never throws on a dynamic table — it is already async and resolves internally when given routes. Meta and body of each page come from a single resolution, so they cannot describe different records.

Implementation

Future<String> seoLlmsFullTxt({
  required String siteBase,
  List<SeoRoute>? routes,
  List<SeoResolvedPage>? pages,
  String? title,
  String? description,
  List<String> additionalPaths = const [],
  int concurrency = 8,
}) async {
  if ((routes == null) == (pages == null)) {
    throw ArgumentError('Pass exactly one of `routes:` or `pages:`.');
  }
  if (pages != null && additionalPaths.isNotEmpty) {
    throw ArgumentError(
      '`additionalPaths` cannot be combined with `pages:`.',
    );
  }
  final base = siteBase.endsWith('/')
      ? siteBase.substring(0, siteBase.length - 1)
      : siteBase;
  final resolved = pages ??
      await resolveSeoPages(
        routes: routes!,
        canonicalBase: siteBase,
        additionalPaths: additionalPaths,
        detail: SeoDetail.full,
        concurrency: concurrency,
      );

  final rootMeta = _rootMeta(resolved);
  final siteTitle = title ?? rootMeta?.title ?? Uri.parse(base).host;
  final siteDescription = description ?? rootMeta?.description;

  final buffer = StringBuffer()..writeln('# ${_singleLine(siteTitle)}');
  if (siteDescription != null && siteDescription.isNotEmpty) {
    buffer.writeln('> ${_singleLine(siteDescription)}');
  }

  for (final page in resolved) {
    if (!page.isIndexable) continue;
    final doc = page.document!;
    final meta = doc.meta;
    final url = page.path == '/' ? '$base/' : '$base${page.path}';
    buffer
      ..writeln()
      ..writeln('## ${_singleLine(meta.title ?? page.path)}')
      ..writeln(url);
    final pageDescription = meta.description;
    if (pageDescription != null && pageDescription.isNotEmpty) {
      buffer.writeln('> ${_singleLine(pageDescription)}');
    }
    final markdown = _nodesToMarkdown(doc.body, base);
    if (markdown.isNotEmpty) {
      buffer
        ..writeln()
        ..writeln(markdown);
    }
  }
  return buffer.toString();
}