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
@@ -47,6 +47,19 @@ class ProcessedFileReferenceToArray extends FileReferenceToArray
|
||||
$mimeType = (string)$file->getMimeType();
|
||||
$extension = strtolower((string)$file->getExtension());
|
||||
|
||||
// Alt-text fallback (Seobility: 215 pages with empty alt): the
|
||||
// reference title, then the file's title property. Editors can
|
||||
// still override per reference.
|
||||
if (trim((string)($data['alt'] ?? '')) === '') {
|
||||
$fallbackAlt = trim((string)($file->getTitle() ?? ''));
|
||||
if ($fallbackAlt === '') {
|
||||
$fallbackAlt = trim((string)($file->getOriginalFile()->getProperty('title') ?? ''));
|
||||
}
|
||||
if ($fallbackAlt !== '') {
|
||||
$data['alt'] = $fallbackAlt;
|
||||
}
|
||||
}
|
||||
|
||||
$data['mimeType'] = $mimeType;
|
||||
$data['size'] = (int)$file->getSize();
|
||||
|
||||
|
||||
@@ -141,6 +141,20 @@ class VitecMetaHandler extends MetaHandler
|
||||
$image = $this->firstImage('pages', 'media', (int)$page['uid']);
|
||||
}
|
||||
|
||||
// --- short page titles get their section as context -------------------
|
||||
// "Aligo | VITEC" (Seobility: 456 titles "zu kurz") becomes
|
||||
// "Aligo | AV over IP & KVM | VITEC"; only for plain pages without an
|
||||
// editor-set SEO title, never for record detail pages.
|
||||
if ($newsUid === 0 && $productArg === '' && $usecaseUid === 0
|
||||
&& $title !== '' && mb_strlen($title) < 30
|
||||
&& trim((string)($page['seo_title'] ?? '')) === ''
|
||||
) {
|
||||
$parentTitle = $this->parentTitle($pageInfo);
|
||||
if ($parentTitle !== '' && mb_stripos($title, $parentTitle) === false) {
|
||||
$title .= ' | ' . $parentTitle;
|
||||
}
|
||||
}
|
||||
|
||||
// --- uniform brand suffix ---------------------------------------------
|
||||
if ($title !== '' && mb_stripos($title, 'vitec') === false) {
|
||||
$title .= ' | VITEC';
|
||||
@@ -209,6 +223,31 @@ class VitecMetaHandler extends MetaHandler
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigation title of the parent page, or '' when the parent is the site
|
||||
* root, a technical container (user_nav, folders) or missing.
|
||||
*/
|
||||
private function parentTitle($pageInfo): string
|
||||
{
|
||||
if ($pageInfo === null) {
|
||||
return '';
|
||||
}
|
||||
$rootLine = $pageInfo->getRootLine();
|
||||
$parent = $rootLine[1] ?? null;
|
||||
if (!is_array($parent) || count($rootLine) < 3) {
|
||||
// parent is the root page (rootline: current, root) or missing
|
||||
return '';
|
||||
}
|
||||
if (in_array((int)($parent['doktype'] ?? 1), [3, 4, 199, 254, 255], true)) {
|
||||
return '';
|
||||
}
|
||||
$name = trim((string)($parent['nav_title'] ?? '')) ?: trim((string)($parent['title'] ?? ''));
|
||||
if ($name === '' || preg_match('/^user[_ -]?nav$/i', $name) === 1 || str_contains($name, '_')) {
|
||||
return '';
|
||||
}
|
||||
return $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Route arguments of the current request (record detail context).
|
||||
*
|
||||
|
||||
@@ -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) ?? '';
|
||||
|
||||
Reference in new issue
Block a user