Backend of the sub-market template (spec agreed section by section with

the customer workflow in mind: the customer fills one XLSX per sub-market,
an importer builds the page).

- Frontend layout 21 'Sub-Market Page' in the existing
  BackendLayoutDataProvider: six zones, one per Figma section, emitted as
  colPos110..colPos160 - the JSON groups plus appearance.layout are the
  front-end contract (spec clause 7.17). A first attempt via page-TSconfig
  BackendLayouts was the wrong mechanism next to the provider and was
  removed again
- New Content Blocks vitec_markethero, vitec_marketintro,
  vitec_marketinsights (field bags, serialized unprefixed under data);
  the workflow section reuses intro-paragraph + vitec_card flip cards
- Stories gained quote/quote_person/quote_role (Testimonial tab),
  maintained once per story, emitted on every story card; usecaselist
  gained the layout value 'testimonials'
- New plugin vitec_productfeature: one referenced product as a full
  product card, serialisation shared with the product list (cObj made
  protected for the subclass)
- vitec_productlist gained hand-picked product selection: exactly the
  chosen products in the editor's order, category filter ignored,
  visibility filters kept - closes the long-standing single-select gap
- New CLI vitec:generate-submarket-workbooks: one protected customer
  XLSX per sub-market (9 main markets, 25 sub-markets from the Market
  category tree) - dropdowns filled live from the database, named ranges
  as the importer contract, hidden Meta sheet pinning submarket/group/
  record uid, soft character-limit warnings, asset URL fields

Verified end to end on Broadcast & Media: layout-21 payload with colPos110
carrying the hero element and its subline. Spec raised to v1.20.
This commit is contained in:
orasche committed 2026-10-02 10:18:36 +02:00
1 parent f0b0694842
commit 90ae6803a4
31 files changed
+493 -7

No files matched your search

