Module: DocsKit::LlmsText

Defined in:
lib/docs_kit/llms_text.rb

Overview

Builds the two AI-readable artifacts a docs-kit site serves, straight from the registry — zero authoring:

/llms.txt      — the llmstxt.org index: H1 brand, `> tagline` blockquote,
               one `## {group}` section per nav group, and a
               `- [title](abs .md url)` line per authored page.
/llms-full.txt — every page's Markdown twin concatenated, `# {title}` +
               body, separated by `---`.

It's a pure text builder: given a DocsKit::Configuration and (for the full form) already-rendered [title, markdown] pairs, it produces strings with no Rails. The controller owns the Rails view context — it renders each page to Markdown (DocsKit::MarkdownExport) and hands the pairs to .full — so all the shaping is unit-testable without booting Rails.

The enumeration source is DocsKit::Registry v2: each registry in config.nav_registries responds to #nav_items ({ group => [NavItem] }, authored pages only) for the index and #all (entries with #view_class) for the authored page list. An unwritten page (no resolvable view_class) is excluded from both, so neither artifact ever links or concatenates a page that doesn't exist yet.

Class Method Summary collapse

Class Method Details

.full(_config, title_markdown_pairs) ⇒ Object

The llms-full.txt body: each [title, markdown] pair as # {title} + body, separated by a --- rule. Empty pairs → "".



66
67
68
# File 'lib/docs_kit/llms_text.rb', line 66

def full(_config, title_markdown_pairs)
  title_markdown_pairs.map { |title, markdown| "# #{title}\n\n#{markdown}" }.join("\n\n---\n\n")
end

.groups(config) ⇒ Object

{ group => [links] } across every registry's #nav_items, in config order. A registry with no authored pages contributes nothing, so no empty section is ever emitted.



73
74
75
76
77
# File 'lib/docs_kit/llms_text.rb', line 73

def groups(config)
  config.nav_registries.values.each_with_object({}) do |registry, acc|
    registry.nav_items.each { |group, links| (acc[group] ||= []).concat(links) }
  end
end

.index(config, base_url: nil) ⇒ Object

The llms.txt index string. base_url absolutizes each page's .md href so agent tooling fetches a portable URL; omit it for relative links.

Blocks (H1, the tagline blockquote, and one per section) are separated by a blank line; within a section the ## heading and its - [..] bullets are a single tight list (no blank lines between bullets), per the llmstxt.org convention.



35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/docs_kit/llms_text.rb', line 35

def index(config, base_url: nil)
  blocks = ["# #{config.brand}"]
  tagline = config.tagline
  blocks << "> #{tagline}" if tagline && !tagline.to_s.empty?
  blocks.concat(section_blocks(config, base_url))

  # Advertise the built-in MCP endpoint last, so an agent that reads llms.txt
  # discovers it can also connect over the protocol (native tools vs fetching
  # text). Only when the endpoint is actually live (gem present + c.mcp on).
  blocks << mcp_block(base_url) if config.mcp_enabled?

  blocks.join("\n\n")
end

- [label](absolute .md url). The .md suffix targets the page's Markdown twin (DocsKit::Controller#render_page).



92
93
94
# File 'lib/docs_kit/llms_text.rb', line 92

def link_line(link, base_url)
  "- [#{link.label}](#{md_url(link.href, base_url)})"
end

.mcp_block(base_url) ⇒ Object

The ## MCP section pointing an agent at the read-only MCP endpoint. The /mcp URL is absolutized against base_url when available (agents connect to a portable URL); relative otherwise.



82
83
84
85
86
87
88
# File 'lib/docs_kit/llms_text.rb', line 82

def mcp_block(base_url)
  url = base_url ? "#{base_url.chomp('/')}/mcp" : "/mcp"
  "## MCP\n" \
    "This documentation is also available over the Model Context Protocol " \
    "(search, page retrieval) at #{url} — add it to an MCP client " \
    "(Claude Code, Claude.ai, Cursor) to query these docs as tools."
end

.md_url(href, base_url) ⇒ Object

href + ".md", absolutized against base_url when given. base_url has no trailing slash concerns here (hrefs are root-relative like "/docs/x").



98
99
100
101
102
103
# File 'lib/docs_kit/llms_text.rb', line 98

def md_url(href, base_url)
  path = "#{href}.md"
  return path unless base_url

  "#{base_url.chomp('/')}#{path}"
end

.pages(config) ⇒ Object

The authored pages across every registry, in config/registry order — each responds to #title / #href / #view_class. The controller renders these to Markdown for .full.



60
61
62
# File 'lib/docs_kit/llms_text.rb', line 60

def pages(config)
  config.nav_registries.values.flat_map { |registry| registry.all.select(&:view_class) }
end

.section_blocks(config, base_url) ⇒ Object

One ## {group} block per nav group, each a tight bullet list of its authored pages' .md links, in registry order.



51
52
53
54
55
# File 'lib/docs_kit/llms_text.rb', line 51

def section_blocks(config, base_url)
  groups(config).map do |group, links|
    ["## #{group}", *links.map { |link| link_line(link, base_url) }].join("\n")
  end
end