Skip to content

Add llms.txt support with markdown format negotiation#20

Merged
dbernheisel merged 22 commits intomainfrom
dbern/llms
Apr 13, 2026
Merged

Add llms.txt support with markdown format negotiation#20
dbernheisel merged 22 commits intomainfrom
dbern/llms

Conversation

@dbernheisel
Copy link
Copy Markdown
Owner

Summary

  • Add SEO.LLMs Plug that serves /llms.txt per the llmstxt.org spec
  • Introduce FooMD view module convention for serving resources as markdown, following Phoenix's FooHTML/FooJSON pattern
  • Reuse existing SEO config (site_name, description) for the llms.txt title and summary
  • Support both string interpolation and MDEx ~MD sigil for markdown templates

New modules

Module Purpose
SEO.LLMs Plug serving /llms.txt + @callback entry/1 behaviour for FooMD view modules
SEO.LLMs.Entry Struct for index entries + group_by_section/1 helper
SEO.LLMs.Provider Behaviour for assembling sections (static + dynamic from MD modules)

How it works

FooMD view module (@behaviour SEO.LLMs)
  ├─ show/1, index/1    → Phoenix view functions (md format rendering)
  └─ entry/1            → llms.txt index metadata (section, title, url, description)

Provider (sections/0)
  └─ collects entries from MD modules → groups by section

Plug (forward "/llms.txt")
  └─ renders markdown index from provider + SEO config

Define the callback interface that providers implement to return
structured sections for generating llms.txt files per the llmstxt.org
spec. Includes initial test with a static provider module.
Renders llms.txt-compliant markdown from a map of options including
title, description, body, and categorized link sections.
Implements init/1 and call/2 so SEO.LLMs can be used directly as a
Plug (e.g. via `forward "/llms.txt", SEO.LLMs, opts`). Sections can
be provided statically or resolved from a provider module at request
time. Includes resolve_config/1 for deriving title/description from
a SEO config module.
- Test that provider module sections are resolved and rendered via the Plug
- Test that config module derives title from open_graph.site_name and
  description from site.description
- Test that explicit title/description options override config values
Define the Build protocol for LLMs entries, following the same
pattern as SEO.OpenGraph.Build and other existing Build protocols.
The Any fallback returns nil since there is no sensible default
for LLMs entries.
Entry represents a single resource in an llms.txt file with section,
title, url, description, and content fields. The group_by_section/1
function converts entries into the tuple format expected by render/1.
Add SEO.LLMs.Build implementation for MyApp.Article in test support,
along with tests for the Build protocol (including Any fallback) and
a full protocol-driven provider integration test that exercises the
flow from protocol through provider to plug rendering.
Controllers can call SEO.LLMs.render_content/2 to serve a resource's
markdown content as text/markdown. Supports string and zero-arity
function content fields, returning 406 when unimplemented.
Remove SEO.LLMs.Build protocol and render_content/2 in favor of a
behaviour callback on SEO.LLMs. Markdown view modules (FooMD) implement
the behaviour's entry/1 callback to provide llms.txt entries, following
Phoenix's convention of format-specific view modules (FooHTML, FooJSON).
…ation

Show the complete setup: router pipeline, FooMD view modules, controller
registration, provider assembly. Include MDEx sigil example for markdown
templates alongside standard string interpolation approach.
Cover setup, the ~MD sigil as markdown's equivalent of ~H, when to
use compile-time templates vs runtime interpolation, and converting
HTML content to markdown.
… MDEx

Sets up a minimal Phoenix endpoint, router, and controllers to validate
the complete format negotiation flow (Accept: text/markdown → ArticleMD/PageMD
view dispatch), the llms.txt plug endpoint, MDEx ~MD sigil rendering,
and the provider behaviour contract.
Section entries can now be:
- {name, url} — a link
- {name, url, description} — a link with description
- {name, entries} — a sub-section rendered as H3
- "string" — inline markdown rendered as-is

Consecutive links are joined with single newlines; strings and
sub-sections are separated by double newlines. This matches
real-world patterns from sites like Vercel, Vite, Turso, and NVIDIA.
…ples

- Replace Enum.map |> Enum.join with Enum.map_join
- Add alias SEO.LLMs.Entry throughout test files and support modules
- Update moduledoc examples to use ~p sigil for VerifiedRoutes
MDEx requires phoenix_live_view ~> 1.0, which conflicts with blend
tests against older LV versions. MDEx usage is documented in the
moduledoc but not tested directly — that's MDEx's responsibility.
@dbernheisel dbernheisel marked this pull request as ready for review April 12, 2026 23:47
@dbernheisel dbernheisel merged commit 216c5f3 into main Apr 13, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant