Kev's keyword CSV and TYPO3 hold the same solutions under slightly
different names, so the structure check filed them as missing/extra.
Matching is now slug -> normalized title -> record alias, using the
per-model alias store the import tabs got on 2026-08-18
(tx_vitec_import_mapping.record_aliases, key = lowercased CSV name).
One link therefore serves the import union list, the structure check
and vitec:create-markets (same service - linked rows are no longer
proposed as new records).
- every "Missing in TYPO3" row carries a select of all records the
direct match did not claim; "Save record links" persists per model
- alias-matched rows show under "Both" with a "linked" badge and the
same select; option 0 removes the link
- SeoResearchService::recordsCheck() additionally returns `linkable`
(the select options) and flags alias matches with via=link
- new backend POST route seo_aliases
Success stories: --markets-only repair mode
- /success-stories emitted empty `markets` for all 48 stories - a data
problem (code and TCA untouched since 17.08.); read-only diagnosis
script migrations/check_usecase_markets.php added
- vitec:import-success-stories --markets-only re-derives each story's
industries from the export exactly like the full import and rewrites
ONLY the markets MM relation of the existing record via DataHandler;
nothing else is touched, no usecase created or deleted, unresolved
stories keep their current relations
- decision: industries are translated onto the nine main markets of
the current taxonomy (INDUSTRY_TO_MARKET) instead of recreating the
seven deleted industry markets; the repair mode never creates
market records (root cause: those industry markets were deleted
after the 07.08. taxonomy import, orphaning all story relations)
- ensureMarkets() gained a $create flag for reuse by the full import
Story detail pages: real page title
- every story page answered with the generic page title "Story"; same
defect and same fix as the product pages on 21.08.: new
UsecasePageTitleProvider (singleton), registered as vitecUsecase in
config.pageTitleProviders, fed from UsecaseShowJsonRenderer with
seo_title falling back to title
Requires on the server:
vendor/bin/typo3 cache:flush
594 lines
19 KiB
Plaintext
Executable File
594 lines
19 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'
|
|
|
|
# =============================================================================
|
|
# 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
|
|
}
|