Megamenu: rail display for page trees, cache tags, editorial promo boxes

- Page-tree entries get a "Page tree display" field (columns | rail |
  rail-compact), emitted as display + cardSize: Markets and Solutions
  show their main pages as a list and the sub-pages as image cards on
  hover. Page-tree items carry image (page media) and teaser (abstract)
  from stage 2. Additive; front ends ignoring display render columns.
- vitec:attach-page-media gains --depth=2 and falls back to the page's
  hero background before the grey placeholder; the 39 sub-solution
  pages got their hero image as page media.
- MegamenuJsonRenderer tags every page carrying the menu with the menu
  tables and tt_content_<uid>, so saving menu records refreshes all
  pages instead of waiting for a full flush.
- Products catalog: drop catalog.news; both promo boxes ("Product
  highlight", "Product news") are the entry's editorial teaser cards.
  ProvideInsightsSpotlight back to its previous state.
- Spec v1.29 and v1.30 (header version corrected), README.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
orascheandClaude Opus 5.5 committed 2026-11-02 17:35:23 +01:00
1 parent 9fb7f0ca77
commit 9211c8a395
10 files changed
+171 -37

No files matched your search

@@ -136,6 +136,19 @@ final class MenuBuilder
'columns' => $columns, 'columns' => $columns,
]; ];
// Page-tree display (2026-11-02): same columns, two presentations -
// `columns` side by side, or `rail`: the columns as a list on the
// left, the hovered column's items as image cards on the right
// (`cardSize` regular|compact). Additive: a front end that does not
// know `display` keeps rendering columns.
if ($layout === 'pagetree') {
$display = (string)($entry['tree_display'] ?? 'columns');
$built['display'] = str_starts_with($display, 'rail') ? 'rail' : 'columns';
if ($built['display'] === 'rail') {
$built['cardSize'] = $display === 'rail-compact' ? 'compact' : 'regular';
}
}
// Catalog panels: the teaser cards serve as the featured tiles (left // Catalog panels: the teaser cards serve as the featured tiles (left
// column), the catalog data itself comes from a listener of // column), the catalog data itself comes from a listener of
// BuildCatalogEvent - this extension does not know the data source. // BuildCatalogEvent - this extension does not know the data source.
@@ -229,10 +242,20 @@ final class MenuBuilder
foreach ($this->pageRepository->getMenu($rootUid, '*', 'sorting', self::NAV_WHERE) as $child) { foreach ($this->pageRepository->getMenu($rootUid, '*', 'sorting', self::NAV_WHERE) as $child) {
$items = []; $items = [];
foreach ($this->pageRepository->getMenu((int)$child['uid'], '*', 'sorting', self::NAV_WHERE) as $grandchild) { foreach ($this->pageRepository->getMenu((int)$child['uid'], '*', 'sorting', self::NAV_WHERE) as $grandchild) {
$items[] = [ $item = [
'title' => (string)(($grandchild['nav_title'] ?? '') !== '' ? $grandchild['nav_title'] : $grandchild['title']), 'title' => (string)(($grandchild['nav_title'] ?? '') !== '' ? $grandchild['nav_title'] : $grandchild['title']),
'link' => $this->url('t3://page?uid=' . (int)$grandchild['uid'], $cObj), 'link' => $this->url('t3://page?uid=' . (int)$grandchild['uid'], $cObj),
]; ];
// card data for the `rail` display (2026-11-02), same rules
// as the spotlight nav: abstract as teaser, page media image
if ($stage >= 2) {
$teaser = trim((string)($grandchild['abstract'] ?? ''));
if ($teaser !== '') {
$item['teaser'] = $teaser;
}
$item['image'] = $this->image('pages', 'media', (int)$grandchild['uid']);
}
$items[] = $item;
} }
$column = [ $column = [
'title' => (string)(($child['nav_title'] ?? '') !== '' ? $child['nav_title'] : $child['title']), 'title' => (string)(($child['nav_title'] ?? '') !== '' ? $child['nav_title'] : $child['title']),
@@ -7,6 +7,8 @@ namespace Evomedien\EvoMegamenuJson\UserFunc;
use Doctrine\DBAL\ParameterType; use Doctrine\DBAL\ParameterType;
use Evomedien\EvoMegamenuJson\Service\MenuBuilder; use Evomedien\EvoMegamenuJson\Service\MenuBuilder;
use TYPO3\CMS\Core\Attribute\AsAllowedCallable; use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
use TYPO3\CMS\Core\Cache\CacheDataCollectorInterface;
use TYPO3\CMS\Core\Cache\CacheTag;
use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer; use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer;
use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -24,10 +26,24 @@ use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
* TYPO3 v14 hands the ContentObjectRenderer in through the setter rather * TYPO3 v14 hands the ContentObjectRenderer in through the setter rather
* than the constructor; without it $this->cObj stays null and the element * than the constructor; without it $this->cObj stays null and the element
* path cannot see which record it is rendering. * path cannot see which record it is rendering.
*
* Cache (2026-11-02): the menu is part of every cached page, but its records
* live on a storage folder - saving them cleared only that folder, and every
* page cached before kept the old menu until a full flush. The page is
* therefore tagged with the menu tables; the DataHandler flushes the tags
* `<table>` and `<table>_<uid>` of every record it writes, so any save in the
* menu refreshes all pages carrying it. Data the panels read from the page
* tree (pagetree, catalog, spotlight) is not covered and still needs a flush.
*/ */
final class MegamenuJsonRenderer final class MegamenuJsonRenderer
{ {
private const CTYPE = 'evomegamenujson_megamenu'; private const CTYPE = 'evomegamenujson_megamenu';
private const RECORD_TABLES = [
'tx_evomegamenujson_entry',
'tx_evomegamenujson_column',
'tx_evomegamenujson_item',
'tx_evomegamenujson_teaser',
];
protected ?ContentObjectRenderer $cObj = null; protected ?ContentObjectRenderer $cObj = null;
@@ -63,6 +79,7 @@ final class MegamenuJsonRenderer
} }
$menu = GeneralUtility::makeInstance(MenuBuilder::class)->build($uid, $cObj, $row); $menu = GeneralUtility::makeInstance(MenuBuilder::class)->build($uid, $cObj, $row);
$this->tagPageCache($cObj, $uid);
return (string)json_encode($menu, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); return (string)json_encode($menu, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
} catch (\Throwable) { } catch (\Throwable) {
@@ -71,6 +88,21 @@ final class MegamenuJsonRenderer
} }
} }
/** Tags the page cache with the menu element and tables (see class comment). */
private function tagPageCache(ContentObjectRenderer $cObj, int $contentUid): void
{
try {
$collector = $cObj->getRequest()->getAttribute('frontend.cache.collector');
if (!$collector instanceof CacheDataCollectorInterface) {
return;
}
$tags = array_map(static fn(string $table): CacheTag => new CacheTag($table), self::RECORD_TABLES);
$collector->addCacheTags(new CacheTag('tt_content_' . $contentUid), ...$tags);
} catch (\Throwable) {
// without tags the menu still renders, it just ages until a flush
}
}
/** uid of the first megamenu element on a storage folder, 0 when none. */ /** uid of the first megamenu element on a storage folder, 0 when none. */
private function firstElementOn(int $pid): int private function firstElementOn(int $pid): int
{ {
@@ -43,7 +43,7 @@ return [
'1' => [ '1' => [
'showitem' => ' 'showitem' => '
--div--;' . $ll . 'tab.general, --div--;' . $ll . 'tab.general,
hidden, title, link, layout, hidden, title, link, layout, tree_display,
--div--;' . $ll . 'tab.columns, --div--;' . $ll . 'tab.columns,
menu_columns, menu_columns,
--div--;' . $ll . 'tab.teasers, --div--;' . $ll . 'tab.teasers,
@@ -115,6 +115,22 @@ return [
], ],
], ],
], ],
'tree_display' => [
// page-tree panels only: same data, two presentations
'displayCond' => 'FIELD:layout:=:pagetree',
'label' => $ll . 'tree_display',
'description' => $ll . 'tree_display.description',
'config' => [
'type' => 'select',
'renderType' => 'selectSingle',
'default' => 'columns',
'items' => [
['label' => $ll . 'tree_display.columns', 'value' => 'columns'],
['label' => $ll . 'tree_display.rail', 'value' => 'rail'],
['label' => $ll . 'tree_display.rail-compact', 'value' => 'rail-compact'],
],
],
],
'menu_columns' => [ 'menu_columns' => [
'label' => $ll . 'menu_columns', 'label' => $ll . 'menu_columns',
+13 -1
View File
@@ -125,7 +125,15 @@ page, its children as the items. Hidden or nav-hidden pages leave the menu
with the next cache flush. Curated columns are ignored; teasers and the with the next cache flush. Curated columns are ignored; teasers and the
featured slot stay editorial. From stage 2 a child page with media gets its featured slot stay editorial. From stage 2 a child page with media gets its
first image as `image` on the column - attach images in the page first image as `image` on the column - attach images in the page
properties and the panel turns visual by itself. properties and the panel turns visual by itself. From stage 2 the items
carry `image` (first page media or `null`) and, when set, the page
`abstract` as `teaser`.
The entry field *Page tree display* picks the presentation and travels as
`display`: `columns` (default, columns side by side) or `rail` (the columns
as a list on the left, the hovered column's items as image cards on the
right) plus `cardSize` (`regular` | `compact`) for `rail`. A front end that
ignores `display` keeps rendering columns.
### Layout `spotlight` ### Layout `spotlight`
@@ -163,5 +171,9 @@ The VITEC site package ships `ProvideInsightsSpotlight`, which supplies
entry. entry.
* The renderer never throws: a broken menu returns an empty value rather than * The renderer never throws: a broken menu returns an empty value rather than
taking the page payload down. taking the page payload down.
* Caching: every page carrying the menu is tagged with the menu tables and
`tt_content_<uid>` of the menu element, so saving any menu record refreshes
all of them. Panels built from the page tree (`pagetree`, `catalog`,
`spotlight`) pick up page changes with the next cache flush.
* Without `friendsoftypo3/headless` the element has no JSON envelope to render * Without `friendsoftypo3/headless` the element has no JSON envelope to render
into — the extension is built for headless installations. into — the extension is built for headless installations.
@@ -27,6 +27,11 @@
<trans-unit id="tx_evomegamenujson_entry.layout.catalog"><source>Catalog (data-driven: categories and items from the site package, teaser cards as featured tiles)</source></trans-unit> <trans-unit id="tx_evomegamenujson_entry.layout.catalog"><source>Catalog (data-driven: categories and items from the site package, teaser cards as featured tiles)</source></trans-unit>
<trans-unit id="tx_evomegamenujson_entry.layout.pagetree"><source>Page tree (data-driven: one column per child of the linked page, its children as items; curated columns are ignored)</source></trans-unit> <trans-unit id="tx_evomegamenujson_entry.layout.pagetree"><source>Page tree (data-driven: one column per child of the linked page, its children as items; curated columns are ignored)</source></trans-unit>
<trans-unit id="tx_evomegamenujson_entry.layout.spotlight"><source>Spotlight (data-driven: left-rail nav from the linked page's children plus live visual cards - latest news, next events - supplied by the site package; curated columns are ignored)</source></trans-unit> <trans-unit id="tx_evomegamenujson_entry.layout.spotlight"><source>Spotlight (data-driven: left-rail nav from the linked page's children plus live visual cards - latest news, next events - supplied by the site package; curated columns are ignored)</source></trans-unit>
<trans-unit id="tx_evomegamenujson_entry.tree_display"><source>Page tree display</source></trans-unit>
<trans-unit id="tx_evomegamenujson_entry.tree_display.description"><source>Columns: one column per child page with its children as links. List + cards: the child pages as a list on the left, the children of the hovered one as image cards on the right (images from the page media).</source></trans-unit>
<trans-unit id="tx_evomegamenujson_entry.tree_display.columns"><source>Columns side by side</source></trans-unit>
<trans-unit id="tx_evomegamenujson_entry.tree_display.rail"><source>List on the left, cards on hover</source></trans-unit>
<trans-unit id="tx_evomegamenujson_entry.tree_display.rail-compact"><source>List on the left, small cards on hover (for many sub-pages)</source></trans-unit>
<trans-unit id="tx_evomegamenujson_entry.menu_columns"><source>Columns</source></trans-unit> <trans-unit id="tx_evomegamenujson_entry.menu_columns"><source>Columns</source></trans-unit>
<trans-unit id="tx_evomegamenujson_entry.menu_columns.description"><source>One column per group. In the teaser layout only the first column is rendered, next to the cards.</source></trans-unit> <trans-unit id="tx_evomegamenujson_entry.menu_columns.description"><source>One column per group. In the teaser layout only the first column is rendered, next to the cards.</source></trans-unit>
<trans-unit id="tx_evomegamenujson_entry.menu_teasers"><source>Teaser cards</source></trans-unit> <trans-unit id="tx_evomegamenujson_entry.menu_teasers"><source>Teaser cards</source></trans-unit>
@@ -8,6 +8,7 @@ CREATE TABLE tx_evomegamenujson_entry (
title varchar(255) DEFAULT '' NOT NULL, title varchar(255) DEFAULT '' NOT NULL,
link varchar(1024) DEFAULT '' NOT NULL, link varchar(1024) DEFAULT '' NOT NULL,
layout varchar(30) DEFAULT 'columns' NOT NULL, layout varchar(30) DEFAULT 'columns' NOT NULL,
tree_display varchar(30) DEFAULT 'columns' NOT NULL,
menu_columns int(11) DEFAULT '0' NOT NULL, menu_columns int(11) DEFAULT '0' NOT NULL,
menu_teasers int(11) DEFAULT '0' NOT NULL, menu_teasers int(11) DEFAULT '0' NOT NULL,
featured_enable tinyint(1) unsigned DEFAULT '0' NOT NULL, featured_enable tinyint(1) unsigned DEFAULT '0' NOT NULL,
@@ -24,12 +24,17 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
* Image per page: the numbered placeholder * Image per page: the numbered placeholder
* `fileadmin/placeholders/pages/<pid>-08.png` when it exists (slot 08 = * `fileadmin/placeholders/pages/<pid>-08.png` when it exists (slot 08 =
* page media / menu tile; the number in the picture doubles as the image * page media / menu tile; the number in the picture doubles as the image
* wish list towards the agency), otherwise the generic wide grey * wish list towards the agency), else the page's own hero background
* placeholder. A page that already has a media reference is NEVER touched - * (`vitec_bgimage`, same file referenced - 2026-11-02, for the sub-market /
* editors swap the placeholder for the real image in the page properties. * sub-solution cards of the megamenu `rail` display), otherwise the generic
* wide grey placeholder. A page that already has a media reference is NEVER
* touched - editors swap the image in the page properties.
*
* --depth=2 targets the grandchildren (e.g. the sub-markets below
* /markets/<market>) instead of the children.
* *
* vendor/bin/typo3 vitec:attach-page-media --root=/support * vendor/bin/typo3 vitec:attach-page-media --root=/support
* vendor/bin/typo3 vitec:attach-page-media --root=/support --apply * vendor/bin/typo3 vitec:attach-page-media --root=/markets --depth=2 --apply
*/ */
#[AsCommand( #[AsCommand(
name: 'vitec:attach-page-media', name: 'vitec:attach-page-media',
@@ -40,6 +45,7 @@ final class AttachPageMediaCommand extends Command
protected function configure(): void protected function configure(): void
{ {
$this->addOption('root', null, InputOption::VALUE_REQUIRED, 'Slug of the root page, e.g. /support'); $this->addOption('root', null, InputOption::VALUE_REQUIRED, 'Slug of the root page, e.g. /support');
$this->addOption('depth', null, InputOption::VALUE_REQUIRED, '1 = children of the root, 2 = their children', '1');
$this->addOption('apply', null, InputOption::VALUE_NONE, 'Write changes (default: dry run)'); $this->addOption('apply', null, InputOption::VALUE_NONE, 'Write changes (default: dry run)');
} }
@@ -56,11 +62,15 @@ final class AttachPageMediaCommand extends Command
$output->writeln('<error>Root page ' . $rootSlug . ' not found.</error>'); $output->writeln('<error>Root page ' . $rootSlug . ' not found.</error>');
return Command::FAILURE; return Command::FAILURE;
} }
$children = $pages->fetchAllAssociative( $depth = max(1, min(2, (int)$input->getOption('depth')));
'SELECT uid, title FROM pages WHERE deleted = 0 AND hidden = 0 AND doktype = 1 AND pid = ? AND sys_language_uid IN (0,-1) ORDER BY sorting', $parents = [(int)$root['uid']];
[(int)$root['uid']] for ($level = 1; $level <= $depth; $level++) {
); $children = $pages->fetchAllAssociative(
$output->writeln(($apply ? 'APPLY' : 'DRY-RUN') . sprintf(' · %s #%d · %d child page(s)', $rootSlug, (int)$root['uid'], count($children))); 'SELECT uid, pid, title FROM pages WHERE deleted = 0 AND hidden = 0 AND doktype = 1 AND pid IN (' . implode(',', $parents ?: [0]) . ') AND sys_language_uid IN (0,-1) ORDER BY pid, sorting'
);
$parents = array_map('intval', array_column($children, 'uid'));
}
$output->writeln(($apply ? 'APPLY' : 'DRY-RUN') . sprintf(' · %s #%d · depth %d · %d page(s)', $rootSlug, (int)$root['uid'], $depth, count($children)));
$storage = GeneralUtility::makeInstance(StorageRepository::class)->getDefaultStorage(); $storage = GeneralUtility::makeInstance(StorageRepository::class)->getDefaultStorage();
$refs = $pool->getConnectionForTable('sys_file_reference'); $refs = $pool->getConnectionForTable('sys_file_reference');
@@ -76,10 +86,19 @@ final class AttachPageMediaCommand extends Command
$output->writeln(sprintf(' = #%d "%s": has media (%d) - untouched', $uid, (string)$child['title'], $existing)); $output->writeln(sprintf(' = #%d "%s": has media (%d) - untouched', $uid, (string)$child['title'], $existing));
continue; continue;
} }
$fileUid = 0;
$identifier = 'placeholders/pages/' . $uid . '-08.png'; $identifier = 'placeholders/pages/' . $uid . '-08.png';
if (!$storage->hasFile($identifier)) { if (!$storage->hasFile($identifier)) {
$identifier = 'placeholders/placeholder-grey-wide.png'; // the page's own hero background, first hero on the page
if (!$storage->hasFile($identifier)) { $fileUid = (int)$refs->fetchOne(
"SELECT r.uid_local FROM sys_file_reference r JOIN tt_content c ON c.uid = r.uid_foreign
WHERE r.deleted = 0 AND r.tablenames = 'tt_content' AND r.fieldname = 'vitec_bgimage'
AND c.deleted = 0 AND c.hidden = 0 AND c.pid = ?
ORDER BY c.colPos, c.sorting, r.sorting_foreign LIMIT 1",
[$uid]
);
$identifier = $fileUid > 0 ? 'hero image (sys_file #' . $fileUid . ')' : 'placeholders/placeholder-grey-wide.png';
if ($fileUid === 0 && !$storage->hasFile($identifier)) {
$output->writeln(sprintf(' <comment>! #%d "%s": no placeholder file - skipped</comment>', $uid, (string)$child['title'])); $output->writeln(sprintf(' <comment>! #%d "%s": no placeholder file - skipped</comment>', $uid, (string)$child['title']));
continue; continue;
} }
@@ -88,11 +107,13 @@ final class AttachPageMediaCommand extends Command
if (!$apply) { if (!$apply) {
continue; continue;
} }
$file = $storage->getFile($identifier); if ($fileUid === 0) {
$fileUid = $storage->getFile($identifier)->getUid();
}
$datamap['sys_file_reference']['NEWPM' . (++$newIndex)] = [ $datamap['sys_file_reference']['NEWPM' . (++$newIndex)] = [
'pid' => $uid, 'pid' => $uid,
'table_local' => 'sys_file', 'table_local' => 'sys_file',
'uid_local' => $file->getUid(), 'uid_local' => $fileUid,
'tablenames' => 'pages', 'tablenames' => 'pages',
'uid_foreign' => $uid, 'uid_foreign' => $uid,
'fieldname' => 'media', 'fieldname' => 'media',
@@ -50,13 +50,8 @@ final class ProvideInsightsSpotlight
]); ]);
} }
/** /** @return array<int,array<string,mixed>> */
* Public since 2026-11-02: the products catalog panel shows the same private function latestNews(int $stage): array
* news cards ("Product news" slot) - one query, one card shape.
*
* @return array<int,array<string,mixed>>
*/
public function latestNews(int $stage, int $count = self::NEWS_COUNT): array
{ {
$queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::NEWS_TABLE); $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::NEWS_TABLE);
$queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class)); $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
@@ -68,7 +63,7 @@ final class ProvideInsightsSpotlight
$queryBuilder->expr()->eq('type', $queryBuilder->createNamedParameter('0')) $queryBuilder->expr()->eq('type', $queryBuilder->createNamedParameter('0'))
) )
->orderBy('datetime', 'DESC') ->orderBy('datetime', 'DESC')
->setMaxResults($count) ->setMaxResults(self::NEWS_COUNT)
->executeQuery()->fetchAllAssociative(); ->executeQuery()->fetchAllAssociative();
} catch (\Throwable) { } catch (\Throwable) {
return []; return [];
@@ -29,9 +29,10 @@ use TYPO3\CMS\Extbase\Service\ImageService;
* pages the visitor reaches from the overview page; the menu does not show * pages the visitor reaches from the overview page; the menu does not show
* them, the count and the list serve the "N sub-products" line and search. * them, the count and the list serve the "N sub-products" line and search.
* *
* Next to the tree: viewAll (/products), news (the latest news cards, * Next to the tree: viewAll (/products) and the type-ahead endpoint. The
* shared with the Insights spotlight) and the type-ahead endpoint. The * boxes of the left column ("Product highlight", "Product news") are the
* entry's teaser cards stay the editorial "Product highlight". * entry's editorial teaser cards, not data from here (decision Olli
* 2026-11-02: no automatic news).
* *
* Version 1 (2026-10-09) listed sys_category groups with their products; * Version 1 (2026-10-09) listed sys_category groups with their products;
* the nine old category pages are plain pages below /products and are not * the nine old category pages are plain pages below /products and are not
@@ -45,12 +46,10 @@ final class ProvideProductCatalog
private const DOKTYPE_PAGE = 1; private const DOKTYPE_PAGE = 1;
private const DOKTYPE_SHORTCUT = 4; private const DOKTYPE_SHORTCUT = 4;
private const DOKTYPE_FOLDER = 254; private const DOKTYPE_FOLDER = 254;
private const NEWS_COUNT = 2;
public function __construct( public function __construct(
private readonly ConnectionPool $connectionPool, private readonly ConnectionPool $connectionPool,
private readonly FileRepository $fileRepository, private readonly FileRepository $fileRepository,
private readonly ProvideInsightsSpotlight $spotlight,
) {} ) {}
public function __invoke(BuildCatalogEvent $event): void public function __invoke(BuildCatalogEvent $event): void
@@ -66,7 +65,6 @@ final class ProvideProductCatalog
'placeholder' => 'Search products', 'placeholder' => 'Search products',
], ],
'viewAll' => ['title' => 'View all products', 'link' => self::PRODUCTS_SLUG], 'viewAll' => ['title' => 'View all products', 'link' => self::PRODUCTS_SLUG],
'news' => $this->spotlight->latestNews($stage, self::NEWS_COUNT),
'categories' => $this->categories($stage), 'categories' => $this->categories($stage),
]); ]);
} catch (\Throwable) { } catch (\Throwable) {
@@ -3,9 +3,9 @@
| | | | | |
|---|---| |---|---|
| **Document identifier** | EVO‑VITEC‑HL‑001 | | **Document identifier** | EVO‑VITEC‑HL‑001 |
| **Version** | 1.26 | | **Version** | 1.30 |
| **Status** | Released | | **Status** | Released |
| **Date** | 2026‑10‑26 | | **Date** | 2026‑11‑02 |
| **Applies to** | `evomedien/vitec` on TYPO3 v14.3 (headless) | | **Applies to** | `evomedien/vitec` on TYPO3 v14.3 (headless) |
| **Owner** | evomedien — VITEC relaunch | | **Owner** | evomedien — VITEC relaunch |
@@ -41,6 +41,8 @@
| 1.26 | 2026‑10‑26 | **Interface change (additive).** Product detail resources band simplified (customer CR): the `resources` envelope gains **`sectionLayout`** — `50-50` (default) \| `33-33-33` — from the new per‑product select *Resources Section Layout* (Layout 1: Technical Highlights left, resource cards right, 50/50; Layout 2: highlights over two columns, cards in the third). Capabilities leave the band editorially; the TCA fields `capabilities`, `datasheets` and `downloads` are parked in the Misc tab — all three stay in the payload unchanged. The automatic newest‑datasheet/documentation cards remain deferred; meanwhile the manual card `restype` vocabulary gains `datasheet` and `brochure` — a pinned datasheet card links the download record and stays current because the download route always serves the newest file. | | 1.26 | 2026‑10‑26 | **Interface change (additive).** Product detail resources band simplified (customer CR): the `resources` envelope gains **`sectionLayout`** — `50-50` (default) \| `33-33-33` — from the new per‑product select *Resources Section Layout* (Layout 1: Technical Highlights left, resource cards right, 50/50; Layout 2: highlights over two columns, cards in the third). Capabilities leave the band editorially; the TCA fields `capabilities`, `datasheets` and `downloads` are parked in the Misc tab — all three stay in the payload unchanged. The automatic newest‑datasheet/documentation cards remain deferred; meanwhile the manual card `restype` vocabulary gains `datasheet` and `brochure` — a pinned datasheet card links the download record and stays current because the download route always serves the newest file. |
| 1.27 | 2026‑11‑02 | **Interface change (additive).** Product *Resources Section Layout* gained the value `hardware` (“Layout 3 - Hardware option”): `resources.sectionLayout = "hardware"` — Technical Highlights over two columns, no resource cards (`resources.items` is emitted empty even when cards are maintained, so they reappear on a switch back). | | 1.27 | 2026‑11‑02 | **Interface change (additive).** Product *Resources Section Layout* gained the value `hardware` (“Layout 3 - Hardware option”): `resources.sectionLayout = "hardware"` — Technical Highlights over two columns, no resource cards (`resources.items` is emitted empty even when cards are maintained, so they reappear on a switch back). |
| 1.28 | 2026‑11‑02 | **Interface change (breaking for the products menu, additive elsewhere).** Product section rebuilt after the agency sitemap/menu prototype of 2026-11-02: 20 product overview pages `/products/<slug>` grouped by sysfolders (3 categories → 9 groups) below /products. The megamenu `catalog` payload is now **version 2**: `categories[].groups[].items[]` of type `page` (overview page with `productCount`/`products`/`image`) or `shortcut` (`target` indices), plus `viewAll` and `news`; the menu no longer lists products. Transitional v1 fields (`groups[].products` etc.) stay until the front end has switched. The product type-ahead (`?type=7385`) gained `overviews` and a per-product `overview` with menu path. | | 1.28 | 2026‑11‑02 | **Interface change (breaking for the products menu, additive elsewhere).** Product section rebuilt after the agency sitemap/menu prototype of 2026-11-02: 20 product overview pages `/products/<slug>` grouped by sysfolders (3 categories → 9 groups) below /products. The megamenu `catalog` payload is now **version 2**: `categories[].groups[].items[]` of type `page` (overview page with `productCount`/`products`/`image`) or `shortcut` (`target` indices), plus `viewAll` and `news`; the menu no longer lists products. Transitional v1 fields (`groups[].products` etc.) stay until the front end has switched. The product type-ahead (`?type=7385`) gained `overviews` and a per-product `overview` with menu path. |
| 1.29 | 2026‑11‑02 | **Interface change (removal within the unreleased catalog v2).** `catalog.news` dropped (decision: no automatic news in the products menu). Both promo boxes of the products panel, "Product highlight" and "Product news", are the entry's editorial `teasers` (`kicker` = box label). Megamenu caching: pages carrying the menu are now tagged with the menu tables, so saving menu records refreshes every page; data read from the page tree still needs a cache flush. |
| 1.30 | 2026‑11‑02 | **Interface change (additive).** Megamenu `pagetree` entries carry `display` (`columns` \| `rail`) and, for `rail`, `cardSize` (`regular` \| `compact`): Markets and Solutions switch to a left list with the sub-pages as image cards on hover. Page-tree items gained `image` (page media) and `teaser` (abstract) from stage 2; the sub-solution pages received their hero background as page media (sub-markets had page media already). |
This document is drafted in the style of, and adopts the terminology conventions of, This document is drafted in the style of, and adopts the terminology conventions of,
ISO/IEC/IEEE 42010 (architecture description), ISO/IEC/IEEE 26514 (information for ISO/IEC/IEEE 42010 (architecture description), ISO/IEC/IEEE 26514 (information for
@@ -1220,7 +1222,6 @@ pages are reached from there.
"version": 2, "version": 2,
"search": { "endpoint": "/_json/?type=7385", "param": "q", "minChars": 2, "placeholder": "Search products" }, "search": { "endpoint": "/_json/?type=7385", "param": "q", "minChars": 2, "placeholder": "Search products" },
"viewAll": { "title": "View all products", "link": "/products" }, "viewAll": { "title": "View all products", "link": "/products" },
"news": [ { "title": "…", "date": "2026-10-28", "link": "/news/article/…", "teaser": "…", "image": {} } ],
"categories": [ { "title": "IP Video Streaming, Distribution & KVM", "categories": [ { "title": "IP Video Streaming, Distribution & KVM",
"groups": [ { "title": "Encoders & Decoders", "groups": [ { "title": "Encoders & Decoders",
"items": [ "items": [
@@ -1241,9 +1242,11 @@ pages are reached from there.
their number - a card without a resolvable product is not counted. their number - a card without a resolvable product is not counted.
- `image` (from content stage 2) is the overview page's first page media as a - `image` (from content stage 2) is the overview page's first page media as a
640 px WebP, `null` when none is set. 640 px WebP, `null` when none is set.
- `news` are the latest news cards (same shape and query as the Insights - The promo boxes of the left column ("Product highlight", "Product news")
spotlight, here two); the entry's `teasers` remain the editorial product are the entry's editorial `teasers` next to `catalog` (`kicker` = box
highlight. label, plus `title`, `text`, `link`, `image`; designed for two, an empty
array hides the column). There are no automatic news (`catalog.news` was
dropped in v1.29).
- Hidden pages and pages with *Hide in menu* drop out; order = page tree - Hidden pages and pages with *Hide in menu* drop out; order = page tree
order. Version 1 (2026-10-09, sys_category groups with products) is no order. Version 1 (2026-10-09, sys_category groups with products) is no
longer the source. longer the source.
@@ -1257,6 +1260,34 @@ pages are reached from there.
by link); and a `teaser` (card text, plain) on every product. Do not build by link); and a `teaser` (card text, plain) on every product. Do not build
on these fields. on these fields.
**Markets and Solutions menus (megamenu `layout: "pagetree"`, `display`
since v1.30).** Page-tree entries build their `columns` live from the
linked page (one column per child page, its children as `items`). The
entry field *Page tree display* adds the presentation:
```json
{ "title": "Markets", "link": "/markets", "layout": "pagetree",
"display": "rail", "cardSize": "regular",
"columns": [ { "title": "Transport", "link": "/markets/transport", "image": { "url": "…" },
"items": [ { "title": "Airports", "link": "/markets/transport/airports",
"image": { "url": "/fileadmin/_processed_/…webp", "width": 640, "height": 360, "alternative": "", "title": "" },
"teaser": "…" } ] } ] }
```
- `display: "columns"` (default; Support and every entry not switched):
columns side by side as before. `display: "rail"`: the columns as a list
on the left; hovering (or focusing) one shows its `items` as image cards
on the right, the first column is preselected. `cardSize` (`regular` |
`compact`, only with `rail`) sizes the cards - Solutions uses `compact`
so five sub-solutions fit next to the list.
- From content stage 2 every item carries `image` (first page media as a
640 px WebP, `null` when none is set) and, when the page has an abstract,
`teaser`. The sub-market pages already had page media; the 39 sub-solution
pages got their hero background as page media
(`vitec:attach-page-media --depth=2`); editors replace it in
the page properties.
- Additive: a front end that ignores `display` keeps rendering columns.
**Operations.** Indexing runs automatically: record and page saves enter the **Operations.** Indexing runs automatically: record and page saves enter the
index queue, a scheduler task (Index Queue Worker, every 5 minutes) pushes them index queue, a scheduler task (Index Queue Worker, every 5 minutes) pushes them
to Solr; the CLI command `vitec:solr-index` (with `--initialize` and `--debug`) to Solr; the CLI command `vitec:solr-index` (with `--initialize` and `--debug`)
@@ -1674,4 +1705,4 @@ remediation.
- Header convention — the uniform header section across CEs, plugins and containers. - Header convention — the uniform header section across CEs, plugins and containers.
- `Configuration/Sets/Vitecset/setup.typoscript` — the single TypoScript entry point. - `Configuration/Sets/Vitecset/setup.typoscript` — the single TypoScript entry point.
*End of document EVO‑VITEC‑HL‑001 v1.28.* *End of document EVO‑VITEC‑HL‑001 v1.30.*