From 7a68a72aa50622fe77afa1bc12812dbb2a15e67c Mon Sep 17 00:00:00 2001 From: Claw Explorer Date: Thu, 2 Apr 2026 16:21:18 -0400 Subject: [PATCH] feat: add /llms.txt endpoints for LLM-friendly documentation Add two new routes that serve LLM-friendly plain text documentation following the llms.txt specification (https://llmstxt.org/): - GET /llms.txt - Returns a top-level index of all available documentation with links to each doc section. - GET /:doc/llms.txt - Returns a per-documentation index with all entries grouped by type, linking to individual pages. The format follows the llms.txt standard: - Title as a Markdown heading - Description as a blockquote - Sections with links in Markdown format This enables LLM tools and AI-powered development environments to efficiently discover and reference DevDocs documentation. Closes #2520 --- lib/app.rb | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/lib/app.rb b/lib/app.rb index 675936fbac..db10d2817a 100644 --- a/lib/app.rb +++ b/lib/app.rb @@ -301,6 +301,70 @@ def modern_browser?(browser) 200 end + # llms.txt - LLM-friendly documentation index + # See https://llmstxt.org/ for the specification + get '/llms.txt' do + content_type 'text/plain' + + lines = [] + lines << '# DevDocs' + lines << '' + lines << '> DevDocs combines multiple API documentations in a fast, organized, and searchable interface.' + lines << '' + lines << '## Documentation' + lines << '' + + settings.docs.each do |slug, doc| + lines << "- [#{doc['full_name']}](https://devdocs.io/#{slug}/)" + end + + lines.join("\n") + end + + get %r{/([\w~\.%]+)/llms\.txt} do |doc| + doc.sub! '%7E', '~' + return 404 unless @doc = find_doc(doc) + + content_type 'text/plain' + + index_path = File.join(settings.docs_path, @doc['slug'], 'index.json') + return 404 unless File.exist?(index_path) + + index = JSON.parse(File.read(index_path)) + + lines = [] + lines << "# #{@doc['full_name']}" + lines << '' + lines << "> #{@doc['full_name']} documentation on DevDocs." + lines << '' + + if index['types'] && !index['types'].empty? + grouped = {} + (index['entries'] || []).each do |entry| + type_name = entry['type'] || 'General' + grouped[type_name] ||= [] + grouped[type_name] << entry + end + + grouped.sort_by { |type, _| type }.each do |type, entries| + lines << "## #{type}" + lines << '' + entries.each do |entry| + path = entry['path'].to_s.split('#').first + lines << "- [#{entry['name']}](https://devdocs.io/#{@doc['slug']}/#{path})" + end + lines << '' + end + else + (index['entries'] || []).each do |entry| + path = entry['path'].to_s.split('#').first + lines << "- [#{entry['name']}](https://devdocs.io/#{@doc['slug']}/#{path})" + end + end + + lines.join("\n") + end + %w(docs.json application.js application.css).each do |asset| class_eval <<-CODE, __FILE__, __LINE__ + 1 get '/#{asset}' do