Flowcourier.Umbraco.AEO 17.0.0-alpha.2

This is a prerelease version of Flowcourier.Umbraco.AEO.
dotnet add package Flowcourier.Umbraco.AEO --version 17.0.0-alpha.2
                    
NuGet\Install-Package Flowcourier.Umbraco.AEO -Version 17.0.0-alpha.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Flowcourier.Umbraco.AEO" Version="17.0.0-alpha.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Flowcourier.Umbraco.AEO" Version="17.0.0-alpha.2" />
                    
Directory.Packages.props
<PackageReference Include="Flowcourier.Umbraco.AEO" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Flowcourier.Umbraco.AEO --version 17.0.0-alpha.2
                    
#r "nuget: Flowcourier.Umbraco.AEO, 17.0.0-alpha.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Flowcourier.Umbraco.AEO@17.0.0-alpha.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Flowcourier.Umbraco.AEO&version=17.0.0-alpha.2&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Flowcourier.Umbraco.AEO&version=17.0.0-alpha.2&prerelease
                    
Install as a Cake Tool

Flowcourier.Umbraco.AEO

Answer Engine Optimization (AEO) for Umbraco. Make your Umbraco site readable by LLMs and AI answer engines (ChatGPT, Claude, Perplexity, Gemini, …) by serving your published content in the formats those tools understand — generated live from the content cache, with no editor work and no content changes required.

Install the package, and your site immediately gains three endpoints:

Endpoint What it returns
/llms.txt A curated index of your site — the llms.txt standard: title, summary, and a grouped list of links (one per page) pointing at each page's Markdown version.
/llms-full.txt The entire site rendered to Markdown in a single document — every indexable page, concatenated with source-URL markers. Ideal for feeding a whole site into a model's context or a RAG pipeline.
/{any-page-path}.md Any page as Markdown. Append .md to any URL (e.g. /products/widget.md) and get a clean Markdown rendering of that page instead of HTML.

Everything is derived from the live published-content cache, so the output always reflects what's currently published. Results are cached in memory for a short, configurable window — and refreshed immediately on publish.

The endpoints are also discoverable and crawler-friendly out of the box:

  • Every HTML content page advertises its Markdown version via a Link: <…/page.md>; rel="alternate"; type="text/markdown" response header and a <link rel="alternate" type="text/markdown"> tag in <head>; Markdown responses point back with rel="canonical".
  • Requesting a normal page URL with Accept: text/markdown returns the Markdown representation directly (content negotiation, with Vary: Accept).
  • All AEO responses carry ETag / Last-Modified validators and answer If-None-Match / If-Modified-Since with 304 Not Modified, and support HEAD.

Why AEO?

Search engines read HTML; answer engines prefer Markdown. llms.txt and llms-full.txt are emerging conventions that let AI crawlers discover and ingest your content efficiently — without wading through navigation, scripts, styling and tracking markup. This package produces all three artefacts automatically from your existing Umbraco content.


Installation

dotnet add package Flowcourier.Umbraco.AEO

That's it. The package self-registers through an Umbraco pipeline filter — no changes to Program.cs, no composer of your own, no route configuration. After the next build/run:

  • https://your-site/llms.txt
  • https://your-site/llms-full.txt
  • https://your-site/about-us.md (or any page URL + .md)

are all live.

