diff --git a/composer.json b/composer.json
index f2d65eb..95ab298 100644
--- a/composer.json
+++ b/composer.json
@@ -15,7 +15,7 @@
"sort-packages": true
},
"require": {
- "apache-solr-for-typo3/solr": "^14.0@RC",
+ "apache-solr-for-typo3/solr": "^14.0.2",
"b13/container": "^3.1",
"b13/typo3-updater": "^1.1",
"evomedien/evo-megamenu-json": "^1.0.0",
diff --git a/composer.lock b/composer.lock
index 100baa7..d8c41db 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,20 +4,20 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "0f38c50d2ecb8b6e4b0ec88fc7985249",
+ "content-hash": "f9a73e5875dc0072a5a3853989adda94",
"packages": [
{
"name": "apache-solr-for-typo3/solr",
- "version": "14.0.1",
+ "version": "14.0.2",
"source": {
"type": "git",
"url": "https://github.com/TYPO3-Solr/ext-solr.git",
- "reference": "25fad97ffe192b1c4afc9400b98f4b4c4d3a6ea3"
+ "reference": "d430194b11c0581bd70e96a0483ec813b8345529"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/TYPO3-Solr/ext-solr/zipball/25fad97ffe192b1c4afc9400b98f4b4c4d3a6ea3",
- "reference": "25fad97ffe192b1c4afc9400b98f4b4c4d3a6ea3",
+ "url": "https://api.github.com/repos/TYPO3-Solr/ext-solr/zipball/d430194b11c0581bd70e96a0483ec813b8345529",
+ "reference": "d430194b11c0581bd70e96a0483ec813b8345529",
"shasum": ""
},
"require": {
@@ -129,7 +129,7 @@
"type": "custom"
}
],
- "time": "2026-09-03T14:19:19+00:00"
+ "time": "2026-09-16T13:50:16+00:00"
},
{
"name": "b13/container",
@@ -10325,7 +10325,6 @@
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {
- "apache-solr-for-typo3/solr": 5,
"friendsoftypo3/headless": 5
},
"prefer-stable": false,
diff --git a/packages/vitec/Classes/UserFunc/SearchJsonRenderer.php b/packages/vitec/Classes/UserFunc/SearchJsonRenderer.php
index 27eaa13..0c57155 100644
--- a/packages/vitec/Classes/UserFunc/SearchJsonRenderer.php
+++ b/packages/vitec/Classes/UserFunc/SearchJsonRenderer.php
@@ -27,7 +27,7 @@ use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
* or the FlexForm override on the plugin),
* "numFound": total hits,
* "totalPages": ceil(numFound / resultsPerPage),
- * "results": [ { "title", "url", "type", "teaser" } ],
+ * "results": [ { "title", "url", "type", "teaser", "image" } ],
* "filter": the active type filter or null,
* "facets": { "type": [ { "value", "count", "active" } ] } -
* counts stay complete while a filter is active,
@@ -217,11 +217,20 @@ class SearchJsonRenderer
} else {
$teaser = mb_substr(trim((string)$document->getContent()), 0, self::TEASER_FALLBACK_LENGTH);
}
+ $image = (string)($document['image'] ?? '');
+ // IMG_RESOURCE yields fileadmin/... without a leading slash -
+ // made root-relative like every other URL in the payload
+ if ($image !== '' && !str_starts_with($image, '/') && !preg_match('#^https?://#', $image)) {
+ $image = '/' . $image;
+ }
$results[] = [
'title' => (string)$document->getTitle(),
'url' => (string)$document->getUrl(),
'type' => self::TYPE_LABELS[$document->getType()] ?? (string)$document->getType(),
'teaser' => $teaser,
+ // processed tile image (640px wide) from the index; null when the
+ // document type has none (pages, downloads) or no image is set
+ 'image' => $image !== '' ? $image : null,
];
}
diff --git a/packages/vitec/Configuration/Sets/Vitecset/setup.typoscript b/packages/vitec/Configuration/Sets/Vitecset/setup.typoscript
index 0a88f38..5370695 100755
--- a/packages/vitec/Configuration/Sets/Vitecset/setup.typoscript
+++ b/packages/vitec/Configuration/Sets/Vitecset/setup.typoscript
@@ -394,6 +394,67 @@ plugin.tx_solr.index.queue.pages.fields {
40 = TEXT
40.field = vitec_quote
40.noTrimWrap = || |
+ # Content Blocks with their own text columns (market/solution
+ # templates, flip cards) - the workbook imports fill these
+ 50 = TEXT
+ 50.field = vitec_subline
+ 50.noTrimWrap = || |
+ 60 = TEXT
+ 60.field = vitec_intro_headline
+ 60.noTrimWrap = || |
+ 70 = TEXT
+ 70.field = vitec_intro_text
+ 70.noTrimWrap = || |
+ 80 = TEXT
+ 80.field = vitec_text1
+ 80.noTrimWrap = || |
+ 90 = TEXT
+ 90.field = vitec_text2
+ 90.noTrimWrap = || |
+ 100 = TEXT
+ 100.field = vitec_back_text
+ 100.noTrimWrap = || |
+ 110 = TEXT
+ 110.field = vitec_quote_position
+ 110.noTrimWrap = || |
+ # Content Blocks collection children (cards, icon lists,
+ # FAQ) live in their own tables, keyed on the parent uid
+ 120 = CONTENT
+ 120 {
+ table = vitec_items
+ select {
+ pidInList.field = pid
+ where.dataWrap = {#foreign_table_parent_uid} = {field:uid}
+ orderBy = sorting
+ }
+ renderObj = COA
+ renderObj {
+ 10 = TEXT
+ 10.field = headline
+ 10.noTrimWrap = || |
+ 20 = TEXT
+ 20.field = text
+ 20.noTrimWrap = || |
+ }
+ }
+ 130 = CONTENT
+ 130 {
+ table = vitec_faq_items
+ select {
+ pidInList.field = pid
+ where.dataWrap = {#foreign_table_parent_uid} = {field:uid}
+ orderBy = sorting
+ }
+ renderObj = COA
+ renderObj {
+ 10 = TEXT
+ 10.field = question
+ 10.noTrimWrap = || |
+ 20 = TEXT
+ 20.field = answer
+ 20.noTrimWrap = || |
+ }
+ }
}
}
}
@@ -419,6 +480,37 @@ plugin.tx_solr.index.queue {
products = 1
products {
+ # result tile image: first reference, processed to 640px width
+ fields.image = FILES
+ fields.image {
+ references {
+ table = tx_vitec_domain_model_product
+ uid.data = field:uid
+ fieldName = image
+ }
+ maxItems = 1
+ renderObj = IMG_RESOURCE
+ renderObj {
+ file.import.data = file:current:uid
+ file.treatIdAsReference = 1
+ file.width = 640
+ }
+ stdWrap.ifEmpty.cObject = FILES
+ stdWrap.ifEmpty.cObject {
+ references {
+ table = tx_vitec_domain_model_product
+ uid.data = field:uid
+ fieldName = productimage
+ }
+ maxItems = 1
+ renderObj = IMG_RESOURCE
+ renderObj {
+ file.import.data = file:current:uid
+ file.treatIdAsReference = 1
+ file.width = 640
+ }
+ }
+ }
# sys_category titles - feeds the category facet
fields.category_stringM = SOLR_RELATION
fields.category_stringM {
@@ -470,6 +562,22 @@ plugin.tx_solr.index.queue {
markets = 1
markets {
+ # result tile image: first reference, processed to 640px width
+ fields.image = FILES
+ fields.image {
+ references {
+ table = tx_vitec_domain_model_market
+ uid.data = field:uid
+ fieldName = image
+ }
+ maxItems = 1
+ renderObj = IMG_RESOURCE
+ renderObj {
+ file.import.data = file:current:uid
+ file.treatIdAsReference = 1
+ file.width = 640
+ }
+ }
type = tx_vitec_domain_model_market
# only markets with an editorially assigned detail page are linkable;
# grows automatically as editors fill the field (4 of 34 right now)
@@ -503,6 +611,37 @@ plugin.tx_solr.index.queue {
usecases = 1
usecases {
+ # result tile image: first reference, processed to 640px width
+ fields.image = FILES
+ fields.image {
+ references {
+ table = tx_vitec_domain_model_usecase
+ uid.data = field:uid
+ fieldName = card_image
+ }
+ maxItems = 1
+ renderObj = IMG_RESOURCE
+ renderObj {
+ file.import.data = file:current:uid
+ file.treatIdAsReference = 1
+ file.width = 640
+ }
+ stdWrap.ifEmpty.cObject = FILES
+ stdWrap.ifEmpty.cObject {
+ references {
+ table = tx_vitec_domain_model_usecase
+ uid.data = field:uid
+ fieldName = hero_bgimage
+ }
+ maxItems = 1
+ renderObj = IMG_RESOURCE
+ renderObj {
+ file.import.data = file:current:uid
+ file.treatIdAsReference = 1
+ file.width = 640
+ }
+ }
+ }
# market titles from the MM relation - feeds the market facet
fields.market_stringM = SOLR_RELATION
fields.market_stringM {
@@ -524,6 +663,18 @@ plugin.tx_solr.index.queue {
20 = TEXT
20.field = teaser
20.noTrimWrap = || |
+ # the story body: inline tt_content children (text, columns,
+ # quotations) - same per-element extraction as page content
+ 30 = CONTENT
+ 30 {
+ table = tt_content
+ select {
+ pidInList.field = pid
+ where.dataWrap = {#tx_vitec_usecase_content} = {field:uid}
+ orderBy = sorting
+ }
+ renderObj < plugin.tx_solr.index.queue.pages.fields.content.cObject.10.renderObj
+ }
}
}
url = TEXT
@@ -544,6 +695,22 @@ plugin.tx_solr.index.queue {
# 39 records a detail_page (and slug). Same pattern as markets.
solutions = 1
solutions {
+ # result tile image: first reference, processed to 640px width
+ fields.image = FILES
+ fields.image {
+ references {
+ table = tx_vitec_domain_model_solution
+ uid.data = field:uid
+ fieldName = image
+ }
+ maxItems = 1
+ renderObj = IMG_RESOURCE
+ renderObj {
+ file.import.data = file:current:uid
+ file.treatIdAsReference = 1
+ file.width = 640
+ }
+ }
type = tx_vitec_domain_model_solution
additionalWhereClause = detail_page > 0
fields {
@@ -575,6 +742,25 @@ plugin.tx_solr.index.queue {
news = 1
news {
+ # crdate is the IMPORT date (Jul/Aug 2026) - the publication date
+ # lives in datetime. `created` feeds the news recency boost.
+ fields.created = datetime
+ # result tile image: first reference, processed to 640px width
+ fields.image = FILES
+ fields.image {
+ references {
+ table = tx_news_domain_model_news
+ uid.data = field:uid
+ fieldName = fal_media
+ }
+ maxItems = 1
+ renderObj = IMG_RESOURCE
+ renderObj {
+ file.import.data = file:current:uid
+ file.treatIdAsReference = 1
+ file.width = 640
+ }
+ }
type = tx_news_domain_model_news
# type 1 ("page as news") points at pages that are already indexed as
# pages - indexing them again would duplicate every one of them
@@ -707,3 +893,9 @@ plugin.tx_solr.suggest {
# type and the full indexed content - too fat and off-contract for type-ahead
showTopResults = 0
}
+
+# News recency: old news decays against everything else. Additive edismax
+# boost (bf): non-news documents get the constant 10, news get
+# recip(age) = 10 when new, ~5 after one year, ~0.7 after 13 years.
+# The gate keeps products/pages/stories out of the decay entirely.
+plugin.tx_solr.search.query.boostFunction = if(termfreq(type,'tx_news_domain_model_news'),recip(ms(NOW,created),3.16e-11,10,1),10)
diff --git a/packages/vitec/Documentation/Headless-JSON-Architecture.md b/packages/vitec/Documentation/Headless-JSON-Architecture.md
index 1c6dc72..777940d 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.20 |
+| **Version** | 1.21 |
| **Status** | Released |
-| **Date** | 2026‑10‑01 |
+| **Date** | 2026‑10‑07 |
| **Applies to** | `evomedien/vitec` on TYPO3 v14.3 (headless) |
| **Owner** | evomedien — VITEC relaunch |
@@ -33,6 +33,7 @@
| 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. |
+| 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. |
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
@@ -1033,6 +1034,10 @@ headless by `SearchJsonRenderer` under the payload key `search`. The renderer ru
EXT:solr’s own query pipeline (`SearchRequestBuilder` → `SearchResultSetService`) -
only the rendering differs from the stock Fluid plugin.
+Since the URL swap of 2026‑09‑28 every JSON URL carries the prefix `/_json`:
+the search answers at `/_json/search?q=…`, while the bare `/search` serves the
+single-page app. Paths in this clause are given without the prefix.
+
**Request.** GET parameters on the search page:
| Parameter | Meaning |
@@ -1071,13 +1076,15 @@ identifier).
},
"results": [
{ "title": "MGW Diamond-H", "url": "/product/mgw-diamond-h-hdmi-encoder",
- "type": "product", "teaser": "… 4K HDMI Encoder …" }
+ "type": "product", "teaser": "… 4K HDMI Encoder …",
+ "image": "/fileadmin/_processed_/…/csm_mgw-diamond-h_….jpg" }
],
"suggestions": []
}
```
-- `type` **shall** be one of `page | product | market | story | news | download`
+- `type` **shall** be one of `page | product | market | solution | story | news |
+ download`
(mapping table in the renderer; unknown index types pass through verbatim).
- `teaser` carries the highlighted fragment (`…`, fragments joined
with " ... ") when highlighting applies, otherwise a plain 250-character excerpt
@@ -1085,6 +1092,10 @@ identifier).
- `url` follows the shared link contract: root-relative, resolved server-side.
`download` results point at the forced-download endpoint `/download/`
(5.3) - front ends should present them as file downloads.
+- `image` is a processed tile image (640 px wide, root-relative) or `null`.
+ Sources: products `image` (fallback `productimage`), stories `card_image`
+ (fallback `hero_bgimage`), markets/solutions `image`, news `fal_media`;
+ pages and downloads carry none.
- `facets.type` carries the per-type counts for filter tabs, ordered by count.
While a `filter` is active the counts remain those of the unfiltered query, so
tabs do not collapse - except when the filtered result is empty, where only
@@ -1102,11 +1113,16 @@ resolvable public URL - products, markets (only those with `detail_page`),
use cases, news (`type = 0`; "page as news" records are excluded because their
target pages are already indexed) and downloads (`private_download = 0 AND
hideonwebsite = 0`; their `content` additionally carries the file text extracted
-through Apache Tika, so datasheet specifications are searchable). Solutions are not indexed until they carry slugs or detail
-pages. Ranking boosts products (^10) and stories (^2). Every executed search is
+through Apache Tika, so datasheet specifications are searchable). Solutions are indexed like markets (`detail_page > 0`). Stories include their
+body - the inline `tt_content` children - and both page and story content
+include the text columns of the market/solution Content Blocks and the
+collection children (`vitec_items`, `vitec_faq_items`). Ranking boosts products
+(^10) and stories (^2); news decay with age by their publication date
+(`datetime` mapped to `created` - `crdate` is the import date), all other
+types are exempt. Every executed search is
logged to `tx_solr_statistics` with the last two IP octets masked.
-**Autocomplete.** `GET /search?type=7384&tx_solr[queryString]=` answers as
+**Autocomplete.** `GET /_json/search?type=7384&tx_solr[queryString]=` answers as
a lean JSON page type without the page envelope:
```json
@@ -1120,14 +1136,17 @@ wired as **USER_INT** - the example shipped with EXT:solr predates TYPO3 v14
(`config.no_cache` no longer exists), and a cached USER would pin the first
query’s suggestions for every later request. Front ends should debounce
(~200 ms), start at 2-3 characters, and turn a picked suggestion into a
-regular `/search?q=` request. Top-result documents are deliberately disabled
+regular `/_json/search?q=` request. Top-result documents are deliberately disabled
(`showTopResults = 0`): their shape is hardcoded upstream and carries the raw
document type and the full indexed content.
**Operations.** Indexing runs automatically: record and page saves enter the
index queue, a scheduler task (Index Queue Worker, every 5 minutes) pushes them
to Solr; the CLI command `vitec:solr-index` (with `--initialize` and `--debug`)
-covers manual runs - EXT:solr 14 ships no console commands of its own. Page
+covers manual runs - EXT:solr 14 ships no console commands of its own. Versions:
+EXT:solr 14.0.2 and the matching image `typo3solr/ext-solr:14.0.2` (Solr 10);
+an image upgrade needs a fresh volume, because an existing volume keeps its
+old configset - managed synonyms are restored through the REST API. Page
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.
@@ -1538,4 +1557,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.19.*
+*End of document EVO‑VITEC‑HL‑001 v1.21.*
diff --git a/public/_frontend/index.html b/public/_frontend/index.html
index dd1d6fd..2bd5bd0 100644
--- a/public/_frontend/index.html
+++ b/public/_frontend/index.html
@@ -23,8 +23,8 @@
VITEC
-
-
+
+
diff --git a/public/suggest-demo.html b/public/suggest-demo.html
index 5e5a37a..386a2cd 100644
--- a/public/suggest-demo.html
+++ b/public/suggest-demo.html
@@ -81,7 +81,7 @@
const val = input.value.trim();
if (val.length < 2) { dd.classList.remove('open'); return; }
debounceTimer = setTimeout(async () => {
- const url = '/search?type=7384&tx_solr[queryString]=' + encodeURIComponent(val);
+ const url = '/_json/search?type=7384&tx_solr[queryString]=' + encodeURIComponent(val);
const j = await (await fetch(url)).json();
const terms = Object.entries(j.suggestions || {});
dd.innerHTML = terms.map(([t, c]) =>
@@ -106,7 +106,7 @@
// ---- Suche: GET /search?q=...&filter=...&page=... ----
async function runSearch(q, filter, page) {
if (!q) return;
- const url = '/search?q=' + encodeURIComponent(q)
+ const url = '/_json/search?q=' + encodeURIComponent(q)
+ (filter ? '&filter=' + filter : '') + (page > 1 ? '&page=' + page : '');
const json = await (await fetch(url)).json();
const s = json.content.colPos0[0].content.search;