diff --git a/config.toml b/config.toml
index b169f8434..258f61702 100644
--- a/config.toml
+++ b/config.toml
@@ -5,6 +5,7 @@ title = "Kubermatic Docs"
disableKinds = ["taxonomy"]
ignoreErrors = ["error-disable-taxonomy"]
enableRobotsTXT = true
+enableGitInfo = true
[outputFormats]
[outputFormats.Search]
@@ -17,10 +18,26 @@ enableRobotsTXT = true
mediaType = "application/octet-stream"
isPlainText = true
notAlternative = true
+ [outputFormats.LLMsTxt]
+ baseName = "llms"
+ mediaType = "text/plain"
+ isPlainText = true
+ notAlternative = true
+ [outputFormats.LLMsFullTxt]
+ baseName = "llms-full"
+ mediaType = "text/plain"
+ isPlainText = true
+ notAlternative = true
+ [outputFormats.Markdown]
+ baseName = "index"
+ mediaType = "text/markdown"
+ isPlainText = true
+ notAlternative = true
[outputs]
-home = ["HTML", "Search", "Redirects"]
-section = ["HTML"]
+home = ["HTML", "Search", "Redirects", "LLMsTxt", "Markdown"]
+section = ["HTML", "LLMsTxt", "LLMsFullTxt", "Markdown"]
+page = ["HTML", "Markdown"]
[markup.goldmark.renderer]
unsafe = true
diff --git a/content/developer-platform/_index.en.md b/content/developer-platform/_index.en.md
index 72cf47939..b61b2d13a 100644
--- a/content/developer-platform/_index.en.md
+++ b/content/developer-platform/_index.en.md
@@ -1,6 +1,5 @@
+++
title = "Kubermatic Developer Platform"
-sitemapexclude = true
+++
KDP (Kubermatic Developer Platform) is a new Kubermatic product in development that targets the IDP
diff --git a/layouts/_default/home.llmstxt.txt b/layouts/_default/home.llmstxt.txt
new file mode 100644
index 000000000..2f24f2e2d
--- /dev/null
+++ b/layouts/_default/home.llmstxt.txt
@@ -0,0 +1,29 @@
+# {{ .Site.Title }}
+
+> {{ .Site.Params.description }}
+
+> Last updated: {{ now.Format "2006-01-02" }}
+> Products: {{ len (where (sort hugo.Data.products ".weight") "name" "!=" "") }}
+
+## Products
+{{ range (sort hugo.Data.products ".weight") }}
+{{- $key := urlize .name -}}
+{{- $values := . -}}
+{{- $url := "" -}}
+{{- if .versions -}}
+ {{- $url = (cond (gt (len .versions) 1) (index .versions 1) (index .versions 0)).release -}}
+{{- end -}}
+{{- $pagePath := cond (ne $url "") (printf "/%s/%s" $key $url) (printf "/%s" $key) -}}
+{{- with $.Site.GetPage $pagePath -}}
+ {{- if not (partial "llms/is-excluded.txt" .) -}}
+ {{- $llmsURL := "" -}}
+ {{- with .OutputFormats.Get "LLMsTxt" -}}
+ {{- $llmsURL = .Permalink -}}
+ {{- end -}}
+ {{- if $llmsURL -}}
+ {{- $title := $values.title | default $values.name }}
+- [{{ $title }}]({{ $llmsURL }}): {{ $values.description }}
+{{ end -}}
+{{- end -}}
+{{- end -}}
+{{- end }}
diff --git a/layouts/_default/home.markdown.md b/layouts/_default/home.markdown.md
new file mode 100644
index 000000000..61e2adece
--- /dev/null
+++ b/layouts/_default/home.markdown.md
@@ -0,0 +1,28 @@
+{{- if not .Params.sitemapexclude -}}
+---
+title: {{ .Title | plainify }}
+url: {{ .Permalink }}
+---
+
+# {{ .Title | plainify }}
+{{ with .Description | default .Params.description }}
+> {{ . }}
+{{ end }}
+## Products
+{{ range sort hugo.Data.products "weight" }}
+{{- if not .sitemapexclude -}}
+{{- $key := .name | urlize -}}
+{{- $version := "" -}}
+{{- if .versions -}}
+ {{- $latest := cond (gt (len .versions) 1) (index .versions 1) (index .versions 0) -}}
+ {{- $version = $latest.release -}}
+{{- end -}}
+### {{ .title }}
+
+{{ .description }}
+{{ if $version -}}
+Documentation: [/{{ $key }}/{{ $version }}/](/{{ $key }}/{{ $version }}/)
+{{ end }}
+{{ end -}}
+{{- end -}}
+{{- end -}}
diff --git a/layouts/_default/home.redirects b/layouts/_default/home.redirects
index 7c6328f92..043b4d3d7 100644
--- a/layouts/_default/home.redirects
+++ b/layouts/_default/home.redirects
@@ -7,6 +7,9 @@ https://docs.kubermatic.io/* https://docs.kubermatic.com/:splat 301!
# Optional: Redirect default Netlify subdomain to primary domain
https://cranky-newton-4e6ed2.netlify.com/* https://docs.kubermatic.com/:splat 301!
+# Serve /llms.txt at the well-known discovery path for LLM agent frameworks
+/.well-known/llms.txt /llms.txt 200!
+
/kubermatic/master/cheat_sheets/alerting_runbook/* /kubermatic/main/cheat-sheets/alerting-runbook/:splat 301!
/kubermatic/master/tutorials_howtos/oidc_provider_configuration/share-_clusters_via_delegated_oidc_authentication /kubermatic/main/tutorials-howtos/oidc-provider-configuration/share-clusters-via-delegated-oidc-authentication/ 301!
/kubermatic/master/installation/start_kkp /kubermatic/main/installation/start-kkp/ 301!
diff --git a/layouts/_default/section.llmsfulltxt.txt b/layouts/_default/section.llmsfulltxt.txt
new file mode 100644
index 000000000..b70daafe7
--- /dev/null
+++ b/layouts/_default/section.llmsfulltxt.txt
@@ -0,0 +1,8 @@
+{{- if not (partial "llms/is-excluded.txt" .) -}}
+# {{ .Title | default .Name | plainify }}
+{{ with .Description | default .Params.description }}
+> {{ . }}
+{{ end }}
+{{ partial "llms/render-content.txt" . }}
+{{ partial "llms/full-content.txt" . }}
+{{- end -}}
diff --git a/layouts/_default/section.llmstxt.txt b/layouts/_default/section.llmstxt.txt
new file mode 100644
index 000000000..51a310b85
--- /dev/null
+++ b/layouts/_default/section.llmstxt.txt
@@ -0,0 +1,27 @@
+{{- if not (partial "llms/is-excluded.txt" .) -}}
+{{- $title := .Title | default .Name | plainify -}}
+{{- $pageCount := partial "llms/page-count.txt" . -}}
+{{- $segments := split (strings.TrimPrefix "/" (strings.TrimSuffix "/" .RelPermalink)) "/" -}}
+{{- if gt $pageCount 0 -}}
+# {{ $title }}
+{{ with .Description | default .Params.description }}
+> {{ . }}
+{{ end }}
+> Last updated: {{ .Lastmod.Format "2006-01-02" }}
+{{- if ge (len $segments) 2 }}
+> Version: {{ index $segments 1 }}
+{{- end }}
+> Pages: {{ $pageCount }}
+{{- $llmsFullURL := "" -}}
+{{- with .OutputFormats.Get "LLMsFullTxt" -}}
+ {{- $llmsFullURL = .Permalink -}}
+{{- end -}}
+{{- with $llmsFullURL }}
+## About this section
+
+- [{{ $title }} — Full Content]({{ . }})
+{{ end }}
+## Pages
+{{ partial "llms/list-pages.txt" . }}
+{{- end -}}
+{{- end -}}
diff --git a/layouts/_default/section.markdown.md b/layouts/_default/section.markdown.md
new file mode 100644
index 000000000..856fceab0
--- /dev/null
+++ b/layouts/_default/section.markdown.md
@@ -0,0 +1,12 @@
+{{- if not (partial "llms/is-excluded.txt" .) -}}
+---
+title: {{ .Title | plainify }}
+url: {{ .Permalink }}
+---
+
+# {{ .Title | plainify }}
+{{ with .Description | default .Params.description }}
+> {{ . }}
+{{ end }}
+{{ partial "llms/render-content.txt" . }}
+{{- end -}}
diff --git a/layouts/_default/single.markdown.md b/layouts/_default/single.markdown.md
new file mode 100644
index 000000000..856fceab0
--- /dev/null
+++ b/layouts/_default/single.markdown.md
@@ -0,0 +1,12 @@
+{{- if not (partial "llms/is-excluded.txt" .) -}}
+---
+title: {{ .Title | plainify }}
+url: {{ .Permalink }}
+---
+
+# {{ .Title | plainify }}
+{{ with .Description | default .Params.description }}
+> {{ . }}
+{{ end }}
+{{ partial "llms/render-content.txt" . }}
+{{- end -}}
diff --git a/layouts/partials/llms/full-content.txt b/layouts/partials/llms/full-content.txt
new file mode 100644
index 000000000..733964a93
--- /dev/null
+++ b/layouts/partials/llms/full-content.txt
@@ -0,0 +1,21 @@
+{{- /* Recursive partial: outputs full content of all descendant pages.
+ Context: a page (section or single).
+ Skips excluded pages and pages without content.
+ Outputs each page as a markdown section with heading + content.
+ Uses .RawContent via render-content.txt partial. */ -}}
+{{- range .Pages -}}
+ {{- if not (partial "llms/is-excluded.txt" .) -}}
+ {{- if .RawContent -}}
+ {{- with .Title }}
+
+---
+
+## {{ . | plainify }}
+{{ end -}}
+{{ partial "llms/render-content.txt" . }}
+{{ end -}}
+ {{- if .IsSection -}}
+ {{- partial "llms/full-content.txt" . -}}
+ {{- end -}}
+ {{- end -}}
+{{- end -}}
diff --git a/layouts/partials/llms/is-excluded.txt b/layouts/partials/llms/is-excluded.txt
new file mode 100644
index 000000000..5816be304
--- /dev/null
+++ b/layouts/partials/llms/is-excluded.txt
@@ -0,0 +1,22 @@
+{{- /* Returns true if a page should be excluded from llms.txt outputs.
+ Excluded when:
+ - .Draft is true, OR
+ - .Params.sitemapexclude is true, OR
+ - The page does not belong to the most recent product version
+ (2nd in the versions list from products.yaml, or 1st if only one). */ -}}
+{{- $excluded := or .Draft .Params.sitemapexclude -}}
+{{- if not $excluded -}}
+ {{- with index hugo.Data.products .Section -}}
+ {{- if .versions -}}
+ {{- $latestVersion := cond (gt (len .versions) 1) (index .versions 1).release (index .versions 0).release -}}
+ {{- $pathParts := split $.RelPermalink "/" -}}
+ {{- if gt (len $pathParts) 2 -}}
+ {{- $pageVersion := index $pathParts 2 -}}
+ {{- if ne $pageVersion $latestVersion -}}
+ {{- $excluded = true -}}
+ {{- end -}}
+ {{- end -}}
+ {{- end -}}
+ {{- end -}}
+{{- end -}}
+{{- return $excluded -}}
diff --git a/layouts/partials/llms/list-pages.txt b/layouts/partials/llms/list-pages.txt
new file mode 100644
index 000000000..8b42117b4
--- /dev/null
+++ b/layouts/partials/llms/list-pages.txt
@@ -0,0 +1,39 @@
+{{- /* Hierarchical partial: list only direct children of the current section.
+ Context: a page (section).
+ Skips excluded pages (see llms/is-excluded partial).
+ Skips pages without main content.
+ For child sections with >10 pages, links to their llms.txt (navigate deeper).
+ For child sections with ≤10 pages, links to their llms-full.txt (content directly).
+ For single pages, lists title and description as plain text. */ -}}
+{{- range .Pages -}}
+ {{- if not (partial "llms/is-excluded.txt" .) -}}
+ {{- if .RawContent -}}
+ {{- if .IsSection -}}
+ {{- $childCount := partial "llms/page-count.txt" . -}}
+ {{- $desc := .Description | default .Params.description | default "" -}}
+ {{- if gt $childCount 10 -}}
+ {{- $llmsTxtURL := "" -}}
+ {{- with .OutputFormats.Get "LLMsTxt" -}}
+ {{- $llmsTxtURL = .Permalink -}}
+ {{- end -}}
+ {{- if and .Title $llmsTxtURL -}}
+- [{{ .Title | plainify }}]({{ $llmsTxtURL }}){{ with $desc }}: {{ . }}{{ end }}
+{{ end -}}
+ {{- else -}}
+ {{- $llmsFullURL := "" -}}
+ {{- with .OutputFormats.Get "LLMsFullTxt" -}}
+ {{- $llmsFullURL = .Permalink -}}
+ {{- end -}}
+ {{- if and .Title $llmsFullURL -}}
+- [{{ .Title | plainify }}]({{ $llmsFullURL }}){{ with $desc }}: {{ . }}{{ end }}
+{{ end -}}
+ {{- end -}}
+ {{- else -}}
+ {{- if .Title -}}
+ {{- $desc := .Description | default .Params.description | default "" -}}
+- {{ .Title | plainify }}{{ with $desc }}: {{ . }}{{ end }}
+{{ end -}}
+ {{- end -}}
+ {{- end -}}
+ {{- end -}}
+{{- end -}}
diff --git a/layouts/partials/llms/page-count.txt b/layouts/partials/llms/page-count.txt
new file mode 100644
index 000000000..83e4695e3
--- /dev/null
+++ b/layouts/partials/llms/page-count.txt
@@ -0,0 +1,14 @@
+{{- /* Returns the count of non-excluded pages with content under a section.
+ Context: a section page. */ -}}
+{{- $count := 0 -}}
+{{- range .Pages -}}
+ {{- if not (partial "llms/is-excluded.txt" .) -}}
+ {{- if .RawContent -}}
+ {{- $count = add $count 1 -}}
+ {{- end -}}
+ {{- if .IsSection -}}
+ {{- $count = add $count (partial "llms/page-count.txt" .) -}}
+ {{- end -}}
+ {{- end -}}
+{{- end -}}
+{{- return $count -}}
diff --git a/layouts/partials/llms/render-content.txt b/layouts/partials/llms/render-content.txt
new file mode 100644
index 000000000..0a991051e
--- /dev/null
+++ b/layouts/partials/llms/render-content.txt
@@ -0,0 +1,126 @@
+{{- /* Hybrid content renderer for LLM/MD output.
+ Processes .RawContent with regex transformations to preserve native
+ markdown formatting (fenced code blocks, headings, links, etc.).
+ External content (render_external_markdown/code) is fetched at build
+ time via resources.GetRemote and inlined directly.
+ Context: a single page.
+ Outputs content directly (no return value). */ -}}
+
+{{- $s := .RawContent -}}
+
+{{- /* 1. Strip children shortcode — navigation only, no real content */ -}}
+{{- $s = replaceRE `\{\{[%<] *children[^}]*[%>]\}\}` "" $s -}}
+
+{{- /* 2. Inline readfile shortcodes — output raw file content.
+ The shortcode is typically placed inside a fenced code block in the
+ source markdown, so no extra wrapping is added here. */ -}}
+{{- range findRE `\{\{< *readfile "[^"]+"[^>]*>\}\}` $s -}}
+ {{- $match := . -}}
+ {{- $path := replaceRE `\{\{< *readfile "([^"]+)"[^>]*>\}\}` "$1" $match -}}
+ {{- with readFile $path -}}
+ {{- $s = replace $s $match (. | strings.TrimRight "\n") -}}
+ {{- else -}}
+ {{- $s = replace $s $match "" -}}
+ {{- end -}}
+{{- end -}}
+
+{{- /* 3. Inline render_external_markdown / render_external_code: fetch the remote
+ resource and embed its content directly. Both shortcodes produce raw
+ content (markdown or code) that is already wrapped appropriately by
+ the surrounding source markdown, so no extra wrapping is added. */ -}}
+{{- range findRE `\{\{< *render_external(?:_markdown|_code) "[^"]+"[^>]*>\}\}` $s -}}
+ {{- $match := . -}}
+ {{- $url := replaceRE `\{\{< *render_external(?:_markdown|_code) "([^"]+)"[^>]*>\}\}` "$1" $match -}}
+ {{- with resources.GetRemote $url (dict "headers" (dict "Cache-Control" "no-cache")) -}}
+ {{- $s = replace $s $match (.Content | strings.TrimRight "\n") -}}
+ {{- else -}}
+ {{- warnf "llms/render-content: failed to fetch %s" $url -}}
+ {{- $s = replace $s $match "" -}}
+ {{- end -}}
+{{- end -}}
+
+{{- /* 4. Convert notice shortcodes to labelled blockquotes.
+ Types: note, warning, tip, info */ -}}
+{{- $s = replaceRE `(?s)\{\{% notice (\w+)[^%]*%\}\}\s*(.*?)\s*\{\{% /notice %\}\}` "\n> **$1:** $2\n" $s -}}
+
+{{- /* 5. Strip tabs wrapper; convert tab names to level-4 headings */ -}}
+{{- $s = replaceRE `\{\{< *tabs [^>]*>\}\}` "" $s -}}
+{{- $s = replaceRE `\{\{< */tabs *>\}\}` "" $s -}}
+{{- $s = replaceRE `\{\{% tab name="([^"]+)"[^%]*%\}\}` "\n#### $1\n\n" $s -}}
+{{- $s = replaceRE `\{\{% /tab *%\}\}` "" $s -}}
+
+{{- /* 6. Convert details shortcodes to heading + content */ -}}
+{{- $s = replaceRE `\{\{< *details summary="([^"]+)"[^>]*>\}\}` "\n#### $1\n\n" $s -}}
+{{- $s = replaceRE `\{\{< */details *>\}\}` "" $s -}}
+
+{{- /* 7. Resolve ref/relref in links: [text]({{< ref "path" >}}) → [text](path)
+ Also handles malformed shortcodes with escaped quotes: {{< ref \"path\" >}} */ -}}
+{{- $s = replaceRE `\{\{< *(?:rel)?ref \\?"([^"\\]+)\\?"[^>]*>\}\}` "$1" $s -}}
+
+{{- /* 8. Resolve relative markdown links to absolute URLs.
+ After step 7, ref/relref shortcodes are plain relative paths (e.g. ../../tutorials/kkp).
+ Plain relative links (e.g. ../../installation/) are also still relative at this point.
+ AI parsers cannot resolve relative paths without knowing the base URL, so we convert
+ them to absolute URLs here using manual path resolution (not Hugo's relref) to avoid
+ REF_NOT_FOUND errors for links pointing to non-page resources (images, PDFs, etc.).
+ Two link forms are handled:
+ a) Inline links: [text](../path)
+ b) Reference definitions: [id]: ../path */ -}}
+{{- $siteBase := strings.TrimSuffix "/" .Site.BaseURL -}}
+{{- $baseParts := slice -}}
+{{- range split (strings.TrimSuffix "/" .RelPermalink) "/" -}}
+ {{- if ne . "" -}}
+ {{- $baseParts = $baseParts | append . -}}
+ {{- end -}}
+{{- end -}}
+{{- /* 8a. Inline links: [text](../path) */ -}}
+{{- range findRE `\[([^\]]*)\]\((\.\.?/[^)]*)\)` $s -}}
+ {{- $fullMatch := . -}}
+ {{- $linkText := replaceRE `\[([^\]]*)\]\((\.\.?/[^)]*)\)` "$1" . -}}
+ {{- $href := replaceRE `\[([^\]]*)\]\((\.\.?/[^)]*)\)` "$2" . -}}
+ {{- $anchor := replaceRE `^[^#]*` "" $href -}}
+ {{- $href = replaceRE `#.*$` "" $href -}}
+ {{- $trailingSlash := "" -}}
+ {{- if strings.HasSuffix $href "/" -}}{{- $trailingSlash = "/" -}}{{- end -}}
+ {{- $parts := slice -}}
+ {{- range $baseParts -}}{{- $parts = $parts | append . -}}{{- end -}}
+ {{- range split $href "/" -}}
+ {{- if eq . ".." -}}
+ {{- if gt (len $parts) 0 -}}{{- $parts = first (sub (len $parts) 1) $parts -}}{{- end -}}
+ {{- else if and (ne . ".") (ne . "") -}}
+ {{- $parts = $parts | append . -}}
+ {{- end -}}
+ {{- end -}}
+ {{- $s = replace $s $fullMatch (printf "[%s](%s/%s%s%s)" $linkText $siteBase (delimit $parts "/") $trailingSlash $anchor) -}}
+{{- end -}}
+{{- /* 8b. Reference definitions: [id]: ../path */ -}}
+{{- range findRE `\[[^\]]+\]: *(\.\.?/[^\s]+)` $s -}}
+ {{- $fullMatch := . -}}
+ {{- $id := replaceRE `\[([^\]]+)\]: *(\.\.?/[^\s]+)` "$1" . -}}
+ {{- $href := replaceRE `\[([^\]]+)\]: *(\.\.?/[^\s]+)` "$2" . -}}
+ {{- $anchor := replaceRE `^[^#]*` "" $href -}}
+ {{- $href = replaceRE `#.*$` "" $href -}}
+ {{- $trailingSlash := "" -}}
+ {{- if strings.HasSuffix $href "/" -}}{{- $trailingSlash = "/" -}}{{- end -}}
+ {{- $parts := slice -}}
+ {{- range $baseParts -}}{{- $parts = $parts | append . -}}{{- end -}}
+ {{- range split $href "/" -}}
+ {{- if eq . ".." -}}
+ {{- if gt (len $parts) 0 -}}{{- $parts = first (sub (len $parts) 1) $parts -}}{{- end -}}
+ {{- else if and (ne . ".") (ne . "") -}}
+ {{- $parts = $parts | append . -}}
+ {{- end -}}
+ {{- end -}}
+ {{- $s = replace $s $fullMatch (printf "[%s]: %s/%s%s%s" $id $siteBase (delimit $parts "/") $trailingSlash $anchor) -}}
+{{- end -}}
+
+{{- /* 9. Replace current_version with actual version string from URL */ -}}
+{{- $pathParts := split .RelPermalink "/" -}}
+{{- if gt (len $pathParts) 2 -}}
+ {{- $s = replace $s "{{< current_version >}}" (index $pathParts 2) -}}
+{{- end -}}
+
+{{- /* 10. Strip any remaining unhandled shortcode tags (self-closing and wrappers) */ -}}
+{{- $s = replaceRE `\{\{[<%][^}]*[%>]\}\}` "" $s -}}
+
+{{ $s | safeHTML }}
diff --git a/layouts/partials/meta.html b/layouts/partials/meta.html
new file mode 100644
index 000000000..e0f3ae36e
--- /dev/null
+++ b/layouts/partials/meta.html
@@ -0,0 +1,42 @@
+{{$trimTagsRegex := "(h1|h2|h3|h4|h5|h6|ul|ol|pre|table)"}}
+{{$regexPattern := printf "<%[1]s.*?>(.|\n)*?%[1]s>" $trimTagsRegex}}
+{{$htmlContent := strings.ReplaceRE $regexPattern "" (.Content | safeHTML)}}
+{{$autoDescription := trim (truncate 160 "" (replace ($htmlContent | htmlUnescape | plainify) "\n" " ")) " " | chomp}}
+{{$description := replace $autoDescription "&" "and"}}
+
+{{if $description}}
+ {{$sentences := split $description ". "}}
+ {{range $index, $val := $sentences}}
+ {{$delimiter := ""}}
+
+ {{if lt $index (sub (len $sentences) 2)}}
+ {{$delimiter = ". "}}
+ {{end}}
+
+ {{if eq $index 0}}
+ {{$description = print . $delimiter}}
+ {{else if lt $index (sub (len $sentences) 1)}}
+ {{$description = print $description . $delimiter}}
+ {{end}}
+ {{end}}
+{{end}}
+
+{{with .Description}}
+ {{$description = .}}
+{{end}}
+
+{{with .Site.Params.author}}{{end}}
+
+
+
+{{$product := index hugo.Data.products .Section | default (dict "shareImage" nil)}}
+{{$shareImage := or .Params.shareImage $product.shareImage | default .Site.Params.shareImage}}
+{{with $shareImage}}
+
+
+{{end}}
+
+{{- /* LLMs.txt discoverability */ -}}
+{{- with .OutputFormats.Get "LLMsTxt" }}
+
+{{- end }}
diff --git a/layouts/robots.txt b/layouts/robots.txt
index 0af428494..b751ed448 100644
--- a/layouts/robots.txt
+++ b/layouts/robots.txt
@@ -1,10 +1,13 @@
+{{- /* This is a comment, that is not used in production
# We allows Google to crawl website. It should crawl the pages from custom sitemap.xml.
# sitemap.xml updates every new release and keep the pages for stable version of product.
# The previous versions should be removed from Google index on every new release for product.
# These pages are marked with "noindex" rule in the "robots" meta tag on build time and will be deleted from Google after some time.
# We dont need to disallow crawling for these pages, because Google should have opportunity to analyze previous pages (with "noindex" tag) and update the state of index for those.
-
+*/ -}}
User-agent: *
Disallow:
+Content-Signal: ai-train=yes, search=yes, ai-input=yes
Sitemap: {{"sitemap.xml" | absURL}}
+Llms-txt: {{"llms.txt" | absURL}}
diff --git a/layouts/shortcodes/render_external_markdown.html b/layouts/shortcodes/render_external_markdown.html
index ff1914427..ee02e0987 100644
--- a/layouts/shortcodes/render_external_markdown.html
+++ b/layouts/shortcodes/render_external_markdown.html
@@ -2,5 +2,5 @@
{{- with resources.GetRemote $urlToRender (dict "headers" (dict "Cache-Control" "no-cache")) -}}
{{- $.Page.RenderString .Content -}}
{{- else -}}
- {{- errorf "Unable to get remote resource %q" (.Get 0) -}}
+ {{- warnf "Unable to get remote resource %q" (.Get 0) -}}
{{- end -}}
diff --git a/netlify.toml b/netlify.toml
index 8f06136b2..0fec45373 100644
--- a/netlify.toml
+++ b/netlify.toml
@@ -12,3 +12,8 @@ directory = "functions"
[functions."deploy-succeeded"]
included_files = ["public/search.json"]
+
+[[headers]]
+ for = "/"
+ [headers.values]
+ Link = '''; rel="llms-txt", ; rel="sitemap", ; rel="search"'''
diff --git a/netlify/edge-functions/markdown-for-agents.js b/netlify/edge-functions/markdown-for-agents.js
new file mode 100644
index 000000000..84e232135
--- /dev/null
+++ b/netlify/edge-functions/markdown-for-agents.js
@@ -0,0 +1,55 @@
+// Netlify Edge Function: Markdown for Agents
+//
+// When a request includes "Accept: text/markdown", rewrites to the
+// pre-generated index.md that Hugo places alongside each index.html.
+// Returns Content-Type: text/markdown and x-markdown-tokens as per
+// the Cloudflare Markdown for Agents spec.
+
+export default async (request, context) => {
+ const accept = request.headers.get("accept") ?? "";
+ if (!accept.includes("text/markdown")) {
+ return;
+ }
+
+ const url = new URL(request.url);
+ let path = url.pathname;
+ if (!path.endsWith("/")) {
+ path += "/";
+ }
+ url.pathname = path + "index.md";
+
+ const response = await context.rewrite(url);
+ if (!response || !response.ok) {
+ return;
+ }
+
+ const body = await response.text();
+ if (!body.trim()) {
+ return;
+ }
+ const headers = new Headers(response.headers);
+ headers.set("content-type", "text/markdown; charset=utf-8");
+ headers.set("vary", "Accept");
+ // Approximate token count (1 token ≈ 4 chars)
+ headers.set("x-markdown-tokens", String(Math.ceil(body.length / 4)));
+
+ return new Response(body, { status: 200, headers });
+};
+
+export const config = {
+ path: "/*",
+ excludedPath: [
+ "/_redirects",
+ "/robots.txt",
+ "/sitemap.xml",
+ "/search.json",
+ "/llms.txt",
+ "/llms-full.txt",
+ "/css/*",
+ "/js/*",
+ "/fonts/*",
+ "/webfonts/*",
+ "/img/*",
+ "/mermaid/*",
+ ],
+};