';
+ }
+
+ // ---- record / page resolution -----------------------------------------------
+
+ /** @return array|null the is_group=1 solution record */
+ private function groupRecord(string $title): ?array
+ {
+ $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('tx_vitec_domain_model_solution');
+ foreach ($connection->fetchAllAssociative(
+ 'SELECT * FROM tx_vitec_domain_model_solution WHERE deleted = 0 AND is_group = 1'
+ ) as $row) {
+ if ($this->norm((string)$row['title']) === $this->norm($title)) {
+ return $row;
+ }
+ }
+ return null;
+ }
+
+ private function pageExists(int $uid): bool
+ {
+ $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('pages');
+ return (bool)$connection->fetchOne('SELECT uid FROM pages WHERE deleted = 0 AND uid = ?', [$uid]);
+ }
+
+ // ---- element plumbing (pattern of the sibling importers) ----------------------
+
+ /**
+ * Existing element for this (colPos, CType) in sorting order, consumed
+ * cursor-wise so a CType may appear several times per column (the two
+ * marketintros, the eight cards); falls back to a NEW element.
+ */
+ private function slotNext(int $pageUid, int $colPos, string $ctype): string
+ {
+ $key = $colPos . '/' . $ctype;
+ if (!isset($this->slotCursor[$key])) {
+ $this->slotCursor[$key] = $this->slotElements($pageUid, $colPos, $ctype);
+ }
+ $uid = array_shift($this->slotCursor[$key]);
+ if ($uid !== null) {
+ $this->sequence[$colPos][] = (string)$uid;
+ return (string)$uid;
+ }
+ $id = $this->newId();
+ $this->datamap['tt_content'][$id] = [
+ 'pid' => $pageUid,
+ 'colPos' => $colPos,
+ 'CType' => $ctype,
+ ];
+ $this->sequence[$colPos][] = $id;
+ return $id;
+ }
+
+ /** @return int[] uids in sorting order */
+ private function slotElements(int $pageUid, int $colPos, string $ctype): array
+ {
+ $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('tt_content');
+ return array_map('intval', $connection->fetchFirstColumn(
+ 'SELECT uid FROM tt_content WHERE deleted = 0 AND pid = ? AND colPos = ? AND CType = ? ORDER BY sorting',
+ [$pageUid, $colPos, $ctype]
+ ));
+ }
+
+ /**
+ * Writes tt_content.sorting in page order (DataHandler puts NEW records
+ * with a positive pid at the TOP of their column - lesson 2026-10-07).
+ *
+ * @param array $substNEWwithIDs
+ */
+ private function applySequence(array $substNEWwithIDs): void
+ {
+ $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('tt_content');
+ foreach ($this->sequence as $ids) {
+ $sorting = 0;
+ foreach ($ids as $id) {
+ $uid = str_starts_with($id, 'NEW') ? (int)($substNEWwithIDs[$id] ?? 0) : (int)$id;
+ if ($uid > 0) {
+ $sorting += 256;
+ $connection->update('tt_content', ['sorting' => $sorting], ['uid' => $uid]);
+ }
+ }
+ }
+ }
+
+ /** @param array $fields */
+ private function setFields(string $table, string $id, array $fields): void
+ {
+ foreach ($fields as $field => $value) {
+ $this->datamap[$table][$id][$field] = $value;
+ }
+ }
+
+ /**
+ * Grey numbered stand-in for an image slot (same rules as the sibling
+ * importers): never over an existing reference; numbered file
+ * placeholders/pages/-NN.png preferred, generic grey as fallback.
+ */
+ private function attachPlaceholder(string $elementId, string $fieldName, bool $apply, int $pid, int $slot): void
+ {
+ if (!str_starts_with($elementId, 'NEW') && $this->liveReferences((int)$elementId, $fieldName) !== []) {
+ return;
+ }
+ $storage = GeneralUtility::makeInstance(StorageRepository::class)->getDefaultStorage();
+ $identifier = null;
+ if ($slot > 0) {
+ $candidate = 'placeholders/pages/' . $pid . '-' . str_pad((string)$slot, 2, '0', STR_PAD_LEFT) . '.png';
+ if ($storage->hasFile($candidate)) {
+ $identifier = $candidate;
+ }
+ }
+ $identifier ??= 'placeholders/' . ($fieldName === 'vitec_bgimage' ? 'placeholder-grey-wide.png' : 'placeholder-grey.png');
+ if (!$storage->hasFile($identifier)) {
+ $this->report[] = $fieldName . ': empty in the workbook and fileadmin/' . $identifier . ' is missing - left empty';
+ return;
+ }
+ if (!$apply) {
+ $this->report[] = $fieldName . ': empty in the workbook - would attach ' . $identifier;
+ return;
+ }
+ if ($this->attachFalFile($elementId, $fieldName, $storage->getFile($identifier), $pid) !== 'unchanged') {
+ $this->report[] = $fieldName . ': placeholder ' . basename($identifier) . ' attached - swap in the real asset later';
+ }
+ }
+
+ /**
+ * Attach one FAL file, re-import-idempotent BY CONTENT (lesson
+ * 2026-10-02): reuse the first live reference, delete surplus duplicates.
+ */
+ private function attachFalFile(string $elementId, string $fieldName, \TYPO3\CMS\Core\Resource\File $file, int|string $pid): string
+ {
+ if (!str_starts_with($elementId, 'NEW')) {
+ $existingRefs = $this->liveReferences((int)$elementId, $fieldName);
+ if ($existingRefs !== []) {
+ $first = array_shift($existingRefs);
+ $state = 'unchanged';
+ if ((int)$first['uid_local'] !== $file->getUid()) {
+ $this->datamap['sys_file_reference'][(int)$first['uid']] = ['uid_local' => $file->getUid()];
+ $state = 'swapped';
+ }
+ foreach ($existingRefs as $surplus) {
+ $this->cmdmap['sys_file_reference'][(int)$surplus['uid']]['delete'] = 1;
+ $this->report[] = $fieldName . ': removed duplicate reference #' . $surplus['uid'];
+ }
+ return $state;
+ }
+ }
+ $refId = $this->newId();
+ // pid, tablenames and uid_foreign are REQUIRED - without them
+ // DataHandler silently rejects the reference (lesson 2026-10-02).
+ $this->datamap['sys_file_reference'][$refId] = [
+ 'pid' => $pid,
+ 'table_local' => 'sys_file',
+ 'uid_local' => $file->getUid(),
+ 'tablenames' => 'tt_content',
+ 'fieldname' => $fieldName,
+ 'uid_foreign' => str_starts_with($elementId, 'NEW') ? $elementId : (int)$elementId,
+ ];
+ $this->datamap['tt_content'][$elementId][$fieldName] = $refId;
+ return 'attached';
+ }
+
+ /** @return array live refs on one field, oldest first */
+ private function liveReferences(int $elementUid, string $fieldName): array
+ {
+ $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('sys_file_reference');
+ return array_map(
+ static fn(array $r): array => ['uid' => (int)$r['uid'], 'uid_local' => (int)$r['uid_local']],
+ $connection->fetchAllAssociative(
+ "SELECT uid, uid_local FROM sys_file_reference
+ WHERE deleted = 0 AND hidden = 0 AND tablenames = 'tt_content'
+ AND fieldname = ? AND uid_foreign = ? ORDER BY uid",
+ [$fieldName, $elementUid]
+ )
+ );
+ }
+
+ /**
+ * Page abstract: tags stripped, whitespace collapsed, word-boundary cut
+ * near 300 characters; SEO description as fallback.
+ */
+ private function abstractFrom(string $introHtml, string $fallback): string
+ {
+ $plain = trim((string)preg_replace('/\s+/u', ' ', html_entity_decode(strip_tags($introHtml), ENT_QUOTES | ENT_HTML5)));
+ if ($plain === '') {
+ return $fallback;
+ }
+ if (mb_strlen($plain) <= 300) {
+ return $plain;
+ }
+ $cut = mb_substr($plain, 0, 300);
+ $pos = mb_strrpos($cut, ' ');
+ return rtrim(mb_substr($cut, 0, $pos ?: 300), ' ,;:.') . "\u{2026}";
+ }
+
+ /**
+ * DataHandler flexform array for sheet sDEF.
+ *
+ * @param array $settings
+ * @return array
+ */
+ private function flex(array $settings): array
+ {
+ $fields = [];
+ foreach ($settings as $key => $value) {
+ $fields[$key] = ['vDEF' => $value];
+ }
+ return ['data' => ['sDEF' => ['lDEF' => $fields]]];
+ }
+
+ private function newId(): string
+ {
+ return 'NEW' . str_pad((string)(++$this->newCounter), 6, '0', STR_PAD_LEFT) . substr(md5(uniqid('', true)), 0, 6);
+ }
+
+ private function norm(string $value): string
+ {
+ return (string)preg_replace('/[^a-z0-9]+/', '', str_replace('+', 'plus', mb_strtolower($value)));
+ }
+}
diff --git a/packages/vitec/Classes/View/BackendLayoutDataProvider.php b/packages/vitec/Classes/View/BackendLayoutDataProvider.php
index 812b6eb..40a375a 100755
--- a/packages/vitec/Classes/View/BackendLayoutDataProvider.php
+++ b/packages/vitec/Classes/View/BackendLayoutDataProvider.php
@@ -62,6 +62,9 @@ final class BackendLayoutDataProvider implements DataProviderInterface
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']],
+ // Solution group hero pages (Joe's Template 06, import 2026-10-23) -
+ // same hero/main zones as layout 5, so the front end reuses parts.
+ 22 => ['title' => 'Solution Group Page', 'zones' => ['hero', 'main']],
];
/** Display name for each zone. Shown as column header in the BE page module. */
diff --git a/packages/vitec/Configuration/TCA/Overrides/pages.php b/packages/vitec/Configuration/TCA/Overrides/pages.php
index 94c2726..1574ae4 100755
--- a/packages/vitec/Configuration/TCA/Overrides/pages.php
+++ b/packages/vitec/Configuration/TCA/Overrides/pages.php
@@ -32,6 +32,7 @@ defined('TYPO3') or die();
['label' => $l . 'layout.19', 'value' => '19'],
['label' => $l . 'layout.20', 'value' => '20'],
['label' => $l . 'layout.21', 'value' => '21'],
+ ['label' => $l . 'layout.22', 'value' => '22'],
];
$GLOBALS['TCA']['pages']['columns']['layout']['config']['default'] = '0';
diff --git a/packages/vitec/Documentation/Headless-JSON-Architecture.md b/packages/vitec/Documentation/Headless-JSON-Architecture.md
index a367075..9e0bd2a 100755
--- a/packages/vitec/Documentation/Headless-JSON-Architecture.md
+++ b/packages/vitec/Documentation/Headless-JSON-Architecture.md
@@ -3,9 +3,9 @@
| | |
|---|---|
| **Document identifier** | EVO‑VITEC‑HL‑001 |
-| **Version** | 1.23 |
+| **Version** | 1.24 |
| **Status** | Released |
-| **Date** | 2026‑10‑09 |
+| **Date** | 2026‑10‑23 |
| **Applies to** | `evomedien/vitec` on TYPO3 v14.3 (headless) |
| **Owner** | evomedien — VITEC relaunch |
@@ -36,6 +36,7 @@
| 1.21 | 2026‑10‑07 | **Interface change (additive) and corpus change.** Search results carry `image` (processed 640 px tile image, root-relative, `null` for types without one). Corpus: story documents now include their body (the inline `tt_content` children), page and story content include the text columns of the market/solution Content Blocks and the `vitec_items`/`vitec_faq_items` collection children; solutions are indexed. News rank by publication date (`datetime`; `crdate` is the import date) with an age-decay boost. EXT:solr upgraded from 14.0.0-RC1 to 14.0.2 final (TYPO3 and the Solr image). Editorial: 7.16 now states the `/_json` prefix the URL swap introduced, the `type` vocabulary lists `solution` (delivered since 1.14), and the footer had been stuck at v1.19. |
| 1.22 | 2026‑10‑09 | **Interface change (additive) and defect fix.** Search cards ("best bets", 7.16): editorial `tx_vitec_domain_model_searchcard` records with keyword lists; the search payload gained `cards` (max. 3, priority-ordered, page 1 only, synonym-aware keyword match; record cards mirror the record’s indexed document, filled card fields override). Defect fix: a request without `q` returned an empty body instead of the documented empty shape (`stdClass` resolved inside the renderer namespace). |
| 1.23 | 2026‑10‑09 | **New interface (additive).** Product type-ahead for the megamenu catalog panel: lean page type `?type=7385&q=` (7.16) returning `suggestions` and up to six product cards via Solr, last term completed as a prefix. The megamenu gained the panel layout `catalog`, filled by the VITEC listener `ProvideProductCatalog` (main categories → subcategory groups → products with one-line teasers). |
+| 1.24 | 2026‑10‑23 | **Interface change (additive).** New frontend layout **22 Solution Group Page** for the eight solution group hero pages (zones hero = `colPos1`, main = `colPos0`). Element order: `vitec_markethero` + `vitec_introparagraph` in the hero zone; `vitec_marketintro` 2, `vitec_introparagraph` + 4 `text_only` cards (challenges), `vitec_introparagraph` + 4 alternating `horizontal`/`horizontal_reverse` cards (capabilities, bullets as `
`), `vitec_usecaselist` (testimonials, shipped hidden until stories are picked), `vitec_marketinsights`, `vitec_ctabanner` in main — imported from the agency Template‑06 workbooks by `vitec:import-solution-group-workbooks`; page properties as on the sub‑solution pages. |
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
@@ -1612,4 +1613,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.23.*
+*End of document EVO‑VITEC‑HL‑001 v1.24.*
diff --git a/packages/vitec/Resources/Private/Language/locallang_pages.xlf b/packages/vitec/Resources/Private/Language/locallang_pages.xlf
index e03315b..81379a6 100755
--- a/packages/vitec/Resources/Private/Language/locallang_pages.xlf
+++ b/packages/vitec/Resources/Private/Language/locallang_pages.xlf
@@ -69,6 +69,9 @@
Sub-Market Page
+
+ Solution Group Page
+