- 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>
225 lines
11 KiB
PHP
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";
|