diff --git a/packages/evo_megamenu_json/Classes/Service/MenuBuilder.php b/packages/evo_megamenu_json/Classes/Service/MenuBuilder.php index 6879920..3e12c0d 100644 --- a/packages/evo_megamenu_json/Classes/Service/MenuBuilder.php +++ b/packages/evo_megamenu_json/Classes/Service/MenuBuilder.php @@ -136,6 +136,19 @@ final class MenuBuilder '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 // column), the catalog data itself comes from a listener of // 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) { $items = []; 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']), '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 = [ 'title' => (string)(($child['nav_title'] ?? '') !== '' ? $child['nav_title'] : $child['title']), diff --git a/packages/evo_megamenu_json/Classes/UserFunc/MegamenuJsonRenderer.php b/packages/evo_megamenu_json/Classes/UserFunc/MegamenuJsonRenderer.php index c680d13..196dd65 100644 --- a/packages/evo_megamenu_json/Classes/UserFunc/MegamenuJsonRenderer.php +++ b/packages/evo_megamenu_json/Classes/UserFunc/MegamenuJsonRenderer.php @@ -7,6 +7,8 @@ namespace Evomedien\EvoMegamenuJson\UserFunc; use Doctrine\DBAL\ParameterType; use Evomedien\EvoMegamenuJson\Service\MenuBuilder; 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\Query\Restriction\FrontendRestrictionContainer; 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 * than the constructor; without it $this->cObj stays null and the element * 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 + * `` and `
_` 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 { 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; @@ -63,6 +79,7 @@ final class MegamenuJsonRenderer } $menu = GeneralUtility::makeInstance(MenuBuilder::class)->build($uid, $cObj, $row); + $this->tagPageCache($cObj, $uid); return (string)json_encode($menu, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); } 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. */ private function firstElementOn(int $pid): int { diff --git a/packages/evo_megamenu_json/Configuration/TCA/tx_evomegamenujson_entry.php b/packages/evo_megamenu_json/Configuration/TCA/tx_evomegamenujson_entry.php index 5b25977..d032e91 100644 --- a/packages/evo_megamenu_json/Configuration/TCA/tx_evomegamenujson_entry.php +++ b/packages/evo_megamenu_json/Configuration/TCA/tx_evomegamenujson_entry.php @@ -43,7 +43,7 @@ return [ '1' => [ 'showitem' => ' --div--;' . $ll . 'tab.general, - hidden, title, link, layout, + hidden, title, link, layout, tree_display, --div--;' . $ll . 'tab.columns, menu_columns, --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' => [ 'label' => $ll . 'menu_columns', diff --git a/packages/evo_megamenu_json/README.md b/packages/evo_megamenu_json/README.md index ace79f2..8e6570e 100644 --- a/packages/evo_megamenu_json/README.md +++ b/packages/evo_megamenu_json/README.md @@ -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 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 -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` @@ -163,5 +171,9 @@ The VITEC site package ships `ProvideInsightsSpotlight`, which supplies entry. * The renderer never throws: a broken menu returns an empty value rather than taking the page payload down. +* Caching: every page carrying the menu is tagged with the menu tables and + `tt_content_` 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 into — the extension is built for headless installations. diff --git a/packages/evo_megamenu_json/Resources/Private/Language/locallang_db.xlf b/packages/evo_megamenu_json/Resources/Private/Language/locallang_db.xlf index 6f9cbe5..b1d63b2 100644 --- a/packages/evo_megamenu_json/Resources/Private/Language/locallang_db.xlf +++ b/packages/evo_megamenu_json/Resources/Private/Language/locallang_db.xlf @@ -27,6 +27,11 @@ Catalog (data-driven: categories and items from the site package, teaser cards as featured tiles) Page tree (data-driven: one column per child of the linked page, its children as items; curated columns are ignored) 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) + Page tree display + 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). + Columns side by side + List on the left, cards on hover + List on the left, small cards on hover (for many sub-pages) Columns One column per group. In the teaser layout only the first column is rendered, next to the cards. Teaser cards diff --git a/packages/evo_megamenu_json/ext_tables.sql b/packages/evo_megamenu_json/ext_tables.sql index a45181c..1c6479e 100644 --- a/packages/evo_megamenu_json/ext_tables.sql +++ b/packages/evo_megamenu_json/ext_tables.sql @@ -8,6 +8,7 @@ CREATE TABLE tx_evomegamenujson_entry ( title varchar(255) DEFAULT '' NOT NULL, link varchar(1024) DEFAULT '' 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_teasers int(11) DEFAULT '0' NOT NULL, featured_enable tinyint(1) unsigned DEFAULT '0' NOT NULL, diff --git a/packages/vitec/Classes/Command/AttachPageMediaCommand.php b/packages/vitec/Classes/Command/AttachPageMediaCommand.php index 6bd6ce3..7123fc2 100644 --- a/packages/vitec/Classes/Command/AttachPageMediaCommand.php +++ b/packages/vitec/Classes/Command/AttachPageMediaCommand.php @@ -24,12 +24,17 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; * Image per page: the numbered placeholder * `fileadmin/placeholders/pages/-08.png` when it exists (slot 08 = * page media / menu tile; the number in the picture doubles as the image - * wish list towards the agency), otherwise the generic wide grey - * placeholder. A page that already has a media reference is NEVER touched - - * editors swap the placeholder for the real image in the page properties. + * wish list towards the agency), else the page's own hero background + * (`vitec_bgimage`, same file referenced - 2026-11-02, for the sub-market / + * 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/) instead of the children. * * 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( name: 'vitec:attach-page-media', @@ -40,6 +45,7 @@ final class AttachPageMediaCommand extends Command protected function configure(): void { $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)'); } @@ -56,11 +62,15 @@ final class AttachPageMediaCommand extends Command $output->writeln('Root page ' . $rootSlug . ' not found.'); return Command::FAILURE; } - $children = $pages->fetchAllAssociative( - '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', - [(int)$root['uid']] - ); - $output->writeln(($apply ? 'APPLY' : 'DRY-RUN') . sprintf(' · %s #%d · %d child page(s)', $rootSlug, (int)$root['uid'], count($children))); + $depth = max(1, min(2, (int)$input->getOption('depth'))); + $parents = [(int)$root['uid']]; + for ($level = 1; $level <= $depth; $level++) { + $children = $pages->fetchAllAssociative( + '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(); $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)); continue; } + $fileUid = 0; $identifier = 'placeholders/pages/' . $uid . '-08.png'; if (!$storage->hasFile($identifier)) { - $identifier = 'placeholders/placeholder-grey-wide.png'; - if (!$storage->hasFile($identifier)) { + // the page's own hero background, first hero on the page + $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(' ! #%d "%s": no placeholder file - skipped', $uid, (string)$child['title'])); continue; } @@ -88,11 +107,13 @@ final class AttachPageMediaCommand extends Command if (!$apply) { continue; } - $file = $storage->getFile($identifier); + if ($fileUid === 0) { + $fileUid = $storage->getFile($identifier)->getUid(); + } $datamap['sys_file_reference']['NEWPM' . (++$newIndex)] = [ 'pid' => $uid, 'table_local' => 'sys_file', - 'uid_local' => $file->getUid(), + 'uid_local' => $fileUid, 'tablenames' => 'pages', 'uid_foreign' => $uid, 'fieldname' => 'media', diff --git a/packages/vitec/Classes/EventListener/ProvideInsightsSpotlight.php b/packages/vitec/Classes/EventListener/ProvideInsightsSpotlight.php index a2a73bd..9596d58 100644 --- a/packages/vitec/Classes/EventListener/ProvideInsightsSpotlight.php +++ b/packages/vitec/Classes/EventListener/ProvideInsightsSpotlight.php @@ -50,13 +50,8 @@ final class ProvideInsightsSpotlight ]); } - /** - * Public since 2026-11-02: the products catalog panel shows the same - * news cards ("Product news" slot) - one query, one card shape. - * - * @return array> - */ - public function latestNews(int $stage, int $count = self::NEWS_COUNT): array + /** @return array> */ + private function latestNews(int $stage): array { $queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::NEWS_TABLE); $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class)); @@ -68,7 +63,7 @@ final class ProvideInsightsSpotlight $queryBuilder->expr()->eq('type', $queryBuilder->createNamedParameter('0')) ) ->orderBy('datetime', 'DESC') - ->setMaxResults($count) + ->setMaxResults(self::NEWS_COUNT) ->executeQuery()->fetchAllAssociative(); } catch (\Throwable) { return []; diff --git a/packages/vitec/Classes/EventListener/ProvideProductCatalog.php b/packages/vitec/Classes/EventListener/ProvideProductCatalog.php index 945e059..2696101 100644 --- a/packages/vitec/Classes/EventListener/ProvideProductCatalog.php +++ b/packages/vitec/Classes/EventListener/ProvideProductCatalog.php @@ -29,9 +29,10 @@ use TYPO3\CMS\Extbase\Service\ImageService; * 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. * - * Next to the tree: viewAll (/products), news (the latest news cards, - * shared with the Insights spotlight) and the type-ahead endpoint. The - * entry's teaser cards stay the editorial "Product highlight". + * Next to the tree: viewAll (/products) and the type-ahead endpoint. The + * boxes of the left column ("Product highlight", "Product news") are the + * 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; * 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_SHORTCUT = 4; private const DOKTYPE_FOLDER = 254; - private const NEWS_COUNT = 2; public function __construct( private readonly ConnectionPool $connectionPool, private readonly FileRepository $fileRepository, - private readonly ProvideInsightsSpotlight $spotlight, ) {} public function __invoke(BuildCatalogEvent $event): void @@ -66,7 +65,6 @@ final class ProvideProductCatalog 'placeholder' => 'Search products', ], 'viewAll' => ['title' => 'View all products', 'link' => self::PRODUCTS_SLUG], - 'news' => $this->spotlight->latestNews($stage, self::NEWS_COUNT), 'categories' => $this->categories($stage), ]); } catch (\Throwable) { diff --git a/packages/vitec/Documentation/Headless-JSON-Architecture.md b/packages/vitec/Documentation/Headless-JSON-Architecture.md index e7c322b..777ce89 100755 --- a/packages/vitec/Documentation/Headless-JSON-Architecture.md +++ b/packages/vitec/Documentation/Headless-JSON-Architecture.md @@ -3,9 +3,9 @@ | | | |---|---| | **Document identifier** | EVO‑VITEC‑HL‑001 | -| **Version** | 1.26 | +| **Version** | 1.30 | | **Status** | Released | -| **Date** | 2026‑10‑26 | +| **Date** | 2026‑11‑02 | | **Applies to** | `evomedien/vitec` on TYPO3 v14.3 (headless) | | **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.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/` 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, ISO/IEC/IEEE 42010 (architecture description), ISO/IEC/IEEE 26514 (information for @@ -1220,7 +1222,6 @@ pages are reached from there. "version": 2, "search": { "endpoint": "/_json/?type=7385", "param": "q", "minChars": 2, "placeholder": "Search 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", "groups": [ { "title": "Encoders & Decoders", "items": [ @@ -1241,9 +1242,11 @@ pages are reached from there. 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 640 px WebP, `null` when none is set. -- `news` are the latest news cards (same shape and query as the Insights - spotlight, here two); the entry's `teasers` remain the editorial product - highlight. +- The promo boxes of the left column ("Product highlight", "Product news") + are the entry's editorial `teasers` next to `catalog` (`kicker` = box + 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 order. Version 1 (2026-10-09, sys_category groups with products) is no 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 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 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`) @@ -1674,4 +1705,4 @@ remediation. - Header convention — the uniform header section across CEs, plugins and containers. - `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.*