From 540a03ce82bd57f5848806c183df4fc139f5849d Mon Sep 17 00:00:00 2001 From: Mark Dumay <61946753+markdumay@users.noreply.github.com> Date: Sat, 4 Apr 2026 09:11:02 +0200 Subject: [PATCH 1/2] feat: include section index pages in llms.txt Add site.Sections to the page collection so _index.md pages appear in the LLM index alongside regular pages. Key behaviours: - Standalone section indexes (no children) are placed under ## Pages rather than getting their own heading, keeping the index flat - Section indexes with children are prepended to lead their section; suppressed when title matches the section heading to avoid redundancy - Headless bundles (empty RelPermalink) are filtered out - Homepage entry uses the site title instead of the page tagline - Add section = ["HTML", "markdown"] to example site outputs Co-Authored-By: Claude Sonnet 4.6 --- README.md | 3 +- exampleSite/hugo.toml | 1 + layouts/index.llmstxt.txt | 71 +++++++++++++++++++++++++++++++++------ 3 files changed, 63 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index f8841b2..73239f9 100644 --- a/README.md +++ b/README.md @@ -78,10 +78,11 @@ Activate the formats in the `[outputs]` section of your `hugo.toml`. Include `ll [outputs] home = ["HTML", "llmstxt", "markdown"] # add "llmscomponents" for documentation sites page = ["HTML", "markdown"] + section = ["HTML", "markdown"] ``` > [!NOTE] -> If your site already defines `[outputs]`, extend the existing lists rather than replacing them. The `HTML` output must remain to keep the regular site building correctly. +> If your site already defines `[outputs]`, extend the existing lists rather than replacing them. The `HTML` output must remain to keep the regular site building correctly. The `section` key ensures section index pages (`_index.md`) get a Markdown equivalent and are linked from `llms.txt`. ## Contributing diff --git a/exampleSite/hugo.toml b/exampleSite/hugo.toml index 6567b16..966a1cd 100644 --- a/exampleSite/hugo.toml +++ b/exampleSite/hugo.toml @@ -29,6 +29,7 @@ title = 'Test site for mod-llm' [outputs] home = ["HTML", "llmstxt", "llmscomponents", "markdown"] page = ["HTML", "markdown"] + section = ["HTML", "markdown"] [module] # Build and serve using local mod-llm clone declared in the named Hugo workspace: diff --git a/layouts/index.llmstxt.txt b/layouts/index.llmstxt.txt index f7c8ba9..a7ad975 100644 --- a/layouts/index.llmstxt.txt +++ b/layouts/index.llmstxt.txt @@ -5,38 +5,87 @@ > {{ with $site.Params.main.description }}{{ . }}{{ else }}{{ $site.Title }}{{ end }} -{{- /* Collect pages grouped by section, default language only, skip excluded */ -}} +{{- /* + Collect all pages into an ordered map of section → page list. + $sections: dict mapping section key → slice of pages + $sectionOrder: slice of section keys in encounter order (controls output order) +*/ -}} {{- $sections := dict -}} {{- $sectionOrder := slice -}} -{{- /* Prepend homepage to the pages section */ -}} + +{{- /* Homepage is always first in the Pages section, unless excluded or missing markdown output */ -}} {{- if and (not $site.Home.Params.llm.exclude) ($site.Home.OutputFormats.Get "markdown") -}} {{- $sectionOrder = $sectionOrder | append "pages" -}} {{- $sections = merge $sections (dict "pages" (slice $site.Home)) -}} {{- end -}} -{{- range $site.RegularPages -}} - {{- if and (eq .Language.Lang $site.Language.Lang) (not .Params.llm.exclude) -}} - {{- $section := .Section | default "pages" -}} - {{- if not (index $sections $section) -}} - {{- $sectionOrder = $sectionOrder | append $section -}} + +{{- /* + Iterate over all regular pages and section index pages. + RegularPages first so sections can check whether child pages already registered them. + Headless bundles (.RelPermalink == "") and llm.exclude pages are skipped. +*/ -}} +{{- $allPages := $site.RegularPages | append $site.Sections -}} +{{- range $allPages -}} + {{- if and (eq .Language.Lang $site.Language.Lang) (not .Params.llm.exclude) .RelPermalink -}} + {{- if eq .Kind "section" -}} + {{- $hasChildren := or (len .RegularPages) (len .Sections) -}} + {{- if not $hasChildren -}} + {{- /* + Standalone section index (no child pages) — treat as a regular content page + and place it under the flat Pages section rather than its own heading. + Example: /pricing/, /data-catalog/, /releases/ + */ -}} + {{- if not (index $sections "pages") -}} + {{- $sectionOrder = $sectionOrder | append "pages" -}} + {{- end -}} + {{- $sectionPages := index $sections "pages" | default slice -}} + {{- $sections = merge $sections (dict "pages" ($sectionPages | append .)) -}} + {{- else -}} + {{- /* + Section index with child pages — register the section if not yet seen, then + prepend the index page so it leads the section list. + Skip the index page entirely if its title matches the section key (e.g. "Insights" + under ## Insights), since the heading already communicates the same information. + */ -}} + {{- $section := .Section | default "pages" -}} + {{- if not (index $sections $section) -}} + {{- $sectionOrder = $sectionOrder | append $section -}} + {{- $sections = merge $sections (dict $section (slice)) -}} + {{- end -}} + {{- if ne (.Title | lower) $section -}} + {{- $sectionPages := index $sections $section | default slice -}} + {{- $sections = merge $sections (dict $section (slice . | append $sectionPages)) -}} + {{- end -}} + {{- end -}} + {{- else -}} + {{- /* Regular page — append to its section, registering the section on first encounter */ -}} + {{- $section := .Section | default "pages" -}} + {{- if not (index $sections $section) -}} + {{- $sectionOrder = $sectionOrder | append $section -}} + {{- end -}} + {{- $sectionPages := index $sections $section | default slice -}} + {{- $sections = merge $sections (dict $section ($sectionPages | append .)) -}} {{- end -}} - {{- $sectionPages := index $sections $section | default slice -}} - {{- $sections = merge $sections (dict $section ($sectionPages | append .)) -}} {{- end -}} {{- end -}} +{{- /* Render each section as a level-2 heading followed by its page list */ -}} {{- range $section := $sectionOrder }} ## {{ $section | title }} {{ range $pages := index $sections $section -}} +{{- /* Prefer explicit llm.description, then front matter description, then truncated summary */ -}} {{- $desc := or .Params.llm.description .Description (.Summary | plainify | truncate 120) -}} {{- $hasMarkdown := .OutputFormats.Get "markdown" -}} {{- $link := .Permalink -}} {{- if $hasMarkdown }}{{- $link = printf "%sindex.md" .Permalink -}}{{ end -}} +{{- /* Use site title for the homepage — the page title is typically a marketing tagline */ -}} +{{- $title := cond .IsHome $site.Title .Title -}} {{- if $desc -}} -{{- printf "- [%s](%s): %s\n" .Title $link $desc -}} +{{- printf "- [%s](%s): %s\n" $title $link $desc -}} {{- else -}} -{{- printf "- [%s](%s)\n" .Title $link -}} +{{- printf "- [%s](%s)\n" $title $link -}} {{- end -}} {{- end -}} {{- end -}} From ce905da9e6663c4b647402c75fcd65ca923cb83f Mon Sep 17 00:00:00 2001 From: Mark Dumay <61946753+markdumay@users.noreply.github.com> Date: Sat, 4 Apr 2026 09:12:26 +0200 Subject: [PATCH 2/2] feat: add llm-block-contact-form and llm-block-heading i18n keys Add missing block name fallback translations for contact-form and heading across all supported languages (de, en, fr, nl, pl, pt-br, zh-hans, zh-hant). Co-Authored-By: Claude Sonnet 4.6 --- i18n/de.yaml | 4 ++++ i18n/en.yaml | 4 ++++ i18n/fr.yaml | 4 ++++ i18n/nl.yaml | 4 ++++ i18n/pl.yaml | 4 ++++ i18n/pt-br.yaml | 4 ++++ i18n/zh-hans.yaml | 4 ++++ i18n/zh-hant.yaml | 4 ++++ 8 files changed, 32 insertions(+) diff --git a/i18n/de.yaml b/i18n/de.yaml index 911d6cc..5b4970a 100644 --- a/i18n/de.yaml +++ b/i18n/de.yaml @@ -19,10 +19,14 @@ translation: Artikel - id: llm-block-cards translation: Karten +- id: llm-block-contact-form + translation: Kontaktformular - id: llm-block-cta translation: Handlungsaufruf - id: llm-block-faq translation: FAQ +- id: llm-block-heading + translation: Überschrift - id: llm-block-featured translation: Empfohlen - id: llm-block-hero diff --git a/i18n/en.yaml b/i18n/en.yaml index 950fa67..4b10c2c 100644 --- a/i18n/en.yaml +++ b/i18n/en.yaml @@ -19,10 +19,14 @@ translation: Articles - id: llm-block-cards translation: Cards +- id: llm-block-contact-form + translation: Contact Form - id: llm-block-cta translation: Call to Action - id: llm-block-faq translation: FAQ +- id: llm-block-heading + translation: Heading - id: llm-block-featured translation: Featured - id: llm-block-hero diff --git a/i18n/fr.yaml b/i18n/fr.yaml index 7df0dd7..ca0cc10 100644 --- a/i18n/fr.yaml +++ b/i18n/fr.yaml @@ -19,10 +19,14 @@ translation: Articles - id: llm-block-cards translation: Cartes +- id: llm-block-contact-form + translation: Formulaire de contact - id: llm-block-cta translation: Appel à l'action - id: llm-block-faq translation: FAQ +- id: llm-block-heading + translation: En-tête - id: llm-block-featured translation: En vedette - id: llm-block-hero diff --git a/i18n/nl.yaml b/i18n/nl.yaml index d18b478..7ebabd8 100644 --- a/i18n/nl.yaml +++ b/i18n/nl.yaml @@ -19,10 +19,14 @@ translation: Artikelen - id: llm-block-cards translation: Kaarten +- id: llm-block-contact-form + translation: Contactformulier - id: llm-block-cta translation: Oproep tot actie - id: llm-block-faq translation: FAQ +- id: llm-block-heading + translation: Koptekst - id: llm-block-featured translation: Uitgelicht - id: llm-block-hero diff --git a/i18n/pl.yaml b/i18n/pl.yaml index 8daa1ba..ff49293 100644 --- a/i18n/pl.yaml +++ b/i18n/pl.yaml @@ -19,10 +19,14 @@ translation: Artykuły - id: llm-block-cards translation: Karty +- id: llm-block-contact-form + translation: Formularz kontaktowy - id: llm-block-cta translation: Wezwanie do działania - id: llm-block-faq translation: FAQ +- id: llm-block-heading + translation: Nagłówek - id: llm-block-featured translation: Wyróżnione - id: llm-block-hero diff --git a/i18n/pt-br.yaml b/i18n/pt-br.yaml index a57fa50..fba2a01 100644 --- a/i18n/pt-br.yaml +++ b/i18n/pt-br.yaml @@ -19,10 +19,14 @@ translation: Artigos - id: llm-block-cards translation: Cartões +- id: llm-block-contact-form + translation: Formulário de contato - id: llm-block-cta translation: Chamada para ação - id: llm-block-faq translation: FAQ +- id: llm-block-heading + translation: Cabeçalho - id: llm-block-featured translation: Destaque - id: llm-block-hero diff --git a/i18n/zh-hans.yaml b/i18n/zh-hans.yaml index 80d45fc..4cb9e36 100644 --- a/i18n/zh-hans.yaml +++ b/i18n/zh-hans.yaml @@ -19,10 +19,14 @@ translation: 文章 - id: llm-block-cards translation: 卡片 +- id: llm-block-contact-form + translation: 联系表单 - id: llm-block-cta translation: 行动号召 - id: llm-block-faq translation: 常见问题 +- id: llm-block-heading + translation: 标题 - id: llm-block-featured translation: 精选 - id: llm-block-hero diff --git a/i18n/zh-hant.yaml b/i18n/zh-hant.yaml index 1889880..fcd4aff 100644 --- a/i18n/zh-hant.yaml +++ b/i18n/zh-hant.yaml @@ -19,10 +19,14 @@ translation: 文章 - id: llm-block-cards translation: 卡片 +- id: llm-block-contact-form + translation: 聯絡表單 - id: llm-block-cta translation: 行動號召 - id: llm-block-faq translation: 常見問題 +- id: llm-block-heading + translation: 標題 - id: llm-block-featured translation: 精選 - id: llm-block-hero