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:
1 parent
5121eaa000
commit
8a652063c5
7 files changed
+590
-1
No files matched your search
@@ -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) ?? '';
|
||||
|
||||
Reference in new issue
Block a user