Search by

twtgroup / headliner

twtgroup

Shows and validates the heading structure (H1-H6) of a page in the TYPO3 page module.

Package info

github.com/twt-group-gmbh/headliner

Homepage

Documentation

Type:typo3-cms-extension

pkg:composer/twtgroup/headliner

Statistics

Installs: 9

Dependents: 0

Suggesters: 0

Stars: 2

Open Issues: 0

dev-main 2026-11-10 16:27 UTC

This package is auto-updated.

Last update: 2026-11-10 16:28:06 UTC


README

See the heading structure of every page right in the TYPO3 page module, and fix it in place.

TYPO3 14.3 PHP 8.3+ License GPL-2.0-or-later

headliner (twtgroup/headliner) shows the heading structure (H1-H6) of a page above the content in the page module and points out problems: a missing or repeated H1, skipped levels (an H2 followed by an H4) and empty headings. The tag of a header can be changed right there, without touching the look of the page.

The heading structure in the language comparison of the page module

The full documentation is in Documentation/.

Contents: Features · Installation · Setup · Your templates · Extending · Compatibility · Migration · Requirements · Contributing

Features

The box in the page module

  • A box above the content that can be collapsed (the state is remembered per backend user, so it stays collapsed on other pages).
  • It loads after the page module, also while collapsed, so the summary shows the problems and checking the page does not delay the page module.
  • The headings of one content element are drawn as a group (a coloured bar on the left; hovering one marks the others and the element below). A subheader has the badge "Subheader".
  • It updates itself when content changes in the page module: hide or show an element, or move it by drag and drop, and the structure is checked again.
  • With the language comparison of the page module the box has one section per selected language, side by side like the columns of the page module; if they do not fit, it scrolls sideways.

Changing and opening headings

Heading Badge What you can do in the box
Header or subheader of a content element Change the tag with a select (H1-H6 or div); it is saved with the rights of the backend user and the structure is checked again. A click on the text opens the element in the side panel, as the edit button of the page module does.
Text of a content element (RTE) RTE Open the element. With the site setting headliner.editRteHeadings (off by default) also change the tag, to the headings the RTE preset offers (heading.options, H2 and H3 in the default preset), at the place of the heading in the stored text. If the heading is not found there with the same text, it stays a label.
Field of a child record (a card of a block) name of the table Change the tag, see Content Blocks.
Menu, footer and other sources Shown only.

The select of a heading in the box

The select and the link appear only for backend users who may edit the element. Changing the tag also needs the permission for the field Tag / Subheader tag (Allowed excludefields in the backend user group).

Which headings are checked

  • The content of the page is the elements of the site setting headliner.contentSelector (empty by default: the whole page). Without a matching element the whole page is checked.
  • Headings in the header, the menu and the footer are shown very pale, and only their H1 counts: an H1 outside of the content is an H1 of the page, even if the editor cannot change it. Their other levels cause no problem, but an empty heading is a problem wherever it is.

How the page is read

  • The headings come from the rendered frontend of the page, rendered inside the running PHP process. There is no HTTP request, so a firewall, Basic Auth, TLS settings or a CDN in front of the site do not matter.
  • Content elements, RTE content and other extensions are included without any configuration. Only standard pages are checked.
  • A hidden page is rendered too, so you can check it before it is published.
  • A page with a start or end time is rendered at a time when it is published; a page for frontend user groups is rendered as a member of them; in a workspace the box shows the versions of that workspace.

Fields of the content elements

  • Tag (H1-H6 or div, or "from the layout", the default, which is what content did before). It decides the HTML tag of the header, so the hierarchy can be fixed without touching the look: the layout (Type) only sets the CSS class ce-headline-layout-N.
  • Subheader tag, the same for the subheader. The subheader has no layout of its own, it looks like its header ("Hidden" hides both). Without the field (see the site setting below) the subheader is rendered one level below the header.

Installation

composer require twtgroup/headliner

Or download the extension from the TYPO3 Extension Repository and install it with the Extension Manager.

Setup

Add the site set Headliner (twtgroup/headliner) to your site and clear the caches (the site sets and the TypoScript are cached). fluid_styled_content is optional:

  • With it (its set typo3/fluid-styled-content on the site as well) the set overrides the header partial of fluid_styled_content, so its content elements use the fields Tag and Subheader tag. The set loads fluid_styled_content before itself.
  • Without it the set has the site settings and the page TSconfig only, and nothing is reported or logged. The tags in the frontend come from the ViewHelper in your own templates. The box in the page module works the same.

Note

With fluid_styled_content: content without a tag (the field is empty by default, also in the database) keeps the old behaviour (layout 1-6 = H1-H6), so existing pages do not change until an editor chooses a tag.

The subheader is different: fluid_styled_content always renders it as h3, headliner one level below the header (an h2 under an H1, a div below H6). Check pages with subheaders after the installation, or choose H3 in Subheader tag.

Site settings