@@ -51,6 +51,12 @@ final class UsecaseSerializer
'teaser' => (string)($u['teaser'] ?? ''),
'cardImage' => $this->image($uid, 'card_image', null, true),
'customerLogo' => $this->image($uid, 'customer_logo'),
// Testimonial data (2026-10-01): one quote per story, maintained
// on the record, consumed by testimonial cards (sub-market
// template). Empty strings when not yet maintained.
'quote' => (string)($u['quote'] ?? ''),
'quotePerson' => (string)($u['quote_person'] ?? ''),
'quoteRole' => (string)($u['quote_role'] ?? ''),
'markets' => $this->relationMulti('tx_vitec_usecase_market_mm', 'tx_vitec_domain_model_market', $uid),
'categories' => $this->categories($uid),
'featured' => (bool)($u['featured'] ?? false),
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Evomedien\Vitec\UserFunc;
use Doctrine\DBAL\ParameterType;
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Service\FlexFormService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Featured product card - sub-market template section 5 (spec 2026-10-01).
*
* One referenced product, emitted as a single full product card under the
* payload key `productfeature`. All copy comes from the product record
* (decision Olli 2026-10-01: dropdown only, no free text) - the front end
* renders image, title and teaser from the same card shape the product
* list delivers, so no second contract exists.
*
* Extends ProductListJsonRenderer purely to reuse its serializeProduct()
* and image handling - same filters, same shape, one source of truth.
*/
final class ProductFeatureJsonRenderer extends ProductListJsonRenderer
{
#[AsAllowedCallable]
public function render(string $content = '', array $conf = []): string
{
try {
$row = $this->cObj?->data ?? [];
$flexFormData = GeneralUtility::makeInstance(FlexFormService::class)
->convertFlexFormContentToArray((string)($row['pi_flexform'] ?? ''));
$productUid = (int)($flexFormData['settings']['product'] ?? 0);
if ($productUid <= 0) {
return (string)json_encode(null);
}
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tx_vitec_domain_model_product');
$product = $queryBuilder->select('*')->from('tx_vitec_domain_model_product')
->where(
$queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)),
$queryBuilder->expr()->eq('deleted', 0),
$queryBuilder->expr()->eq('hidden', 0),
$queryBuilder->expr()->eq('hideonwebsite', 0)
)
->executeQuery()->fetchAssociative();
if (!$product) {
return (string)json_encode(null);
}
return (string)json_encode($this->serializeProduct($product), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
} catch (\Throwable) {
return (string)json_encode(null);
}
}
}
@@ -31,7 +31,9 @@ use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
*/
class ProductListJsonRenderer
{
private ?ContentObjectRenderer $cObj = null;
// protected (not private) since 2026-10-01: ProductFeatureJsonRenderer
// extends this class to reuse serializeProduct() and needs the cObj.
protected ?ContentObjectRenderer $cObj = null;
/**
* TYPO3 v14 hands the ContentObjectRenderer over through this setter only -
@@ -119,6 +121,14 @@ class ProductListJsonRenderer
}
$debugMode = (bool)($settings['debug'] ?? false);
$allProducts = (bool)($settings['allproducts'] ?? false);
// Explicit picks (2026-10-01, built for the sub-market template's
// Products section): when set, EXACTLY these products in the
// editor's order - the category filter is ignored. The visibility
// filters below stay in force on purpose, so a hidden/legacy
// record never surfaces through a stale selection.
$selectedUids = array_filter(
array_map('intval', explode(',', (string)($settings['selectedproducts'] ?? '')))
);
// Extbase repositories don't work in UserFunc context — direct query.
$productQueryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
@@ -137,7 +147,11 @@ class ProductListJsonRenderer
$productQueryBuilder->expr()->eq('p.subproduct', 0)
);
if (!empty($categoryUids) && !$allProducts) {
if ($selectedUids !== [] && !$allProducts) {
$productQuery->andWhere(
$productQueryBuilder->expr()->in('p.uid', $productQueryBuilder->createNamedParameter($selectedUids, Connection::PARAM_INT_ARRAY))
);
} elseif (!empty($categoryUids) && !$allProducts) {
$productQuery
->join(
'p',
@@ -159,7 +173,12 @@ class ProductListJsonRenderer
// sitemap. $categoryUids comes from expandWithDescendants in
// depth-first tree order; within one category the uid order is
// kept - the records were created in sitemap-V2 row order.
if (!empty($categoryUids) && !$allProducts && $products !== []) {
if ($selectedUids !== [] && !$allProducts && $products !== []) {
// Editor's order, not storage order.
$rankBySelection = array_flip($selectedUids);
usort($products, static fn(array $a, array $b): int =>
($rankBySelection[(int)$a['uid']] ?? PHP_INT_MAX) <=> ($rankBySelection[(int)$b['uid']] ?? PHP_INT_MAX));
} elseif (!empty($categoryUids) && !$allProducts && $products !== []) {
$rankByCategory = array_flip($categoryUids);
$mmQueryBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\ConnectionPool::class)
->getQueryBuilderForTable('sys_category_record_mm');
@@ -27,6 +27,15 @@ final class BackendLayoutDataProvider implements DataProviderInterface
'hero' => 1,
'sidebar' => 2,
'preFooter' => 3,
// Sub-market template sections (spec 2026-10-01): one slot per Figma
// section. The headless JSON groups these as colPos110..colPos160 -
// that grouping IS the front-end contract for layout 21.
'smHero' => 110,
'smIntro' => 120,
'smWorkflow' => 130,
'smTestimonials' => 140,
'smProducts' => 150,
'smInsights' => 160,
];
/** Per-frontend-layout: title + ordered list of zones. */
@@ -52,6 +61,7 @@ final class BackendLayoutDataProvider implements DataProviderInterface
18 => ['title' => 'Content Page V3', 'zones' => ['hero', 'main', 'sidebar', 'preFooter']],
19 => ['title' => 'Events Overview', 'zones' => ['hero', 'main']],
20 => ['title' => 'Contact Page', 'zones' => ['hero', 'main', 'sidebar']],
21 => ['title' => 'Sub-Market Page', 'zones' => ['smHero', 'smIntro', 'smWorkflow', 'smTestimonials', 'smProducts', 'smInsights']],
];
/** Display name for each zone. Shown as column header in the BE page module. */
@@ -60,6 +70,12 @@ final class BackendLayoutDataProvider implements DataProviderInterface
'hero' => 'Hero',
'sidebar' => 'Sidebar',
'preFooter' => 'Pre-Footer',
'smHero' => '1 · Hero',
'smIntro' => '2 · Intro',
'smWorkflow' => '3 · Workflow (header + flip cards)',
'smTestimonials' => '4 · Testimonials',
'smProducts' => '5 · Products (featured + list)',
'smInsights' => '6 · Industry Insights',
];
public function getIdentifier(): string
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Featured product card - sub-market template section 5.
One product reference, all copy comes from the record.
-->
<T3DataStructure>
<sheets>
<sDEF>
<ROOT>
<sheetTitle>Featured Product</sheetTitle>
<type>array</type>
<el>
<settings.product>
<label>Product</label>
<description>The product shown as the big featured card. Image, title and teaser come from the record.</description>
<config>
<type>group</type>
<allowed>tx_vitec_domain_model_product</allowed>
<size>1</size>
<maxitems>1</maxitems>
</config>
</settings.product>
</el>
</ROOT>
</sDEF>
</sheets>
</T3DataStructure>
@@ -30,6 +30,16 @@
</items>
</config>
</settings.debug>
<settings.selectedproducts>
<label>Hand-picked products</label>
<description>When set, the list shows EXACTLY these products in this order - the category selection below is ignored. Hidden/legacy products stay suppressed. Used e.g. by the sub-market template's Products section.</description>
<config>
<type>group</type>
<allowed>tx_vitec_domain_model_product</allowed>
<size>6</size>
<maxitems>50</maxitems>
</config>
</settings.selectedproducts>
<settings.categories>
<label>Select Category</label>
<description>
@@ -29,6 +29,10 @@
<label>50 / 50</label>
<value>50-50</value>
</numIndex>
<numIndex index="4">
<label>Testimonials (quote carousel, sub-market template)</label>
<value>testimonials</value>
</numIndex>
</items>
<default>grid</default>
</config>
@@ -67,6 +67,21 @@ tt_content {
}
}
# Featured product card (sub-market template section 5): one referenced
# product as a full product card under `productfeature` - same shape as
# the list items, serialization shared with ProductListJsonRenderer.
vitec_productfeature < lib.contentElementWithHeader
vitec_productfeature {
fields {
content {
fields {
productfeature = USER
productfeature.userFunc = Evomedien\Vitec\UserFunc\ProductFeatureJsonRenderer->render
}
}
}
}
# Product Finder - only the category hierarchy for the filter selects.
# Placed once above the product lists; the front end filters the products
# those lists already delivered, client side, via ?cat= and ?subcat=.
@@ -31,6 +31,7 @@ defined('TYPO3') or die();
['label' => $l . 'layout.18', 'value' => '18'],
['label' => $l . 'layout.19', 'value' => '19'],
['label' => $l . 'layout.20', 'value' => '20'],
['label' => $l . 'layout.21', 'value' => '21'],
];
$GLOBALS['TCA']['pages']['columns']['layout']['config']['default'] = '0';
@@ -26,6 +26,7 @@ use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
$registerPluginWithFlexForm('Productlist', 'Show Products by Category', 'FILE:EXT:vitec/Configuration/FlexForms/Productlist.xml', 'vitec-plugin-productlist');
$registerPluginWithFlexForm('Productfinder', 'Product Finder (filter data)', 'FILE:EXT:vitec/Configuration/FlexForms/Productfinder.xml', 'vitec-plugin-productlist');
$registerPluginWithFlexForm('Productfeature', 'Featured Product Card', 'FILE:EXT:vitec/Configuration/FlexForms/Productfeature.xml', 'vitec-plugin-productlist');
$registerPluginWithFlexForm('Simplecard', 'Simple Card', 'FILE:EXT:vitec/Configuration/FlexForms/Simplecard.xml', 'vitec-plugin-simplecard');
$registerPluginWithFlexForm('Productshow', 'Show Single Product', 'FILE:EXT:vitec/Configuration/FlexForms/Productshow.xml', 'vitec-plugin-productshow');
$registerPluginWithFlexForm('Usecaseshow', 'Single Success Story', 'FILE:EXT:vitec/Configuration/FlexForms/Usecase.xml', 'vitec-plugin-usecaseshow');
@@ -29,6 +29,8 @@ return [
title, slug, detail_page, subtitle,
--div--;List view,
card_image, customer_logo, teaser,
--div--;Testimonial,
quote, quote_person, quote_role,
--div--;Detail · Hero,
hero_bgimage, hero_small_image, hero_video, hero_overlay_color, hero_overlay_opacity,
--div--;Detail · Content,
@@ -153,6 +155,26 @@ return [
'config' => ['type' => 'text', 'cols' => 40, 'rows' => 4, 'eval' => 'trim'],
],
// ------------------------------------------------------- Testimonial
// One quote per story, maintained editorially and reused wherever the
// story appears as a testimonial card (first consumer: the sub-market
// template, decision Olli 2026-10-01 - the customer's XLS only picks
// the story, never carries quote text).
'quote' => [
'label' => 'Quote',
'description' => 'Shown on testimonial cards wherever this story is referenced. Plain text, no quotation marks - the front end adds them.',
'config' => ['type' => 'text', 'cols' => 40, 'rows' => 4, 'eval' => 'trim'],
],
'quote_person' => [
'label' => 'Quote: person',
'config' => ['type' => 'input', 'size' => 30, 'max' => 255, 'eval' => 'trim'],
],
'quote_role' => [
'label' => 'Quote: role / title',
'description' => 'E.g. "Vice President of Technology, KCR" - shown below the name.',
'config' => ['type' => 'input', 'size' => 30, 'max' => 255, 'eval' => 'trim'],
],
// ------------------------------------------------------ Detail · Hero
'hero_bgimage' => [
'label' => 'Background Image',
@@ -16,6 +16,14 @@ mod {
CType = vitec_productlist
}
}
productfeature {
iconIdentifier = vitec-plugin-productlist
title = Featured Product Card
description = One product as the big featured card (sub-market template). All copy comes from the record.
tt_content_defValues {
CType = vitec_productfeature
}
}
productfinder {
iconIdentifier = vitec-plugin-productlist
title = Product Finder (filter data)
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
<rect x="1.5" y="2.5" width="13" height="11" rx="1" fill="none" stroke="#000" stroke-width="1.2"/>
<text x="3.4" y="6.6" font-family="serif" font-weight="bold" font-size="6" fill="#000"></text>
<rect x="3" y="8" width="9" height="1" rx="0.2" fill="#000" opacity="0.4"/>
<rect x="3" y="9.6" width="7.5" height="1" rx="0.2" fill="#000" opacity="0.35"/>
<rect x="3" y="11.2" width="4" height="1.6" rx="0.3" fill="#000" opacity="0.55"/>
</svg>

After

Width:  |  Height:  |  Size: 536 B

@@ -0,0 +1,45 @@
# Sub-market template section 1 (Figma "Sub-Market Page", spec 2026-10-01).
# The H1 is NOT a field - it is the page title (dev names are canonical).
name: vitec/market-hero
group: vitec_custom_components
prefixFields: true
prefixType: vendor
fields:
- identifier: subline
type: Text
max: 120
- identifier: cta_section
type: Palette
label: CTA button
fields:
- identifier: cta_label
type: Text
max: 40
- identifier: cta_link
type: Link
- identifier: bgimage
type: File
minitems: 0
maxitems: 1
allowed: common-image-types
- identifier: video_section
type: Palette
label: Video teaser (optional - empty url hides the card)
fields:
- identifier: video_title
type: Text
max: 80
- type: Linebreak
- identifier: video_url
type: Text
max: 500
- type: Linebreak
- identifier: video_cover
type: File
minitems: 0
maxitems: 1
allowed: common-image-types
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.2">
<file source-language="en" datatype="plaintext" original="messages">
<body>
<trans-unit id="title"><source>VITEC · Market Hero</source></trans-unit>
<trans-unit id="description"><source>Sub-market template, section 1: subline, CTA, background image, optional video teaser. The H1 is the page title.</source></trans-unit>
<trans-unit id="subline.label"><source>Subline</source></trans-unit>
<trans-unit id="subline.description"><source>Max ~90 characters visible (two lines in the design).</source></trans-unit>
<trans-unit id="cta_label.label"><source>CTA label</source></trans-unit>
<trans-unit id="cta_label.description"><source>Empty = front-end default "Get in touch".</source></trans-unit>
<trans-unit id="cta_link.label"><source>CTA target</source></trans-unit>
<trans-unit id="cta_link.description"><source>Empty = /contact.</source></trans-unit>
<trans-unit id="bgimage.label"><source>Background image</source></trans-unit>
<trans-unit id="bgimage.description"><source>1920x600, dark enough for white text. Empty = dark gradient fallback.</source></trans-unit>
<trans-unit id="video_title.label"><source>Video title</source></trans-unit>
<trans-unit id="video_url.label"><source>Video URL</source></trans-unit>
<trans-unit id="video_url.description"><source>YouTube/Vimeo or file URL. Empty = no video card, hero runs full width.</source></trans-unit>
<trans-unit id="video_cover.label"><source>Video cover image</source></trans-unit>
</body>
</file>
</xliff>
@@ -0,0 +1,12 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<f:layout name="Preview"/>
<f:section name="Content">
<f:asset.css identifier="vitec-backend-preview" href="EXT:vitec/Resources/Public/Css/backend-preview.css"/>
<div class="vitec-preview">
<div class="vitec-preview__body">
<strong>Market Hero</strong> — {data.vitec_subline}
<f:if condition="{data.vitec_video_url}"><span> · 🎬 {data.vitec_video_title}</span></f:if>
</div>
</div>
</f:section>
</html>
@@ -0,0 +1,3 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<!-- Headless mode: JSON is built by nb-headless-content-blocks -->
</html>
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
<rect x="1.5" y="2.5" width="13" height="11" rx="1" fill="none" stroke="#000" stroke-width="1.2"/>
<text x="3.4" y="6.6" font-family="serif" font-weight="bold" font-size="6" fill="#000"></text>
<rect x="3" y="8" width="9" height="1" rx="0.2" fill="#000" opacity="0.4"/>
<rect x="3" y="9.6" width="7.5" height="1" rx="0.2" fill="#000" opacity="0.35"/>
<rect x="3" y="11.2" width="4" height="1.6" rx="0.3" fill="#000" opacity="0.55"/>
</svg>

After

Width:  |  Height:  |  Size: 536 B

@@ -0,0 +1,50 @@
# Sub-market template section 6 (spec 2026-10-01).
name: vitec/market-insights
group: vitec_custom_components
prefixFields: true
prefixType: vendor
fields:
- identifier: text1
type: Textarea
rows: 4
- identifier: text2
type: Textarea
rows: 4
- identifier: cta_section
type: Palette
label: CTA (optional)
fields:
- identifier: cta_label
type: Text
max: 40
- identifier: cta_target
type: Text
max: 500
- identifier: video_section
type: Palette
label: Video card (optional - empty url hides the card)
fields:
- identifier: video_person
type: Text
max: 80
- identifier: video_role
type: Text
max: 120
- type: Linebreak
- identifier: video_title
type: Text
max: 120
- type: Linebreak
- identifier: video_url
type: Text
max: 500
- type: Linebreak
- identifier: video_cover
type: File
minitems: 0
maxitems: 1
allowed: common-image-types
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.2">
<file source-language="en" datatype="plaintext" original="messages">
<body>
<trans-unit id="title"><source>VITEC · Market Insights</source></trans-unit>
<trans-unit id="description"><source>Sub-market template, section 6: two paragraphs, optional CTA, optional video card with person.</source></trans-unit>
<trans-unit id="text1.label"><source>Paragraph 1</source></trans-unit>
<trans-unit id="text1.description"><source>Plain text, max ~250 characters.</source></trans-unit>
<trans-unit id="text2.label"><source>Paragraph 2</source></trans-unit>
<trans-unit id="text2.description"><source>Plain text, max ~250 characters.</source></trans-unit>
<trans-unit id="cta_label.label"><source>CTA label</source></trans-unit>
<trans-unit id="cta_label.description"><source>Empty = no button.</source></trans-unit>
<trans-unit id="cta_target.label"><source>CTA target</source></trans-unit>
<trans-unit id="cta_target.description"><source>Section anchor OR URL. Empty with a label set = /success-stories.</source></trans-unit>
<trans-unit id="video_person.label"><source>Person</source></trans-unit>
<trans-unit id="video_role.label"><source>Role / title</source></trans-unit>
<trans-unit id="video_title.label"><source>Video title</source></trans-unit>
<trans-unit id="video_url.label"><source>Video URL</source></trans-unit>
<trans-unit id="video_url.description"><source>Empty = no video card, text runs wider.</source></trans-unit>
<trans-unit id="video_cover.label"><source>Cover image</source></trans-unit>
</body>
</file>
</xliff>
@@ -0,0 +1,12 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<f:layout name="Preview"/>
<f:section name="Content">
<f:asset.css identifier="vitec-backend-preview" href="EXT:vitec/Resources/Public/Css/backend-preview.css"/>
<div class="vitec-preview">
<div class="vitec-preview__body">
<strong>Market Insights</strong> — <f:format.crop maxCharacters="100">{data.vitec_text1}</f:format.crop>
<f:if condition="{data.vitec_video_url}"><span> · 🎬 {data.vitec_video_person}</span></f:if>
</div>
</div>
</f:section>
</html>
@@ -0,0 +1,3 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<!-- Headless mode: JSON is built by nb-headless-content-blocks -->
</html>
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
<rect x="1.5" y="2.5" width="13" height="11" rx="1" fill="none" stroke="#000" stroke-width="1.2"/>
<text x="3.4" y="6.6" font-family="serif" font-weight="bold" font-size="6" fill="#000"></text>
<rect x="3" y="8" width="9" height="1" rx="0.2" fill="#000" opacity="0.4"/>
<rect x="3" y="9.6" width="7.5" height="1" rx="0.2" fill="#000" opacity="0.35"/>
<rect x="3" y="11.2" width="4" height="1.6" rx="0.3" fill="#000" opacity="0.55"/>
</svg>

After

Width:  |  Height:  |  Size: 536 B

@@ -0,0 +1,33 @@
# Sub-market template section 2 (spec 2026-10-01).
name: vitec/market-intro
group: vitec_custom_components
prefixFields: true
prefixType: vendor
fields:
- identifier: intro_text
type: Textarea
rows: 6
- identifier: image
type: File
minitems: 0
maxitems: 1
allowed: common-image-types
- identifier: learnmore_section
type: Palette
label: Learn-more button (optional)
fields:
- identifier: learnmore_label
type: Text
max: 40
default: Learn more
- identifier: learnmore_target
type: Text
max: 500
- identifier: show_portfolio
type: Checkbox
renderType: checkboxToggle
default: 1
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.2">
<file source-language="en" datatype="plaintext" original="messages">
<body>
<trans-unit id="title"><source>VITEC · Market Intro</source></trans-unit>
<trans-unit id="description"><source>Sub-market template, section 2: intro text, image right, optional learn-more button, portfolio button toggle.</source></trans-unit>
<trans-unit id="intro_text.label"><source>Intro text</source></trans-unit>
<trans-unit id="intro_text.description"><source>Plain text, max ~350 characters. No HTML.</source></trans-unit>
<trans-unit id="image.label"><source>Image (right)</source></trans-unit>
<trans-unit id="image.description"><source>636x469 (about 4:3).</source></trans-unit>
<trans-unit id="learnmore_label.label"><source>Label</source></trans-unit>
<trans-unit id="learnmore_target.label"><source>Target</source></trans-unit>
<trans-unit id="learnmore_target.description"><source>Section anchor (workflow, testimonials, products, insights) OR a URL. Empty = no button.</source></trans-unit>
<trans-unit id="show_portfolio.label"><source>Show "Product Portfolio" button</source></trans-unit>
<trans-unit id="show_portfolio.description"><source>Links to /products (fixed for now).</source></trans-unit>
</body>
</file>
</xliff>
@@ -0,0 +1,11 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<f:layout name="Preview"/>
<f:section name="Content">
<f:asset.css identifier="vitec-backend-preview" href="EXT:vitec/Resources/Public/Css/backend-preview.css"/>
<div class="vitec-preview">
<div class="vitec-preview__body">
<strong>Market Intro</strong> — <f:format.crop maxCharacters="120">{data.vitec_intro_text}</f:format.crop>
</div>
</div>
</f:section>
</html>
@@ -0,0 +1,3 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<!-- Headless mode: JSON is built by nb-headless-content-blocks -->
</html>
@@ -3,9 +3,9 @@
| | |
|---|---|
| **Document identifier** | EVO‑VITEC‑HL‑001 |
| **Version** | 1.19 |
| **Version** | 1.20 |
| **Status** | Released |
| **Date** | 2026‑09‑25 |
| **Date** | 2026‑10‑01 |
| **Applies to** | `evomedien/vitec` on TYPO3 v14.3 (headless) |
| **Owner** | evomedien — VITEC relaunch |
@@ -32,6 +32,7 @@
| 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 cards (editable title, text and icon around one link, resolved per 9.12; cards without a resolvable link are not emitted) plus a `datasheets` MM selection of download records rendered into the same envelope as `type: datasheet` cards, delivered detail‑only as the `resources` list envelope whose `layout` switches to `carousel` above three cards in total (7.15). The `solutions` relation accepts group‑level records: the eight groups exist as `is_group = 1` solutions (created by `vitec:create-solution-groups`, excluded from lists and from `vitec:align-solutions`) because the agency workbooks reference the group level. Editorial: the footer had been stuck at v1.15 since 1.16/1.17 (2026‑09‑11). |
| 1.19 | 2026‑09‑25 | **Interface change (additive).** The `vitec_card` Content Block gained the `layout_variant` value `flipcard` and a *Card Back* palette: `back_headline` (string, max 100) and `back_text` (richtext → HTML). The two fields are `displayCond`‑gated on the flipcard layout in the backend but serialised unconditionally (7.7) — the front end **shall** key the back face on `layout_variant`, never on field presence, because stored back values persist when an editor switches the layout away. Backend repair: `vitec_layout_variant` / `vitec_background_variant` are shared with `vitec_ctabanner`, and the index‑wise Content‑Blocks merge had dropped four card‑only select values (“Missing label”) — per‑type item lists now come from `columnsOverrides` (7.7); the Card‑Back palette lives in the Layout tab below the variant select. Editorial: the head‑matter version had been stuck at v1.17 since 1.18. |
| 1.20 | 2026‑10‑01 | **Interface change (additive).** Sub-market page template: frontend layout 21 with one colPos group per design section (`colPos110`..`colPos160`, clause 7.17); new Content Blocks `vitec_markethero`, `vitec_marketintro`, `vitec_marketinsights`; new plugin `vitec_productfeature` (key `productfeature`, one product in the 7.15.2 item shape); story cards gained `quote`/`quotePerson`/`quoteRole`; `vitec_usecaselist` gained the layout value `testimonials`; `vitec_productlist` gained hand-picked product selection (editor order, category filter ignored). Field semantics: Template_Sub-Market_Spezifikation_2026-10-01.md. |
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
@@ -1131,6 +1132,43 @@ content is extracted from the page’s `tt_content` rows via
`index.queue.pages.fields.content` because the headless JSON output carries no
`TYPO3SEARCH` markers.
---
### 7.17 Sub-market page template (frontend layout 21)
A sub-market page is identified by `appearance.layout == "layout-21"`. Its
content is NOT read from element order: the backend layout assigns one column
per design section, and the JSON groups them as
| Group | Section | Elements |
|---|---|---|
| `colPos110` | Hero | `vitec_markethero` |
| `colPos120` | Intro | `vitec_marketintro` |
| `colPos130` | Workflow | `vitec_introparagraph` + container with `vitec_card` flip cards |
| `colPos140` | Testimonials | `vitec_usecaselist`, `layout: "testimonials"` |
| `colPos150` | Products | `vitec_productfeature` + `vitec_productlist` (hand-picked) |
| `colPos160` | Industry Insights | `vitec_marketinsights` |
The three `vitec_market*` elements are Content Blocks: their fields arrive
unprefixed under the element's `data` (7.7). Field reference and empty-value
semantics ("empty url = card hidden" etc.) are specified in
`Template_Sub-Market_Spezifikation_2026-10-01.md`; the per-field limits live
in the backend field descriptions.
Related interface additions (all additive):
- **Story cards** (7.5 shape, everywhere they appear) carry `quote`,
`quotePerson`, `quoteRole` - maintained once per story, empty strings until
maintained. The testimonial card renders them; the `vitec_usecaselist`
FlexForm gained the `layout` value `testimonials` for this.
- **`vitec_productfeature`** (payload key `productfeature`): exactly one
referenced product as a full product card in the 7.15.2 item shape, `null`
when unset or the product is not visible. Serialisation is shared with the
product list (one source of truth).
- **`vitec_productlist`** gained the FlexForm field *Hand-picked products*:
when set, the list contains exactly those products in the editor's order;
the category selection is ignored, the visibility filters stay in force.
## 8 Content‑type catalogue
| CType | TS pattern | Renderer / Processor | Payload key | Kind |
@@ -66,6 +66,9 @@
<trans-unit id="layout.20" resname="layout.20">
<source>Contact Page</source>
</trans-unit>
<trans-unit id="layout.21" resname="layout.21">
<source>Sub-Market Page</source>
</trans-unit>
</body>
</file>
</xliff>
+3
View File
@@ -139,6 +139,9 @@ CREATE TABLE tx_vitec_domain_model_usecase (
slug varchar(255) DEFAULT '' NOT NULL,
detail_page int(11) DEFAULT '0' NOT NULL,
subtitle varchar(255) DEFAULT '' NOT NULL,
quote text,
quote_person varchar(255) DEFAULT '' NOT NULL,
quote_role varchar(255) DEFAULT '' NOT NULL,
card_image int(11) unsigned DEFAULT '0' NOT NULL,
customer_logo int(11) unsigned DEFAULT '0' NOT NULL,
+2 -2
View File
@@ -23,8 +23,8 @@
<meta name="twitter:description" content="Enterprise IPTV, video streaming, and digital signage solutions." />
<link rel="icon" type="image/svg+xml" href="/fileadmin/icons/favicon.ico" />
<title>VITEC</title>
<script type="module" crossorigin src="/_frontend/assets/index-DnFsTWc9.js"></script>
<link rel="stylesheet" crossorigin href="/_frontend/assets/index-D2VJrjll.css">
<script type="module" crossorigin src="/_frontend/assets/index-Bp_49Z5l.js"></script>
<link rel="stylesheet" crossorigin href="/_frontend/assets/index-Cj1orKsH.css">
</head>
<body>
<div id="page"></div>␍