SEO record links; story markets repair; story page titles
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
This commit is contained in:
1 parent
d846ba2007
commit
7442d10e68
9 files changed
+378
-9
No files matched your search
@@ -18,7 +18,10 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
* products -> level-3.x.y+ rows against tx_vitec_domain_model_product
|
||||
* (3.x rows are categories, not products - skipped)
|
||||
* Record matching: slug == last URL segment, falling back to a
|
||||
* normalized title comparison (parentheses stripped, & -> and).
|
||||
* normalized title comparison (parentheses stripped, & -> and), and
|
||||
* finally the hand-made record links (record_aliases per model in
|
||||
* tx_vitec_import_mapping - the same store the import tabs use, so
|
||||
* one link serves both views and vitec:create-markets).
|
||||
* - diff against the previous delivery, keyed by URL
|
||||
*
|
||||
* Works on the normalized row schema produced by normalizeRows(). Read-only:
|
||||
@@ -141,7 +144,8 @@ final class SeoResearchService
|
||||
$depth = substr_count($r['ref'], '.') + 1;
|
||||
return $r['section'] === $section && $depth >= $cfg['depth'][0] && $depth <= $cfg['depth'][1];
|
||||
}));
|
||||
$out[$cfg['key']] = $this->recordsCheck($candidates, $cfg['table']);
|
||||
$aliases = GeneralUtility::makeInstance(MappingRepository::class)->load($cfg['key'])['aliases'] ?? [];
|
||||
$out[$cfg['key']] = $this->recordsCheck($candidates, $cfg['table'], $aliases);
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
@@ -174,9 +178,10 @@ final class SeoResearchService
|
||||
|
||||
/**
|
||||
* @param array<int,array<string,string>> $candidates
|
||||
* @return array{total:int,matched:int,missing:array<int,array<string,string>>,extra:array<int,array<string,string>>}
|
||||
* @param array<string,int> $aliases lowercased CSV name -> record uid
|
||||
* @return array{total:int,matched:int,both:array<int,array<string,string>>,missing:array<int,array<string,string>>,extra:array<int,array<string,string>>,linkable:array<int,array<string,string>>}
|
||||
*/
|
||||
private function recordsCheck(array $candidates, string $table): array
|
||||
private function recordsCheck(array $candidates, string $table, array $aliases): array
|
||||
{
|
||||
$qb = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
|
||||
$records = $qb->select('uid', 'title', 'slug')->from($table)
|
||||
@@ -185,28 +190,43 @@ final class SeoResearchService
|
||||
|
||||
$bySlug = [];
|
||||
$byTitle = [];
|
||||
$byUid = [];
|
||||
foreach ($records as $rec) {
|
||||
$slug = mb_strtolower(trim((string)($rec['slug'] ?? '')));
|
||||
if ($slug !== '') {
|
||||
$bySlug[$slug] = $rec;
|
||||
}
|
||||
$byTitle[$this->normalizeTitle((string)$rec['title'])] = $rec;
|
||||
$byUid[(int)$rec['uid']] = $rec;
|
||||
}
|
||||
|
||||
$missing = [];
|
||||
$both = [];
|
||||
$matchedUids = [];
|
||||
$directUids = [];
|
||||
foreach ($candidates as $r) {
|
||||
$segment = mb_strtolower(trim((string)basename(rtrim($r['url'], '/'))));
|
||||
$rec = $bySlug[$segment] ?? $byTitle[$this->normalizeTitle($r['name'])] ?? null;
|
||||
$via = '';
|
||||
if ($rec === null) {
|
||||
$aliasUid = (int)($aliases[mb_strtolower(trim($r['name']))] ?? 0);
|
||||
if ($aliasUid > 0 && isset($byUid[$aliasUid])) {
|
||||
$rec = $byUid[$aliasUid];
|
||||
$via = 'link';
|
||||
}
|
||||
}
|
||||
if ($rec !== null) {
|
||||
$matchedUids[(int)$rec['uid']] = true;
|
||||
if ($via === '') {
|
||||
$directUids[(int)$rec['uid']] = true;
|
||||
}
|
||||
$both[] = [
|
||||
'ref' => $r['ref'],
|
||||
'name' => $r['name'],
|
||||
'url' => $r['url'],
|
||||
'uid' => (string)$rec['uid'],
|
||||
'title' => (string)$rec['title'],
|
||||
'via' => $via,
|
||||
];
|
||||
} else {
|
||||
$missing[] = $r;
|
||||
@@ -214,11 +234,19 @@ final class SeoResearchService
|
||||
}
|
||||
|
||||
$extra = [];
|
||||
$linkable = [];
|
||||
foreach ($records as $rec) {
|
||||
if (!isset($matchedUids[(int)$rec['uid']])) {
|
||||
$extra[] = ['uid' => (string)$rec['uid'], 'title' => (string)$rec['title']];
|
||||
}
|
||||
// Select options for the hand-made links: everything the direct
|
||||
// match (slug/title) did not claim - alias-linked records stay in
|
||||
// here so a linked row can be re-pointed or unlinked.
|
||||
if (!isset($directUids[(int)$rec['uid']])) {
|
||||
$linkable[] = ['uid' => (string)$rec['uid'], 'title' => (string)$rec['title']];
|
||||
}
|
||||
}
|
||||
usort($linkable, static fn(array $a, array $b): int => strcasecmp($a['title'], $b['title']));
|
||||
|
||||
return [
|
||||
'total' => count($candidates),
|
||||
@@ -226,6 +254,7 @@ final class SeoResearchService
|
||||
'both' => $both,
|
||||
'missing' => $missing,
|
||||
'extra' => $extra,
|
||||
'linkable' => $linkable,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user