.. _developer: ========= Developer ========= .. _developer-request-flow: 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. .. code-block:: text 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 .. list-table:: :header-rows: 1 :widths: 30 70 * - Middleware - Task * - ``MarkdownSuffixMiddleware`` - ``/de/page.md`` → routes ``/de/page`` and sets the request attribute ``sillmstxt.markdown``. Responses without Markdown become a plain 404; redirects keep the ``.md`` suffix. * - ``LlmsTxtMiddleware`` - Answers :file:`llms.txt` / :file:`llms-full.txt` for the site root and every language base. * - ``MarkdownRenderMiddleware`` - 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 :ref:`news`). For normal HTML requests each middleware only compares a string and passes the request on. The event listener ``AddMarkdownAlternateLink`` (``AfterCacheableContentIsGeneratedEvent``) adds the ```` and ```` 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 ``MarkdownRenderMiddleware`` on every request (see :ref:`endpoints-discovery`). .. _developer-classes: Classes ======= .. list-table:: :header-rows: 1 :widths: 30 70 * - Class - Purpose * - ``Service\LlmsTxtBuilder`` - Builds :file:`llms.txt`, :file:`llms-full.txt` and the frontmatter of page and news documents; caching * - ``Service\PageCollector`` - Walks the page tree, language overlay (strict) * - ``Service\NewsCollector`` - News records of EXT:news (optional) * - ``Service\MarkdownConverter`` - HTML → Markdown with league/html-to-markdown; removes navigation, breadcrumbs, asides, forms, buttons, scripts, decorative images and links without text; accordion titles and ```` become h3 headings; keeps button titles in headings; resolves lazy-loaded images; moves card links onto their heading; makes links absolute * - ``Service\DiscoveryLinks`` - ``alternate`` and ``describedby`` links of a page, as HTML tags or HTTP ``Link`` header * - ``Service\MarkdownUrl`` - Page URL ↔ ``.md`` URL * - ``Service\SiteConfig`` - Site set active? Settings, included page types .. _developer-events: 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\SiLlmsTxt\Event\ModifyLlmsTxtEvent`` is dispatched before :file:`llms.txt` or :file:`llms-full.txt` of a site language is built. It only runs on a cache miss. * ``getSite()``, ``getLanguage()``, ``getFileName()`` * ``getTitle()`` / ``setTitle()``, ``getIntro()`` / ``setIntro()`` * ``getSections()`` / ``setSections()``, ``getSection($id)``, ``addSection($id, $section, $before)``, ``removeSection($id)`` * ``addItem($sectionId, $item)``: a missing section is created at the end with the id as title * ``addCacheTags(...$tags)``, ``setCacheLifetime($seconds)`` Section ids: ``pages``, ``section-`` (top-level page), ``articles``, ``news``. The sections "Languages" and "Optional" are added afterwards. A ``Woit\SiLlmsTxt\LlmsTxt\Section`` has ``title`` and ``items``. An ``Woit\SiLlmsTxt\LlmsTxt\Item`` has ``title``, ``url``, ``description``, ``date``, ``includeInFull`` and ``markdown``. An item goes into :file:`llms-full.txt` with its ``markdown`` if set, otherwise only with ``includeInFull``: then ``url`` must be a ``.md`` URL of the same site and is fetched like a page. .. code-block:: php :caption: EXT:my_ext/Classes/EventListener/AddRecordsToLlmsTxt.php 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\SiLlmsTxt\Event\ModifyMarkdownEvent`` is dispatched for every ``.md`` response of a page or EXT:news record. * ``getRequest()``, ``getSite()``, ``getLanguage()`` * ``getTable()`` (``pages`` or ``tx_news_domain_model_news``), ``getRecord()`` * ``getFrontmatter()`` / ``setFrontmatter()``: ``title``, ``description``, ``language``, ``date``, ``lastmod``, ``canonical``, ``source``, ``generator``; empty values are not written * ``getMarkdown()`` / ``setMarkdown()``: body without frontmatter * ``getHtml()``: rendered page, ``null`` for the field "LLM Markdown" * ``setNotFound()``: answer 404 A ``.md`` blocked with ``setNotFound()`` is still listed in :file:`llms.txt`; remove it there with ``ModifyLlmsTxtEvent``. Example: list all records on a list page that renders only the first result page. .. code-block:: php :caption: EXT:my_ext/Classes/EventListener/ListAllRecordsInMarkdown.php 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. .. code-block:: php :caption: EXT:my_ext/Classes/EventListener/DescribeDetailRecord.php 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); } } .. _developer-caching: Caching ======= * Cache ``sillmstxt`` (database backend, group ``pages``) * Entries for :file:`llms.txt` and :file:`llms-full.txt` per site language * Tags: ``pageId_`` of every listed page and the root page; with EXT:news also ``tx_news``, ``tx_news_pid_``, ``tx_news_uid_``; plus the tags of ``ModifyLlmsTxtEvent`` listeners * The intro of the language is part of the cache identifier * :file:`llms-full.txt` with pages that could not be fetched is kept for 5 minutes only * ``.md`` documents are not cached separately: the HTML comes from the page cache, only the conversion runs per request .. _developer-database: Database ======== .. code-block:: sql :caption: ext_tables.sql 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_sillmstxt_exclude`` and ``tx_sillmstxt_priority`` use ``l10n_mode = exclude`` (same value in all languages).