99use RuntimeException ;
1010
1111use function array_key_exists ;
12+ use function basename ;
1213use function count ;
14+ use function explode ;
1315use function file_get_contents ;
1416use function file_put_contents ;
17+ use function glob ;
1518use function implode ;
1619use function preg_match ;
1720use function sprintf ;
@@ -65,6 +68,7 @@ public function __construct(
6568 private readonly string $ sourceDir ,
6669 private readonly string $ outputFile ,
6770 private readonly string $ baseUrl ,
71+ private readonly ?string $ pagesDir = null ,
6872 ) {
6973 }
7074
@@ -91,7 +95,10 @@ public function write(): int
9195 $ sections [] = $ this ->buildCategorySection ($ slug , $ posts );
9296 }
9397
94- $ written = $ this ->buildHeader () . implode ("\n" , $ sections );
98+ $ written = $ this ->buildHeader ()
99+ . $ this ->buildPagesSection ()
100+ . "## Categories \n\n"
101+ . implode ("\n" , $ sections );
95102
96103 if (file_put_contents ($ this ->outputFile , $ written ) === false ) {
97104 throw new RuntimeException ('Unable to write llms.txt. ' );
@@ -163,53 +170,98 @@ private function buildCategorySection(string $slug, array $entries): string
163170 */
164171 private function resolveFrontMatter (string $ categorySlug , Post $ post ): array
165172 {
166- $ result = ['title ' => $ post ->getTitle (), 'description ' => $ post ->getExcerpt ()];
167173 $ path = sprintf ('%s/%s/%s.md ' , $ this ->sourceDir , $ categorySlug , $ post ->getSlug ());
168174 $ contents = @file_get_contents ($ path );
169175
170- if ($ contents === false ) {
171- return $ result ;
172- }
176+ return [
177+ 'title ' => $ contents === false
178+ ? $ post ->getTitle ()
179+ : $ this ->extractFrontMatterField ($ contents , 'title ' ) ?? $ post ->getTitle (),
180+ 'description ' => $ contents === false
181+ ? $ post ->getExcerpt ()
182+ : $ this ->extractFrontMatterField ($ contents , 'description ' ) ?? $ post ->getExcerpt (),
183+ ];
184+ }
173185
174- if (preg_match ('/^title:\s*"(.*)"\s*$/m ' , $ contents , $ matches ) === 1 ) {
175- $ result ['title ' ] = trim ($ matches [1 ]);
186+ private function buildPagesSection (): string
187+ {
188+ if ($ this ->pagesDir === null ) {
189+ return '' ;
176190 }
177191
178- if (preg_match ('/^description:\s*"(.*)"\s*$/m ' , $ contents , $ matches ) === 1 ) {
179- $ result ['description ' ] = trim ($ matches [1 ]);
180- }
192+ $ pages = [];
193+ foreach (glob ($ this ->pagesDir . '/*.md ' ) ?: [] as $ path ) {
194+ $ contents = file_get_contents ($ path );
195+ if ($ contents === false ) {
196+ continue ;
197+ }
181198
182- return $ result ;
183- }
199+ $ title = $ this ->extractFrontMatterField ($ contents , 'title ' );
200+ $ description = $ this ->extractFrontMatterField ($ contents , 'description ' );
201+ if ($ title === null || $ description === null ) {
202+ continue ;
203+ }
184204
185- private function buildHeader (): string
186- {
187- return <<<HEADER
188- # Dotkernel Light
205+ $ pages [] = [
206+ 'title ' => trim (explode ('| ' , $ title , 2 )[0 ]),
207+ 'slug ' => basename ($ path , '.md ' ),
208+ 'description ' => $ description ,
209+ ];
210+ }
189211
190- > Dotkernel Light is the technical blog for the Dotkernel headless PHP platform - a PSR-15 compliant
191- application built on Mezzio and Laminas components. It publishes architecture write-ups, how-tos,
192- and release notes for the platform applications - Dotkernel API, Admin and Queue - and for the standalone
193- Dotkernel Light skeleton.
212+ if ($ pages === []) {
213+ return '' ;
214+ }
194215
195- Content spans foundational PHP/middleware architecture (PSR-7, PSR-15, request lifecycle, dependency injection),
196- practical how-tos (Doctrine migrations, CORS, authentication, caching), and the history/release notes of the
197- Dotkernel ecosystem going back to 2008. Posts are organized by category and attributed to an author;
198- URLs follow the pattern `/{category-slug}/{post-slug}/`.
216+ usort ($ pages , static fn (array $ a , array $ b ): int => strcasecmp ($ a ['title ' ], $ b ['title ' ]));
199217
200- ## Docs
218+ $ lines = ['## Pages ' , '' ];
219+ foreach ($ pages as $ page ) {
220+ $ lines [] = sprintf (
221+ '- [%s](%s/%s/): %s ' ,
222+ $ page ['title ' ],
223+ $ this ->baseUrl ,
224+ $ page ['slug ' ],
225+ $ page ['description ' ],
226+ );
227+ }
201228
202- - [Blog]( {$ this ->baseUrl }/blog/): full list of posts, most recent first, paginated
203- - [Categories]( {$ this ->baseUrl }/categories/): all categories with post counts
204- - [About]( {$ this ->baseUrl }/about/): the team behind Dotkernel - how the team works, its commitment to open
205- source and the PHP community, and how it uses AI under guardrails
206- - [OSS Package Lifecycle]( {$ this ->baseUrl }/dotkernel-packages-oss-lifecycle/): support/maintenance status of
207- Dotkernel's open-source packages
208- - [Contact]( {$ this ->baseUrl }/contact/)
229+ return implode ("\n" , $ lines ) . "\n\n" ;
230+ }
209231
210- ## Categories
232+ private function extractFrontMatterField (string $ contents , string $ field ): ?string
233+ {
234+ if (preg_match (sprintf ('/^%s:\s*"(.*)"\s*$/m ' , $ field ), $ contents , $ matches ) === 1 ) {
235+ return trim ($ matches [1 ]);
236+ }
211237
238+ return null ;
239+ }
212240
213- HEADER ;
241+ private function buildHeader (): string
242+ {
243+ $ intro = '> Dotkernel Light is the technical blog for the Dotkernel headless PHP platform - a PSR-15 '
244+ . 'compliant application built on Mezzio and Laminas components. It publishes architecture '
245+ . 'write-ups, how-tos, and release notes for the platform applications - Dotkernel API, Admin '
246+ . 'and Queue - and for the standalone Dotkernel Light skeleton. ' ;
247+
248+ $ body = 'Content spans foundational PHP/middleware architecture (PSR-7, PSR-15, request lifecycle, '
249+ . 'dependency injection), practical how-tos (Doctrine migrations, CORS, authentication, caching), '
250+ . 'and the history/release notes of the Dotkernel ecosystem going back to 2008. Posts are organized '
251+ . 'by category and attributed to an author; URLs follow the pattern `/{category-slug}/{post-slug}/`. ' ;
252+
253+ $ about = 'the team behind Dotkernel - how the team works, its commitment to open source and the PHP '
254+ . 'community, and how it uses AI under guardrails ' ;
255+
256+ return "# Dotkernel Light \n\n"
257+ . $ intro . "\n\n"
258+ . $ body . "\n\n"
259+ . "## Docs \n\n"
260+ . "- [Blog]( {$ this ->baseUrl }/blog/): full list of posts, most recent first, paginated \n"
261+ . "- [Categories]( {$ this ->baseUrl }/categories/): all categories with post counts \n"
262+ . "- [About]( {$ this ->baseUrl }/about/): {$ about }\n"
263+ . "- [OSS Package Lifecycle]( {$ this ->baseUrl }/dotkernel-packages-oss-lifecycle/): "
264+ . "support/maintenance status of Dotkernel's open-source packages \n"
265+ . "- [Contact]( {$ this ->baseUrl }/contact/) \n\n" ;
214266 }
215267}
0 commit comments