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:
2026-08-26 19:48:43 +02:00
parent d846ba2007
commit 7442d10e68
9 changed files with 378 additions and 9 deletions

View File

@@ -0,0 +1,80 @@
<?php
/**
* Read-only diagnosis: why do success stories emit empty `markets`?
*
* Modeled on migrations/check_bg.php - PDO against the credentials from
* config/system/settings.php, no TYPO3 bootstrap, SELECT only.
*
* Run on the server: php migrations/check_usecase_markets.php
*/
declare(strict_types=1);
$settings = require __DIR__ . '/../config/system/settings.php';
$db = $settings['DB']['Connections']['Default'];
$pdo = new PDO(
sprintf('mysql:host=%s;dbname=%s;charset=utf8mb4', $db['host'], $db['dbname']),
$db['user'],
$db['password'],
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
function one(PDO $pdo, string $sql): array
{
return $pdo->query($sql)->fetch(PDO::FETCH_ASSOC) ?: [];
}
function all(PDO $pdo, string $sql): array
{
return $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
}
echo "=== 1. MM-Tabellen: Zeilen ueberhaupt da? ===\n";
foreach (['tx_vitec_usecase_market_mm', 'tx_vitec_usecase_solution_mm'] as $mm) {
$n = one($pdo, "SELECT COUNT(*) c FROM {$mm}");
echo sprintf("%-32s %s Zeilen\n", $mm, $n['c'] ?? '?');
}
echo "\n=== 2. Waisen: MM -> Markets, die es nicht (mehr) gibt ===\n";
echo "verknuepfte Market-uids und ihr Zustand:\n";
$rows = all($pdo, "
SELECT mm.uid_foreign AS market_uid,
COUNT(*) AS stories,
MAX(m.uid IS NULL) AS fehlt_ganz,
MAX(COALESCE(m.deleted, -1)) AS deleted,
MAX(COALESCE(m.hidden, -1)) AS hidden,
MAX(COALESCE(m.title, '?')) AS title
FROM tx_vitec_usecase_market_mm mm
LEFT JOIN tx_vitec_domain_model_market m ON m.uid = mm.uid_foreign
GROUP BY mm.uid_foreign
ORDER BY mm.uid_foreign
");
foreach ($rows as $r) {
$state = $r['fehlt_ganz'] ? 'FEHLT KOMPLETT' : (($r['deleted'] ?? 0) ? 'GELOESCHT' : ((($r['hidden'] ?? 0) ? 'versteckt' : 'ok')));
echo sprintf(" market uid %-5s %-15s %-40s (%s Stories)\n", $r['market_uid'], $state, mb_substr((string)$r['title'], 0, 40), $r['stories']);
}
echo "\n=== 3. Markets-Tabelle: Bestand ===\n";
$m = one($pdo, "SELECT COUNT(*) total, SUM(deleted) del, SUM(hidden) hid, MIN(uid) minuid, MAX(uid) maxuid FROM tx_vitec_domain_model_market");
echo sprintf("gesamt %s | geloescht %s | versteckt %s | uid-Bereich %s-%s\n", $m['total'], $m['del'], $m['hid'], $m['minuid'], $m['maxuid']);
echo "geloeschte Markets (falls vorhanden):\n";
foreach (all($pdo, "SELECT uid, title, FROM_UNIXTIME(tstamp) t FROM tx_vitec_domain_model_market WHERE deleted = 1 ORDER BY uid") as $r) {
echo sprintf(" uid %-5s %-45s zuletzt %s\n", $r['uid'], mb_substr((string)$r['title'], 0, 45), $r['t']);
}
echo "\n=== 4. sys_category-Zuordnungen der Stories (JSON-Feld `categories`) ===\n";
$c = one($pdo, "SELECT COUNT(*) c FROM sys_category_record_mm WHERE tablenames = 'tx_vitec_domain_model_usecase'");
echo "sys_category_record_mm fuer usecases: " . ($c['c'] ?? '?') . " Zeilen\n";
echo "\n=== 5. Stichprobe: 3 Stories mit ihren MM-Zeilen ===\n";
foreach (all($pdo, "
SELECT u.uid, u.title,
(SELECT COUNT(*) FROM tx_vitec_usecase_market_mm mm WHERE mm.uid_local = u.uid) AS mmrows
FROM tx_vitec_domain_model_usecase u
WHERE u.deleted = 0
ORDER BY u.uid LIMIT 3
") as $r) {
echo sprintf(" story uid %-4s %-40s -> %s MM-Zeile(n)\n", $r['uid'], mb_substr((string)$r['title'], 0, 40), $r['mmrows']);
}
echo "\nFertig - nichts geschrieben.\n";