Developer
How the structure is checked
Pagelistens to the PSR-14 eventModule Headlines Modifyof the page module. It only adds a placeholder (the templatePage Layout Content Event Resources/Private/Templates/PageModule/Placeholder.html), so the page module is not delayed.headlines.then loads the box from the AJAX routejs headliner_(structure Structure, which checks that the backend user may see the page), also while the box is collapsed.Controller Structuredoes the work: for every language that the page module shows (Box Selected, which follows the page module, passed asLanguages languagesto the route) it renders the page.Pagerenders the page with a sub-request through the core'sHtml Fetcher Request, in the running PHP process. The frontend middleware changesHandler BE_,USER TYPO3_,REQUEST LANG, the Context aspects and thePage; the fetcher puts them back.Renderer - The request carries the request attribute
headliner.. The frontend middlewareinspect Inspectthen switches the page cache off for it, includes hidden pages in the visibility of the request (it has to run before the page is resolved, which is where a hidden page is turned down) and, with the attributeRequest headliner., renders it at that time (the date aspect andinspect. time SIM_, as the preview of the core does). WithACCESS_ TIME headliner.it renders the page as a member of those frontend user groups (the aspectinspect. groups frontend., so it has to run after the frontend user is authenticated), and withuser headliner.in a workspace (the aspectinspect. workspace workspace, which the repositories of the frontend read to overlay versions). The ViewHelpers then add the marks to the headings:data-(headliner- edit="table: uid: field" headliner:from the record,headline headliner:fromheadline. values edit) anddata-. The attribute exists only inside the process, so a request from outside cannot set it.headliner- element Headingreads the headings (Extractor h1toh6) and the marked headers from the HTML, and the content element each is rendered in: what the heading says itself (data-), else the frameheadliner- element id="c12". It reads the record a heading in a rich text is written in fromdata-on the heading itself, never from an element around it (the page is written by editors). It marks the headings that are not in the elements of the site settingheadliner- record headliner.as outside;content Selector symfony/turns the CSS selector into the XPath the DOM needs.css- selector Headingfinds the problems. Then the eventValidator Modifyis dispatched, so a project can add rules of its own or take built-in ones away (see Your own rules).Heading Issues Event - A heading from the text is matched with its place in the field
bodytextbyRte: the n-th heading of the text, in the order the frontend renders them (the box counts the headings of an element,Headings data-). The select sends the place, the new tag and the text it showed to the AJAX routeheadliner- rte="uid: n" headliner_(rte_ tag Rte);Heading Controller Rtechecks the rights, that the tag is one the editor offers (Heading Editor heading.of the RTE configuration fromoptions Richtext::, in the select and again on saving) and that the text at that place is the same, changes only the tag (get Configuration () <handN> </, the attributes stay) and saves the field through the DataHandler. If the text is not the same, nothing is changed.h N> - The marks for a record that is not the content element come from two places.
Mark, a listener on the core'sRich Text Headings Record, writesCreation Event data-andheadliner- record="table: uid" data-into every heading of the rich text fields of a record object, when the record is created for a request withheadliner- field headliner.: it does not matter how a template renders the text. For a row as an array there is no record to create, soinspect headliner:puts the same mark into the headings of the text (text Heading), with the table from the row or from the inline field of the content element (Mark Row) and the one rich text field of the table.Table Structurethen finds the rich text of each such heading in the record (Box rich: the first heading of a rich text field with the same level and text) and, for a record whose heading names no element, the content element it is linked to from the inline field ofText Of () tt_(content element). The box only believes a mark for a field thatOf Record () Schemasays is a select of tags, on its own page, and for a table the user may edit.Tag Field Editdecides what the backend user may do with each element, and the Fluid templateAccess Resources/Private/Templates/PageModule/Headlines.htmldraws the box.
Your own rules
The box checks four things: a missing H1, more than one H1, a skipped level and an empty heading. A project can change
that with the PSR-14 event Twtgroup. It is dispatched after the built-in
rules have run, once for each language the box shows, while the page module loads the box.
The event gives you the headings of the page (get, in the order of the page, each with level, text and
outside), the page and the language (get, get) and the issues found so far (get). You
can:
- add an issue:
add.Issue (string $type, ?int $index, ?string $label) $typeis a name, prefix it with your extension key (myext.).too Long $indexis the position of the heading inget, orHeadings () nullfor a problem of the page as a whole.$labelis the text of the badge, plain text or anLLL:reference; the box prints it as text, so markup in it is not interpreted. The issue is counted in the summary like the others. - remove the issues of a rule:
remove(the names of the built-in rules areIssues ('multiple H1') missing,H1 multiple,H1 skippedandLevel empty). A site that uses more than one H1 on purpose switches that rule off this way.Heading - replace all issues with
set.Issues ()
use Twtgroup\Headliner\Event\ModifyHeadingIssuesEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;
#[AsEventListener]
final class HeadingTooLong
{
public function __invoke(ModifyHeadingIssuesEvent $event): void
{
foreach ($event->getHeadings() as $index => $heading) {
if (!$heading->outside && mb_strlen($heading->text) > 70) {
$event->addIssue('myext.tooLong', $index, 'LLL:EXT:myext/Resources/Private/Language/locallang.xlf:headingTooLong');
}
}
}
}
What comes back is checked: an issue without a name, or with an index that is not a heading of the page, is ignored. If a listener throws an exception, the box is drawn with the issues of the built-in rules, without what the listeners did, and the exception is written to the log (level error, with the exception in the context), so a broken listener does not take the box down. A listener that is slow cannot be stopped (PHP cannot interrupt it in a web request), the box waits for it; if the listeners take 500 milliseconds or more together, a warning with the time is written to the log. The listener runs every time somebody opens the page in the page module, so keep it quick: the headings are extracted already, do not render or fetch anything in it. A listener does not change what visitors see, only the box.
Logging
The box writes two things to the log, with the logger Twtgroup.: an error if a listener
throws an exception, and a warning if the listeners take 500 milliseconds or more together. TYPO3 writes everything from
warning up to a file in var/ by default, also in the context Production. Nothing else is logged on a normal
call of the box.
The log configuration of TYPO3 decides what happens to it. The most specific configuration replaces the general one, so a
project can set it for the extension (the key Twtgroup, Headliner) in config/system/additional.php:
use Psr\Log\LogLevel;
use TYPO3\CMS\Core\Log\Writer\FileWriter;
// only errors (a listener that throws), no warnings about slow listeners
$GLOBALS['TYPO3_CONF_VARS']['LOG']['Twtgroup']['Headliner']['writerConfiguration'] = [
LogLevel::ERROR => [FileWriter::class => []],
];
// nothing at all: a level with a writer that is switched off
$GLOBALS['TYPO3_CONF_VARS']['LOG']['Twtgroup']['Headliner']['writerConfiguration'] = [
LogLevel::WARNING => [FileWriter::class => ['disabled' => true]],
];
// also in the table sys_log, where the Log module of the backend shows it
$GLOBALS['TYPO3_CONF_VARS']['LOG']['Twtgroup']['Headliner']['writerConfiguration'] = [
LogLevel::WARNING => [
FileWriter::class => [],
\TYPO3\CMS\Core\Log\Writer\DatabaseWriter::class => [],
],
];
An empty array does nothing: TYPO3 then takes the general configuration. A listener that is slow all the time writes one line for every call of the box, which is a sign to make the listener faster.
JavaScript
Resources/ saves the collapsed state with Persistent and changes tags with
@typo3/, then loads the box again. It also loads the box when the page module is shown (see above), and again when the page module changes content without a reload: the event typo3: (hide and show of an element) and typo3: (drag and drop). Opening an element uses the core's
be: with contextual="true"; nothing builds edit URLs by hand.
Tests
ddev test unit
ddev test functional
ddev test js
ddev lint
The JavaScript tests (ddev test js, Vitest with jsdom) cover headlines.: loading the box, the reload after
changes, collapsing, and changing a tag. The unit tests need no TYPO3 bootstrap. The functional tests render the frontend through the real application, with the
site sets of fluid_ and of the extension, and use the testing framework: the tag and layout of the
headers, the subheader and its site setting, the marks for the page module (which a visitor and a request with a header
from outside must never get), the page cache, the fields and the sub-request of the page module.
ddev docs
renders this documentation with the TYPO3 documentation toolchain (Docker). Fix every warning it prints.