Files
VITEC-website/packages/vitec/Configuration/Sets/Vitecset/setup.typoscript
T
orascheandClaude Fable 5.1 4bc38b1760 Product content check as page content (vitec_productcheck)
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>
2026-11-12 11:34:59 +01:00

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]
}
}
}
}
}