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

@@ -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
* `<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
{
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
{