Frontend Theme for Extension Development 

Extension key

theme_extension_development

Package name

sbuerk/theme-extension-development

Version

main

Language

en

Author

sbuerk

License

This document is published under the Open Content License.

Rendered

Mon, 17 Aug 2026 10:46:00 +0000


TYPO3 frontend theme for development purposes: extension development, DDEV based test instances and acceptance tests.


Introduction 

Learn what the extension is for and which TYPO3 and PHP versions are supported.

Installation 

Install the extension in your development or test instance.

Configuration 

Enable the theme for a site and override its templates.

Changelog 

Overview of the changes per released version.

Introduction 

What does it do? 

The Frontend Theme for Extension Development extension provides a TYPO3 frontend theme for development purposes. Its job is to give a TYPO3 installation a reasonable frontend to look at and to render against, without building a site package for it first.

The situations it is built for, where an extension has to be seen or exercised in a frontend rather than only in a test assertion:

  • Extension development, to click through what an extension actually outputs instead of reading the rendered HTML in a test failure.
  • DDEV based test instances of an extension repository, where a throwaway TYPO3 installation needs a frontend rendering pages, navigation and content elements.
  • Acceptance tests, which need a stable and predictable frontend to drive a browser against.
  • Reproducing an issue in a minimal installation before debugging it.

Core version aware implementations 

Code that has to differ between the supported TYPO3 versions lives below Core13/ and Core14/ in the repository root. Shared code — interfaces, abstract base classes and everything working on both core versions — lives in Classes/.

Only the directory matching the running TYPO3 version is registered in the dependency injection container, so a service asking for an interface always receives the implementation matching the current core version.

Compatibility 

Branch Extension TYPO3 PHP
main 2.x v13.4 / v14.3 8.2 - 8.5
1 1.x v12.4 / v13.4 8.1 - 8.4

This manual documents the main branch. Branch 1 is the maintained line for the previous core version tuple and has a manual of its own; PHP 8.1 is supported there for TYPO3 v12 only, because TYPO3 v13.4 requires PHP 8.2.

Contributing 

Contributions are welcome. The development setup, the quality gates and the commit message rules are described in the CONTRIBUTING.md file of the source repository.

Installation 

The extension has to be installed like any other TYPO3 CMS extension.

Composer mode 

Being a development tool, it usually belongs in require-dev — of the extension repository whose frontend is to be looked at, or of the test instance set up for it:

composer require --dev sbuerk/theme-extension-development
Copied!

A TYPO3 extension required that way is installed and activated exactly like any other one, because typo3/cms-composer-installers makes no distinction between require and require-dev. A deployment installing with composer install --no-dev simply leaves it out, which is the point.

Classic mode 

  1. Get it from the Extension Manager: Switch to the module Admin Tools > Extensions, switch to Get Extensions and search for the extension key theme_extension_development, then import the extension from the repository.
  2. Get it from typo3.org: You can always get the current version from TER by downloading the zip version. Upload the file afterwards in the Extension Manager.

Installing the extension does not render anything yet. The theme still has to be enabled for a site — see Configuration.

Configuration 

Enabling the theme for a site 

The theme ships a site set. A site enables it by depending on that set in its config/sites/<identifier>/config.yaml:

dependencies:
  - sbuerk/theme-extension-development
Copied!

Nothing else is required. The set brings the TypoScript, the page rendering and the stylesheet with it, and no sys_template record is needed. The set itself declares neither dependencies nor settings — everything an integrator changes is a TypoScript constant, and those are described below.

In the backend the same set can be selected under Site Management > Sites in the Sets field of the site.

Installations without site sets 

For an installation that does not use site sets, the theme additionally registers a classic static template. Create a sys_template record on the root page and include Theme Extension Development in Include static (from extensions).

Both paths read the same TypoScript files, so what they deliver is identical.

Templates and stylesheet 

The Fluid paths and the stylesheet are TypoScript constants, so an integrator can render their own templates without editing the theme:

Constant Default
theme.templateRootPath EXT:theme_extension_development/Resources/Private/Templates/
theme.partialRootPath EXT:theme_extension_development/Resources/Private/Partials/
theme.layoutRootPath EXT:theme_extension_development/Resources/Private/Layouts/
theme.stylesheet EXT:theme_extension_development/Resources/Public/Css/theme.css
theme {
    templateRootPath = EXT:my_site_package/Resources/Private/Templates/
    partialRootPath = EXT:my_site_package/Resources/Private/Partials/
    layoutRootPath = EXT:my_site_package/Resources/Private/Layouts/
    stylesheet = EXT:my_site_package/Resources/Public/Css/my-theme.css
}
Copied!

The stylesheet is compiled from SCSS sources that ship with the extension, so it can also be rebuilt with different design tokens instead of being replaced — see Feature: Design tokens, with light and dark appearance.

Image width 

The image based elements scale their images to the width the layout gives the content column. That width is a constant, because nothing in TypoScript can read it out of the stylesheet:

theme.media {
    # The width in pixels the gallery is computed for.
    maxGalleryWidth = 1200

    # The same, for an element positioned beside the text.
    maxGalleryWidthInText = 420
}
Copied!

The default matches the 75rem of --theme-content-max-width. Set it too low and images are processed smaller than they are displayed, which shows. Set it far too high and every image is processed at a size no visitor ever sees.

Appearance and palette 

The theme renders in a light and a dark appearance and carries five colour palettes. What is rendered server side — before the frontend switcher can restore a visitor's choice from localStorage — is configured with three constants:

Constant Default Values
theme.appearance.default auto auto, light, dark
theme.appearance.palette neutral neutral, ember, ocean, moss, violet
theme.appearance.contentOutline on on, off

They are written onto the <html> tag as data-theme, data-palette and data-theme-content-outline. Two of those are worth knowing exactly:

  • auto renders no data-theme attribute at all. The appearance is then left to the operating system through color-scheme, which is what light-dark() in the stylesheet resolves against. A palette has no such case, so neutral is still written out.
  • contentOutline draws the labelled outline around every content element. Only off has a rule of its own; on is simply the absence of it. It is a development and staging affordance — a site package rendering for real visitors sets it to off.

See Feature: Appearance switcher for the switcher itself and Feature: Design tokens, with light and dark appearance for what the palettes are built from.

Backend layouts and page templates 

The theme ships five backend layouts through page TSconfig, and the layout an editor selects picks the Fluid template the page is rendered with:

