The product audit of public/product-check/ becomes data for the React frontend: new content element vitec_productcheck (plugin registration, wizard entry, TypoScript USER func) delivers under content.productcheck either the overview (every product with one {ok,count} check per field - hero, photos, og, video, teaser, subtitle, description, description2, highlights, capabilities, applications, features incl. icon count, resources, datasheets, solutions - plus per-field summary) or, with ?check=<uid>, the detail of one product (texts as stored HTML, images with original URL and 480px thumbnail, videos, feature cards with icons, resources, datasheets, solutions). ProductCheckService ports the PDO queries of the standalone page to Doctrine; the renderer is always USER_INT so the audit is never served from the page cache; check is excluded from the cHash. Migration adds the element to page 607 (/products/product-check). The standalone page stays untouched. Verified on DEV: overview 85 products in 0.13 s warm, detail 0.2-0.9 s, thumbnails resolve.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1093 lines
37 KiB
Plaintext
Executable File
1093 lines
37 KiB
Plaintext
Executable File
config {
|
|
# EXT:solr indexes pages only when indexing is explicitly enabled
|
|
index_enable = 1
|
|
pageTitleProviders {
|
|
vitec {
|
|
provider = Evomedien\Vitec\PageTitle\ProductPageTitleProvider
|
|
before = record
|
|
before = seo
|
|
}
|
|
vitecUsecase {
|
|
provider = Evomedien\Vitec\PageTitle\UsecasePageTitleProvider
|
|
before = record
|
|
before = seo
|
|
}
|
|
}
|
|
}
|
|
|
|
@import 'EXT:headless/Configuration/TypoScript/ContentElement/*.typoscript'
|
|
|
|
# =============================================================================
|
|
# header_layout 100 = "Hidden" (decision 2026-09-04): such headers stay OUT of
|
|
# the JSON - header, subheader and headerLink render empty. headerLayout (100)
|
|
# stays in the payload so the front end could still tell why. Placed BEFORE
|
|
# the `< lib.contentElementWithHeader` copies below, which snapshot this lib.
|
|
# The content-blocks `data` path gets the same rule via HiddenHeaderProcessor.
|
|
# =============================================================================
|
|
|
|
lib.contentElementWithHeader.fields.content.fields {
|
|
header.stdWrap.if {
|
|
value = 100
|
|
equals.field = header_layout
|
|
negate = 1
|
|
}
|
|
subheader.stdWrap.if {
|
|
value = 100
|
|
equals.field = header_layout
|
|
negate = 1
|
|
}
|
|
headerLink.stdWrap.if {
|
|
value = 100
|
|
equals.field = header_layout
|
|
negate = 1
|
|
}
|
|
}
|
|
|
|
lib.contentBlock.fields.data.dataProcessing.20 = Evomedien\Vitec\DataProcessing\HiddenHeaderProcessor
|
|
|
|
# =============================================================================
|
|
# Headless list-plugin JSON renderers (TYPO3 v14: each plugin is its own CType)
|
|
#
|
|
# For every VITEC plugin the renderer's USER output is exposed as a sub-field
|
|
# of the standard content-element envelope (`content.<key>`), inheriting all
|
|
# default fields (id, type, colPos, categories, appearance, …) from
|
|
# lib.contentElement provided by friendsoftypo3/headless.
|
|
# =============================================================================
|
|
|
|
tt_content {
|
|
vitec_productlist < lib.contentElementWithHeader
|
|
vitec_productlist {
|
|
fields {
|
|
content {
|
|
fields {
|
|
products = USER
|
|
products.userFunc = Evomedien\Vitec\UserFunc\ProductListJsonRenderer->render
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# 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
|
|
# No product selected/visible -> real JSON null, not
|
|
# the string "null" (ticket 0068-FE).
|
|
productfeature.ifEmptyReturnNull = 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# 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=.
|
|
vitec_productfinder < lib.contentElementWithHeader
|
|
vitec_productfinder {
|
|
fields {
|
|
content {
|
|
fields {
|
|
productfinder = USER
|
|
productfinder.userFunc = Evomedien\Vitec\UserFunc\ProductFinderJsonRenderer->render
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Product Content Check - the product audit table as data (page
|
|
# /products/product-check). Always USER_INT, see ProductCheckJsonRenderer.
|
|
vitec_productcheck < lib.contentElementWithHeader
|
|
vitec_productcheck {
|
|
fields {
|
|
content {
|
|
fields {
|
|
productcheck = USER
|
|
productcheck.userFunc = Evomedien\Vitec\UserFunc\ProductCheckJsonRenderer->render
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# EXT:solr search results as JSON - the search endpoint for the React frontend
|
|
solr_pi_results < lib.contentElementWithHeader
|
|
solr_pi_results {
|
|
fields {
|
|
content {
|
|
fields {
|
|
search = USER
|
|
search.userFunc = Evomedien\Vitec\UserFunc\SearchJsonRenderer->render
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
news_pi1 < lib.contentElementWithHeader
|
|
news_pi1 {
|
|
fields {
|
|
content {
|
|
fields {
|
|
news = USER
|
|
news.userFunc = Evomedien\Vitec\UserFunc\NewsJsonRenderer->render
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
news_newsliststicky < tt_content.news_pi1
|
|
news_newsselectedlist < tt_content.news_pi1
|
|
news_newsdetail < tt_content.news_pi1
|
|
news_newsdatemenu < tt_content.news_pi1
|
|
news_categorylist < tt_content.news_pi1
|
|
news_newssearchform < tt_content.news_pi1
|
|
news_newssearchresult < tt_content.news_pi1
|
|
news_taglist < tt_content.news_pi1
|
|
|
|
vitec_productshow < lib.contentElementWithHeader
|
|
vitec_productshow {
|
|
fields {
|
|
content {
|
|
fields {
|
|
product = USER
|
|
product.userFunc = Evomedien\Vitec\UserFunc\ProductShowJsonRenderer->render
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
vitec_usecaselist < lib.contentElementWithHeader
|
|
vitec_usecaselist {
|
|
fields {
|
|
content {
|
|
fields {
|
|
usecases = USER
|
|
usecases.userFunc = Evomedien\Vitec\UserFunc\UsecaseListJsonRenderer->render
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
vitec_usecaseshow < lib.contentElementWithHeader
|
|
vitec_usecaseshow {
|
|
fields {
|
|
content {
|
|
fields {
|
|
usecase = USER
|
|
usecase.userFunc = Evomedien\Vitec\UserFunc\UsecaseShowJsonRenderer->render
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
vitec_marketshow < lib.contentElementWithHeader
|
|
vitec_marketshow {
|
|
fields {
|
|
content {
|
|
fields {
|
|
market = USER
|
|
market.userFunc = Evomedien\Vitec\UserFunc\MarketShowJsonRenderer->render
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
vitec_marketlist < lib.contentElementWithHeader
|
|
vitec_marketlist {
|
|
fields {
|
|
content {
|
|
fields {
|
|
markets = USER
|
|
markets.userFunc = Evomedien\Vitec\UserFunc\MarketListJsonRenderer->render
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
vitec_solutionlist < lib.contentElementWithHeader
|
|
vitec_solutionlist {
|
|
fields {
|
|
content {
|
|
fields {
|
|
solutions = USER
|
|
solutions.userFunc = Evomedien\Vitec\UserFunc\SolutionListJsonRenderer->render
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
vitec_solutionshow < lib.contentElementWithHeader
|
|
vitec_solutionshow {
|
|
fields {
|
|
content {
|
|
fields {
|
|
solution = USER
|
|
solution.userFunc = Evomedien\Vitec\UserFunc\SolutionShowJsonRenderer->render
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
vitec_downloadcard < lib.contentElementWithHeader
|
|
vitec_downloadcard {
|
|
fields {
|
|
content {
|
|
fields {
|
|
downloadcard = USER
|
|
downloadcard.userFunc = Evomedien\Vitec\UserFunc\DownloadcardJsonRenderer->render
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
vitec_downloadcardcollection < lib.contentElementWithHeader
|
|
vitec_downloadcardcollection {
|
|
fields {
|
|
content {
|
|
fields {
|
|
downloadcardcollection = USER
|
|
downloadcardcollection.userFunc = Evomedien\Vitec\UserFunc\DownloadcardcollectionJsonRenderer->render
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
vitec_datasheets < lib.contentElementWithHeader
|
|
vitec_datasheets {
|
|
fields {
|
|
content {
|
|
fields {
|
|
datasheets = USER
|
|
datasheets.userFunc = Evomedien\Vitec\UserFunc\DatasheetsJsonRenderer->render
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
vitec_eventlist =< lib.contentElementWithHeader
|
|
vitec_eventlist {
|
|
fields {
|
|
content {
|
|
fields {
|
|
eventlist = USER
|
|
eventlist.userFunc = Evomedien\Vitec\UserFunc\EventlistJsonRenderer->render
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
vitec_contactform < lib.contentElementWithHeader
|
|
vitec_contactform {
|
|
fields {
|
|
content {
|
|
fields {
|
|
form = USER
|
|
form.userFunc = Evomedien\Vitec\UserFunc\FormsJsonRenderer->render
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
vitec_demoform < tt_content.vitec_contactform
|
|
vitec_helpdeskform < tt_content.vitec_contactform
|
|
|
|
vitec_modelcard < lib.contentElementWithHeader
|
|
vitec_modelcard {
|
|
fields {
|
|
content {
|
|
fields {
|
|
card = USER
|
|
card.userFunc = Evomedien\Vitec\UserFunc\ModelcardJsonRenderer->render
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
vitec_customerlogos < lib.contentElementWithHeader
|
|
vitec_customerlogos {
|
|
fields {
|
|
content {
|
|
fields {
|
|
customerlogos = USER
|
|
customerlogos.userFunc = Evomedien\Vitec\UserFunc\CustomerlogosJsonRenderer->render
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
vitec_locationlist < lib.contentElementWithHeader
|
|
vitec_locationlist {
|
|
fields {
|
|
content {
|
|
fields {
|
|
locations = USER
|
|
locations.userFunc = Evomedien\Vitec\UserFunc\LocationsJsonRenderer->render
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Include container (layout) rendering definitions
|
|
@import 'EXT:vitec/Configuration/TypoScript/Headless/vitec_containers.typoscript'
|
|
|
|
# Include menu (navigation) JSON definitions
|
|
@import 'EXT:vitec/Configuration/TypoScript/Headless/vitec_menus.typoscript'
|
|
|
|
# =============================================================================
|
|
# Page-level structured data (schema.org JSON-LD @graph)
|
|
#
|
|
# Organization + WebSite + BreadcrumbList + NewsArticle, auto-generated server
|
|
# side from page/site data. The headless frontend wraps the returned string in
|
|
# <script type="application/ld+json"> inside the page <head>.
|
|
# =============================================================================
|
|
page.10.fields.jsonLd = USER
|
|
page.10.fields.jsonLd.userFunc = Evomedien\Vitec\UserFunc\PageJsonLdRenderer->render
|
|
|
|
# =============================================================================
|
|
# No browser caching of the page JSON (2026-11-05). EXT:headless sets
|
|
# page.config.sendCacheHeaders = 1, and TYPO3 14 caches a page for ONE YEAR
|
|
# by default (CacheLifetimeCalculator::defaultCacheTimeout) - every /_json/
|
|
# response went out with "Cache-Control: max-age=~31536000", so browsers
|
|
# kept an old menu/page for up to a year (found when the clickable product
|
|
# categories did not show up). The server-side page cache is unaffected;
|
|
# without the header, .htaccess sends application/json "access plus 0 seconds".
|
|
# =============================================================================
|
|
page.config.sendCacheHeaders = 0
|
|
|
|
############################################################
|
|
# Frontend resolution for the record links from the link
|
|
# browser (page.tsconfig TCEMAIN.linkHandler):
|
|
# download -> forced download endpoint (DownloadFileMiddleware)
|
|
# product -> detail view, same /product/<slug> convention as
|
|
# the `link` field in the product JSON (page 10)
|
|
# A hidden or deleted target makes the typolink fail -> null.
|
|
############################################################
|
|
|
|
config.recordLinks {
|
|
download {
|
|
forceLink = 1
|
|
typolink {
|
|
parameter.cObject = TEXT
|
|
parameter.cObject.dataWrap = /download/file/{field:uid}
|
|
}
|
|
}
|
|
product {
|
|
forceLink = 1
|
|
typolink {
|
|
parameter.cObject = TEXT
|
|
parameter.cObject.dataWrap = /product/{field:slug}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Highlighted teaser fragments for the JSON search results (SearchJsonRenderer)
|
|
plugin.tx_solr.search.results.resultsHighlighting = 1
|
|
plugin.tx_solr.search.results.resultsHighlighting.wrap = <mark>|</mark>
|
|
|
|
|
|
# Headless has no TYPO3SEARCH_begin/end markers in its JSON output, so the
|
|
# default page content extraction indexes an EMPTY content field. Build the
|
|
# content from the page's tt_content rows instead: header, subheader, bodytext
|
|
# cover core CTypes and the Content Blocks (their text fields reuse bodytext),
|
|
# vitec_quote is the only Content Blocks text column of its own. SOLR_CONTENT
|
|
# strips markup before indexing. Collection children (FAQ items, cards) live in
|
|
# their own tables and are not covered yet.
|
|
plugin.tx_solr.index.queue.pages.fields {
|
|
content = SOLR_CONTENT
|
|
content {
|
|
cObject = COA
|
|
cObject {
|
|
10 = CONTENT
|
|
10 {
|
|
table = tt_content
|
|
select {
|
|
orderBy = sorting
|
|
}
|
|
renderObj = COA
|
|
renderObj {
|
|
10 = TEXT
|
|
10.field = header
|
|
10.noTrimWrap = || |
|
|
20 = TEXT
|
|
20.field = subheader
|
|
20.noTrimWrap = || |
|
|
30 = TEXT
|
|
30.field = bodytext
|
|
30.noTrimWrap = || |
|
|
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 = || |
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Log every executed search (keywords, hit count) into tx_solr_statistics -
|
|
# feeds the Search Phrase Statistics backend module, most useful for the
|
|
# zero-hit list. anonymizeIP masks the last two octets before storing (GDPR).
|
|
plugin.tx_solr.statistics = 1
|
|
plugin.tx_solr.statistics.anonymizeIP = 2
|
|
|
|
# =============================================================================
|
|
# Record indexing: custom domain models as their own search documents.
|
|
# The configuration name becomes the document `type` in the search JSON, so the
|
|
# frontend can facet/label results. Every type needs a resolvable `url` -
|
|
# a hit without a link is useless, hence the where-clauses below.
|
|
# NOT indexed (yet): solutions (no slug, no detail_page - nothing to link to),
|
|
# downloads/events/locations (no public detail pages), Datapath news (pid 442,
|
|
# import awaiting an editorial decision).
|
|
# =============================================================================
|
|
|
|
# Detail-page fallbacks for markets and solutions (2026-10-09). The records
|
|
# themselves are mostly empty (no teaser/description/image) - the real copy
|
|
# lives in the market/solution Content Blocks on the record's detail_page,
|
|
# filled by the workbook importers. Copied into both queues below, so search
|
|
# results AND search cards (which mirror the indexed document) get a teaser
|
|
# and a tile image without double maintenance. Selected by CType, so it works
|
|
# for layout 21 (colPos 110/120) and layout 5 (colPos 1/0) alike.
|
|
#
|
|
# Image chain: record image -> detail page intro image -> hero background.
|
|
lib.vitecIndexDetailImage = CONTENT
|
|
lib.vitecIndexDetailImage {
|
|
table = tt_content
|
|
select {
|
|
pidInList.field = detail_page
|
|
where = {#CType} = 'vitec_marketintro'
|
|
orderBy = sorting
|
|
max = 1
|
|
}
|
|
renderObj = FILES
|
|
renderObj {
|
|
references {
|
|
table = tt_content
|
|
uid.data = field:uid
|
|
fieldName = vitec_image
|
|
}
|
|
maxItems = 1
|
|
renderObj = IMG_RESOURCE
|
|
renderObj {
|
|
file.import.data = file:current:uid
|
|
file.treatIdAsReference = 1
|
|
file.width = 640
|
|
}
|
|
}
|
|
stdWrap.ifEmpty.cObject = CONTENT
|
|
stdWrap.ifEmpty.cObject {
|
|
table = tt_content
|
|
select {
|
|
pidInList.field = detail_page
|
|
where = {#CType} = 'vitec_markethero'
|
|
orderBy = sorting
|
|
max = 1
|
|
}
|
|
renderObj = FILES
|
|
renderObj {
|
|
references {
|
|
table = tt_content
|
|
uid.data = field:uid
|
|
fieldName = vitec_bgimage
|
|
}
|
|
maxItems = 1
|
|
renderObj = IMG_RESOURCE
|
|
renderObj {
|
|
file.import.data = file:current:uid
|
|
file.treatIdAsReference = 1
|
|
file.width = 640
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Searchable text of the detail page: hero subline, intro headline, intro
|
|
# text - in page order (hero first), appended to the record's own content.
|
|
lib.vitecIndexDetailText = CONTENT
|
|
lib.vitecIndexDetailText {
|
|
table = tt_content
|
|
select {
|
|
pidInList.field = detail_page
|
|
where = {#CType} IN ('vitec_markethero', 'vitec_marketintro')
|
|
orderBy = CType ASC
|
|
}
|
|
renderObj = COA
|
|
renderObj {
|
|
10 = TEXT
|
|
10.field = vitec_subline
|
|
10.noTrimWrap = || |
|
|
20 = TEXT
|
|
20.field = vitec_intro_headline
|
|
20.noTrimWrap = || |
|
|
30 = TEXT
|
|
30.field = vitec_intro_text
|
|
30.noTrimWrap = || |
|
|
}
|
|
}
|
|
|
|
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 {
|
|
localField = categories
|
|
multiValue = 1
|
|
}
|
|
type = tx_vitec_domain_model_product
|
|
additionalWhereClause = hideonwebsite = 0
|
|
fields {
|
|
title = title
|
|
content = SOLR_CONTENT
|
|
content {
|
|
cObject = COA
|
|
cObject {
|
|
10 = TEXT
|
|
10.field = subtitle
|
|
10.noTrimWrap = || |
|
|
20 = TEXT
|
|
20.field = teaser
|
|
20.noTrimWrap = || |
|
|
30 = TEXT
|
|
30.field = description
|
|
30.noTrimWrap = || |
|
|
40 = TEXT
|
|
40.field = description2
|
|
40.noTrimWrap = || |
|
|
50 = TEXT
|
|
50.field = capabilities
|
|
50.noTrimWrap = || |
|
|
60 = TEXT
|
|
60.field = highlights
|
|
60.noTrimWrap = || |
|
|
70 = TEXT
|
|
70.field = applications
|
|
70.noTrimWrap = || |
|
|
}
|
|
}
|
|
url = TEXT
|
|
url {
|
|
typolink {
|
|
parameter = 10
|
|
additionalParams = &tx_vitec_productshow[product]={field:uid}
|
|
additionalParams.insertData = 1
|
|
returnLast = url
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
stdWrap.ifEmpty.cObject < lib.vitecIndexDetailImage
|
|
}
|
|
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)
|
|
additionalWhereClause = detail_page > 0
|
|
fields {
|
|
title = title
|
|
content = SOLR_CONTENT
|
|
content {
|
|
cObject = COA
|
|
cObject {
|
|
10 = TEXT
|
|
10.field = subtitle
|
|
10.noTrimWrap = || |
|
|
20 = TEXT
|
|
20.field = teaser
|
|
20.noTrimWrap = || |
|
|
30 = TEXT
|
|
30.field = description
|
|
30.noTrimWrap = || |
|
|
40 < lib.vitecIndexDetailText
|
|
}
|
|
}
|
|
url = TEXT
|
|
url {
|
|
typolink {
|
|
parameter.field = detail_page
|
|
returnLast = url
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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 {
|
|
localField = markets
|
|
multiValue = 1
|
|
}
|
|
type = tx_vitec_domain_model_usecase
|
|
# no_index is the editorial opt-out per story (concept 2026-08-20)
|
|
additionalWhereClause = hideonwebsite = 0 AND no_index = 0
|
|
fields {
|
|
title = title
|
|
content = SOLR_CONTENT
|
|
content {
|
|
cObject = COA
|
|
cObject {
|
|
10 = TEXT
|
|
10.field = subtitle
|
|
10.noTrimWrap = || |
|
|
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
|
|
url {
|
|
typolink {
|
|
# page 26 carries the vitec_usecaseshow plugin; the route
|
|
# enhancer turns this into /success-stories/story/<slug>
|
|
parameter = 26
|
|
additionalParams = &tx_vitec_usecaseshow[usecase]={field:uid}
|
|
additionalParams.insertData = 1
|
|
returnLast = url
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Solutions became indexable on 2026-09-11: the sitemap alignment gave all
|
|
# 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
|
|
}
|
|
stdWrap.ifEmpty.cObject < lib.vitecIndexDetailImage
|
|
}
|
|
type = tx_vitec_domain_model_solution
|
|
additionalWhereClause = detail_page > 0
|
|
fields {
|
|
title = title
|
|
content = SOLR_CONTENT
|
|
content {
|
|
cObject = COA
|
|
cObject {
|
|
10 = TEXT
|
|
10.field = subtitle
|
|
10.noTrimWrap = || |
|
|
20 = TEXT
|
|
20.field = teaser
|
|
20.noTrimWrap = || |
|
|
30 = TEXT
|
|
30.field = description
|
|
30.noTrimWrap = || |
|
|
40 < lib.vitecIndexDetailText
|
|
}
|
|
}
|
|
url = TEXT
|
|
url {
|
|
typolink {
|
|
parameter.field = detail_page
|
|
returnLast = url
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
additionalWhereClause = pid = 11 AND type = '0'
|
|
fields {
|
|
title = title
|
|
content = SOLR_CONTENT
|
|
content {
|
|
cObject = COA
|
|
cObject {
|
|
10 = TEXT
|
|
10.field = teaser
|
|
10.noTrimWrap = || |
|
|
20 = TEXT
|
|
20.field = bodytext
|
|
20.noTrimWrap = || |
|
|
}
|
|
}
|
|
url = TEXT
|
|
url {
|
|
typolink {
|
|
parameter = 21
|
|
additionalParams = &tx_news_pi1[controller]=News&tx_news_pi1[action]=detail&tx_news_pi1[news]={field:uid}
|
|
additionalParams.insertData = 1
|
|
returnLast = url
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Products are what visitors of a product site are usually looking for -
|
|
# without this boost, decades of news articles dominate the first result page
|
|
# for product-ish terms like "encoder". Stories get a smaller nudge.
|
|
plugin.tx_solr.search.query.boostQuery (
|
|
(type:tx_vitec_domain_model_product)^10.0 (type:tx_vitec_domain_model_usecase)^2.0
|
|
)
|
|
|
|
# Downloads have no detail page but a public file endpoint served by
|
|
# DownloadFileMiddleware. The where-clause keeps private / website-hidden
|
|
# downloads out of the public index (the middleware itself does not enforce
|
|
# private_download - the index must not advertise those files).
|
|
plugin.tx_solr.index.queue {
|
|
downloads = 1
|
|
downloads {
|
|
type = tx_vitec_domain_model_download
|
|
additionalWhereClause = private_download = 0 AND hideonwebsite = 0
|
|
fields {
|
|
title = title
|
|
content = SOLR_CONTENT
|
|
content {
|
|
cObject = COA
|
|
cObject {
|
|
10 = TEXT
|
|
10.field = teaser
|
|
10.noTrimWrap = || |
|
|
20 = TEXT
|
|
20.field = keywords
|
|
20.noTrimWrap = || |
|
|
30 = TEXT
|
|
30.field = description
|
|
30.noTrimWrap = || |
|
|
}
|
|
}
|
|
url = TEXT
|
|
url.dataWrap = /download/{field:slug}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Type facet for the search JSON: counts per document type, serialized by
|
|
# SearchJsonRenderer as content.search.facets.type. keepAll* keeps the full
|
|
# counts visible while one type is selected - otherwise the tabs would
|
|
# collapse to the active one.
|
|
plugin.tx_solr.search.faceting = 1
|
|
plugin.tx_solr.search.faceting {
|
|
keepAllFacetsOnSelection = 1
|
|
facets {
|
|
type {
|
|
label = Type
|
|
field = type
|
|
keepAllOptionsOnSelection = 1
|
|
}
|
|
market {
|
|
label = Market
|
|
field = market_stringM
|
|
keepAllOptionsOnSelection = 1
|
|
}
|
|
category {
|
|
label = Category
|
|
field = category_stringM
|
|
keepAllOptionsOnSelection = 1
|
|
}
|
|
}
|
|
}
|
|
|
|
# =============================================================================
|
|
# Autocomplete endpoint: GET /search?type=7384&tx_solr[queryString]=<input>
|
|
# Lean JSON page type (no page envelope) for type-ahead in the React search
|
|
# field. Response: { suggestions: {term: count}, suggestion, documents[],
|
|
# didSecondSearch }.
|
|
# 10 is deliberately USER_INT, not USER as in the shipped example: the example
|
|
# predates TYPO3 v14 (config.no_cache is gone), and a cached USER would pin
|
|
# the first query's suggestions for every later request.
|
|
# =============================================================================
|
|
tx_solr_suggest = PAGE
|
|
tx_solr_suggest {
|
|
typeNum = 7384
|
|
config {
|
|
disableAllHeaderCode = 1
|
|
additionalHeaders.10.header = Content-Type: application/json; charset=utf-8
|
|
}
|
|
|
|
10 = USER_INT
|
|
10 {
|
|
userFunc = TYPO3\CMS\Extbase\Core\Bootstrap->run
|
|
extensionName = Solr
|
|
pluginName = pi_suggest
|
|
vendorName = ApacheSolrForTypo3
|
|
controller = Suggest
|
|
action = suggest
|
|
}
|
|
}
|
|
|
|
# =============================================================================
|
|
# Product type-ahead of the megamenu catalog panel (2026-10-09):
|
|
# GET /_json/?type=7385&q=<input> -> { query, suggestions[], products[] }
|
|
# (ProductMenuSearchJsonRenderer). USER_INT for the same reason as 7384:
|
|
# every keystroke is a new query. `q` is excluded from cHash (ext_localconf).
|
|
# =============================================================================
|
|
vitec_product_menu_search = PAGE
|
|
vitec_product_menu_search {
|
|
typeNum = 7385
|
|
config {
|
|
disableAllHeaderCode = 1
|
|
additionalHeaders.10.header = Content-Type: application/json; charset=utf-8
|
|
}
|
|
|
|
10 = USER_INT
|
|
10.userFunc = Evomedien\Vitec\UserFunc\ProductMenuSearchJsonRenderer->render
|
|
}
|
|
|
|
plugin.tx_solr.suggest = 1
|
|
plugin.tx_solr.suggest {
|
|
numberOfSuggestions = 8
|
|
suggestField = spell
|
|
# term suggestions only: the built-in top-results carry the raw document
|
|
# 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)
|
|
|
|
|
|
# ======================================================================
|
|
# XML-Sitemap (SEO-Audit 2026-11-09)
|
|
#
|
|
# - news: the georgringer/news-sitemap set ships pid=0 / pageId=0, which
|
|
# rendered an EMPTY news sitemap. Records live in folders 11 (Blog) and
|
|
# 442 (Press); the detail route sits on page 21 (/news/article/<slug>).
|
|
# - products: record sitemap for /product/<slug> detail pages (folder 30).
|
|
# - stories: record sitemap for success stories (folder 14) with the
|
|
# PUBLIC /success-stories/<slug> URLs (custom provider strips the
|
|
# internal /story/ segment).
|
|
#
|
|
# Requires vitecset to load AFTER typo3/seo-sitemap + georgringer/
|
|
# news-sitemap - see the dependencies in this set's config.yaml.
|
|
# ======================================================================
|
|
plugin.tx_seo.config.xmlSitemap.sitemaps {
|
|
news.config {
|
|
pid = 11,442
|
|
url.pageId = 21
|
|
}
|
|
|
|
products {
|
|
provider = TYPO3\CMS\Seo\XmlSitemap\RecordsXmlSitemapDataProvider
|
|
config {
|
|
table = tx_vitec_domain_model_product
|
|
sortField = title
|
|
lastModifiedField = tstamp
|
|
pid = 30
|
|
additionalWhere = {#hideonwebsite} = 0
|
|
url {
|
|
pageId = 10
|
|
fieldToParameterMap {
|
|
uid = tx_vitec_productshow[product]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
stories {
|
|
provider = Evomedien\Vitec\Seo\StoriesXmlSitemapDataProvider
|
|
config {
|
|
table = tx_vitec_domain_model_usecase
|
|
sortField = title
|
|
lastModifiedField = tstamp
|
|
pid = 14
|
|
additionalWhere = {#hideonwebsite} = 0 AND {#no_index} = 0
|
|
url {
|
|
pageId = 26
|
|
fieldToParameterMap {
|
|
uid = tx_vitec_usecaseshow[usecase]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|