Compare commits

...
2 Commits
Author SHA1 Message Date
orascheandClaude Fable 5 3df66b1353 Product texts: mapping completed; product model: solutions + resources
Two work packages, one deploy.

Workbook field mapping completed (2026-09-18)
- highlights <- g:Technical Highlight (new spec-row component of the
  full agency delivery; composeGroup() is generic, no change needed)
- applications <- g:Why Choose Card (benefit cards as one block)
- seometa <- new composed source x:seometa (Introduction body trimmed
  at a word boundary to ~155 chars - the SEO sheet ships no meta
  description, a trimmed intro beats an empty tag)
- teaser stays the hero one-liner, settling the varchar(255) question
- fix: "Save mapping" now merges over the stored mapping instead of
  overwriting it - an empty dropdown no longer wipes a stored selector

Product model: solutions relation and Resources tab (2026-09-24)
- closes the gaps the full workbook walkthrough exposed (rows the
  import deliberately skips because the model had no place for them)
- solutions: MM multi select to tx_vitec_domain_model_solution
  (tx_vitec_product_solution_mm, relatedprodukt pattern), emitted by
  both product renderers as solution cards in the shared shape
- Resources tab: inline child table tx_vitec_product_resource - per
  entry a type (documentation/support/datasheet), editable card title
  and text, an icon (free text with a value-picker seed set) and one
  link (page/url/file/record via the link wizard, so the Download tab
  works too). Deliberately no Extbase model: only the UserFunc path
  reads the rows.
- JSON `resources` (detail payload only, like heroimage): list envelope
  {layout, items[]}; a card whose link is empty or unresolvable is not
  emitted; layout switches to carousel above three cards
- spec raised to v1.18 (7.15 member table, revision row; the footer had
  been stuck at v1.15 since 1.16/1.17)

Requires on the server:
    vendor/bin/typo3 database:updateschema "*.add,*.change"
    vendor/bin/typo3 cache:flush

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-24 14:15:43 +02:00
orascheandClaude Fable 5 4a7ea387a0 Product list: success stories on the all-products element
The big product list on /products (all products, no category context)
deliberately delivered the empty successStories envelope; the front end
now renders story cards there too and filters them client-side, so the
element aggregates over the WHOLE product category tree instead:

- new allProductCategoryUids(): root category 'Product' (same detection
  as ProductFinderJsonRenderer), children plus descendants depth-first,
  fed through the existing successStoriesFor()
- per-category elements are untouched; forCategories keeps carrying the
  category uids a story is attached to, so the ?cat=/?subcat= filter
  behaves identically on both element kinds
- layout rule unchanged: first contributing category in tree order

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-16 14:43:00 +02:00
9 changed files with 428 additions and 6 deletions

No files matched your search

