diff --git a/packages/vitec/Classes/EventListener/ProvideProductCatalog.php b/packages/vitec/Classes/EventListener/ProvideProductCatalog.php index 3792450..1297038 100644 --- a/packages/vitec/Classes/EventListener/ProvideProductCatalog.php +++ b/packages/vitec/Classes/EventListener/ProvideProductCatalog.php @@ -29,6 +29,14 @@ 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. * + * An overview page with exactly ONE product is shown as that product + * (decision Harry 2026-11-12, e.g. Avedia, EZ TV, X-Player, ChannelLink, + * ActiveSQX): `link` is the product detail page and `image` the product's + * own image (productimage, else heroimage, else the page media). `linkType` + * says which case it is (`product` | `overview`), `overviewLink` always + * keeps the overview page. The page itself stays reachable (breadcrumb, + * search, sitemap). + * * A and B were sysfolders (menu labels only) until 2026-11-05; Olli: every * category and sub-category is a clickable page. Both levels now carry * `link` (additive; '' while a level is still a folder). Folders are still @@ -119,16 +127,21 @@ final class ProvideProductCatalog $uid = (int)$itemRow['uid']; $pagePos[$uid] = [$c, $g, $p]; $products = $productsByPage[$uid] ?? []; + // exactly one product: the tile is that product (link + image), see class comment + $single = count($products) === 1 ? $products[0] : null; $item = [ 'type' => 'page', 'uid' => $uid, 'title' => (string)$itemRow['title'], - 'link' => (string)$itemRow['slug'], + 'link' => $single !== null ? $single['link'] : (string)$itemRow['slug'], + 'linkType' => $single !== null ? 'product' : 'overview', + 'overviewLink' => (string)$itemRow['slug'], 'productCount' => count($products), - 'products' => $products, + 'products' => array_map(static fn(array $p): array => ['title' => $p['title'], 'link' => $p['link']], $products), ]; if ($stage >= 2) { - $item['image'] = $this->pageImage($uid); + $item['image'] = ($single !== null && $single['uid'] > 0 ? $this->productImage($single['uid']) : null) + ?? $this->pageImage($uid); } $items[] = $item; } @@ -228,7 +241,7 @@ final class ProvideProductCatalog * product detail page (container children included), creation order. * * @param int[] $pageUids - * @return array> page uid => products + * @return array> page uid => products */ private function productsOnPages(array $pageUids): array { @@ -244,16 +257,49 @@ final class ProvideProductCatalog ) ->orderBy('uid') ->executeQuery()->fetchAllAssociative(); + $uidBySlug = $rows === [] ? [] : $this->productUidsBySlug(); $byPage = []; foreach ($rows as $row) { + $link = (string)$row['vitec_primary_cta']; + $slug = trim((string)parse_url($link, PHP_URL_PATH), '/'); + $slug = substr($slug, strlen('product/')); $byPage[(int)$row['pid']][] = [ 'title' => (string)$row['header'], - 'link' => (string)$row['vitec_primary_cta'], + 'link' => $link, + 'uid' => $uidBySlug[$slug] ?? 0, ]; } return $byPage; } + /** @return array product slug (without slashes) => uid, live products only */ + private function productUidsBySlug(): array + { + $queryBuilder = $this->connectionPool->getQueryBuilderForTable('tx_vitec_domain_model_product'); + $rows = $queryBuilder->select('uid', 'slug')->from('tx_vitec_domain_model_product') + ->executeQuery()->fetchAllAssociative(); + $map = []; + foreach ($rows as $row) { + $map[trim((string)$row['slug'], '/')] = (int)$row['uid']; + } + return $map; + } + + /** The product's own picture (productimage, else heroimage) as 640px tile - or null. */ + private function productImage(int $productUid): ?array + { + try { + foreach (['productimage', 'heroimage'] as $field) { + $references = $this->fileRepository->findByRelation('tx_vitec_domain_model_product', $field, $productUid); + if ($references !== []) { + return $this->tile($references[0]); + } + } + } catch (\Throwable) { + } + return null; + } + /** First page media (page properties > resources), 640px tile, root-relative - or null. */ private function pageImage(int $pageUid): ?array { @@ -262,6 +308,17 @@ final class ProvideProductCatalog if ($references === []) { return null; } + return $this->tile($references[0]); + } catch (\Throwable) { + return null; + } + } + + /** @return array{url:string,alternative:string,width:int,height:int}|null */ + private function tile(\TYPO3\CMS\Core\Resource\FileReference $reference): ?array + { + try { + $references = [$reference]; $imageService = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(ImageService::class); $processed = $imageService->applyProcessingInstructions($references[0], ['width' => '640', 'fileExtension' => 'webp']); $url = $imageService->getImageUri($processed); diff --git a/packages/vitec/Documentation/Headless-JSON-Architecture.md b/packages/vitec/Documentation/Headless-JSON-Architecture.md index 678abc3..79ffbac 100755 --- a/packages/vitec/Documentation/Headless-JSON-Architecture.md +++ b/packages/vitec/Documentation/Headless-JSON-Architecture.md @@ -45,6 +45,7 @@ | 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. | | 1.32 | 2026‑11‑05 | **Interface change (additive).** Products menu: categories and sub-categories are clickable pages (were sysfolders/labels): `categories[].link` and `groups[].link`. Six sub-categories are the 2026-10 category pages (content/slug/SEO kept, sitemap name as nav_title = menu `title`); 3 categories, End-Points, RF Gateway and Capture & Graphics (content of Graphics & Multi-Display + Video Capture) are new pages; Graphics & Multi-Display, Video Capture and Infrastructure & Accessories left the menus (reachable). Breadcrumbs run through category and sub-category. Spec text on the removed v1.28 transitional fields corrected. **Caching:** `/_json/` responses no longer carry browser cache headers (`page.config.sendCacheHeaders = 0`) - EXT:headless enables them and TYPO3 14's default page lifetime of one year made browsers keep every JSON response up to a year; front ends should request with `cache: 'no-cache'` to get past copies cached before. | +| 1.33 | 2026‑11‑12 | **Interface change (additive).** Products menu: an overview page with exactly one product is presented as that product — `items[].link` is the product detail page and `items[].image` the product picture (productimage, else heroimage, else page media); new `items[].linkType` (`product` | `overview`) and `items[].overviewLink` (always the overview page). Applies today to Avedia, EZ TV, X-Player End-Points, ChannelLink IP Gateway, ActiveSQX (decision Harry 2026-11-12). Front end: no sub-product count for `linkType = product`. | 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 @@ -1229,8 +1230,13 @@ reached from the overview pages. "groups": [ { "title": "Encoders & Decoders", "link": "/products/video-encoding-and-streaming", "items": [ { "type": "page", "uid": 587, "title": "Appliances", "link": "/products/appliances", + "linkType": "overview", "overviewLink": "/products/appliances", "productCount": 5, "products": [ { "title": "MGW Diamond-H", "link": "/product/…" } ], "image": null }, + { "type": "page", "uid": 575, "title": "Avedia", "link": "/product/avedia-platform-iptv-signage-cmsserver", + "linkType": "product", "overviewLink": "/products/avedia", + "productCount": 1, "products": [ { "title": "Avedia Platform", "link": "/product/avedia-platform-iptv-signage-cmsserver" } ], + "image": { "url": "/fileadmin/_processed_/…webp", "alternative": "", "width": 640, "height": 360 } }, { "type": "shortcut", "title": "Aligo KVM", "link": "/products/aligo", "target": { "category": 1, "group": 3, "page": 0 } } ] } ] } ] } @@ -1251,8 +1257,18 @@ reached from the overview pages. - `products` are the product cards of the overview page that link a detail page (container children included, creation order); `productCount` is their number - a card without a resolvable product is not counted. +- **Single-product overviews (v1.33, additive):** an overview page with + exactly one product is presented as that product - `link` is the product + detail page (`/product/`) and `image` the product's own picture + (*productimage*, else *heroimage*, else the page media). `linkType` tells + the two cases apart: `product` | `overview`; `overviewLink` always holds + the overview page (`/products/`) for breadcrumb-style uses. Today + this applies to Avedia, EZ TV, X-Player End-Points, ChannelLink IP Gateway + and ActiveSQX; it switches automatically when an editor adds a second + card. The front end should not render a sub-product count for these. - `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 (`linkType = overview`) or the product picture (`linkType = + product`), `null` when none is set. - 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 @@ -1710,4 +1726,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.32.* +*End of document EVO‑VITEC‑HL‑001 v1.33.*