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;