Identifier Backend label Columns (colPos) Page template
default Default main (0) Page/Default.html
content Content page stage (2), main (0), footer 1-4 (11-14), footer meta (10) Page/Content.html
content_sidebar Content page with sidebar stage (2), main (0), sidebar (1), footer 1-4 (11-14), footer meta (10) Page/ContentSidebar.html
start Start page stage (2), main (0), footer 1-4 (11-14), footer meta (10) Page/Start.html
styleguide Styleguide unused (999) Page/Styleguide.html

The colPos numbers are a contract, not an implementation detail: the same number means the same slot in every layout, which is what lets an editor change a page's layout without content disappearing. The footer columns and the footer meta row slide down the rootline, so they are edited once on the start page and appear on every page below it.

The mapping is by convention rather than by configuration: the identifier is upper-camel-cased and prefixed with Page/, so a new layout needs a TSconfig file and a template of the matching name and nothing else. A page without a layout, and a page whose layout is TYPO3's built-in [None], both render with Page/Default.html.

See Feature: Backend layouts decide the page template.

Content elements 

The theme brings its own content element rendering and does not depend on fluid_styled_content — that extension is not required here, and on TYPO3 v14 it is not installed at all. What that covers:

Nothing an editor can create is left without a rendering definition, and a test asserts exactly that: it walks the content types registered in TCA and fails if the core's "no rendering definition" notice appears for any of them.

Demo content 

A page tree to look at is written by a console command rather than by hand:

vendor/bin/typo3 theme:seed
Copied!
Argument or option Default Meaning
definition EXT:theme_extension_development/Configuration/Seeds/Demo.yaml The YAML definition to write. An EXT: path is resolved.
--root-page 0 The page the definition is written below. 0 is the page tree root.
--force Seed even though the page tree is not empty. A definition declaring uids will collide.

The shipped definition seeds a start page, pages for typography and media, one page deliberately without a backend layout, a /elements branch carrying every content type, and a /styleguide page rendering the whole component library — see Feature: A seeded showcase of every element and Feature: A styleguide page. The definition format is described in Feature: Seed a page tree from a definition.

Changelog 

Every notable change to the Frontend Theme for Extension Development extension is documented here, grouped by version and change type.

2.0 Changes 

Table of contents

Breaking Changes 

Features 

Deprecation 

Important 

Feature: Appearance switcher 

Description 

The theme now ships the script and the control that let a visitor change appearance, palette, and the main menu - the piece Feature: Navigation shipped without. Three new TypoScript constants under theme.appearance, in Configuration/TypoScript/Appearance.typoscript, set the server-rendered default; an inline script and Resources/Public/JavaScript/theme.js apply a visitor's stored choice and operate the controls from there.

Constant Values Default
theme.appearance.default auto, light, dark auto
theme.appearance.palette neutral, ember, ocean, moss, violet neutral
theme.appearance.contentOutline on, off on

These are constants, not Site Settings. A constant is read identically by the site set and by the classic sys_template static include, which is the property this theme is built around; a settings.definitions.yaml would only serve the set and introduce a second source of truth for the same value. A site package that wants these editable in the Site Settings UI can still declare them in its own set.

Server-rendered attributes 

config.htmlTag.attributes.* writes every value onto the <html> tag raw - RequestHandler::generateHtmlTag() only applies stdWrap when a matching . sub-array is configured, and none is here - so a constant is the only thing that can go on this path, never a cObject:

config.htmlTag.attributes.data-palette = {$theme.appearance.palette}
config.htmlTag.attributes.data-theme-content-outline = {$theme.appearance.contentOutline}

["{$theme.appearance.default}" != "auto"]
    config.htmlTag.attributes.data-theme = {$theme.appearance.default}
[END]
Copied!

data-theme is behind that condition rather than assigned directly because auto means the absence of the attribute, not a value for it - no selector matches data-theme="auto", and rendering it would look like a working default while doing nothing.

The no-flash script 

An inline script in the document head applies a stored appearance and palette before first paint. It is emitted through page.headerData rather than f:asset.script, because the asset collector may move a script to the end of the body, and a stored dark appearance would then paint light first - the exact flash this script exists to prevent.

It sets data-js on the root first and unconditionally, then reads localStorage for the stored appearance and palette, each inside its own try/catch. localStorage throws rather than returning null in Safari's private mode and when cookies are blocked, and an uncaught throw would abort the script before the marker was set - which is why the marker comes first, not after. Losing it costs more than a missed colour choice: data-js is also what the navigation collapse and the switcher's visibility are gated behind, so a page that lost it keeps the always-expanded navigation and a permanently hidden switcher for the rest of that visit.

The switcher itself is hidden until data-js is set, for the same reason: a control that cannot work yet is a visible promise the page cannot keep, not a degraded one.

The module script 

Resources/Public/JavaScript/theme.js, loaded with page.includeJSFooter and .type = module. A module is deferred by specification, so it needs no defer attribute, and it runs in the footer rather than the head because it only wires up listeners on elements that already exist by then. It owns the appearance control, the palette control, and - now that its script has arrived - the main menu toggle that Feature: Navigation shipped inert.

The file uses no optional chaining (?.) anywhere. A browser that does not recognise type="module" skips the element unparsed and can never fail on syntax inside it, but "recognises modules" and "supports optional chaining" are not the same floor - module support landed 2017-2018, optional chaining in 2020 - and a syntax error anywhere in a module aborts the whole file, with no per-statement fallback the way a classic script has. Plain if checks cost nothing and remove that failure mode entirely.

Palette swatches 

Each palette button in the switcher carries a small swatch in that palette's primary colour. The swatch colour is the one duplicated colour in the stylesheet: every palette lives entirely inside its own [data-palette='…'] selector, and CSS has no mechanism to ask what a custom property would resolve to under a different attribute value, so the swatch cannot reference the token live. ComponentLibraryTest::everyPaletteHasASwatchInTheSwitcher is what keeps the copy from drifting out of step with abstracts/_palettes.scss.

Impact 

No cookie is set anywhere in this mechanism, and nothing is persisted server side. localStorage is the whole story - two keys, theme-appearance and theme-palette - which is also why there is no consent question to design around: the choice never leaves the browser it was made in.

The main menu toggle, shipped inert in Feature: Navigation, is now fully wired: clicking it flips aria-expanded, Escape and an outside click close the menu again. Nothing about the navigation's no-JavaScript layout changes - the collapse remains gated behind data-js, exactly as before.

Feature: Backend layouts decide the page template 

Description 

The theme ships five backend layouts, and the layout selected on a page now decides which template renders it. Until now every page rendered the same file.

