Developer
Request flow
The extension adds three PSR-15 middlewares to the frontend stack. No route
enhancer is involved: the middlewares answer the text files themselves and
remove .md before TYPO3 routes the request.
request
├─ woit/si-llms-txt/markdown-suffix before typo3/cms-frontend/site
├─ typo3/cms-frontend/site
├─ woit/si-llms-txt/llms-txt before typo3/cms-frontend/base-redirect-resolver
├─ … page-resolver, prepare-tsfe-rendering, shortcut redirect
├─ woit/si-llms-txt/markdown-render after prepare-tsfe-rendering
└─ rendering
| Middleware | Task |
|---|---|
Markdown | /de/ → routes /de/ and sets the request attribute
sillmstxt.. Responses without Markdown become a plain
404; redirects keep the .md suffix. |
Llms | Answers llms. / llms- for the site root
and every language base. |
Markdown | Uses the page field "LLM Markdown" or lets the page render and converts the HTML. For the EXT:news detail page it takes the news record (see EXT:news (tx_news)). |
For normal HTML requests each middleware only compares a string and passes the request on.
The event listener Add
(After) adds the <link rel="alternate">
and <link rel="describedby"> tags when a page is rendered; the result is
part of the page cache. The HTTP Link header with the same relations is
added by Markdown on every request (see
Discovery links).
Classes
| Class | Purpose |
|---|---|
Service\ | Builds llms., llms- and the frontmatter
of page and news documents; caching |
Service\ | Walks the page tree, language overlay (strict) |
Service\ | News records of EXT:news (optional) |
Service\ | HTML → Markdown with league/html-to-markdown; removes navigation,
breadcrumbs, asides, forms, buttons, scripts, decorative images
and links without text; accordion titles and <summary>
become h3 headings; keeps button titles in headings; resolves
lazy-loaded images; moves card links onto their heading; makes
links absolute |
Service\ | alternate and describedby links of a page, as HTML tags or
HTTP Link header |
Service\ | Page URL ↔ .md URL |
Service\ | Site set active? Settings, included page types |
PSR-14 events
Listeners of other extensions can change what the extension generates, for example list all records of a plugin that shows only the first result page.
ModifyLlmsTxtEvent
Woit\ is dispatched before
llms. or llms- of a site language is built. It
only runs on a cache miss.
get,Site () get,Language () getFile Name () get/Title () set,Title () get/Intro () setIntro () get/Sections () set,Sections () get,Section ($id) add,Section ($id, $section, $before) removeSection ($id) add: a missing section is created at the end with the id as titleItem ($section Id, $item) add,Cache Tags (...$tags) setCache Lifetime ($seconds)
Section ids: pages, section-<uid> (top-level page), articles,
news. The sections "Languages" and "Optional" are added afterwards.
A Woit\ has title and items. An
Woit\ has title, url, description,
date, include and markdown. An item goes into
llms- with its markdown if set, otherwise only with
include: then url must be a .md URL of the same site and
is fetched like a page.
use TYPO3\CMS\Core\Attribute\AsEventListener;
use Woit\SiLlmsTxt\Event\ModifyLlmsTxtEvent;
use Woit\SiLlmsTxt\LlmsTxt\Item;
use Woit\SiLlmsTxt\LlmsTxt\Section;
final class AddRecordsToLlmsTxt
{
#[AsEventListener]
public function __invoke(ModifyLlmsTxtEvent $event): void
{
$event->addSection('records', new Section('Records'), 'news');
foreach ($this->repository->findAll() as $record) {
$event->addItem('records', new Item(
$record->getTitle(),
$record->getUrl(),
$record->getTeaser(),
));
}
$event->addCacheTags('tx_myext_record');
}
}
ModifyMarkdownEvent
Woit\ is dispatched for every
.md response of a page or EXT:news record.
get,Request () get,Site () getLanguage () get(Table () pagesortx_),news_ domain_ model_ news getRecord () get/Frontmatter () set:Frontmatter () title,description,language,date,lastmod,canonical,source,generator; empty values are not writtenget/Markdown () set: body without frontmatterMarkdown () get: rendered page,Html () nullfor the field "LLM Markdown"set: answer 404Not Found ()
A .md blocked with set is still listed in
llms.; remove it there with Modify.
Example: list all records on a list page that renders only the first result page.
use TYPO3\CMS\Core\Attribute\AsEventListener;
use Woit\SiLlmsTxt\Event\ModifyMarkdownEvent;
final class ListAllRecordsInMarkdown
{
private const LIST_PAGE_UID = 42;
#[AsEventListener]
public function __invoke(ModifyMarkdownEvent $event): void
{
if ($event->getTable() !== 'pages' || (int)($event->getRecord()['l10n_parent'] ?: $event->getRecord()['uid']) !== self::LIST_PAGE_UID) {
return;
}
$lines = [];
foreach ($this->repository->findAll() as $record) {
$lines[] = '- [' . $record->getTitle() . '](' . $record->getUrl() . '): ' . $record->getTeaser();
}
$event->setMarkdown($event->getMarkdown() . "\n\n## All records\n\n" . implode("\n", $lines));
}
}
Example: describe the record shown on a detail page, answer 404 without a record.
use TYPO3\CMS\Core\Attribute\AsEventListener;
use Woit\SiLlmsTxt\Event\ModifyMarkdownEvent;
final class DescribeDetailRecord
{
private const DETAIL_PAGE_UID = 43;
#[AsEventListener]
public function __invoke(ModifyMarkdownEvent $event): void
{
if ($event->getTable() !== 'pages' || (int)($event->getRecord()['l10n_parent'] ?: $event->getRecord()['uid']) !== self::DETAIL_PAGE_UID) {
return;
}
$arguments = $event->getRequest()->getAttribute('routing')->getArguments();
$record = $this->repository->findByUid((int)($arguments['tx_myext_pi1']['record'] ?? 0));
if ($record === null) {
$event->setNotFound();
return;
}
$frontmatter = $event->getFrontmatter();
$frontmatter['title'] = $record->getTitle();
$frontmatter['description'] = $record->getTeaser();
$frontmatter['canonical'] = $record->getUrl();
$event->setFrontmatter($frontmatter);
}
}
Caching
- Cache
sillmstxt(database backend, grouppages) - Entries for
llms.andtxt llms-per site languagefull. txt - Tags:
pageof every listed page and the root page; with EXT:news alsoId_<uid> tx_,news tx_,news_ pid_<pid> tx_; plus the tags ofnews_ uid_<uid> ModifylistenersLlms Txt Event - The intro of the language is part of the cache identifier
llms-with pages that could not be fetched is kept for 5 minutes onlyfull. txt .mddocuments are not cached separately: the HTML comes from the page cache, only the conversion runs per request
Database
CREATE TABLE pages (
tx_sillmstxt_exclude smallint(5) unsigned DEFAULT '0' NOT NULL,
tx_sillmstxt_priority int(11) DEFAULT '0' NOT NULL,
tx_sillmstxt_description text,
tx_sillmstxt_markdown mediumtext
);
tx_ and tx_ use
l10n_ (same value in all languages).