" / "" 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/:\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";