Flowcourier.Umbraco.AEO
17.0.0-alpha.2
dotnet add package Flowcourier.Umbraco.AEO --version 17.0.0-alpha.2
NuGet\Install-Package Flowcourier.Umbraco.AEO -Version 17.0.0-alpha.2
<PackageReference Include="Flowcourier.Umbraco.AEO" Version="17.0.0-alpha.2" />
<PackageVersion Include="Flowcourier.Umbraco.AEO" Version="17.0.0-alpha.2" />
<PackageReference Include="Flowcourier.Umbraco.AEO" />
paket add Flowcourier.Umbraco.AEO --version 17.0.0-alpha.2
#r "nuget: Flowcourier.Umbraco.AEO, 17.0.0-alpha.2"
#:package Flowcourier.Umbraco.AEO@17.0.0-alpha.2
#addin nuget:?package=Flowcourier.Umbraco.AEO&version=17.0.0-alpha.2&prerelease
#tool nuget:?package=Flowcourier.Umbraco.AEO&version=17.0.0-alpha.2&prerelease
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 withrel="canonical". - Requesting a normal page URL with
Accept: text/markdownreturns the Markdown representation directly (content negotiation, withVary: Accept). - All AEO responses carry
ETag/Last-Modifiedvalidators and answerIf-None-Match/If-Modified-Sincewith304 Not Modified, and supportHEAD.
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.txthttps://your-site/llms-full.txthttps://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 culturehttps://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.txtURLs support a single path segment (/es/,/de-at/). Host-based domains have no such limit..mdpages 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.txtfor 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.txtwhile 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 withRobots.Directiveif 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: Acceptto 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, setEnableContentNegotiation: false(which also stops theVaryemission) — the.mdURLs 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)
AeoComposerbinds options, registers the services, and installs anUmbracoPipelineFilterthat maps the two endpoints and inserts the.mdmiddleware (pre-routing) — this is what makes install zero-config.AeoSiteResolvermaps every request to the correct content root and culture using Umbraco's domain configuration (multi-site and multilingual safe), and seeds theVariationContextso content resolves in that culture.AeoPageResolvermaps a page path to published content within that scope — shared by the.mdsuffix handler andAccept: text/markdownnegotiation.AeoResponseHeadersMiddleware(post-routing) decorates rendered HTML content pages with the Markdown-alternateLinkheader andVary: Accept;AeoAlternateLinkTagHelperComponentadds the matching<link>in<head>.MarkdownConverterServiceturns a content node into Markdown by walking its properties generically.AeoContentServiceselects the indexable pages and assemblesllms.txt/llms-full.txt, caching per (site root, culture) withETag/Last-Modifiedvalidators;AeoCacheInvalidationHandlerclears those caches on publish.
License
MIT © Flowcourier ApS
| Product | Versions 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. |
-
net10.0
- Flowcourier.Umbraco.Foundation (>= 17.0.0-alpha.1)
- HtmlAgilityPack (>= 1.12.4)
- ReverseMarkdown (>= 5.4.0)
- Umbraco.Cms.Web.Website (>= 17.5.0)
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 |