Skip to content

Commit 47f41cb

Browse files
committed
feat: adds llms.txt
1 parent 3db5d1d commit 47f41cb

File tree

2 files changed

+220
-0
lines changed

2 files changed

+220
-0
lines changed
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
<?php
2+
3+
namespace App\Http\Controllers;
4+
5+
use Illuminate\Contracts\Cache\Repository as Cache;
6+
use Illuminate\Filesystem\Filesystem;
7+
use Illuminate\Http\Response;
8+
use Illuminate\Support\Str;
9+
use Spatie\YamlFrontMatter\YamlFrontMatter;
10+
11+
class LlmsTxtController extends Controller
12+
{
13+
protected string $docsPath;
14+
15+
public function __construct(
16+
protected Filesystem $filesystem,
17+
protected Cache $cache,
18+
) {
19+
$this->docsPath = resource_path('docs/'.config('site.defaultVersion'));
20+
}
21+
22+
public function index(): Response
23+
{
24+
$content = $this->cache->remember('llms.txt', 5, function () {
25+
return $this->generateIndex();
26+
});
27+
28+
return response($content)->header('Content-Type', 'text/plain; charset=utf-8');
29+
}
30+
31+
public function full(): Response
32+
{
33+
$content = $this->cache->remember('llms-full.txt', 5, function () {
34+
return $this->generateFull();
35+
});
36+
37+
return response($content)->header('Content-Type', 'text/plain; charset=utf-8');
38+
}
39+
40+
public function page(string $page): Response
41+
{
42+
$body = $this->getBody($page);
43+
44+
if ($body === null) {
45+
abort(404);
46+
}
47+
48+
$content = $this->cache->remember("llms.page.{$page}", 5, fn () => $body."\n");
49+
50+
return response($content)->header('Content-Type', 'text/plain; charset=utf-8');
51+
}
52+
53+
protected function generateIndex(): string
54+
{
55+
$sections = $this->parseSections();
56+
$baseUrl = 'https://pestphp.com';
57+
58+
$lines = [];
59+
$lines[] = '# Pest';
60+
$lines[] = '';
61+
$lines[] = '> Pest is an elegant PHP testing framework with a focus on simplicity, designed to bring the joy of testing to PHP.';
62+
$lines[] = '';
63+
$lines[] = "- [Full Documentation]({$baseUrl}/llms-full.txt): Complete Pest documentation in a single file";
64+
$lines[] = '';
65+
66+
$optionalPages = [];
67+
68+
foreach ($sections as $section => $pages) {
69+
if (in_array($section, ['Press', 'More'])) {
70+
$optionalPages = array_merge($optionalPages, $pages);
71+
72+
continue;
73+
}
74+
75+
$lines[] = "## {$section}";
76+
$lines[] = '';
77+
78+
foreach ($pages as $page) {
79+
$lines[] = $this->formatIndexEntry($page, $baseUrl);
80+
}
81+
82+
$lines[] = '';
83+
}
84+
85+
if ($optionalPages !== []) {
86+
$lines[] = '## Optional';
87+
$lines[] = '';
88+
89+
foreach ($optionalPages as $page) {
90+
$lines[] = $this->formatIndexEntry($page, $baseUrl);
91+
}
92+
93+
$lines[] = '';
94+
}
95+
96+
return implode("\n", $lines);
97+
}
98+
99+
protected function generateFull(): string
100+
{
101+
$sections = $this->parseSections();
102+
103+
$parts = [];
104+
105+
foreach ($sections as $pages) {
106+
foreach ($pages as $page) {
107+
$body = $this->getBody($page['slug']);
108+
109+
if ($body !== null) {
110+
$parts[] = $body;
111+
}
112+
}
113+
}
114+
115+
return implode("\n\n---\n\n", $parts)."\n";
116+
}
117+
118+
/**
119+
* @return array<string, list<array{slug: string, title: string}>>
120+
*/
121+
protected function parseSections(): array
122+
{
123+
$indexPath = "{$this->docsPath}/documentation.md";
124+
$content = $this->filesystem->get($indexPath);
125+
126+
$sections = [];
127+
$currentSection = '';
128+
$excluded = ['readme', 'license'];
129+
130+
foreach (explode("\n", $content) as $line) {
131+
$line = trim($line);
132+
133+
if (preg_match('/^-\s+##\s+(.+)$/', $line, $matches)) {
134+
$currentSection = trim($matches[1]);
135+
$sections[$currentSection] = [];
136+
} elseif (preg_match('/^-\s+\[(.+?)\]\(\/docs\/(.+?)\)/', $line, $matches)) {
137+
$title = $matches[1];
138+
$slug = $matches[2];
139+
140+
if (in_array($slug, $excluded)) {
141+
continue;
142+
}
143+
144+
$sections[$currentSection][] = [
145+
'slug' => $slug,
146+
'title' => $title,
147+
];
148+
}
149+
}
150+
151+
return $sections;
152+
}
153+
154+
/**
155+
* @param array{slug: string, title: string} $page
156+
*/
157+
protected function formatIndexEntry(array $page, string $baseUrl): string
158+
{
159+
$url = "{$baseUrl}/docs/{$page['slug']}/llms.txt";
160+
$description = $this->getShortDescription($page['slug']);
161+
162+
if ($description !== '') {
163+
return "- [{$page['title']}]({$url}): {$description}";
164+
}
165+
166+
return "- [{$page['title']}]({$url})";
167+
}
168+
169+
protected function getShortDescription(string $slug): string
170+
{
171+
$description = $this->getDescription($slug);
172+
173+
if ($description === '') {
174+
return '';
175+
}
176+
177+
$firstSentence = Str::before($description, '. ');
178+
179+
if ($firstSentence !== $description) {
180+
$firstSentence .= '.';
181+
}
182+
183+
if (mb_strlen($firstSentence) > 120) {
184+
return mb_substr($firstSentence, 0, 117).'...';
185+
}
186+
187+
return $firstSentence;
188+
}
189+
190+
protected function getDescription(string $slug): string
191+
{
192+
$path = "{$this->docsPath}/{$slug}.md";
193+
194+
if (! $this->filesystem->exists($path)) {
195+
return '';
196+
}
197+
198+
$parsed = YamlFrontMatter::parse($this->filesystem->get($path));
199+
200+
return trim((string) $parsed->matter('description'));
201+
}
202+
203+
protected function getBody(string $slug): ?string
204+
{
205+
$path = "{$this->docsPath}/{$slug}.md";
206+
207+
if (! $this->filesystem->exists($path)) {
208+
return null;
209+
}
210+
211+
$parsed = YamlFrontMatter::parse($this->filesystem->get($path));
212+
213+
return trim($parsed->body());
214+
}
215+
}

routes/web.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
use App\Http\Controllers\DocsController;
44
use App\Http\Controllers\IDEPluginsController;
5+
use App\Http\Controllers\LlmsTxtController;
56
use Illuminate\Support\Facades\Route;
67

78
/*
@@ -20,7 +21,11 @@
2021
], function () {
2122
Route::get('/', fn () => file_get_contents(public_path('www/index.html')));
2223

24+
Route::get('/llms.txt', [LlmsTxtController::class, 'index']);
25+
Route::get('/llms-full.txt', [LlmsTxtController::class, 'full']);
26+
2327
Route::get('/docs/editor-setup', IDEPluginsController::class)->name('ide-plugins');
28+
Route::get('/docs/{page}/llms.txt', [LlmsTxtController::class, 'page']);
2429
Route::get('/docs/{page?}', DocsController::class)->name('docs')->where('page', '.*');
2530
});
2631

0 commit comments

Comments
 (0)