Applied on all three render paths: stdWrap.if rule on lib.contentElementWithHeader (before the plugin copies), the same rule in the container element definitions, and a HiddenHeaderProcessor behind the nb content-blocks data processor. headerLayout itself stays in the payload so the front end can tell why the header is empty.
622 lines
20 KiB
Plaintext
Executable File
622 lines
20 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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# 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 = || |
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# 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).
|
|
# =============================================================================
|
|
plugin.tx_solr.index.queue {
|
|
|
|
products = 1
|
|
products {
|
|
type = tx_vitec_domain_model_product
|
|
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 {
|
|
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 = || |
|
|
}
|
|
}
|
|
url = TEXT
|
|
url {
|
|
typolink {
|
|
parameter.field = detail_page
|
|
returnLast = url
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
usecases = 1
|
|
usecases {
|
|
type = tx_vitec_domain_model_usecase
|
|
fields {
|
|
title = title
|
|
content = SOLR_CONTENT
|
|
content {
|
|
cObject = COA
|
|
cObject {
|
|
10 = TEXT
|
|
10.field = subtitle
|
|
10.noTrimWrap = || |
|
|
20 = TEXT
|
|
20.field = teaser
|
|
20.noTrimWrap = || |
|
|
}
|
|
}
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
news = 1
|
|
news {
|
|
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
|
|
}
|
|
}
|
|
}
|
|
|
|
# =============================================================================
|
|
# 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
|
|
}
|