Files
orascheandClaude Fable 5 b9736b9ac7 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>
2026-11-09 22:11:01 +01:00

225 lines
11 KiB
PHP

<?php
declare(strict_types=1);
/**
* SEO fixes (audit 2026-11-09):
*
* 1. Clear SELF-referencing canonical_link values. The page imports wrote
* "t3://page?uid=<self>" / "<self>" into 161 pages; TYPO3 emits a
* self-canonical anyway, and the pages sitemap EXCLUDES every page with
* a canonical_link (additionalWhere) - which is why markets, solutions,
* support, contact, insights and even the home page were missing from
* sitemap.xml.
* 2. no_index = 1 for technical pages (search, megamenu, footer shells...)
* and the imported old news pages under /en/news + /news (duplicates of
* the EXT:news articles).
* 3. 301 redirects for old-site URLs onto their new equivalents - the
* footer link targets plus every old news page that matches an EXT:news
* article by slug segment.
* 4. Rewrites the old-site hrefs inside tt_content bodytext (footer html
* columns) onto the new targets, so no page carries dead internal links.
*
* Dry run: php migrations/seo_fixes_20261109.php
* Write: php migrations/seo_fixes_20261109.php --apply
*/
error_reporting(E_ALL);
ini_set('display_errors', '1');
$apply = in_array('--apply', $argv, true);
$root = null;
$dir = __DIR__;
for ($i = 0; $i < 5; $i++) {
if (is_file($dir . '/config/system/settings.php')) { $root = $dir; break; }
$dir = dirname($dir);
}
if ($root === null) { exit("no project root\n"); }
$settings = require $root . '/config/system/settings.php';
$db = $settings['DB']['Connections']['Default'];
$pdo = new PDO(
sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''),
$db['user'] ?? '', $db['password'] ?? '',
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
);
echo $apply ? "== APPLY ==\n" : "== DRY RUN (add --apply to write) ==\n";
/* ------------------------------------------------------------------ */
/* 1. Self-canonicals */
/* ------------------------------------------------------------------ */
// Comparison in PHP: CONCAT() in SQL trips over mixed column collations.
$selfCanonical = [];
foreach ($pdo->query("SELECT uid, canonical_link FROM pages WHERE deleted = 0 AND canonical_link <> ''") as $row) {
$uid = (int)$row['uid'];
$link = trim((string)$row['canonical_link']);
if ($link === 't3://page?uid=' . $uid || $link === (string)$uid) {
$selfCanonical[] = $uid;
}
}
echo "1. Self-Canonicals leeren: " . count($selfCanonical) . " Seiten\n";
if ($apply && $selfCanonical !== []) {
$pdo->exec(
"UPDATE pages SET canonical_link = '', tstamp = UNIX_TIMESTAMP()
WHERE uid IN (" . implode(',', $selfCanonical) . ")"
);
}
/* ------------------------------------------------------------------ */
/* 2. no_index for technical + old news pages */
/* ------------------------------------------------------------------ */
$technicalUids = [12, 16, 19, 106, 107, 448, 450, 516, 517, 519, 521, 523, 607];
$in = implode(',', $technicalUids);
$cnt = (int)$pdo->query("SELECT COUNT(*) FROM pages WHERE deleted = 0 AND no_index = 0 AND (uid IN ($in) OR pid = 106)")->fetchColumn();
echo "2. no_index=1 setzen: $cnt Seiten (technische Seiten + alte News-Seiten unter #106)\n";
if ($apply && $cnt > 0) {
$pdo->exec("UPDATE pages SET no_index = 1, tstamp = UNIX_TIMESTAMP() WHERE deleted = 0 AND no_index = 0 AND (uid IN ($in) OR pid = 106)");
}
/* ------------------------------------------------------------------ */
/* 3a. Old-site URL mapping (footer targets) */
/* ------------------------------------------------------------------ */
$mapping = [
'/about/careers' => '/insights/careers',
'/about/company' => '/insights/who-we-are',
'/about/events' => '/insights/events',
'/about/locations' => '/contact/locations',
'/about/news' => '/insights/news',
'/about/sustainability' => '/insights/sustainability',
'/about/who-we-are' => '/insights/who-we-are',
'/about/why-vitec' => '/insights/who-we-are',
'/downloads' => '/datasheets',
'/markets/accommodation' => '/markets/hospitality',
'/markets/broadcast' => '/markets/broadcast-and-telecoms',
'/markets/corporate' => '/markets/corporation',
'/markets/healthcare' => '/markets',
'/markets/military' => '/markets/defense-and-security',
'/markets/sports' => '/markets/sports-and-entertainment',
'/markets/venues' => '/markets/sports-and-entertainment',
'/products/encoders-decoders' => '/products/video-encoding-and-streaming',
'/products/encoders-decoders/appliances' => '/products/appliances',
'/products/encoders-decoders/modular-systems' => '/products/modular-systems',
'/products/encoders-decoders/modular-systems/avedia-encoders' => '/products/avedia-modular-system',
'/products/encoders-decoders/modular-systems/iptv-blade-system' => '/products/avedia-modular-system',
'/products/encoders-decoders/modular-systems/vitec-og' => '/products/modular-systems',
'/products/encoders-decoders/pcie-encoder' => '/products/activesqx',
'/products/encoders-decoders/rugged' => '/products/rugged-appliances',
'/products/ip/rf-gateways-transcoders' => '/products/rf-gateway',
'/products/ip/rf-gateways-transcoders/ip-gateways' => '/products/channellink-ip-gateway',
'/products/ip/rf-gateways-transcoders/prism-transcoder' => '/products/prism-transcoder',
'/products/ip/rf-gateways-transcoders/rf-gateways' => '/products/rf-gateway',
'/products/iptv-digital-signage-platform' => '/products/iptv-and-digital-signage',
'/products/iptv-digital-signage-platform/all-in-one-platform' => '/products/avedia',
'/products/iptv-digital-signage-platform/digital-signage' => '/products/iptv-and-digital-signage',
'/products/iptv-digital-signage-platform/end-points' => '/products/end-points',
'/products/iptv-digital-signage-platform/integrated-displays' => '/products/x-view-integrated-displays',
'/products/iptv-digital-signage-platform/interactive-tv-portal' => '/products/ez-tv',
'/products/iptv-digital-signage-platform/video-wall-controllers' => '/products/video-wall-controllers-and-management',
'/products/iptv-digital-signage-platform/video-wall-controllers/aligo' => '/products/aligo',
'/products/iptv-digital-signage-platform/video-wall-controllers/fx4-series' => '/products/video-wall-controllers-and-management',
'/products/iptv-digital-signage-platform/video-wall-controllers/vsn-series' => '/products/vsn-series-video-wall-controllers',
'/products/iptv-digital-signage-platform/x-player' => '/products/x-player-end-points',
'/products/services' => '/support',
'/products/services/deployment-services' => '/support/commissioning',
'/products/services/digital-signage-and-design-services' => '/support',
'/products/services/training' => '/support/training',
'/products/telemetry' => '/products',
'/solutions/contribution-remote-production' => '/solutions/video-capture-encoding-processing/contribution-remote-production',
'/solutions/iptv-distribution' => '/solutions/video-distribution-streaming/iptv-distribution',
'/solutions/situational-awareness-and-isr' => '/solutions/real-time-monitoring-situational-awareness',
'/solutions/video-streaming' => '/solutions/video-distribution-streaming',
'/support/material-compliance' => '/support/materials-compliance',
'/support/request-helpdesk-access' => '/support/login-to-helpdesk',
'/en/news' => '/insights/news',
];
// Resolve every target slug to a page uid (sanity: fail loudly on typos).
$targetUids = [];
$stmt = $pdo->prepare("SELECT uid FROM pages WHERE deleted = 0 AND hidden = 0 AND slug = ? LIMIT 1");
$missing = 0;
foreach (array_unique(array_values($mapping)) as $target) {
$stmt->execute([$target]);
$uid = (int)$stmt->fetchColumn();
if ($uid <= 0) {
echo " !! Zielseite fehlt: $target\n";
$missing++;
continue;
}
$targetUids[$target] = $uid;
}
if ($missing > 0 && $apply) {
exit("Abbruch: $missing Zielseiten nicht gefunden - Mapping pruefen.\n");
}
echo "3a. Redirects Alt-URLs -> neue Seiten:\n";
$hasRedirect = $pdo->prepare("SELECT COUNT(*) FROM sys_redirect WHERE deleted = 0 AND source_path = ?");
$insertRedirect = $pdo->prepare(
"INSERT INTO sys_redirect (pid, createdon, updatedon, source_host, source_path, target, target_statuscode)
VALUES (0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), '*', ?, ?, 301)"
);
$newRedirects = 0;
foreach ($mapping as $old => $new) {
if (!isset($targetUids[$new])) { continue; }
$hasRedirect->execute([$old]);
if ((int)$hasRedirect->fetchColumn() > 0) { continue; }
$newRedirects++;
if ($apply) {
$insertRedirect->execute([$old, 't3://page?uid=' . $targetUids[$new]]);
}
}
echo " $newRedirects neue Redirects (bestehende unangetastet)\n";
/* ------------------------------------------------------------------ */
/* 3b. Old imported news pages -> EXT:news articles (slug match) */
/* ------------------------------------------------------------------ */
echo "3b. Alte News-Seiten -> /news/article/<slug>:\n";
$oldNewsPages = $pdo->query(
"SELECT uid, slug FROM pages WHERE deleted = 0 AND (pid = 106 OR uid = 107) AND slug <> ''"
)->fetchAll();
$newsBySegment = $pdo->prepare(
"SELECT uid FROM tx_news_domain_model_news WHERE deleted = 0 AND hidden = 0 AND path_segment = ? LIMIT 1"
);
$matched = 0; $unmatched = [];
foreach ($oldNewsPages as $page) {
$segment = basename((string)$page['slug']);
$newsBySegment->execute([$segment]);
$newsUid = (int)$newsBySegment->fetchColumn();
if ($newsUid <= 0) {
$unmatched[] = $page['slug'];
continue;
}
$hasRedirect->execute([$page['slug']]);
if ((int)$hasRedirect->fetchColumn() > 0) { continue; }
$matched++;
if ($apply) {
$insertRedirect->execute([(string)$page['slug'], '/news/article/' . $segment]);
}
}
echo " $matched Redirects auf Artikel, " . count($unmatched) . " ohne Artikel-Match (bleiben als noindex-Seiten erreichbar)\n";
foreach ($unmatched as $slug) {
echo " kein Match: $slug\n";
}
/* ------------------------------------------------------------------ */
/* 4. Dead hrefs inside tt_content bodytext */
/* ------------------------------------------------------------------ */
echo "4. Alt-Links in Inhalten ersetzen:\n";
$findContent = $pdo->prepare("SELECT COUNT(*) FROM tt_content WHERE deleted = 0 AND bodytext LIKE ?");
$fixContent = $pdo->prepare("UPDATE tt_content SET bodytext = REPLACE(bodytext, ?, ?), tstamp = UNIX_TIMESTAMP() WHERE deleted = 0 AND bodytext LIKE ?");
$replaced = 0;
foreach ($mapping as $old => $new) {
$needle = 'href="' . $old . '"';
$findContent->execute(['%' . $needle . '%']);
$count = (int)$findContent->fetchColumn();
if ($count === 0) { continue; }
echo " $needle -> href=\"$new\" ($count Element(e))\n";
$replaced += $count;
if ($apply) {
$fixContent->execute([$needle, 'href="' . $new . '"', '%' . $needle . '%']);
}
}
echo " Summe: $replaced Element-Treffer\n";
echo $apply ? "FERTIG. Cache flushen (Redirects + Seiten sind gecacht).\n" : "DRY RUN beendet.\n";