SEO-Audit 2026-11-09: Meta-Komposition, Sitemap, Redirects, robots.txt
- VitecMetaHandler: Record-Metadaten fuer News-/Produkt-/Story-Details (Titel, Description, og:/twitter:-Tags, og:image), '| VITEC'-Suffix, Fallback-Description aus Seitenfeldern, twitter:card large_image, Story-Canonical auf die oeffentliche Kurz-URL - PageJsonLdRenderer: Product- und NewsArticle-Nodes auf Detailseiten (News-Details hatten <title>article</title> und kein Schema) - NewsJsonRenderer: Listen ohne Volltext-Body (JSON 2.6 MB -> ~0.6 MB) - XML-Sitemap: News-Teil konfiguriert (Ordner 11+442, Detailseite 21), Record-Sitemaps fuer Produkte und Success Stories (kurze URLs), Route-Enhancer fuer alle Teile, Shell-/Altseiten ausgeschlossen - Migration seo_fixes_20261109: 161 Self-Canonicals geleert (warfen u.a. Home, Markets, Solutions, Support aus der Sitemap), noindex fuer technische und alte News-Seiten, 71 Redirects Alt-URLs -> neue Ziele, 51 tote Footer-Links ersetzt - public/robots.txt mit Sitemap-Verweis Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
be519f9bc1
commit
b9736b9ac7
11 files changed
+814
-25
No files matched your search
@@ -112,6 +112,53 @@ final class PageJsonLdRenderer
|
||||
|
||||
|
||||
|
||||
// Product on product detail pages (routed record, SEO audit 2026-11-09).
|
||||
$args = $this->routeArguments($request);
|
||||
$productArg = (string)($args['tx_vitec_productshow']['product'] ?? '');
|
||||
if ($productArg !== '') {
|
||||
$productRow = $this->fetchDetailRecord(
|
||||
'tx_vitec_domain_model_product',
|
||||
is_numeric($productArg) ? 'uid' : 'slug',
|
||||
$productArg
|
||||
);
|
||||
if ($productRow !== null) {
|
||||
$productImages = $this->recordImageUrls('tx_vitec_domain_model_product', 'productimage', (int)$productRow['uid'], $base, $service);
|
||||
if ($productImages === []) {
|
||||
$productImages = $this->recordImageUrls('tx_vitec_domain_model_product', 'ogimage', (int)$productRow['uid'], $base, $service);
|
||||
}
|
||||
$nodes[] = $service->buildProduct([
|
||||
'uid' => (int)$productRow['uid'],
|
||||
'title' => (string)($productRow['title'] ?? ''),
|
||||
'teaser' => (string)($productRow['teaser'] ?? ''),
|
||||
'description' => (string)($productRow['description'] ?? ''),
|
||||
'slug' => (string)($productRow['slug'] ?? ''),
|
||||
], $base, $productImages, $hasOrganization);
|
||||
}
|
||||
}
|
||||
|
||||
// NewsArticle on EXT:news detail pages (the shell page carries no record data).
|
||||
$newsUid = (int)($args['tx_news_pi1']['news'] ?? 0);
|
||||
if ($newsUid > 0) {
|
||||
$newsRow = $this->fetchDetailRecord('tx_news_domain_model_news', 'uid', (string)$newsUid);
|
||||
if ($newsRow !== null) {
|
||||
$newsTitle = trim((string)($newsRow['alternative_title'] ?? ''));
|
||||
if ($newsTitle === '') {
|
||||
$newsTitle = (string)($newsRow['title'] ?? '');
|
||||
}
|
||||
$nodes[] = $service->buildNewsArticle([
|
||||
'seo_title' => '',
|
||||
'title' => $newsTitle,
|
||||
'description' => trim(strip_tags((string)($newsRow['teaser'] ?? ''))),
|
||||
'abstract' => '',
|
||||
'crdate' => (int)($newsRow['datetime'] ?? 0),
|
||||
'SYS_LASTCHANGED' => (int)($newsRow['tstamp'] ?? 0),
|
||||
'slug' => '/news/article/' . (string)($newsRow['path_segment'] ?? ''),
|
||||
'author' => (string)($newsRow['author'] ?? ''),
|
||||
'author_email' => (string)($newsRow['author_email'] ?? ''),
|
||||
], $base, $this->recordImageUrls('tx_news_domain_model_news', 'fal_media', $newsUid, $base, $service), $hasOrganization);
|
||||
}
|
||||
}
|
||||
|
||||
return $service->encodeGraph($nodes);
|
||||
} catch (\Throwable $e) {
|
||||
return '';
|
||||
@@ -410,4 +457,77 @@ final class PageJsonLdRenderer
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Route arguments of the current request (record detail context).
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function routeArguments($request): array
|
||||
{
|
||||
$routing = $request->getAttribute('routing');
|
||||
if ($routing instanceof \TYPO3\CMS\Core\Routing\PageArguments) {
|
||||
return array_replace_recursive($request->getQueryParams(), $routing->getArguments());
|
||||
}
|
||||
return $request->getQueryParams();
|
||||
}
|
||||
|
||||
/**
|
||||
* One visible record by uid or slug. Null when missing.
|
||||
*
|
||||
* @return array<string,mixed>|null
|
||||
*/
|
||||
private function fetchDetailRecord(string $table, string $field, string $value): ?array
|
||||
{
|
||||
try {
|
||||
$qb = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
|
||||
$row = $qb->select('*')->from($table)
|
||||
->where(
|
||||
$qb->expr()->eq('deleted', 0),
|
||||
$qb->expr()->eq('hidden', 0),
|
||||
$field === 'uid'
|
||||
? $qb->expr()->eq('uid', $qb->createNamedParameter((int)$value, ParameterType::INTEGER))
|
||||
: $qb->expr()->eq($field, $qb->createNamedParameter($value, ParameterType::STRING))
|
||||
)
|
||||
->setMaxResults(1)->executeQuery()->fetchAssociative();
|
||||
return is_array($row) ? $row : null;
|
||||
} catch (\Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Absolute URLs of a record's FAL images (unprocessed originals).
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
private function recordImageUrls(string $table, string $field, int $uid, string $base, StructuredDataService $service): array
|
||||
{
|
||||
try {
|
||||
$qb = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_file_reference');
|
||||
$rows = $qb->select('f.identifier')
|
||||
->from('sys_file_reference', 'fr')
|
||||
->join('fr', 'sys_file', 'f', 'fr.uid_local = f.uid')
|
||||
->where(
|
||||
$qb->expr()->eq('fr.tablenames', $qb->createNamedParameter($table, ParameterType::STRING)),
|
||||
$qb->expr()->eq('fr.fieldname', $qb->createNamedParameter($field, ParameterType::STRING)),
|
||||
$qb->expr()->eq('fr.uid_foreign', $qb->createNamedParameter($uid, ParameterType::INTEGER)),
|
||||
$qb->expr()->eq('fr.deleted', 0),
|
||||
$qb->expr()->eq('fr.hidden', 0),
|
||||
$qb->expr()->eq('f.missing', 0)
|
||||
)
|
||||
->orderBy('fr.sorting_foreign', 'ASC')
|
||||
->executeQuery()->fetchAllAssociative();
|
||||
$urls = [];
|
||||
foreach ($rows as $row) {
|
||||
$identifier = (string)($row['identifier'] ?? '');
|
||||
if ($identifier !== '') {
|
||||
$urls[] = $service->absUrl('/fileadmin' . $identifier, $base);
|
||||
}
|
||||
}
|
||||
return $urls;
|
||||
} catch (\Throwable $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user