diff --git a/migrations/seo_report_fixes_20261110.php b/migrations/seo_report_fixes_20261110.php new file mode 100644 index 0000000..a198d31 --- /dev/null +++ b/migrations/seo_report_fixes_20261110.php @@ -0,0 +1,414 @@ + PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC] +); + +echo $apply ? "== APPLY ==\n" : "== DRY RUN (add --apply to write) ==\n"; +$now = time(); + +$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)" +); +$redirectsAdded = 0; +$addRedirect = static function (string $source, string $target) use ($hasRedirect, $insertRedirect, $apply, &$redirectsAdded): void { + $hasRedirect->execute([$source]); + if ((int)$hasRedirect->fetchColumn() > 0) { + return; + } + $redirectsAdded++; + if ($apply) { + $insertRedirect->execute([$source, $target]); + } +}; +$pageBySlug = $pdo->prepare("SELECT uid FROM pages WHERE deleted = 0 AND hidden = 0 AND slug = ? LIMIT 1"); +$productBySlug = $pdo->prepare("SELECT uid FROM tx_vitec_domain_model_product WHERE deleted = 0 AND hidden = 0 AND slug = ? LIMIT 1"); +$newsBySegment = $pdo->prepare("SELECT uid FROM tx_news_domain_model_news WHERE deleted = 0 AND path_segment = ? LIMIT 1"); +$targetExists = static function (string $target) use ($pageBySlug, $productBySlug, $newsBySegment): bool { + if (str_starts_with($target, '/product/')) { + $productBySlug->execute([substr($target, 9)]); + return (int)$productBySlug->fetchColumn() > 0; + } + if (str_starts_with($target, '/news/article/')) { + $newsBySegment->execute([substr($target, 14)]); + return (int)$newsBySegment->fetchColumn() > 0; + } + $pageBySlug->execute([$target]); + return (int)$pageBySlug->fetchColumn() > 0; +}; + +/* ------------------------------------------------------------------ */ +echo "\n1. Platzhalter-SEO-Titel/-Beschreibungen\n"; +$rows = $pdo->query("SELECT uid, slug, seo_title FROM pages WHERE deleted = 0 AND (seo_title LIKE 'Title for search engines%' OR description LIKE 'Description for search engines%')")->fetchAll(); +foreach ($rows as $r) { + echo " #{$r['uid']} {$r['slug']}: seo_title/description geleert\n"; + if ($apply) { + $pdo->prepare("UPDATE pages SET seo_title = IF(seo_title LIKE 'Title for search engines%', '', seo_title), description = IF(description LIKE 'Description for search engines%', '', description), tstamp = ? WHERE uid = ?")->execute([$now, $r['uid']]); + } +} + +/* ------------------------------------------------------------------ */ +echo "\n2. News mit verdoppeltem Titel / path_segment\n"; +$halve = static function (string $s): ?string { + $len = mb_strlen($s); + if ($len < 20 || $len % 2 !== 0) { return null; } + $h = mb_substr($s, 0, intdiv($len, 2)); + return $h === mb_substr($s, intdiv($len, 2)) ? $h : null; +}; +foreach ($pdo->query("SELECT uid, title, path_segment FROM tx_news_domain_model_news WHERE deleted = 0 AND CHAR_LENGTH(title) > 40") as $r) { + $newTitle = $halve((string)$r['title']); + if ($newTitle === null) { continue; } + $seg = (string)$r['path_segment']; + $newSeg = $seg; + // doubled slug: "a-b-ca-b-c" -> "a-b-c" + $segLen = strlen($seg); + if ($segLen % 2 === 0 && substr($seg, 0, intdiv($segLen, 2)) === substr($seg, intdiv($segLen, 2))) { + $newSeg = substr($seg, 0, intdiv($segLen, 2)); + } + echo " #{$r['uid']}: Titel -> [$newTitle]" . ($newSeg !== $seg ? ", Slug -> $newSeg (+Redirect)" : '') . "\n"; + if ($apply) { + $pdo->prepare("UPDATE tx_news_domain_model_news SET title = ?, path_segment = ?, tstamp = ? WHERE uid = ?")->execute([$newTitle, $newSeg, $now, $r['uid']]); + } + if ($newSeg !== $seg) { + $addRedirect('/news/article/' . $seg, '/news/article/' . $newSeg); + } +} + +/* ------------------------------------------------------------------ */ +echo "\n3. News path_segment mit Slash\n"; +foreach ($pdo->query("SELECT uid, path_segment FROM tx_news_domain_model_news WHERE deleted = 0 AND path_segment LIKE '%/%'") as $r) { + $old = (string)$r['path_segment']; + $new = basename($old); + $newsBySegment->execute([$new]); + $taken = (int)$newsBySegment->fetchColumn(); + if ($taken > 0 && $taken !== (int)$r['uid']) { + $new .= '-' . $r['uid']; + } + echo " #{$r['uid']}: $old -> $new (+Redirect)\n"; + if ($apply) { + $pdo->prepare("UPDATE tx_news_domain_model_news SET path_segment = ?, tstamp = ? WHERE uid = ?")->execute([$new, $now, $r['uid']]); + } + $addRedirect('/news/article/' . $old, '/news/article/' . $new); +} + +/* ------------------------------------------------------------------ */ +echo "\n4. Tippfehler\n"; +$typos = [ + 'continously' => 'continuously', 'continous' => 'continuous', 'indpendent' => 'independent', + 'aircrafts' => 'aircraft', 'sponser' => 'sponsor', 'auxillary' => 'auxiliary', + 'processer' => 'processor', 'accomodation' => 'accommodation', + 'Accomodation' => 'Accommodation', 'Continous' => 'Continuous', +]; +$typoTargets = [ + 'tt_content' => ['bodytext', 'header'], + 'tx_news_domain_model_news' => ['bodytext', 'teaser', 'title'], + 'tx_vitec_domain_model_usecase' => ['description', 'teaser'], +]; +foreach ($typos as $wrong => $right) { + foreach ($typoTargets as $table => $fields) { + foreach ($fields as $field) { + $n = (int)$pdo->query("SELECT COUNT(*) FROM $table WHERE deleted = 0 AND $field LIKE " . $pdo->quote('%' . $wrong . '%'))->fetchColumn(); + if ($n === 0) { continue; } + echo " $wrong -> $right: $table.$field ($n)\n"; + if ($apply) { + $pdo->exec("UPDATE $table SET $field = REPLACE($field, " . $pdo->quote($wrong) . ", " . $pdo->quote($right) . "), tstamp = $now WHERE deleted = 0 AND $field LIKE " . $pdo->quote('%' . $wrong . '%')); + } + } + } +} + +/* ------------------------------------------------------------------ */ +echo "\n5. Tote Alt-Links (zweite Welle, inkl. datapath.co.uk / utm-Varianten)\n"; +$awardSeg = 'aetria-award-wins-set-the-tone-for-datapaths-best-ever-ise-week'; +$newsBySegment->execute([$awardSeg]); +$awardTarget = (int)$newsBySegment->fetchColumn() > 0 ? '/news/article/' . $awardSeg : '/insights/news'; +$mapping2 = [ + '/resource-center/legacy-products' => '/products/capture-and-graphics', + '/resource-center/legacy-products/imagedp4' => '/products/image-graphics-cards', + '/resource-center/legacy-products/datapath-x4' => '/products/video-wall-controllers-and-management', + '/resource-center/legacy-products/datapath-dl8' => '/products/video-wall-controllers-and-management', + '/legacy-products/imagedp4-plus' => '/products/image-graphics-cards', + '/resource-center/get-support' => '/support', + '/services/premium-service-package' => '/support/support-plans', + '/services/training' => '/support/training', + '/contact-sales' => '/contact/contact-form', + '/aetria-use-cases/oil-and-gas-control-room' => '/markets/critical-national-infrastructure/energy-and-utilities', + '/aetria-use-cases/energy-trading-and-dispatch' => '/markets/critical-national-infrastructure/energy-and-utilities', + '/aetria-use-cases/cctv-for-university-campus' => '/markets/education', + '/video-wall-solutions/commercial-video-walls' => '/solutions/video-wall-data-visualisation', + '/video-wall-solutions/control-rooms' => '/solutions/control-room-command-centre-solutions', + '/video-wall-solutions/projection-mapping' => '/solutions/video-wall-data-visualisation', + '/video-wall-solutions/video-wall-for-security-suites' => '/solutions/security-surveillance-solutions', + '/video-wall-solutions' => '/solutions/video-wall-data-visualisation', + '/insights/success-stories' => '/success-stories', + '/datapath-products' => '/products', + '/datapath-products/aetria-control-room-solutions' => '/products/aetria', + '/datapath-products/aetria-control-room-solutions/aetria-overview' => '/products/aetria', + '/datapath-products/aetria-control-room-solutions/aetria-command-center' => '/product/aetria-command-center', + '/datapath-products/aetria-control-room-solutions/aetria-workstation' => '/product/aetria-workstation', + '/datapath-products/aetria-control-room-solutions/aetria-wallcontrol' => '/product/aetria-wallcontrol', + '/datapath-products/video-wall-controllers/vsn-v3' => '/product/vsn-v3', + '/datapath-products/video-wall-controllers/vsn' => '/products/vsn-series-video-wall-controllers', + '/datapath-products/video-wall-controllers/vsn-series' => '/products/vsn-series-video-wall-controllers', + '/datapath-products/video-wall-controllers/vsnmicro-600' => '/product/vsnmicro-600', + '/datapath-products/video-wall-controllers/datapath-fx4' => '/product/fx4', + '/datapath-products/video-distribution-kvm/aligo-4k-avoip-with-kvm' => '/products/aligo', + '/datapath-products/video-distribution-kvm' => '/products/av-over-ip-and-kvm', + '/datapath-products/video-capture-cards/visionsc-range/visionsc-uhd2' => '/product/visionsc-uhd2', + '/datapath-products/video-capture-cards/visionsc-range/visionsc-hd4' => '/product/visionsc-hd4', + '/datapath-products/video-capture-cards/visionsc-range/visionsc-s2' => '/product/visionsc-s2', + '/datapath-products/video-wall-management-software/wallcontrol-10-software' => '/product/wall-control-10', + '/datapath-products/graphics-products/image2k' => '/product/image-2k', + '/datapath-products/components-accessories/rack-mounting-kits' => '/products', + '/datapath-products/sqx-ip-capture/active-sqx' => '/product/activesqx', + '/datapath-products/video-wall-control' => '/products/video-wall-controllers-and-management', + '/' . $awardSeg => $awardTarget, +]; +$missing = 0; +foreach (array_unique(array_values($mapping2)) as $t) { + if (!$targetExists($t)) { echo " !! Ziel fehlt: $t\n"; $missing++; } +} +if ($missing > 0 && $apply) { exit("Abbruch: Ziele fehlen.\n"); } + +$hostPrefix = '(?:https?://(?:www\.)?datapath(?:-us)?\.(?:co\.uk|com))?'; +$linkTables = ['tx_news_domain_model_news' => 'bodytext', 'tt_content' => 'bodytext']; +$linkHits = 0; +foreach ($linkTables as $table => $field) { + $select = $pdo->query("SELECT uid, $field AS body FROM $table WHERE deleted = 0 AND ($field LIKE '%datapath%' OR $field LIKE '%/resource-center/%' OR $field LIKE '%/services/%' OR $field LIKE '%/contact-sales%' OR $field LIKE '%/aetria-use-cases/%' OR $field LIKE '%/video-wall-solutions%' OR $field LIKE '%/insights/success-stories%' OR $field LIKE '%/legacy-products/%' OR $field LIKE '%aetria-award-wins%')"); + $update = $pdo->prepare("UPDATE $table SET $field = ?, tstamp = ? WHERE uid = ?"); + foreach ($select as $r) { + $body = (string)$r['body']; + $new = $body; + foreach ($mapping2 as $old => $target) { + $pattern = '#href="' . $hostPrefix . preg_quote($old, '#') . '/?(?:\?[^"]*)?"#'; + $new = preg_replace($pattern, 'href="' . $target . '"', $new) ?? $new; + } + if ($new !== $body) { + $linkHits++; + echo " $table #{$r['uid']}: Links ersetzt\n"; + if ($apply) { $update->execute([$new, $now, $r['uid']]); } + } + } +} +echo " Records mit ersetzten Links: $linkHits\n"; +foreach ($mapping2 as $old => $target) { + $addRedirect($old, $target); + $addRedirect($old . '/', $target); +} + +/* ------------------------------------------------------------------ */ +echo "\n6. http://www.vitec.com./ (Punkt-Tippfehler)\n"; +$n = (int)$pdo->query("SELECT COUNT(*) FROM tx_news_domain_model_news WHERE deleted = 0 AND bodytext LIKE '%www.vitec.com./%'")->fetchColumn(); +echo " $n Record(s)\n"; +if ($apply && $n > 0) { + $pdo->exec("UPDATE tx_news_domain_model_news SET bodytext = REPLACE(REPLACE(bodytext, 'http://www.vitec.com./', 'https://www.vitec.com/'), 'https://www.vitec.com./', 'https://www.vitec.com/'), tstamp = $now WHERE deleted = 0 AND bodytext LIKE '%www.vitec.com./%'"); +} + +/* ------------------------------------------------------------------ */ +echo "\n7. Redirects fuer Record-Shells und verwaiste Produkt-URLs\n"; +foreach ([ + '/product' => '/products', + '/news/article' => '/insights/news', + '/success-stories/story' => '/success-stories', + '/product/artiosign' => '/products/video-wall-management-software', + '/product/artioview' => '/products/video-wall-management-software', + '/product/x-view-integrated-displays' => '/products/x-view-integrated-displays', +] as $old => $target) { + $addRedirect($old, $target); + $addRedirect($old . '/', $target); +} + +/* ------------------------------------------------------------------ */ +echo "\n8. Meta-Description /datasheets\n"; +$st = $pdo->prepare("UPDATE pages SET description = ?, tstamp = ? WHERE uid = 17 AND (description IS NULL OR description = '')"); +$desc = 'Download VITEC and Datapath product datasheets: encoders and decoders, IPTV and digital signage platforms, video wall controllers, capture cards and more - searchable by product and category.'; +if ($apply) { $st->execute([$desc, $now]); echo " gesetzt (" . $st->rowCount() . ")\n"; } else { echo " wird gesetzt, falls leer\n"; } + +/* ------------------------------------------------------------------ */ +echo "\n9. WordPress-Reste in den SEO-Artikeln\n"; +$seoUids = [232,234,235,236,237,238,240,241,242,243,244,245,246,247,248,249,252]; +$sel = $pdo->query("SELECT uid, path_segment, bodytext FROM tx_news_domain_model_news WHERE deleted = 0 AND uid IN (" . implode(',', $seoUids) . ")"); +$upd = $pdo->prepare("UPDATE tx_news_domain_model_news SET bodytext = ?, tstamp = ? WHERE uid = ?"); +foreach ($sel as $r) { + $b = (string)$r['bodytext']; + $o = $b; + $log = []; + $b = preg_replace('#]*src="data:image/[^"]*"[^>]*>\s*#i', '', $b, -1, $c1) ?? $b; if ($c1) { $log[] = "$c1 Platzhalter-Bilder"; } + $b = preg_replace('#

[^<]*getWR360PopupSkin[^<]*

\s*#i', '', $b, -1, $c2) ?? $b; if ($c2) { $log[] = "Script-Rest"; } + $b = preg_replace('#\s+class="wp-[^"]*"#i', '', $b, -1, $c3) ?? $b; if ($c3) { $log[] = "$c3 wp-Klassen"; } + $b = preg_replace('#\s+data-[a-z0-9-]+="[^"]*"#i', '', $b, -1, $c4) ?? $b; if ($c4) { $log[] = "$c4 data-Attribute"; } + // bare description text between a product

and the following link + $b = preg_replace('#(

)\s*([^<\s][^<]{20,}?)\s*(?=|