Seobility-Report 2026-11-10: Backend-Befunde umgesetzt

Crawl nach dem Footer-Fix: 1.094 Seiten (vorher 9). Backend-Anteil:

- Migration seo_report_fixes_20261110: Platzhalter-SEO-Titel auf /solutions
  geleert; News mit verdoppeltem Titel/Slug und Slugs mit Slash bereinigt
  (+Redirects); 8 Tippfehler; zweite Welle toter Alt-Links (datapath.co.uk-
  Absolutlinks, utm-Varianten, /resource-center, /services, /contact-sales,
  /aetria-use-cases, /video-wall-solutions ...) in 48 News ersetzt + 101
  Redirects; Record-Shells /product, /news/article, /success-stories/story
  leiten auf ihre Uebersichten; Meta-Description /datasheets; WordPress-
  Reste in den 17 SEO-Artikeln entfernt; 53 News-Zeiger-Records (type=1
  auf alte /en/news-Seiten, ohne eigenen Text) bekommen den Seiteninhalt
  (Text + Bilder) in den Record und werden normale Artikel - die alten
  Seiten leiten per 301 auf die Artikel.
- VitecMetaHandler: kurze Seitentitel ohne SEO-Titel erhalten ihren
  Bereich als Kontext ('Aligo | AV over IP & KVM | VITEC').
- ProcessedFileReferenceToArray: Alt-Text-Fallback (Referenz-Titel, dann
  Dateititel) fuer Content-Block-Bilder.
- UsecaseListJsonRenderer: Testimonial-Baender liefern nur noch Stories
  mit Zitat, aktueller Markt zuerst, max. 12 (vorher alle 70 = 226 kB auf
  jeder Markt-/Solution-Unterseite, identische Bloecke auf ~90 Seiten).
- NewsJsonRenderer: paginierte Listen-Payloads 15 Min. im hash-Cache
  (USER_INT rendert sonst jede Variante neu; 50 langsame URLs im Report).
- robots.txt host-abhaengig: dev bekommt robots-dev.txt mit der dev-Sitemap
  (Seobility fand '0 Sitemaps', weil die Sitemap-Zeile auf www zeigte).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
orascheandClaude Fable 5.1 committed 2026-11-10 17:47:55 +01:00
1 parent 5121eaa000
commit 8a652063c5
7 files changed
+590 -1

No files matched your search

@@ -430,6 +430,25 @@ final class NewsJsonRenderer
$requestedPage = max(1, (int)($queryParams['page'] ?? 1));
$requestedCategory = strtolower(trim((string)($queryParams['category'] ?? '')));
// USER_INT renders on every request; the 12-card page payload (image
// processing included) took 1-6 s on cold SSR misses (Seobility: 50
// slow URLs, all ?page=N). Keep each page/category variant for 15 min
// in the hash cache - cache:flush clears it, news edits are rare.
$cache = null;
$cacheId = '';
try {
$cache = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Cache\CacheManager::class)->getCache('hash');
$cacheId = 'vitec-newslist-' . (int)($contentElement['uid'] ?? 0)
. '-' . (int)($contentElement['sys_language_uid'] ?? 0)
. '-' . $requestedPage . '-' . md5($requestedCategory);
$cached = $cache->get($cacheId);
if (is_string($cached) && $cached !== '') {
return $cached;
}
} catch (\Throwable $e) {
$cache = null;
}
// All categories of all entries in ONE query - feeds both the filter
// chips (counts over the FULL list, independent of the active filter)
// and the per-card category labels.
@@ -504,7 +523,16 @@ final class NewsJsonRenderer
'settings' => $this->buildSettingsPayload($contentElement, $settings),
];
return json_encode($response) ?: '';
$json = json_encode($response) ?: '';
if ($cache !== null && $json !== '') {
try {
$cache->set($cacheId, $json, ['tx_news'], 900);
} catch (\Throwable $e) {
// caching is best effort
}
}
return $json;
}
/**
@@ -165,6 +165,16 @@ class UsecaseListJsonRenderer
$rows = $ordered;
}
// Testimonial bands on sub-market/solution pages shipped ALL 70
// stories (226 kB per page, the same quotes on ~90 pages - Seobility
// 2026-11-10: 34 pages > 500 kB, 1,846 repeated text blocks). Only
// stories with a maintained quote are testimonials; the current
// market's stories come first, capped at twelve. Editor-picked
// lists stay untouched.
if ($layout === 'testimonials' && $selectedUids === []) {
$rows = $this->testimonialRows($rows);
}
$usecases = array_map(
static function (array $u) use ($serializer, $detailBase): array {
$item = $serializer->serializeListItem($u);
@@ -210,6 +220,80 @@ class UsecaseListJsonRenderer
* Callers here build paths by concatenation, so the empty string stays the
* "no link" value; LinkResolver's null is coalesced away.
*/
/**
* Stories for a testimonial band: quoted ones only, current market first,
* at most twelve. Falls back to the first twelve rows when no story has
* a quote yet.
*
* @param array<int,array<string,mixed>> $rows
* @return array<int,array<string,mixed>>
*/
private function testimonialRows(array $rows): array
{
$quoted = array_values(array_filter(
$rows,
static fn (array $r): bool => trim((string)($r['quote'] ?? '')) !== ''
));
if ($quoted === []) {
return array_slice($rows, 0, 12);
}
try {
$marketUids = $this->currentMarketUids();
if ($marketUids !== []) {
$qb = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tx_vitec_usecase_market_mm');
$inMarket = array_map('intval', $qb
->select('uid_local')
->from('tx_vitec_usecase_market_mm')
->where($qb->expr()->in('uid_foreign', $qb->createNamedParameter($marketUids, \Doctrine\DBAL\ArrayParameterType::INTEGER)))
->executeQuery()
->fetchFirstColumn());
$matching = array_values(array_filter($quoted, static fn (array $r): bool => in_array((int)$r['uid'], $inMarket, true)));
if (count($matching) >= 3) {
$others = array_values(array_filter($quoted, static fn (array $r): bool => !in_array((int)$r['uid'], $inMarket, true)));
$quoted = array_merge($matching, $others);
}
}
} catch (\Throwable $e) {
// keep the quoted stories in default order
}
return array_slice($quoted, 0, 12);
}
/**
* Market records whose detail page lies on the current rootline.
*
* @return list<int>
*/
private function currentMarketUids(): array
{
$request = $GLOBALS['TYPO3_REQUEST'] ?? null;
$pageInfo = $request?->getAttribute('frontend.page.information');
if ($pageInfo === null) {
return [];
}
$pageUids = array_values(array_filter(array_map(
static fn (array $p): int => (int)($p['uid'] ?? 0),
$pageInfo->getRootLine()
)));
if ($pageUids === []) {
return [];
}
$qb = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tx_vitec_domain_model_market');
return array_map('intval', $qb
->select('uid')
->from('tx_vitec_domain_model_market')
->where(
$qb->expr()->eq('deleted', 0),
$qb->expr()->in('detail_page', $qb->createNamedParameter($pageUids, \Doctrine\DBAL\ArrayParameterType::INTEGER))
)
->executeQuery()
->fetchFirstColumn());
}
private function resolvePageUrl(int $pageUid): string
{
return LinkResolver::pageUrl($pageUid) ?? '';