Insights spotlight: news from folders 11 + 442, source editable in the menu

- Megamenu entries with the catalog or spotlight layout get two editor
  fields, "Data source folders" (source_pages) and "Data source
  categories" (source_categories); the extension stores them, the site
  package's listener reads them from the entry row.
- ProvideInsightsSpotlight reads its news from those folders (empty =
  11 + 442; the newer posts live in 442, the query only read 11) and,
  when set, only from those categories (Insights: Blog Post + Press
  Release). Newest datetime first, three cards.
- Pages carrying the menu are tagged with the news and event tables, so
  saving such a record refreshes the menu without a cache flush.
- VITEC field descriptions via a TCA override; spec v1.31, README.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
orascheandClaude Opus 5.5 committed 2026-11-05 10:59:56 +01:00
1 parent ad1f3c179c
commit 73e4298b75
7 files changed
+142 -18

No files matched your search

@@ -43,7 +43,7 @@ return [
'1' => [
'showitem' => '
--div--;' . $ll . 'tab.general,
hidden, title, link, layout, tree_display,
hidden, title, link, layout, tree_display, source_pages, source_categories,
--div--;' . $ll . 'tab.columns,
menu_columns,
--div--;' . $ll . 'tab.teasers,
@@ -131,6 +131,27 @@ return [
],
],
],
// data-driven layouts: where the site package's listener reads its
// records from - the extension only passes the entry row on
'source_pages' => [
'displayCond' => 'FIELD:layout:IN:catalog,spotlight',
'label' => $ll . 'source_pages',
'description' => $ll . 'source_pages.description',
'config' => [
'type' => 'group',
'allowed' => 'pages',
'size' => 3,
'maxitems' => 20,
],
],
'source_categories' => [
'displayCond' => 'FIELD:layout:IN:catalog,spotlight',
'label' => $ll . 'source_categories',
'description' => $ll . 'source_categories.description',
'config' => [
'type' => 'category',
],
],
'menu_columns' => [
'label' => $ll . 'menu_columns',
+14 -3
View File
@@ -117,6 +117,13 @@ final class ProvideCatalog
`getEntry()` hands the listener the raw entry row (to tell several catalog
entries apart), `getStage()` the content stage.
**Data source fields** (catalog and spotlight entries): *Data source
folders* (`source_pages`, comma list of page uids) and *Data source
categories* (`source_categories`, sys_category MM with
`tablenames = tx_evomegamenujson_entry`, `fieldname = source_categories`).
The extension only stores them; the listener reads them from the entry row
and decides what they mean (empty = its own default).
### Layout `pagetree`
A structure panel that can never age against the site: the columns mirror
@@ -156,9 +163,13 @@ The VITEC site package ships `ProvideInsightsSpotlight`, which supplies
}
```
- `news`: the three latest news records (storage pid 11, type 0 - the Solr
convention), `link` = `/news/article/<path_segment>` per the NewsDetail
route enhancer, image from `fal_media` as processed 640px webp.
- `news`: the three latest news records (type 0 - the Solr convention)
from the entry's *Data source folders* (empty = 11 + 442) and, when set,
in one of its *Data source categories*; `link` =
`/news/article/<path_segment>` per the NewsDetail route enhancer, image
from `fal_media` as processed 640px webp. Pages carrying the menu are
tagged with the news and event tables, so saving such a record refreshes
the menu.
- `events`: the next two events (`hideonwebsite` = 0, upcoming by
eventstart/eventend), `link` = the record's `eventurl` or the events
page, image processed the same way.
@@ -27,6 +27,10 @@
<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.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.source_pages"><source>Data source folders</source></trans-unit>
<trans-unit id="tx_evomegamenujson_entry.source_pages.description"><source>For data-driven panels: the folders the site package reads its records from (e.g. news folders). Empty = the site package's default.</source></trans-unit>
<trans-unit id="tx_evomegamenujson_entry.source_categories"><source>Data source categories</source></trans-unit>
<trans-unit id="tx_evomegamenujson_entry.source_categories.description"><source>For data-driven panels: only records in one of these categories. Empty = no category restriction.</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>
@@ -9,6 +9,8 @@ CREATE TABLE tx_evomegamenujson_entry (
link varchar(1024) DEFAULT '' NOT NULL,
layout varchar(30) DEFAULT 'columns' NOT NULL,
tree_display varchar(30) DEFAULT 'columns' NOT NULL,
source_pages text,
source_categories int(11) unsigned DEFAULT '0' 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,
@@ -4,9 +4,12 @@ declare(strict_types=1);
namespace Evomedien\Vitec\EventListener;
use Doctrine\DBAL\ArrayParameterType;
use Doctrine\DBAL\ParameterType;
use Evomedien\EvoMegamenuJson\Event\BuildSpotlightEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;
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\Resource\FileRepository;
@@ -19,17 +22,23 @@ use TYPO3\CMS\Extbase\Service\ImageService;
* news and the next events as image cards, queried live - editors touch
* news and event records, never the menu.
*
* Conventions mirrored from the site set: news live on pid 11 (type 0 only,
* same filter as the Solr indexing TS), their details render on page 21
* through the NewsDetail route enhancer (/news/article/<path_segment>).
* Conventions mirrored from the site set: news are type 0 only (same filter
* as the Solr indexing TS), their details render on page 21 through the
* NewsDetail route enhancer (/news/article/<path_segment>).
* Events come from the vitec event table; an event links to its own
* eventurl when set, otherwise to the events page.
*
* News sources (2026-11-05, ticket 0046-FE: the newer posts sit in folder
* 442, the query only read 11): the entry's "Data source folders" and
* "Data source categories" decide, editable in the menu record. No folder
* set = 11 + 442 (like the /insights/news plugin), no category = all.
*/
#[AsEventListener(identifier: 'vitec/megamenu-insights-spotlight')]
final class ProvideInsightsSpotlight
{
private const NEWS_TABLE = 'tx_news_domain_model_news';
private const NEWS_STORAGE_PID = 11;
private const DEFAULT_NEWS_PIDS = [11, 442];
private const ENTRY_TABLE = 'tx_evomegamenujson_entry';
private const NEWS_DETAIL_PATH = '/news/article/';
private const EVENT_TABLE = 'tx_vitec_domain_model_event';
private const EVENTS_PAGE = '/insights/events';
@@ -44,25 +53,83 @@ final class ProvideInsightsSpotlight
public function __invoke(BuildSpotlightEvent $event): void
{
$stage = $event->getStage();
$entry = $event->getEntry();
// group field: "11,442" (older notation "pages_11" tolerated)
$pids = array_values(array_filter(array_map(
static fn(string $value): int => (int)preg_replace('/^\D+_/', '', trim($value)),
explode(',', (string)($entry['source_pages'] ?? ''))
)));
$event->setSpotlight([
'news' => $this->latestNews($stage),
'news' => $this->latestNews($stage, $pids ?: self::DEFAULT_NEWS_PIDS, $this->entryCategories((int)($entry['uid'] ?? 0))),
'events' => $this->upcomingEvents($stage),
]);
$this->tagPageCache();
}
/** @return array<int,array<string,mixed>> */
private function latestNews(int $stage): array
/**
* The menu sits in every cached page; tagging the page with the news and
* event tables lets the DataHandler refresh those pages as soon as an
* editor saves a news or event record (it flushes the tag `<table>`).
*/
private function tagPageCache(): void
{
try {
$collector = ($GLOBALS['TYPO3_REQUEST'] ?? null)?->getAttribute('frontend.cache.collector');
if ($collector instanceof CacheDataCollectorInterface) {
$collector->addCacheTags(new CacheTag(self::NEWS_TABLE), new CacheTag(self::EVENT_TABLE));
}
} catch (\Throwable) {
// untagged, the menu only ages until the next flush
}
}
/**
* Categories picked in the entry's "Data source categories".
*
* @return int[]
*/
private function entryCategories(int $entryUid): array
{
if ($entryUid <= 0) {
return [];
}
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_category_record_mm');
return array_map('intval', $queryBuilder->select('uid_local')->from('sys_category_record_mm')
->where(
$queryBuilder->expr()->eq('tablenames', $queryBuilder->createNamedParameter(self::ENTRY_TABLE)),
$queryBuilder->expr()->eq('fieldname', $queryBuilder->createNamedParameter('source_categories')),
$queryBuilder->expr()->eq('uid_foreign', $queryBuilder->createNamedParameter($entryUid, ParameterType::INTEGER))
)
->executeQuery()->fetchFirstColumn());
}
/**
* @param int[] $pids news folders
* @param int[] $categories only news in one of these; empty = all
* @return array<int,array<string,mixed>>
*/
private function latestNews(int $stage, array $pids, array $categories): array
{
$queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::NEWS_TABLE);
$queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
try {
$rows = $queryBuilder->select('uid', 'title', 'teaser', 'datetime', 'path_segment')
$queryBuilder->select('uid', 'title', 'teaser', 'datetime', 'path_segment')
->from(self::NEWS_TABLE)
->where(
$queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter(self::NEWS_STORAGE_PID, ParameterType::INTEGER)),
$queryBuilder->expr()->in('pid', $queryBuilder->createNamedParameter($pids, ArrayParameterType::INTEGER)),
$queryBuilder->expr()->eq('type', $queryBuilder->createNamedParameter('0'))
)
->orderBy('datetime', 'DESC')
);
if ($categories !== []) {
$mm = $this->connectionPool->getQueryBuilderForTable('sys_category_record_mm');
$mm->select('uid_foreign')->from('sys_category_record_mm')
->where(
$mm->expr()->eq('tablenames', $queryBuilder->createNamedParameter(self::NEWS_TABLE)),
$mm->expr()->eq('fieldname', $queryBuilder->createNamedParameter('categories')),
$mm->expr()->in('uid_local', $queryBuilder->createNamedParameter($categories, ArrayParameterType::INTEGER))
);
$queryBuilder->andWhere($queryBuilder->expr()->in('uid', $mm->getSQL()));
}
$rows = $queryBuilder->orderBy('datetime', 'DESC')
->setMaxResults(self::NEWS_COUNT)
->executeQuery()->fetchAllAssociative();
} catch (\Throwable) {
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
/**
* VITEC meaning of the megamenu's data-source fields (2026-11-05, ticket
* 0046-FE): the Insights spotlight (ProvideInsightsSpotlight) reads its
* "Latest news" cards from these folders and categories.
*/
defined('TYPO3') or die();
if (isset($GLOBALS['TCA']['tx_evomegamenujson_entry']['columns']['source_pages'])) {
$GLOBALS['TCA']['tx_evomegamenujson_entry']['columns']['source_pages']['description']
= 'Insights: news folders for the "Latest news" cards. Empty = 11 + 442 (like the /insights/news list).';
$GLOBALS['TCA']['tx_evomegamenujson_entry']['columns']['source_categories']['description']
= 'Insights: only news in one of these categories (e.g. Blog Post, Press Release). Empty = all categories.';
}
@@ -3,9 +3,9 @@
| | |
|---|---|
| **Document identifier** | EVO‑VITEC‑HL‑001 |
| **Version** | 1.30 |
| **Version** | 1.31 |
| **Status** | Released |
| **Date** | 2026‑11‑02 |
| **Date** | 2026‑11‑05 |
| **Applies to** | `evomedien/vitec` on TYPO3 v14.3 (headless) |
| **Owner** | evomedien — VITEC relaunch |
@@ -43,6 +43,7 @@
| 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). |
| 1.31 | 2026‑11‑05 | **No interface change (data source).** Megamenu Insights `spotlight.news` read only news folder 11; the newer posts live in 442 (ticket 0046-FE). Catalog and spotlight entries gained the editor fields *Data source folders* and *Data source categories*; the Insights listener reads news from them (empty = 11 + 442, no category filter), newest `datetime` first, three cards. Pages carrying the menu are tagged with the news and event tables, so a saved news or event record refreshes the menu. Contract unchanged. |
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
@@ -1705,4 +1706,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.30.*
*End of document EVO‑VITEC‑HL‑001 v1.31.*