- New record type tx_vitec_domain_model_searchcard ("best bets"):
keyword list, priority, optional referenced record (page, product,
story, solution, market, news) and overridable title/teaser/image/link;
hidden/start/end time allow timed campaigns
- SearchCardService delivers content.search.cards on page 1: max. 3,
priority-ordered, deduplicated by URL, independent of filter, facets
and result count. Keywords match as whole words/phrases, case- and
punctuation-insensitive, including one level of the Solr managed
synonyms in both directions (cached for an hour, fail-soft). Record
cards read the record's indexed Solr document, so a card always
matches the record's own search result
- Index fallback for markets and solutions: when the record carries no
image or text, the document takes the intro image (then the hero
background) and the hero subline, intro headline and intro text from
the content blocks of its detail page - selected by CType, so it works
for layout 21 and layout 5 alike; improves results and cards
- Fix: a search request without q returned an empty body instead of the
documented empty shape (stdClass resolved inside the renderer namespace)
- migrations/check_solution_fields.php: shows live/workspace versions of
solution records
- Spec v1.22 (clause 7.16: cards)
991 lines
33 KiB
Plaintext
Executable File
991 lines
33 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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# 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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# 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
|
|
|
|
############################################################
|
|
# 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
|
|
}
|
|
}
|
|
|
|
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)
|