Setting Default Description
headliner.subheader.enabled on Show the field Subheader tag. If switched off for a site, it is hidden in the backend and the subheader follows the tag of the header.
headliner.editRteHeadings off Allow the tag of a heading in the rich text of a content element to be changed in the box (only to the headings the RTE offers). The text of the element is changed, so it is off by default.
headliner.contentSelector empty CSS selector of the content of the page. The headings in it are checked; the ones around it are shown very pale and only their H1 counts. If nothing matches, or if empty, the whole page is checked.

In your own templates

The content elements of the core need nothing: the site set changes their header partial. For a template of your own use the ViewHelper headliner:headline. Give it the record and it reads the tag and the layout, and names the record for the page module:

<headliner:headline record="{record}">{record -> f:render.text(field: 'header')}</headliner:headline>
<headliner:headline record="{record}" subheader="1">{record -> f:render.text(field: 'subheader')}</headliner:headline>

{record} is the record object of a template of fluid_styled_content, {data} in a Content Block. Without a record (an array, a fixed heading) pass the values: tag, layout and, for the page module, edit. The documentation (Integration) says which arguments you need when, and how child records (cards) work.

Extending with your own rules

The box checks four things: a missing H1, more than one H1, a skipped level and an empty heading. A project changes that with the PSR-14 event Twtgroup\Headliner\Event\ModifyHeadingIssuesEvent. It is dispatched after the built-in rules, once for each language the box shows.

Method Does
getHeadings() The headings of the page, each with level, text and outside.
getIssues() The issues found so far.
addIssue($type, $index, $label) Add an issue ($index = null for a problem of the page). $label is plain text or an LLL: reference.
removeIssues($type) Remove the issues of a rule (missingH1, multipleH1, skippedLevel, emptyHeading).
setIssues($issues) Replace all 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 a listener gives back is checked, and a listener that throws an exception does not take the box down (the built-in issues are shown, the exception is logged). A slow listener is logged as a warning. The listener runs every time the page module is opened, so keep it quick. More in the documentation (Developer).

Compatibility

Content Blocks

The Basics Twtgroup/Headline and Twtgroup/Subheadline give a block the header and subheader with their tags. The headings of a block are grouped, and the headings of its collection records (cards) get a select for their tag and for the headings in their rich text. Give the ViewHelper the record: it reads the tag and names the record for the page module itself. It finds the one tag field of the card, and a card without a tag is one level below the header of the element. Nothing else is needed, the headings in the rich text of the card are found by themselves:

<f:for each="{data.cards}" as="card">
    <headliner:headline record="{card}">{card -> f:render.text(field: 'title')}</headliner:headline>
    {card -> f:render.text(field: 'text')}
</f:for>

Or pass the values and wrap each record in headliner:headline.children, which names the record for the page module:

<f:for each="{data.cards}" as="card">
    <headliner:headline.children record="{card}">
        <headliner:headline tag="{card.tag}">{card -> f:render.text(field: 'title')}</headliner:headline>
        {card -> f:render.text(field: 'text')}
    </headliner:headline.children>
</f:for>

With the record there is no wrapper; with the values there is. Both need a record object, which a Content Block gives. Rows as arrays (from database-query, say) are no records: turn them into records with the core's record-transformation data processor, or pass each row as row to the wrapper (<headliner:headline.children row="{item.data}">). The documentation (Integration) shows the differences.

The box with the headings of a Content Block and of its cards

The same works for child records made with the means of the core. See the documentation (Integration).

Visual Editor

The header partial keeps the f:render.text calls of the partial of fluid_styled_content, which is how visual_editor finds editable texts, so both extensions can be used together. For the content elements of the core headliner needs no ViewHelper in your templates; the ViewHelpers headliner:headline (with record="{record}", or with the values tag and layout) and headliner:headline.children (only for the values) are for your own templates.

Themes with their own header

A theme that does not use the header partial of fluid_styled_content ignores the fields Tag and Subheader tag for the content elements it renders itself. This was seen with the Regular Text Element of typo3/theme-camino: the box shows their headings but cannot change their tag, and some of its own elements (Text Teaser, Text Media Teaser Grid) render paragraphs instead of headings, so they do not appear in the box.

The documentation (Integration) says how to hide the fields for such element types with page TSconfig (TCEFORM.tt_content.header_tag.types.<type>.disabled = 1).

Migration

Coming from the Semantilizer (z7_semantilizer)? The upgrade wizard headlinerMigrateSemantilizerFields takes its field header_type into the tags of headliner:

vendor/bin/typo3 upgrade:run headlinerMigrateSemantilizerFields

See the documentation for what is left to do by hand.

Requirements

  • PHP 8.3+
  • TYPO3 14.3 LTS
  • Optional: fluid_styled_content, for the header partial of its content elements

Contributing

A ready-to-use DDEV demo instance and the test workflow:

ddev start                # installs dependencies and builds TYPO3 at https://headliner.ddev.site/
ddev test all && ddev lint

Backend login: https://headliner.ddev.site/typo3/, user admin, password Password.1 (local demo only).

How to work on the extension, what a change needs and how to write a commit message: see CONTRIBUTING.md.

License

This extension is released under the GNU General Public License 2.0 (or later).