@@ -95,6 +95,13 @@ final class ProductTextImportController
'description' => 'c:Product Introduction#1:body',
'description2' => 'c:Product Overview — H2#1:body',
'capabilities' => 'g:Key Capability Group',
// Decisions 2026-09-18 (field walkthrough completed): the new
// 'Technical Highlight' spec rows and the Why-Choose benefit cards
// each compose into one block; meta description is trimmed from the
// intro because the SEO sheet still ships none.
'highlights' => 'g:Technical Highlight',
'applications' => 'g:Why Choose Card',
'seometa' => 'x:seometa',
'textrelatedproducts' => 'p:Related Products — H2#1',
'keywords' => 'm:primaryKeyword',
];
@@ -372,7 +379,12 @@ final class ProductTextImportController
}
if (!empty($body['saveMapping'])) {
$this->mappingRepository->save(self::MAPPING_KEY, array_filter($map), '-');
// Merge instead of overwrite: a field whose dropdown was empty on
// THIS submit keeps its stored selector (this is how `teaser` got
// lost once). Clearing a mapping on purpose means picking another
// source, not leaving the select blank.
$stored = $this->mappingRepository->load(self::MAPPING_KEY)['mapping'] ?? [];
$this->mappingRepository->save(self::MAPPING_KEY, array_filter($map) + $stored, '-');
}
$summary = [];
@@ -722,6 +722,7 @@ class Product extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
$this->productimage = new ObjectStorage();
$this->heroimage = new ObjectStorage();
$this->downloads = new ObjectStorage();
$this->solutions = new ObjectStorage();
}
/**
@@ -1070,6 +1071,32 @@ class Product extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
$this->textrelatedproducts = $textrelatedproducts;
}
/**
* Solutions this product belongs to (MM). No companion property for the
* `resources` child table on purpose: it has no Extbase model - the
* headless renderers read tx_vitec_product_resource directly and the
* Extbase path never touches it.
*
* @var ObjectStorage<\Evomedien\Vitec\Domain\Model\Solution>
*/
protected $solutions;
/**
* @return ObjectStorage<\Evomedien\Vitec\Domain\Model\Solution>
*/
public function getSolutions(): ObjectStorage
{
return $this->solutions;
}
/**
* @param ObjectStorage<\Evomedien\Vitec\Domain\Model\Solution> $solutions
*/
public function setSolutions(ObjectStorage $solutions): void
{
$this->solutions = $solutions;
}
/* --------------------------------------------------------------------- */
}
@@ -227,6 +227,10 @@ final class ProductXlsxReader
if ($seoTitle !== null) {
$groups['Composed'][] = $this->entry('x:seotitle', 'Hero title — Intro heading (SEO title)', $seoTitle);
}
$seoMeta = $this->composeSeoMeta($parsed);
if ($seoMeta !== null) {
$groups['Composed'][] = $this->entry('x:seometa', 'Introduction body, trimmed to ~155 chars (meta description)', $seoMeta);
}
foreach ($parsed['seo'] ?? [] as $header => $value) {
$groups['Page SEO Check'][] = $this->entry('s:' . $header, 'SEO · ' . $header, (string)$value);
@@ -262,6 +266,9 @@ final class ProductXlsxReader
if ($selector === 'x:seotitle') {
return $this->composeSeoTitle($parsed);
}
if ($selector === 'x:seometa') {
return $this->composeSeoMeta($parsed);
}
if (str_starts_with($selector, 's:')) {
$value = $parsed['seo'][substr($selector, 2)] ?? null;
return is_string($value) && $value !== '' ? $value : null;
@@ -310,6 +317,37 @@ final class ProductXlsxReader
return $hero . ' — ' . $intro;
}
/**
* Meta description from the Product Introduction body: plain text,
* whitespace collapsed, cut at a word boundary to the customary ~155
* characters with an ellipsis. Decision 2026-09-18: composed here because
* the SEO sheet still ships no meta description at all - a trimmed intro
* beats an empty tag at go-live, and the editor can always overwrite.
*/
private function composeSeoMeta(array $parsed): ?string
{
foreach ($parsed['components'] ?? [] as $component) {
if ((string)($component['component'] ?? '') === 'Product Introduction'
&& (int)($component['occurrence'] ?? 1) === 1
) {
$text = trim((string)preg_replace('/\s+/u', ' ', strip_tags((string)($component['body'] ?? ''))));
if ($text === '') {
return null;
}
if (mb_strlen($text) <= 155) {
return $text;
}
$cut = mb_substr($text, 0, 152);
$lastSpace = mb_strrpos($cut, ' ');
if ($lastSpace !== false && $lastSpace > 100) {
$cut = mb_substr($cut, 0, $lastSpace);
}
return rtrim($cut, " ,;:") . ' …';
}
}
return null;
}
/**
* One component row as a heading/paragraph pair. The markup - centred h3
* with an inner span, centred p - is copied verbatim from what the
@@ -207,9 +207,13 @@ class ProductListJsonRenderer
// Success stories the editor attached to these categories
// (sys_category, Options tab). They ride along with the list
// so the front end can hide them with the same category
// filter it applies to the products.
// filter it applies to the products. An all-products element
// aggregates over the WHOLE product category tree (change
// 2026-09-16 on front-end request - it used to deliver the
// empty envelope), so forCategories keeps driving the
// ?cat=/?subcat= filter exactly like on per-category lists.
'successStories' => $this->successStoriesFor(
$allProducts ? [] : $categoryUids,
($allProducts || $categoryUids === []) ? $this->allProductCategoryUids() : $categoryUids,
(int)($settings['storysinglepid'] ?? 0)
),
];
@@ -375,6 +379,34 @@ class ProductListJsonRenderer
* @param int[] $categoryUids
* @return int[]
*/
/**
* The whole product category tree - the root's children plus all
* descendants in depth-first backend order. Story source for elements
* that render all products. Root detection mirrors
* ProductFinderJsonRenderer: the root-level sys_category named 'Product'.
*
* @return int[]
*/
private function allProductCategoryUids(): array
{
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
->getQueryBuilderForTable('sys_category');
$rootUid = (int)($queryBuilder->select('uid')->from('sys_category')
->where(
$queryBuilder->expr()->eq('parent', 0),
$queryBuilder->expr()->eq('deleted', 0),
$queryBuilder->expr()->eq('title', $queryBuilder->createNamedParameter('Product', ParameterType::STRING))
)
->setMaxResults(1)
->executeQuery()->fetchOne() ?: 0);
if ($rootUid <= 0) {
return [];
}
// Drop the root itself - stories hang on the categories below it.
return array_values(array_diff($this->expandWithDescendants([$rootUid]), [$rootUid]));
}
private function expandWithDescendants(array $categoryUids): array
{
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
@@ -456,6 +488,7 @@ class ProductListJsonRenderer
'ogimage' => $this->getProductOgImage($uid),
'videofile' => $this->getProductVideoFile($uid),
'relatedprodukt' => $this->getRelatedProducts($uid),
'solutions' => $this->getProductSolutions($uid),
];
}
@@ -879,6 +912,48 @@ class ProductListJsonRenderer
return $fallbackFiles;
}
/**
* Solutions linked to this product (tx_vitec_product_solution_mm, editor
* order) in the shared solution card shape - the same fields the solution
* list emits, so the front end reuses its card component. detailUrl is
* null while a solution has no detail page (9.12 contract).
*/
protected function getProductSolutions(int $productUid): array
{
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
->getQueryBuilderForTable('tx_vitec_domain_model_solution');
$rows = $queryBuilder
->select('s.*')
->from('tx_vitec_domain_model_solution', 's')
->join('s', 'tx_vitec_product_solution_mm', 'mm', 'mm.uid_foreign = s.uid')
->where(
$queryBuilder->expr()->eq('mm.uid_local', $queryBuilder->createNamedParameter($productUid, \Doctrine\DBAL\ParameterType::INTEGER)),
$queryBuilder->expr()->eq('s.deleted', 0),
$queryBuilder->expr()->eq('s.hidden', 0)
)
->orderBy('mm.sorting', 'ASC')
->executeQuery()
->fetchAllAssociative();
$serializer = GeneralUtility::makeInstance(\Evomedien\Vitec\Service\UsecaseSerializer::class);
$result = [];
foreach ($rows as $solutionRow) {
$solutionUid = (int)$solutionRow['uid'];
$result[] = [
'uid' => $solutionUid,
'title' => (string)($solutionRow['title'] ?? ''),
'slug' => (string)($solutionRow['slug'] ?? ''),
'subtitle' => (string)($solutionRow['subtitle'] ?? ''),
'teaser' => (string)($solutionRow['teaser'] ?? ''),
'description' => \Evomedien\Vitec\Service\RteResolver::html($solutionRow['description'] ?? ''),
'detailUrl' => \Evomedien\Vitec\Service\LinkResolver::pageUrl((int)($solutionRow['detail_page'] ?? 0)),
'image' => $serializer->image($solutionUid, 'image', 'tx_vitec_domain_model_solution', true),
];
}
return $result;
}
protected function getProductDownloads(int $productUid): array
{
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
@@ -252,6 +252,10 @@ class ProductShowJsonRenderer
'ogimage' => $this->getProductOgImage($uid),
'videofile' => $this->getProductVideoFile($uid),
'relatedprodukt' => $this->getRelatedProducts($uid),
'solutions' => $this->getProductSolutions($uid),
// Detail-only like heroimage: resource cards are detail-page
// furniture a list never shows.
'resources' => $this->getProductResources($uid),
];
// schema.org JSON-LD (Product + optional VideoObject) as a ready-to-emit
@@ -732,6 +736,93 @@ class ProductShowJsonRenderer
return $fallbackFiles;
}
/**
* Solutions linked to this product (tx_vitec_product_solution_mm, editor
* order) in the shared solution card shape - the same fields the solution
* list emits, so the front end reuses its card component. detailUrl is
* null while a solution has no detail page (9.12 contract).
*/
protected function getProductSolutions(int $productUid): array
{
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
->getQueryBuilderForTable('tx_vitec_domain_model_solution');
$rows = $queryBuilder
->select('s.*')
->from('tx_vitec_domain_model_solution', 's')
->join('s', 'tx_vitec_product_solution_mm', 'mm', 'mm.uid_foreign = s.uid')
->where(
$queryBuilder->expr()->eq('mm.uid_local', $queryBuilder->createNamedParameter($productUid, \Doctrine\DBAL\ParameterType::INTEGER)),
$queryBuilder->expr()->eq('s.deleted', 0),
$queryBuilder->expr()->eq('s.hidden', 0)
)
->orderBy('mm.sorting', 'ASC')
->executeQuery()
->fetchAllAssociative();
$serializer = GeneralUtility::makeInstance(\Evomedien\Vitec\Service\UsecaseSerializer::class);
$result = [];
foreach ($rows as $solutionRow) {
$solutionUid = (int)$solutionRow['uid'];
$result[] = [
'uid' => $solutionUid,
'title' => (string)($solutionRow['title'] ?? ''),
'slug' => (string)($solutionRow['slug'] ?? ''),
'subtitle' => (string)($solutionRow['subtitle'] ?? ''),
'teaser' => (string)($solutionRow['teaser'] ?? ''),
'description' => \Evomedien\Vitec\Service\RteResolver::html($solutionRow['description'] ?? ''),
'detailUrl' => \Evomedien\Vitec\Service\LinkResolver::pageUrl((int)($solutionRow['detail_page'] ?? 0)),
'image' => $serializer->image($solutionUid, 'image', 'tx_vitec_domain_model_solution', true),
];
}
return $result;
}
/**
* Resource cards from the product's Resources tab (child table
* tx_vitec_product_resource): documentation / support / datasheet
* entries, each an editable card (title, text, icon) around one link -
* page, external URL, file or download record, resolved per 9.12. A card
* whose link is empty or unresolvable is dropped (decision 2026-09-24).
* List envelope: `layout` switches to `carousel` above three cards.
*/
protected function getProductResources(int $productUid): array
{
$queryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
->getQueryBuilderForTable('tx_vitec_product_resource');
$rows = $queryBuilder
->select('*')
->from('tx_vitec_product_resource')
->where(
$queryBuilder->expr()->eq('product', $queryBuilder->createNamedParameter($productUid, \Doctrine\DBAL\ParameterType::INTEGER)),
$queryBuilder->expr()->eq('deleted', 0),
$queryBuilder->expr()->eq('hidden', 0)
)
->orderBy('sorting', 'ASC')
->executeQuery()
->fetchAllAssociative();
$items = [];
foreach ($rows as $resourceRow) {
$url = \Evomedien\Vitec\Service\LinkResolver::typolinkUrl((string)($resourceRow['link'] ?? ''));
if ($url === null) {
continue;
}
$items[] = [
'type' => (string)($resourceRow['restype'] ?? ''),
'title' => (string)($resourceRow['title'] ?? ''),
'text' => (string)($resourceRow['cardtext'] ?? ''),
'icon' => (string)($resourceRow['icon'] ?? ''),
'url' => $url,
];
}
return [
'layout' => count($items) > 3 ? 'carousel' : 'grid',
'items' => $items,
];
}
protected function getProductDownloads(int $productUid): array
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
@@ -23,7 +23,8 @@ return [
],
],
'types' => [
'1' => ['showitem' => 'title, subtitle, slug, teaser, description, description2, applications, capabilities, highlights, contentelement, contentelementcta, downloads, portfolio, showdatapath, textrelatedproducts, relatedprodukt,
'1' => ['showitem' => 'title, subtitle, slug, teaser, description, description2, applications, capabilities, highlights, contentelement, contentelementcta, downloads, portfolio, showdatapath, textrelatedproducts, relatedprodukt, solutions,
--div--;Resources, resources,
--div--;SEO, seotitle, urltitle, seometa, keywords, structureddata,
--div--;Images and Videos, heroimage, productimage, ogimage, video, videofile,
--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:categories, categories,
@@ -431,6 +432,37 @@ return [
'enableMultiSelectFilterTextfield' => true, // Optional: Adds a search box for filtering
],
],
'solutions' => [
'exclude' => true,
'label' => 'Related Solutions',
'description' => 'Solutions this product belongs to - emitted as solution cards',
'config' => [
'type' => 'select',
'renderType' => 'selectMultipleSideBySide',
'foreign_table' => 'tx_vitec_domain_model_solution',
'MM' => 'tx_vitec_product_solution_mm',
'size' => 10,
'maxitems' => 9999,
'minitems' => 0,
'enableMultiSelectFilterTextfield' => true,
],
],
'resources' => [
'exclude' => true,
'label' => 'Resources',
'description' => 'Resource cards (documentation, support, datasheet) on the product detail page. A card without a link is not emitted.',
'config' => [
'type' => 'inline',
'foreign_table' => 'tx_vitec_product_resource',
'foreign_field' => 'product',
'foreign_sortby' => 'sorting',
'appearance' => [
'collapseAll' => true,
'levelLinksPosition' => 'top',
'useSortable' => true,
],
],
],
'productimage' => [
'exclude' => true,
'label' => 'Product Image(s)',
@@ -0,0 +1,116 @@
<?php
/**
* Resource cards on the product's Resources tab (decision 2026-09-24):
* documentation / support / datasheet entries, several per type, each an
* editable card (title, text, icon) around one link. Child table of
* tx_vitec_domain_model_product via the `resources` inline field - edited
* only there (hideTable), no Extbase model on purpose: the headless
* renderers query the rows directly, the Extbase path never touches them.
*/
return [
'ctrl' => [
'title' => 'Product Resource',
'label' => 'title',
'label_alt' => 'restype',
'label_alt_force' => true,
'tstamp' => 'tstamp',
'crdate' => 'crdate',
'delete' => 'deleted',
'sortby' => 'sorting',
'hideTable' => true,
'enablecolumns' => [
'disabled' => 'hidden',
],
'iconfile' => 'EXT:vitec/Resources/Public/Icons/tx_vitec_domain_model_download.gif',
'security' => [
'ignorePageTypeRestriction' => true,
],
],
'types' => [
'1' => ['showitem' => 'restype, title, cardtext, icon, link, hidden'],
],
'columns' => [
'hidden' => [
'exclude' => true,
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.visible',
'config' => [
'type' => 'check',
'renderType' => 'checkboxToggle',
'items' => [
[
'label' => '',
'invertStateDisplay' => true,
],
],
],
],
'product' => [
'config' => [
'type' => 'passthrough',
],
],
'restype' => [
'exclude' => true,
'label' => 'Type',
'config' => [
'type' => 'select',
'renderType' => 'selectSingle',
'items' => [
['label' => 'Product Documentation', 'value' => 'documentation'],
['label' => 'Technical Support', 'value' => 'support'],
['label' => 'Datasheet', 'value' => 'datasheet'],
],
'default' => 'documentation',
],
],
'title' => [
'exclude' => true,
'label' => 'Card title',
'config' => [
'type' => 'input',
'size' => 40,
'eval' => 'trim',
'default' => '',
],
],
'cardtext' => [
'exclude' => true,
'label' => 'Card text',
'config' => [
'type' => 'text',
'cols' => 40,
'rows' => 3,
'eval' => 'trim',
],
],
'icon' => [
'exclude' => true,
'label' => 'Icon',
'description' => 'Icon identifier as used by the front end. Free text; the picker offers the seed set.',
'config' => [
'type' => 'input',
'size' => 20,
'eval' => 'trim',
'default' => '',
'valuePicker' => [
'items' => [
['label' => 'Document', 'value' => 'document'],
['label' => 'Support', 'value' => 'support'],
['label' => 'Datasheet', 'value' => 'datasheet'],
],
],
],
],
'link' => [
'exclude' => true,
'label' => 'Link',
'description' => 'Page, external URL, file or a Download record (link browser tab "Download"). A card without a link is not emitted to the front end.',
'config' => [
'type' => 'link',
'allowedTypes' => ['page', 'url', 'file', 'record'],
'size' => 50,
],
],
],
];
@@ -30,6 +30,7 @@
| 1.15 | 2026‑09‑11 | **Corpus change (additive, index side).** Download documents now carry the extracted file text: Solr 10 has no ExtractingRequestHandler, so a dedicated Apache Tika container on the Solr VPS (Caddy route `/tika/*`, own basic-auth credential) extracts PDF/Office contents during indexing (`TikaDownloadContentIndexer` on `BeforeDocumentIsProcessedForIndexingEvent`, fail-soft per 9.5, cached in `var/tika-cache` keyed on path+size+mtime). Datasheet specifications are now searchable ("625i", "genlock"). No interface change. |
| 1.16 | 2026‑09‑11 | **New plugin** `vitec_productfinder` (`ProductFinderJsonRenderer`, payload key `productfinder`): the product category hierarchy on its own, as the data source for the filter selects above the product lists (7.15.3, 8). No change to `vitec_productlist` — filtering happens client side over the products already delivered, keyed on the category uids this payload carries. |
| 1.17 | 2026‑09‑11 | **Interface change (additive).** The product list payload gained `successStories` (7.15.2.1): stories an editor attaches to a `sys_category` (tab *Options*, with its own layout), emitted in the list envelope beside `products`. Each card carries `forCategories` — the product categories it came from — so the front end can hide it with the same `?cat=`/`?subcat=` filter it applies to the products. Story links reuse the 7.5 rule, with the detail page taken from the element or the new site setting `vitec.storyDetailPid`. |
| 1.18 | 2026‑09‑24 | **Interface change (additive).** Products gained `solutions` — an MM relation to solutions, emitted as solution cards in both product payloads — and a **Resources tab**: child table `tx_vitec_product_resource` with documentation / support / datasheet cards (editable title, text and icon around one link, resolved per 9.12; cards without a resolvable link are not emitted), delivered detail‑only as the `resources` list envelope whose `layout` switches to `carousel` above three cards (7.15). Editorial: the footer had been stuck at v1.15 since 1.16/1.17 (2026‑09‑11). |
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
@@ -897,12 +898,14 @@ Produced by `ProductShowJsonRenderer::serializeProduct()`. Members:
| `downloads` | array | download records incl. the resolved `file` |
| `ogimage`, `videofile` | object \| null | FAL singletons |
| `relatedprodukt` | array | card shape `{uid, title, slug, subtitle, teaser, description, link, images}`, backend order |
| `solutions` | array | solution card shape `{uid, title, slug, subtitle, teaser, description, detailUrl, image}` via `tx_vitec_product_solution_mm`, backend order |
| `resources` | object | list envelope `{layout, items[]}` of hand‑written resource cards: `type` (`documentation\|support\|datasheet`), `title`, `text`, `icon`, resolved `url` (9.12); a card without a resolvable link is dropped; `layout` is `carousel` above three cards — **detail payload only** |
| `jsonLd` | string | ready‑to‑emit schema.org `@graph` (6.6), detail only |
#### 7.15.2 List payload (`vitec_productlist`, key `products`)
List envelope per 7.3 (`layout`, `showToolbar`, `products[]`) — since v1.9. Each item
carries the member set of 7.15.1 **except** `heroimage` and `jsonLd`. `heroimage` is
carries the member set of 7.15.1 **except** `heroimage`, `resources` and `jsonLd`. `heroimage` is
deliberately detail‑only: one further FAL query per product would buy data a list
rarely shows. The `layout` vocabulary remains `0`–`3` (see the v1.9 note).
@@ -1469,4 +1472,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.15.*
*End of document EVO‑VITEC‑HL‑001 v1.18.*
+28
View File
@@ -58,6 +58,8 @@ CREATE TABLE tx_vitec_domain_model_product (
capabilities text,
portfolio varchar(1024) DEFAULT '' NOT NULL,
textrelatedproducts text,
solutions int(11) DEFAULT '0' NOT NULL,
resources int(11) DEFAULT '0' NOT NULL,
KEY parent (pid)
);
CREATE TABLE tx_vitec_domain_model_download (
@@ -97,6 +99,32 @@ CREATE TABLE tx_vitec_product_related_mm (
PRIMARY KEY (uid_local, uid_foreign)
);
CREATE TABLE tx_vitec_product_solution_mm (
uid_local INT(11) NOT NULL,
uid_foreign INT(11) NOT NULL,
sorting INT(11) DEFAULT '0' NOT NULL,
PRIMARY KEY (uid_local, uid_foreign)
);
CREATE TABLE tx_vitec_product_resource (
uid int(11) NOT NULL auto_increment,
pid int(11) DEFAULT '0' NOT NULL,
tstamp int(11) DEFAULT '0' NOT NULL,
crdate int(11) DEFAULT '0' NOT NULL,
deleted tinyint(4) DEFAULT '0' NOT NULL,
hidden tinyint(4) DEFAULT '0' NOT NULL,
sorting int(11) DEFAULT '0' NOT NULL,
product int(11) unsigned DEFAULT '0' NOT NULL,
restype varchar(32) DEFAULT 'documentation' NOT NULL,
title varchar(255) DEFAULT '' NOT NULL,
cardtext text,
icon varchar(64) DEFAULT '' NOT NULL,
link varchar(1024) DEFAULT '' NOT NULL,
PRIMARY KEY (uid),
KEY parent (pid),
KEY product (product)
);
CREATE TABLE tx_vitec_domain_model_usecase (
title varchar(255) DEFAULT '' NOT NULL,
slug varchar(255) DEFAULT '' NOT NULL,