Layout Template Content areas
Default Page/Default.html main
Content page Page/Content.html stage, main, four footer columns, footermeta
Content page with sidebar Page/ContentSidebar.html the same, plus sidebar beside main
Start page Page/Start.html the same as Content page
Styleguide Page/Styleguide.html none - it renders components directly

The column numbers are the ones typo3/theme-camino uses, so content is portable between the two themes.

Registration 

The layouts are page TSconfig, in Configuration/page.tsconfig, which TYPO3 auto-loads from every package since v12.0 (forge#96614). No registration call and no database record is involved, and it applies whether the theme is delivered through its site set or through the classic sys_template static include - the alternatives would each only work for one of those.

Overriding one layout means overriding one file:

mod.web_layout.BackendLayouts.content.config.backend_layout.rows.2.columns.1.colPos = 5
Copied!

Content areas 

Each column is its own TypoScript object, so a site package can replace one without touching the rest:

<f:cObject typoscriptObjectPath="lib.content.main" />
Copied!

The four footer columns and the footer meta row carry slide = -1. Footer content is therefore edited once on the site root and inherited by every page below it - what typo3/theme-camino gets from slideMode = slide, without depending on typo3/cms-fluid-styled-content .

Impact 

The template is resolved with data = pagelayout, not field = backend_layout. The getter resolves through PageLayoutResolver, which falls back to the first ancestor's backend_layout_next_level when a page carries no layout of its own. Reading the field directly would ignore that, and every sub-page of a configured parent would silently render the wrong template.

Two edges of that inheritance are worth knowing, and both are covered by Tests/Functional/BackendLayoutRenderingTest.php:

  • A page's own backend_layout_next_level applies to its children and never to itself - the resolver removes the current page from the rootline before searching.
  • Choosing TYPO3's built-in [None] option in the page properties resolves to the literal identifier none, which is not empty. It is mapped back to default; without that it would ask for a Page/None.html that no theme ships and end the request in an exception.

Every column declares an identifier as well as a name and a colPos. TYPO3 v14 raises a deprecation for a column without one and will throw in v15; v13 ignores it, so one spelling serves both versions.

Feature: Component library 

Description 

The theme now ships a component library built entirely from the design tokens: an element baseline covering everything a rich text editor can emit, native form controls with their validation states, and the components a page is composed from - navigation, breadcrumb, hero, teaser, card, alert, badge, accordion, quote, table, pagination, gallery, and the page chrome.

There is no CSS framework. No Bootstrap, no Tailwind, no reset library, and no external request of any kind.

Class names carry a theme- prefix. That is not decoration: this theme exists to host other extensions while they are being developed, those extensions bring their own stylesheets, and an unprefixed .button or .card would collide with them.

Component tokens 

Each component declares its own token layer, every entry falling back to a global token and then to a literal:

.theme-card {
    --theme-card-background: var(--theme-color-surface, #f4f6fa);

    background-color: var(--theme-card-background);
}
Copied!

A site package can therefore re-theme globally by moving --theme-color-surface , or surgically by moving --theme-card-background , without touching the other. A modifier re-points a token rather than restating a property.

The fallback literal appears once per component rather than once per declaration, which keeps abstracts/_tokens.scss the single source of truth. It also makes a component portable into a shadow root: custom properties inherit across a shadow boundary, so only the token file has to live in the outer document.

The CType outline 

Every content element is wrapped in .theme-content-element , which draws a dashed outline and a small chip naming its CType. It exists so that the column and element boundaries a backend layout produces are visible without opening the page module.

It is a development aid, and it is switched off wholesale with one attribute:

<html data-theme-content-outline="off">
Copied!

A single element opts out with .theme-content-element--plain .

Impact 

Nothing renders differently yet: the Fluid templates that emit this markup are not part of this release. What ships is the stylesheet those templates will be written against, and the markup contract each component documents in its own file header.

The compiled stylesheet grows from roughly 6 kB to roughly 54 kB, uncompressed and before transport compression.

Feature: Content element rendering 

Description 

The theme renders content elements itself, without depending on fluid_styled_content.

Two content elements are rendered:

Content element Rendered
Header The heading, at the level chosen in Type. The Hidden option of that field is honoured.
Text The heading and the rich text of the element.

Both are wrapped in an element frame carrying the familiar c<uid> anchor, so links to a content element keep working.

Overriding the templates 

The element templates live below Resources/Private/Templates/ContentElements/ and use the same Fluid paths as the page templates, so the constants under theme. redirect them together - see Configuration.

The rendering definition itself is the TypoScript object lib.contentElement, which fills the role the object of the same name has in fluid_styled_content.

Feature: Core content elements 

Description 

The theme now renders the rest of the classic content element set:

Content element Rendered
Text & Images Images, positioned beside Text.
Text & Media The same, from the Media field, which is not restricted to images.
Bullet List Text as a list, unordered, ordered or a definition list, per Type of list.
Table Text as a table, honouring caption, delimiter, enclosure, header position and footer.
File Links The Files and Collections fields as a file list - name, optional size, optional thumbnail.
Insert Records The referenced records, rendered exactly as they render on their own.
Divider A horizontal rule.
HTML Text, unescaped.

Together with Header, Text and Images (Feature: Content element rendering, Feature: Rendering of the Images element), every classic content element EXT:frontend registers now renders, except the eleven menu elements - tracked separately, they need a MenuProcessor configured per menu type rather than only a template.

No TCA of this extension's own is added for any of them. Every one of these elements was already creatable in the backend before this change - their TCA comes from EXT:frontend, on TYPO3 v13.4 and v14 alike, and fluid_styled_content is not a dependency of this theme on either version - so what changes is only that they now render instead of TYPO3's own "no rendering definition" notice.

Two elements needed a decision 

Table 

Text for Table is delimited text, and the delimiter and enclosure fields are stored as TCA character codes, not characters. Neither core data processor is enough on its own: SplitProcessor splits on one delimiter into a flat list, with no nesting and no quoting; CommaSeparatedValueProcessor is built for exactly this field but has no stdWrap property that turns a numeric code into a character, and does not shape a header row, a header column or a footer row.

SBUERKThemeExtensionDevelopmentDataProcessingTableProcessor does the decode and the shaping. One PHP detail is worth knowing if this class is ever touched: the enclosure field's own default is "None" (code 0), and PHP's fgetcsv() throws a ValueError when handed an empty enclosure string - which is what the TYPO3 backend's own table wizard falls back to for that same "None" option. chr(0) is used instead, so the field's default configuration does not throw.

Insert Records 

Insert Records renders other content elements, through the core's RECORDS cObject configured with conf.tt_content =< tt_content - a referenced record renders through the very same object this theme builds for every content element, including, if it is itself an Insert Records element, going through this same branch again.

Escaping 

Table cells, bullet items, the table caption and file names/descriptions are plain Fluid interpolation, HTML-escaped. Text & Images and Text & Media run their Text field through f:format.html (the RTE parse), the same as the existing Text element. HTML is the one deliberate exception: f:format.raw, completely unescaped - not f:format.html, which would parse it and change it. Access to HTML is restricted the same way as every CType: the field carries authMode = explicitAllow, and it is a site administration decision, not something this theme enforces, which backend groups are actually granted it.

Impact 

An installation using the theme no longer renders the TYPO3 "no rendering definition" notice for any of the elements listed above. Tests/Functional/CoreContentElementRenderingTest.php sweeps every covered CType and fails if any of them regresses to the notice.

The markup is generated from the templates below Resources/Private/Templates/ContentElements/, redirected together with every other template through the Fluid path constants under theme. - see Configuration. See docs/architecture/content-elements.md in the developer documentation for the full coverage table, the bullets_type/table_*/uploads_type field-by-field reasoning, and the recursion finding above.

Feature: Design tokens, with light and dark appearance 

Description 

The stylesheet is built from a documented set of design tokens covering typography, colour, spacing, border radius, borders, focus, controls, stacking, motion and layout width. Every one of them is a CSS custom property.

Every colour is declared once and carries both appearances, using the CSS light-dark() function:

:root {
    color-scheme: light dark;
    --theme-color-background: light-dark(#ffffff, #0f1319);
}
Copied!

Appearance 

The theme follows the appearance the visitor asked for. The operating system decides by default, and an explicit request on the root element overrules it in both directions:

<html data-theme="dark">
<html data-theme="light">
Copied!
:root                     { color-scheme: light dark; }
:root[data-theme='light'] { color-scheme: light; }
:root[data-theme='dark']  { color-scheme: dark; }
Copied!

That is the whole mechanism. It works because light-dark() resolves against the used value of color-scheme , so switching one property switches every colour. It does not work by influencing what prefers-color-scheme matches, which is tied to the operating system and cannot be changed from CSS.

Setting color-scheme per appearance also makes form controls, scrollbars and the canvas follow along.

No JavaScript ships with the extension - the attribute is there for whoever wants to build a switch.

Palettes 

Four alternate palettes ship alongside the neutral default. A palette varies accents only - primary , secondary , their hover states and the focus ring - while neutrals, semantic colour, spacing, radius and typography stay shared:

<html data-palette="ocean">
Copied!

Available: ember, ocean, moss, violet. Omitting the attribute selects the neutral default.

For a theme whose purpose is extension development, the palettes are a test surface rather than decoration: an extension that renders correctly across every palette in both appearances is one that is not hardcoding colour.

Re-theming without a build 

Custom properties survive compilation, so a site package can re-theme the extension from its own CSS without rebuilding the SCSS:

:root {
    --theme-color-primary: light-dark(#7b1fa2, #ce93d8);
    --theme-font-family-sans: 'Mulish', system-ui, sans-serif;
}
Copied!

Impact 

The palette is deliberately neutral. This is a theme for extension development, and its job is to make document structure legible without biasing the design of the extension being built against it.

Every colour was checked for contrast against both the background and the surface of its own appearance: body text clears 4.5:1 and any border that delimits a control clears 3:1. Semantic colour carries three tokens per meaning - the accent for text and borders, --theme-color-on-* for a foreground on a solid fill, and --theme-color-*-surface for the soft tint an alert sits on - because one value cannot do all three jobs.

The design is flat. There are no elevation tokens, and the only shadow is the focus ring, because a visible focus indicator is a requirement rather than decoration. It is split into --theme-focus-ring-color and a composite built around it, because light-dark() takes colours and not shadows.

Feature: Extbase plugin rendering 

Description 

A third-party Extbase plugin now renders on an installation using this theme, whether it is registered as a dedicated CType (the way TYPO3CMSExtbaseUtilityExtensionUtility::configurePlugin() recommends, and the only registration TYPO3 v14 accepts) or, on TYPO3 v13.4 only, through the historical General Plugin / list registration.

configurePlugin() generates tt_content.<pluginSignature> =< lib.contentElement for every plugin, unconditionally, on both installed core versions - even on v14, where nothing outside this theme defines lib.contentElement at all, because fluid_styled_content is not installed there. Before this change nothing rendered that object's templateName = Generic, so every such plugin fell through to TYPO3's own "no rendering definition" notice, indistinguishable to an editor from a broken content element.

Resources/Private/Templates/Generic.html is the new template that fixes that - shared by every plugin regardless of extension, because templateName = Generic is a fixed string the core writes itself, not something a plugin author controls. It reads back the per-plugin 20 cObject the core places beside templateName at a path built from the record being rendered (tt_content.{data.CType}.20), rather than a fixed one, so the one template serves every plugin without knowing which one it is.

General Plugin / list 

TYPO3 v13.4 still offers the historical registration TCA (types.list in EXT:frontend's own Configuration/TCA/tt_content.php), deprecated but present (Deprecation forge#105076). fluid_styled_content supplied the tt_content.list object that rendered it, as a CASE keyed on the plugin's Type (list_type) field, and removed it outright in v14.0 together with the list CType itself (Breaking forge#105377). This theme now supplies that object too, in its own house style, reusing the same Generic.html template - a list record's own CType is list, so the same {data.CType}.20 path resolves to the CASE rather than to a single plugin.

It is declared unconditionally rather than behind a version condition: on v14 the list_type database column itself was dropped along with the CType, so nothing can ever reach the branch - verified directly against the installed v14.3.6 core, not only argued from the changelog. See docs/architecture/content-elements.md in the developer documentation for the full verification and the reasoning for leaving it unconditional.

Impact 

An Extbase plugin registered by a third-party extension - one this theme does not control the TypoScript of - now renders instead of the core's "no rendering definition" notice, on both TYPO3 v13.4 and v14.3, regardless of whether it is registered as its own CType or, on v13.4, through the historical General Plugin type.

Tests/Functional/Fixtures/Extensions/plugin-fixture is a fixture extension that registers a plugin with no TypoScript rendering definition of its own - unlike Tests/Functional/Fixtures/Extensions/ example-fixture, which deliberately overrides what configurePlugin() generates - so the only thing that can make it render is this theme's own lib.contentElement and Generic.html.

Feature: Rendering of the Images element 

Description 

The content element Images (CType image) is rendered by the theme.

No TCA is added for it. The element is registered by EXT:frontend, in Configuration/TCA/Overrides/225-tt_content-content_type-image.php, on TYPO3 v13.4 and v14 alike - what fluid_styled_content contributes for it is the rendering, and that is what this theme now brings itself.

The backend fields of the element decide the layout, and all of them are honoured:

Field Effect
Images The file references that are rendered, in their sorted order.
Number of columns The number of images per row.
Position and alignment Rendered as the modifier classes gallery--above, gallery--below, gallery--intext, gallery--left, gallery--center and gallery--right.
Width / Height A fixed dimension all images are scaled to, with the row scaled down when it would exceed the gallery width.
Enable click-enlarge Wraps the image in a link to the original file. No lightbox: this extension ships no JavaScript.

The Alternative text, Title and Description of a file reference become the alt attribute, the title attribute and a <figcaption>.

Every image is rendered with a width and a height attribute, so the browser can reserve its box before the file has loaded.

Impact 

An installation using the theme no longer renders the TYPO3 "no rendering definition" notice for Images.

The width the gallery is computed for is a constant and should match the width the layout gives the content column:

theme.media {
    maxGalleryWidth = 1200
    maxGalleryWidthInText = 420
}
Copied!

The markup is generated from Resources/Private/Templates/ContentElements/Image.html and Resources/Private/Partials/ContentElement/Gallery.html, which the Fluid path constants redirect together with every other template - see Configuration.

Feature: Initial theme development package 

Description 

Initial release of sbuerk/theme-extension-development. The package is a TYPO3 frontend theme for development purposes: its job is to give a TYPO3 installation a reasonable frontend to look at and to render against, without building a site package for it first — for extension development, for DDEV based test instances and for acceptance tests.

What this release provides:

  • TYPO3 v13 and v14 support on PHP 8.2 up to 8.5, with core version aware implementations below Core13/ and Core14/.
  • Dependency injection wiring through Configuration/Services.php, with services configured by Symfony dependency injection attributes on the classes themselves.
  • Container based tooling through Build/Scripts/runTests.sh covering linting, coding guidelines, static analysis, unit and functional tests and documentation rendering.
  • GitHub Actions workflows running these gates for TYPO3 v13 and v14 on pull requests.
  • A functional test setup ready to build on: strict PHPUnit configuration, an example fixture extension loaded by its composer package name, site based tests issuing frontend sub-requests in several languages, and repository tests running in a built frontend environment.
  • Developer documentation below docs/, covering the architecture, the quality gates, both test suites and the release workflow.

The extension ships a SBUERKThemeExtensionDevelopmentDummy placeholder class and an SBUERKThemeExtensionDevelopmentExampleExampleInterface example service, both meant to be removed once the first real implementation is added.

Feature: Menu content elements 

Description 

The theme now renders the eleven menu_* content elements EXT:frontend registers - the last of the classic content element set left uncovered by Feature: Core content elements:

Content element Rendered
Pages A link per page in Pages, or the site root's own subpages when none are selected.
Subpages A link per child of the selected pages, or the current page's own children.
Section index The same, two levels deep, rooted at the current page when Pages is empty.
Section index of subpages from selected pages Subpages, two levels deep.
Sitemap The whole site, seven levels down from the site root.
Sitemaps of selected pages The same, rooted at the selected pages instead.
Abstracts Subpages, each link followed by that page's own Abstract field.
Recently updated pages Pages beneath the selection (or the current page), sorted by last change, each link followed by that date.
Related pages Pages beneath the same entry point sharing a keyword with it.
Categorized pages Pages carrying the selected category.
Categorized content Content elements carrying the selected category, linked by heading and anchor.

No TCA of this extension's own is added for any of them, the same as every other element Feature: Core content elements covered: all eleven were already creatable in the backend before this change, on TYPO3 v13.4 and v14 alike, and only rendered TYPO3's own "no rendering definition" notice.

Nine on MenuProcessor, two on a category query 

Nine of the eleven are the core's MenuProcessor, unchanged between v13.4 and v14.3, configured with a different special per type - list, directory, updated or keywords - and, for two of them, one extra level. Abstracts and Recently updated pages needed no data processor beyond that: MenuProcessor already JSON-encodes the whole page row onto every menu item, so the abstract text and the last-changed timestamp were already there for the reading.

Categorized pages and Categorized content select by category membership, which MenuProcessor cannot express at all, and are built on two different mechanisms on purpose, not by accident:

  • Categorized pages uses the core's RECORDS cObject, which can select by category directly.
  • Categorized content uses TYPO3CMSFrontendDataProcessingDatabaseQueryProcessor with a subquery against sys_category_record_mm instead of RECORDS, even though RECORDS could select the same rows. Rendering through RECORDS would render every matched content element in full, nested inside this one - the wrong shape for a menu, and exposed to the same reference-cycle risk documented for Insert Records in Feature: Core content elements, which TYPO3 v14 does not guard at all. Rendering the matched rows as links instead means nothing can nest, so the cycle cannot form in the first place - no structural break was needed here the way one was added for Insert Records.

Markup 

A single new component, .theme-content-menu, is shared by all eleven elements - a list of links, optionally carrying a date or an abstract line, nested one level for Sitemap's tree. It is deliberately not the existing sub-navigation component: a menu_* element is authored content in the content column, not section-scoped site chrome, and reusing the navigation component would pull navigation styling into content rendering. See docs/development/component-library.md for the markup contract.

Known gap 

Historical fluid_styled_content additionally embedded each listed page's own content elements flagged "section index" into Section index and Section index of subpages from selected pages, linked by anchor. That is not implemented here. A second menu level - the listed pages' own children - stands in for it instead. A site package that needs anchor-level section navigation has to add it itself.

Impact 

An installation using the theme no longer renders the TYPO3 "no rendering definition" notice for any menu_* element. Tests/Functional/CoreContentElementRenderingTest.php sweeps all eleven together with every other covered CType and fails if any of them regresses to the notice; two further assertions render past the wrapper to confirm a menu actually lists what it should, and that the two categorized elements actually select by category rather than rendering a correct but empty wrapper.

The markup is generated from the templates below Resources/Private/Templates/ContentElements/, redirected together with every other template through the Fluid path constants under theme. - see Configuration. See docs/architecture/content-elements.md in the developer documentation for the full special/level table, the category-query reasoning, and the known gap above.

Feature: Navigation 

Description 

The theme now renders its three navigations: a main menu, a left-hand sub navigation for the current section, and a breadcrumb trail. All three are the same core MenuProcessor, configured three different ways, in Configuration/TypoScript/Navigation.typoscript.

Navigation Source Rendered on
Main menu Site root, two levels every page, in the header
Sub navigation The current page's section, two levels the Content page with sidebar layout only
Breadcrumb The rootline, root to current page Content page and Content page with sidebar

The sub navigation fixes a rootline position, not the current page 

The sub navigation is configured against the current page's first-level ancestor, not the current page itself, using special.value.data = leveluid:1 rather than entryLevel.

The difference matters because it is easy to get backwards, and the wrong choice looks correct in the one place it is checked first. entryLevel is relative to the current page's depth - the same value resolves to a different ancestor on a second-level page than on a third-level page. leveluid:1 instead indexes the current page's rootline directly: index 0 is always the site root and index 1 is always the first page below it, on every page regardless of how deep it is.

That is what keeps the sidebar showing the same section three levels down as it does on the section's own landing page, rather than emptying out underneath it. A sub navigation built from the current page's own children looks entirely correct on a first-level page - there, the section root is the current page - and only fails on the pages below it, exactly where a reader needs the navigation most. Tests/Functional/NavigationRenderingTest.php therefore runs against a fixture three levels deep and asserts the same section content at all three, rather than stopping at the depth where the bug would already be invisible.

Placement follows the backend layout 

Each navigation is its own numbered TypoScript key (page.10.dataProcessing.10/.20/.30), so a site package can remove exactly one without touching the others:

page.10.dataProcessing.20 >
Copied!

The sub navigation is rendered only on the Content page with sidebar layout - the one layout with a left column, per Backend layouts decide the page template. Choosing that layout in the page module is how an editor asks for the left navigation; there is no separate flag. The breadcrumb renders on Content page and Content page with sidebar, and deliberately not on Start page (no trail is worth showing there) or Default (the bare layout, which is also the one layout that already renders the page title itself).

Accessibility 

  • Every <nav> carries a translated aria-label - three navigation landmarks on one page are indistinguishable to a screen reader without one.
  • The current page carries aria-current="page", and the stylesheet styles that attribute directly rather than a modifier class kept in sync with it - the visual state and the announced state cannot disagree.
  • The breadcrumb's last item is plain text with aria-current="page" on the list item, not a link: the destination of a breadcrumb is not somewhere it still points to.
  • The breadcrumb separator is generated content on a ::before , never a character in the markup, so it is not part of the accessible name and is not announced.

Impact 

The main menu's toggle button ships without its script. The markup - the <button aria-expanded="false" aria-controls="nav-main"> and the matching id on the list - is in place, but the script that flips aria-expanded and the data-js marker the stylesheet gates collapsing behind both arrive with the appearance switcher. Until then the button is inert and hidden by CSS, and the menu is simply always expanded - the intended, working state without JavaScript, not a degraded one. See Feature: Component library for the data-js switch itself.

Feature: A seeded showcase of every element 

Description 

Feature: Seed a page tree from a definition writes a page tree from a YAML definition. The shipped definition EXT:theme_extension_development/Configuration/Seeds/Demo.yaml now describes a tree that demonstrates the whole theme rather than a handful of pages:

Page Backend layout What it shows
Theme demo (/) Start page The site root, and the footer columns that layout adds.
Typography (/typography) Content page Headings, running text and the inline cases a stylesheet has to answer for.
Media (/media) Content page A single image, and a two column gallery.
Empty page (/empty) none A page with no layout selected, which falls back to the default.
Elements (/elements) Content page The showcase branch, and the parent of the three pages below.
Core elements (/elements/core) Content page with sidebar Every classic content element the theme renders, once each.
Menu elements (/elements/menu) Content page with sidebar All eleven menu elements, each pointed at a different part of the tree so they are told apart by what they list.
Theme elements (/elements/theme) Content page All ten elements the extension registers itself, with their inline children filled in.
Styleguide (/styleguide) Styleguide The component library, rendered from Fluid rather than from content.

Between them the pages use every backend layout the extension registers and carry every content type it renders, so a single seeded instance answers what the theme does with each. Two of the pages are deliberate special cases: Empty page selects no backend layout at all, which is the only way to see the default fallback, and Styleguide is set to Page not enabled in menus rather than disabled - a disabled page returns 404 in the frontend and is only reachable through a preview link, which defeats the point of seeding a page that exists to be opened.

Inline children in the seed format 

Four of the theme's own content elements - Author, Link list, Social links and Media teaser grid - read their entries from an inline child table. A seed definition can now describe those entries with a new structural key, inline: a map of the field on the parent record to the child records declared for it.

content:
  - identifier: showcase-linklist
    CType: theme_linklist
    header: 'Where to read more'
    inline:
      tx_theme_list_items:
        - identifier: showcase-docs
          table: tx_theme_list_item
          link: 't3://page?uid=2'
          link_label: 'Typography'
        - identifier: showcase-media
          table: tx_theme_list_item
          link: 't3://page?uid=3'
          link_label: 'Media'
Copied!

Each child names the table it belongs to. That is never inferred from the TCA of the parent's field, so a definition stays readable on its own and a mistyped field name is reported rather than dereferenced. The children come out in the order they are declared, and they may carry uid and files like any other record.

The structural keys of the format are therefore identifier, uid, children, content, files and inline, plus table on an inline child. Everything else is a field of the record and is written as it stands - which is why the backend layout and the "hide in menus" flag of the pages above need nothing from the seeding at all.

Identifiers may no longer contain an underscore 

An identifier in a seed definition may contain letters, digits and dashes, and has to start with a letter or a digit. A definition using anything else is now rejected with an exception naming the identifier.

This is not a style rule. The identifier ends up inside the placeholder DataHandler is given for the record, and a placeholder used as the value of a relation field is read as the <table>_<uid> form when it contains an underscore - so NEWtt_content_home is split into a table NEWtt_content and an id home, neither of which resolves. The relation is then written empty, with nothing logged. Rejecting the identifier is what turns a seed that silently loses its relations into one that refuses to run.

Two fixes come with it 

Both failed silently, and both are now covered by a regression test:

  • A declared uid was not honoured. DataHandler reads a suggested uid from the data map row and looks it up under a <table>:<uid> key; the seeding supplied neither, so the next free uid was assigned and the command reported whatever it got. That looked correct only for as long as the declaration order of a definition happened to match its insertion order.
  • File references were not ordered. The placeholder of a reference had the same underscore problem, so sorting_foreign stayed at 0 on every seeded reference and the order of a gallery with more than one image was left to the database.

Known limitation 

Categorized pages and Categorized content are part of the seeded tree but select nothing: the format expresses neither sys_category records nor the MM rows relating them to a page or a content element. Both elements render an empty menu, which is the correct rendering of "no category chosen". Supporting this needs a way to declare records outside the page tree and a relation between two of them, which is out of proportion to demonstrating two elements.

Impact 

vendor/bin/typo3 theme:seed produces a frontend that exercises the theme end to end, so a development or test instance no longer needs pages built by hand to see what an element looks like. Definitions of your own can describe inline relations, and have to use identifiers without underscores.

Feature: Seed a page tree from a definition 

Description 

The extension ships a console command that writes a page tree and its content from a YAML definition:

vendor/bin/typo3 theme:seed
Copied!

It is meant for development and test instances, so a frontend to look at can be rebuilt from nothing rather than clicked together by hand. The definition lives in the repository, which makes the instance reproducible for everyone working on it.

The shipped definition is EXT:theme_extension_development/Configuration/Seeds/Demo.yaml. Another one is written by passing its path:

vendor/bin/typo3 theme:seed EXT:my_package/Configuration/Seeds/Other.yaml
vendor/bin/typo3 theme:seed --root-page=12 --force
Copied!

The format keeps its structural keys to identifier, uid, children and content; every other key is a field of the record:

identifier: demo

pages:
  - identifier: home
    uid: 1
    title: 'Theme demo'
    slug: '/'
    is_siteroot: 1
    content:
      - identifier: home-heading
        CType: header
        header: 'A frontend to look at'
Copied!

Records are written through DataHandler rather than as database rows, so slugs, TCA defaults, sorting, the reference index and the caches are handled by the core rather than reimplemented.

Feature: Records of any table in a seed definition 

Description 

A seed definition can now declare records of any table on a page, through the structural key records. Until now a definition expressed pages (children), content elements (content) and the children of a relation (inline) — so the page tree of a development instance could be seeded, but not the data a plugin on it reads.

A record under records declares the table it belongs to itself, exactly as an inline child does:

pages:
  - identifier: persons-storage
    title: 'Persons'
    doktype: 254
    records:
      - identifier: profile-doe
        table: tx_academicpersons_domain_model_profile
        first_name: 'Jane'
        last_name: 'Doe'
        inline:
          contracts:
            - identifier: contract-doe
              table: tx_academicpersons_domain_model_contract
              position: 'Professor'
Copied!

The structural keys of the format are therefore identifier, uid, children, content, records, files and inline, plus table on an inline or records child.

A record declared this way is a record like any other: it may declare a uid, carry files and carry inline children, and its pid is the page that declares it. Declaration order is kept per table, so records of several tables on one page do not disturb each other's sorting.

Relations to seeded records 

A relation is expressed by declaring the uid of the target and writing it into the relation field. DataHandler resolves the rest, including MM relations, whose rows go into a table the seeding never names:

records:
  - identifier: category-news
    table: sys_category
    uid: 4711
    title: 'News'
Copied!
categories: 4711
Copied!

Where records may appear 

records is structure on a page and an ordinary field everywhere else. tt_content has a column of that name — the one the Insert records element writes tt_content_<uid> into — so the key is decided per level, exactly as table is. Declaring records on a content element therefore writes a field and nests nothing.

Impact 

Existing seed definitions are unaffected: the key is new, and on the level where it could collide with a column it keeps being a column. A development instance can now be rebuilt from nothing including its records, rather than including only the pages around them.

Feature: Site set and page rendering 

Description 

The theme now renders pages, and ships a site set to enable it.

A site enables the theme by depending on the set in its site configuration:

dependencies:
  - sbuerk/theme-extension-development
Copied!

No sys_template record is needed. The set brings the TypoScript, a Fluid based page rendering and the compiled stylesheet with it.

For installations that do not use site sets, the theme additionally registers a classic static template, selectable in the Include static (from extensions) field of a sys_template record. The two mechanisms are safe side by side: the static include detects an active set and skips its own import, so a site configured with both is not served the theme twice.

What is rendered 

  • A page object with a Fluid template, layout and partials below Resources/Private/.
  • The compiled stylesheet from Resources/Public/Css/theme.css.
  • The content of the normal column.

The Fluid paths and the stylesheet are TypoScript constants under theme., so an integrator can point them at their own files without editing the extension — see Configuration.

Feature: A styleguide page 

Description 

The extension now ships a styleguide: one page that renders every component of the component library from its own Fluid templates, so the whole theme can be looked at in one place, in the appearance and colour palette currently selected.

The seeded demo tree carries it at /styleguide (Feature: A seeded showcase of every element), reachable by URL and hidden from every menu. It is set to Page not enabled in menus rather than disabled: a disabled page answers 404 in the frontend and needs a backend preview link, which defeats the point of a page that exists to be opened.

Seven sections, each its own partial:

Partial Section What it shows
Styleguide/Tokens.html Tokens All 27 colour tokens as swatches, plus the type scale, weight and family, the spacing scale, radius, shadow and the focus ring.
Styleguide/Typography.html Typography The element baseline every tag gets without a class - headings, running text, inline elements, lists, quotations, preformatted text, rules - and the table component.
Styleguide/Buttons.html Buttons .theme-button with every modifier, size and state it ships, the button group, and .theme-badge in both severities and fills.
Styleguide/Boxes.html Boxes Card, teaser, hero, quote, alert, accordion and author, each with every modifier its stylesheet defines.
Styleguide/Forms.html Forms The complete form contract: the field wrapper, every input type, select and textarea, fieldsets, checkboxes and radios, the validation states, disabled and read-only.
Styleguide/Navigation.html Navigation Main and sub navigation, breadcrumb, pagination and the content menu.
Styleguide/Media.html Media The gallery in one, two and three columns, and the content element wrapper with its outline switch.

The page ignores its own content 

Nothing on this page comes from a content element, a record or a data processor. That is deliberate: a styleguide shows the contract, and a specimen assembled from a record would show whatever that record happened to contain and would break for reasons that have nothing to do with the component.

Two things implement it. The Styleguide backend layout offers a single column with colPos 999, which no TypoScript object in the extension reads - anything an editor places there is stored and never rendered, inert rather than broken. And the template contains no f:cObject at all, not even for the main column.

Overriding a section 

Each section is a partial of its own under EXT:theme_extension_development/Resources/Private/Partials/Styleguide/, so a site package that wants its own forms section overrides Styleguide/Forms.html and keeps the other six. That is the same fine-grained override the rest of the theme follows.

Specimen copy is literal English and is not routed through locallang.xlf. Specimen text exists to be set in a typeface; it has nothing to localise, and roughly 150 translated labels would make the specimens unreadable in the source, which is the one place they have to be readable.

Impact 

Running vendor/bin/typo3 theme:seed gives a frontend where /styleguide answers with the whole component library. It is the only place the design tokens can be seen resolved rather than read as values, and because of that it doubles as a live test of the appearance and palette switchers: flip either control in the header, and every swatch and every specimen on the page has to move together. One that does not is a colour that escaped the token layer.

The Elements pages of the demo tree remain complementary rather than redundant. Those render the same components from real content records, so they prove the wiring; this page proves the library. A component can break in one without the other noticing - most of the form contract, for instance, is reached by no content element at all.

Three fixes come with it 

All three were found by putting a second instance of a component on one page, which nothing before this did:

  • Only the first main navigation toggle was bound. The stylesheet collapses every .theme-nav-main whose toggle is not expanded, while the script wired up only the first toggle on the page. A second navigation - a footer menu repeating the main one - was collapsed below the breakpoint by a control that did nothing, with no way left to open it. Every toggle is now bound, scoped to the navigation it belongs to.
  • .theme-form-summary--error matched no rule. It was part of the published markup contract and of the documentation but had never been declared. The base rule already carries the danger palette, so it looked correct; it is now declared explicitly, symmetric with .theme-form-summary--success .
  • The alert markup contract showed the wrong role. It illustrated role="status" on a .theme-alert--warning , which invites copying an assertive severity as a polite one. --info and --success take role="status"; --warning and --danger take role="alert".

Feature: Theme content elements 

Description 

The theme now ships ten content elements of its own - unlike every element covered in Feature: Core content elements and Feature: Menu content elements, these do not exist in the core at all. Their TCA, their own columns and a shared inline child table all belong to this extension:

Content element Renders through
Hero A full hero: heading, text, media and call-to-action links.
Hero, small The same, reduced.
Hero, text only The same, without media.
Teaser A short text teaser without media.
Media teaser Text placed beside a single image.
Media teaser grid Several media teasers arranged in a grid.
Testimonial A quotation with its attribution.
Author A person: portrait, name, role and links.
Link list A list of links.
Social links The same, labelled instead of rendered as icons.

All ten are grouped under their own Theme entry in the Create new content element wizard, so an editor can tell them apart from the core set at a glance. No page TSconfig registers that group - since TYPO3 v13 the wizard is generated straight from the label, description, group and icon already given to ExtensionManagementUtility::addRecordType().

Naming 

CTypes are prefixed theme_, columns tx_theme_, and the shared inline child table is tx_theme_list_item - short rather than the full extension key, because themeextensiondevelopment_hero is unusable in a showitem string and in TypoScript. This follows the reference implementation's own equally short prefix for the identical reason, and it accepts the same collision risk deliberately: another extension is free to also prefix its own fields theme_.

No schema of its own 

This extension ships no ext_tables.sql. The whole schema - the four tx_theme_* columns added to tt_content and every column of tx_theme_list_item - is derived from TCA by TYPO3CMSCoreDatabaseSchemaDefaultTcaSchema::enrich(), on both supported core versions.

One column needed to be declared explicitly rather than left to that derivation. An inline relation's foreign_field and foreign_table_field are auto-created on the child table if not already present; a field used only in foreign_match_fields - here, the column that records which of the four inline relations sharing this child table a row belongs to - is not part of that special case and gets no column for free. fieldname on tx_theme_list_item is therefore a real type=input column, the same way the core's own sys_file_reference declares its own fieldname for the identical reason.

Inline children, and the item.data trap 

No core data processor resolves a generic database relation the way FilesProcessor resolves FAL - that class only ever wraps FileCollector, which is FAL-specific by construction. Author, Link list, Social links and Media teaser grid all resolve their shared inline relation with TYPO3CMSFrontendDataProcessingDatabaseQueryProcessor instead.

That processor wraps every row as ['data' => $record] - the same wrapping Feature: Menu content elements already documents for Categorized content. Every template reads item.data.link, never item.link: the latter resolves to nothing and renders an empty list with no error, not a broken one. This is not a hypothetical risk here either - it happened during this element set's own development, before the functional test suite caught it.

GalleryProcessor is deliberately not used 

Hero, Hero, small, Media teaser and Author all resolve their image with FilesProcessor alone. Each shows exactly one image in a fixed-shape box, and none of their forms expose Columns, Orientation or the other gallery fields GalleryProcessor reads - wiring it in would bind the layout to columns an editor can never set.

Markup 

Link list and Social links reuse the existing .theme-content-menu component - the same one every menu_* element uses - rather than a list component of their own: structurally the shape is identical, and a purpose-built list component would only duplicate styling that already exists. Author's own profile/contact links reuse the identical pair for the same reason.

Only one new component was needed: .theme-author - a portrait, a role line and a bio. It does not render the person's own name; that goes through the shared content-element heading like every other element, so .theme-author sits below it rather than repeating it.

Known gaps 

This theme ships no icon assets and no icon component. Social links therefore renders the same text-label list as Link list - link_label stands in for a platform icon, not a glyph approximating one.

.theme-hero__eyebrow exists in the hero component's stylesheet, but no hero variant's TCA offers an eyebrow field to back it - omitted rather than invented.

A field was removed 

Testimonial originally exposed the core Images field, the same way Hero and Author do. The quote component it renders through has no media slot at all, so a filled-in image would never have appeared on the page - an editor attaches a portrait and the work is silently gone. The field was removed from the form rather than left inert.

Impact 

An installation using the theme can create all ten elements from its own wizard group and gets working output for every one of them. Tests/Functional/ThemeContentElementRenderingTest.php renders a page carrying one of each and asserts every element reaches the content-element wrapper, that the inline-relation based elements actually list their children (not merely an empty, correct-looking wrapper), that inline children keep the order an editor gave them, and that the button variant and the link field resolve to a real, followable URL rather than a raw t3:// reference.

See docs/architecture/content-elements.md in the developer documentation for the full CType table, the schema derivation details, and the reasoning behind each decision summarised above.