Requirements: Umbraco 17 (net10.0). Works on single-site and multi-site installs (output is scoped to the site root that matches the request's hostname via your Umbraco domain configuration).

Multilingual sites

On culture-variant sites the output is generated per language, following the same domain configuration Umbraco uses to route your pages — both host-based (es.your-site/llms.txt) and path-prefix (your-site/es/llms.txt) setups:

  • https://your-site/llms.txt → default culture
  • https://your-site/es/llms.txt → Spanish (and /es/llms-full.txt, /es/about-us.md)

Titles, summaries, body content and the generated .md links all resolve in that culture, and a page is only listed for a language when it is actually published in it — so each language's llms.txt matches what Umbraco itself serves. Each culture is cached independently.

Culture-prefixed llms.txt / llms-full.txt URLs support a single path segment (/es/, /de-at/). Host-based domains have no such limit. .md pages work at any depth. (Multi-segment domain prefixes for the two text endpoints aren't routed.)

Discoverability: the package can advertise the AEO files in robots.txt for you — see robots.txt hint below. It's opt-in.


What gets included?

A page appears in /llms.txt and /llms-full.txt when it is:

  • Routable — it has a real, resolvable URL (element/folder nodes without a template are skipped automatically).
  • Not no-indexed — none of the configured no-index properties (default noIndex, hideFromSearch) are ticked.
  • Not excluded from listings — none of the configured exclude properties (default hideFromSitemap, excludeFromSitemap, umbracoNaviHide) are ticked.
  • Not an excluded document type — its doc-type alias isn't in the exclude list.

Pages whose doc-type is in OptionalDocumentTypeAliases (default: error, search, login, thank-you pages) are grouped under a ## Optional heading in llms.txt so small-context models can safely skip them.

Individual .md requests serve any published page directly, but return 404 for no-indexed pages and excluded document types.

How a page becomes Markdown

The converter walks each page's properties generically:

  • Rich text / grid HTML → Markdown (via ReverseMarkdown), after stripping scripts, iframes, style blocks, icon fonts and inline SVG.
  • Block List & Block Grid content → flattened recursively so nested blocks are included.
  • Media pickers → Markdown image links (video files are skipped). Relative URLs are made absolute (configurable).
  • Text / multi-value properties → paragraphs / bullet lists.
  • Layout, theme, SEO and system properties → skipped, so they don't pollute the output (see SkippedPropertyAliases).

The page title and description are resolved by trying a configurable list of property aliases (falling back to the node name), so it works with any content model without ModelsBuilder types.


Configuration

All settings are optional — the defaults work out of the box. Configure under the Flowcourier:Aeo section of appsettings.json:

{
  "Flowcourier": {
    "Aeo": {
      "Enabled": true,

      // Feature toggles
      "EnableLlmsTxt": true,
      "EnableLlmsFullTxt": true,
      "EnableMarkdownPages": true,

      // Route customization
      "LlmsTxtPath": "/llms.txt",
      "LlmsFullTxtPath": "/llms-full.txt",
      "MarkdownSuffix": ".md",

      // Discovery & HTTP behaviour
      "EnableAlternateLinkHeaders": true,
      "EnableAlternateHeadLink": true,
      "EnableContentNegotiation": true,
      "EnableConditionalRequests": true,
      "InvalidateCacheOnPublish": true,

      // robots.txt hint (opt-in)
      "Robots": {
        "Enabled": false,
        "Directive": "Llms",
        "IncludeFullTxt": true
      },

      // Behaviour
      "AbsoluteUrls": true,
      "LlmsTxtCacheSeconds": 60,
      "LlmsFullCacheSeconds": 600,

      // Content model mapping (which property aliases mean what)
      "TitlePropertyAliases": [ "seoTitle", "metaTitle", "pageTitle", "browserTitle", "title" ],
      "DescriptionPropertyAliases": [ "seoMetaDescription", "metaDescription", "description", "summary", "abstract" ],
      "NoIndexPropertyAliases": [ "noIndex", "hideFromSearch" ],
      "ExcludeFromListingPropertyAliases": [ "hideFromSitemap", "excludeFromSitemap", "umbracoNaviHide" ],

      // Document-type filtering
      "ExcludedDocumentTypeAliases": [],
      "OptionalDocumentTypeAliases": [ "errorPage", "error404", "searchPage", "loginPage", "thankYouPage" ],

      // Extra property aliases to keep out of Markdown bodies
      // (added on top of a built-in baseline of common system/SEO/layout aliases)
      "SkippedPropertyAliases": []
    }
  }
}

Settings reference

Setting Default Description
Enabled true Master switch. When false, all endpoints and the .md handler become no-ops.
EnableLlmsTxt true Serve /llms.txt.
EnableLlmsFullTxt true Serve /llms-full.txt.
EnableMarkdownPages true Serve pages as Markdown on the .md suffix.
LlmsTxtPath /llms.txt Route for the index.
LlmsFullTxtPath /llms-full.txt Route for the full dump.
MarkdownSuffix .md Suffix that triggers Markdown rendering.
EnableAlternateLinkHeaders true Emit Link: <….md>; rel="alternate"; type="text/markdown" on HTML content pages, and rel="canonical" / rel="alternate"; type="text/html" back-pointers on Markdown responses.
EnableAlternateHeadLink true Inject <link rel="alternate" type="text/markdown"> into <head> of rendered pages (no-op on hosts without the default MVC tag helpers — the Link header still provides discovery).
EnableContentNegotiation true Serve Markdown from the canonical page URL when the Accept header explicitly prefers text/markdown; emits Vary: Accept on both representations.
EnableConditionalRequests true Emit ETag/Last-Modified and answer If-None-Match/If-Modified-Since with 304.
InvalidateCacheOnPublish true Clear the cached llms.txt/llms-full.txt whenever the published-content cache changes (TTLs stay as a safety net).
Robots.Enabled false Serve /robots.txt with an AEO discovery line (see below).
Robots.Directive Llms Keyword used for the hint line.
Robots.IncludeFullTxt true Also emit a {Directive}-full: line for /llms-full.txt.
AbsoluteUrls true Rewrite relative media/link URLs to absolute using the request scheme + host.
LlmsTxtCacheSeconds 60 In-memory cache lifetime for the index (per site root).
LlmsFullCacheSeconds 600 In-memory cache lifetime for the full dump (per site root).
TitlePropertyAliases see above Property aliases checked in order for a page title; falls back to the node name.
DescriptionPropertyAliases see above Property aliases checked in order for a page description/summary.
NoIndexPropertyAliases noIndex, hideFromSearch Boolean aliases that mark a page as no-index (excluded everywhere; .md returns 404).
ExcludeFromListingPropertyAliases hideFromSitemap, excludeFromSitemap, umbracoNaviHide Boolean aliases that exclude a page from the listings (still reachable individually as .md).
ExcludedDocumentTypeAliases [] Doc-type aliases to exclude entirely from AEO.
OptionalDocumentTypeAliases error/search/login/thank-you Doc-type aliases grouped under ## Optional in llms.txt.
SkippedPropertyAliases [] Extra property aliases to keep out of Markdown bodies, added to the built-in baseline.

robots.txt hint

Set Robots.Enabled to true and the package serves /robots.txt with discovery lines for the AEO files:

Llms: https://your-site/llms.txt
Llms-full: https://your-site/llms-full.txt

User-agent: *
Allow: /
  • If you already have a physical wwwroot/robots.txt, its content is preserved — the hint lines are prepended and any previous copies of them are de-duplicated.
  • If you don't, the minimal permissive block above is generated.
  • The middleware runs before the static-file handler, so it wins over a physical robots.txt while keeping that file's rules intact.

Llms: is not an official robots.txt directive (crawlers ignore unknown lines), so it's a discovery pointer, not a crawl rule. Change the keyword with Robots.Directive if you prefer another convention.

Markdown discovery & content negotiation

Every rendered HTML content page is self-describing:

GET /products/widget
→ Link: <https://your-site/products/widget.md>; rel="alternate"; type="text/markdown"
→ Vary: Accept
→ …<head> contains <link rel="alternate" type="text/markdown" href="/products/widget.md">…

and the Markdown side points back:

GET /products/widget.md
→ Link: <https://your-site/products/widget>; rel="canonical"
→ Link: <https://your-site/products/widget>; rel="alternate"; type="text/html"

A client can also skip the .md suffix entirely and negotiate:

GET /products/widget
Accept: text/markdown
→ 200 text/markdown, Vary: Accept, Content-Location: /products/widget.md

Negotiation is deliberately conservative: only an explicit text/markdown token that outranks HTML triggers it — browser Accept headers (text/html,…,*/*;q=0.8) can never be hijacked, because wildcards don't count toward Markdown. Pages that are no-indexed or on an excluded document type never advertise and never negotiate.

CDN note: negotiation adds Vary: Accept to HTML content pages so shared caches keep the two representations apart. If you front the site with a CDN and would rather not fragment its HTML cache, set EnableContentNegotiation: false (which also stops the Vary emission) — the .md URLs keep working.

HTTP caching

All AEO responses (.md pages, /llms.txt, /llms-full.txt) carry a weak ETag and Last-Modified (from the content's culture-aware update dates), answer If-None-Match / If-Modified-Since with 304 Not Modified, and support HEAD — so AI crawlers that re-fetch aggressively can revalidate cheaply. On publish, the cached llms.txt / llms-full.txt are cleared immediately (InvalidateCacheOnPublish), with the TTLs (LlmsTxtCacheSeconds / LlmsFullCacheSeconds) as a safety net.


How it works

Request ─┬─ /llms.txt ───────────► AeoContentService.GetLlmsTxt()  ─┐
         ├─ /llms-full.txt ──────► AeoContentService.GetLlmsFullTxt()├─► live content cache
         └─ /*.md ───────────────► MarkdownPageMiddleware ───────────┘   (scoped to site root)
                                          │
                                   MarkdownConverterService (HTML/blocks → Markdown)
  • AeoComposer binds options, registers the services, and installs an UmbracoPipelineFilter that maps the two endpoints and inserts the .md middleware (pre-routing) — this is what makes install zero-config.
  • AeoSiteResolver maps every request to the correct content root and culture using Umbraco's domain configuration (multi-site and multilingual safe), and seeds the VariationContext so content resolves in that culture.
  • AeoPageResolver maps a page path to published content within that scope — shared by the .md suffix handler and Accept: text/markdown negotiation.
  • AeoResponseHeadersMiddleware (post-routing) decorates rendered HTML content pages with the Markdown-alternate Link header and Vary: Accept; AeoAlternateLinkTagHelperComponent adds the matching <link> in <head>.
  • MarkdownConverterService turns a content node into Markdown by walking its properties generically.
  • AeoContentService selects the indexable pages and assembles llms.txt / llms-full.txt, caching per (site root, culture) with ETag/Last-Modified validators; AeoCacheInvalidationHandler clears those caches on publish.

License

MIT © Flowcourier ApS

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
17.0.0-alpha.2 54 7/4/2026