/products/aetria (and the two redirects * follow), X-View self-link cleared, chassis card -> surviving product. * B. News shells still carrying the InfoComm import placeholder text: * #6/#7/#5 are duplicates of real articles (#17/#18/#16) -> deleted + 301; * #4 (EKOM) gets its text from the old /en/news page. #551 is a byte- * identical copy of #550 under a title that does not match the text -> 301. * C. Three typos. * D. Dead vitec.com links in old articles: "https://www.vitec.com/ ", * trailing-dot host, /industries, /green-initiative, two dead PDFs -> * internal targets. * E. SEO titles for the short top-level pages (/solutions "Solutions | VITEC" etc.). * F. Meta descriptions for the nine category pages whose generated text was * under 60 characters ("VITEC Avedia: Avedia Platform."). * * Dry run: php migrations/crawl2_fixes_20261110.php * Write: php migrations/crawl2_fixes_20261110.php --apply (--only=A,B / --skip=F) */ error_reporting(E_ALL); ini_set('display_errors', '1'); $apply = in_array('--apply', $argv, true); $only = []; $skip = []; foreach ($argv as $arg) { if (str_starts_with($arg, '--only=')) { $only = array_map('trim', explode(',', strtoupper(substr($arg, 7)))); } if (str_starts_with($arg, '--skip=')) { $skip = array_map('trim', explode(',', strtoupper(substr($arg, 7)))); } } $runs = static fn(string $s): bool => (!$only || in_array($s, $only, true)) && !in_array($s, $skip, 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"; $now = time(); $purgePaths = []; $plain = static fn(?string $h): string => trim((string)preg_replace('/\s+/', ' ', html_entity_decode(strip_tags((string)$h), ENT_QUOTES, 'UTF-8'))); // --- helpers ----------------------------------------------------------------- $hasRedirect = $pdo->prepare("SELECT uid, target FROM sys_redirect WHERE deleted = 0 AND source_path = ? LIMIT 1"); $insertRedirect = $pdo->prepare( "INSERT INTO sys_redirect (pid, createdon, updatedon, source_host, source_path, target, target_statuscode) VALUES (0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), '*', ?, ?, 301)" ); $addRedirect = static function (string $source, string $target) use ($pdo, $hasRedirect, $insertRedirect, $apply, &$purgePaths): void { $purgePaths[$source] = true; $hasRedirect->execute([$source]); $existing = $hasRedirect->fetch(); if ($existing) { if ($existing['target'] === $target) { return; } echo " redirect $source: Ziel {$existing['target']} -> $target\n"; if ($apply) { $pdo->prepare("UPDATE sys_redirect SET target = ?, updatedon = UNIX_TIMESTAMP() WHERE uid = ?")->execute([$target, (int)$existing['uid']]); } return; } echo " redirect $source -> $target\n"; if ($apply) { $insertRedirect->execute([$source, $target]); } }; $retarget = static function (string $oldTarget, string $newTarget) use ($pdo, $apply): void { $st = $pdo->prepare("SELECT uid, source_path FROM sys_redirect WHERE deleted = 0 AND target = ?"); $st->execute([$oldTarget]); foreach ($st->fetchAll() as $r) { echo " redirect #{$r['uid']} {$r['source_path']}: Ziel $oldTarget -> $newTarget\n"; if ($apply) { $pdo->prepare("UPDATE sys_redirect SET target = ?, updatedon = UNIX_TIMESTAMP() WHERE uid = ?")->execute([$newTarget, (int)$r['uid']]); } } }; $softDelete = static function (string $table, int $uid) use ($pdo, $apply, $now): void { echo " loeschen $table #$uid\n"; if ($apply) { $pdo->prepare("UPDATE $table SET deleted = 1, tstamp = ? WHERE uid = ?")->execute([$now, $uid]); $pdo->prepare("DELETE FROM sys_category_record_mm WHERE tablenames = ? AND uid_foreign = ?")->execute([$table, $uid]); } }; $newsRow = static function (int $uid) use ($pdo): ?array { $st = $pdo->prepare("SELECT uid, title, path_segment, type, LENGTH(bodytext) AS bl FROM tx_news_domain_model_news WHERE deleted = 0 AND uid = ?"); $st->execute([$uid]); $r = $st->fetch(); return $r ?: null; }; $updateField = static function (string $table, int $uid, string $field, string $value) use ($pdo, $apply, $now): void { if ($apply) { $pdo->prepare("UPDATE $table SET $field = ?, tstamp = ? WHERE uid = ?")->execute([$value, $now, $uid]); } }; // Replace patterns in a text field of every matching row; prints each change. $replaceIn = static function (string $table, string $field, array $pairs, string $label) use ($pdo, $apply, $now, &$purgePaths): int { // pair = [needle, replacement, isRegex, likeNeedle (plain text for the SQL prefilter; required for regex pairs)] $like = implode(' OR ', array_fill(0, count($pairs), "$field LIKE ?")); $params = array_map(fn($p) => '%' . ($p[3] ?? $p[0]) . '%', $pairs); $extra = $table === 'tx_news_domain_model_news' ? ', path_segment AS ref' : ($table === 'tt_content' ? ', pid AS ref' : ', uid AS ref'); $st = $pdo->prepare("SELECT uid, $field AS v $extra FROM $table WHERE deleted = 0 AND ($like)"); $st->execute($params); $n = 0; foreach ($st->fetchAll() as $r) { $new = (string)$r['v']; foreach ($pairs as [$needle, $replacement, $isRegex]) { $new = $isRegex ? (string)preg_replace($needle, $replacement, $new) : str_replace($needle, $replacement, $new); } if ($new === (string)$r['v']) { continue; } $n++; echo " $label: $table #{$r['uid']} ({$r['ref']})\n"; if ($table === 'tx_news_domain_model_news') { $purgePaths['/news/article/' . $r['ref']] = true; } if ($apply) { $pdo->prepare("UPDATE $table SET $field = ?, tstamp = ? WHERE uid = ?")->execute([$new, $now, (int)$r['uid']]); } } return $n; }; // --- A. card CTAs onto hidden/deleted products ----------------------------------- if ($runs('A')) { echo "\nA. Karten-Buttons auf versteckte/geloeschte Produkt-URLs\n"; $ctas = [ 11744 => ['/product/artiosign', '/products/aetria'], 11745 => ['/product/artioview', '/products/aetria'], 11649 => ['/product/artiosign', '/products/aetria'], 11650 => ['/product/artioview', '/products/aetria'], 12179 => ['/product/avedia-modular-chassis-c1101-c1103-c1210-1', '/product/avedia-modular-chassis-c1101-c1103-c1210'], ]; $ce = $pdo->prepare("SELECT uid, pid, header, vitec_primary_cta, vitec_primary_cta_label FROM tt_content WHERE deleted = 0 AND uid = ?"); foreach ($ctas as $uid => [$old, $new]) { $ce->execute([$uid]); $c = $ce->fetch(); if (!$c) { continue; } if (trim((string)$c['vitec_primary_cta']) !== $old) { echo " CE #$uid: CTA ist '{$c['vitec_primary_cta']}', erwartet '$old' - uebersprungen\n"; continue; } echo " CE #$uid (Seite {$c['pid']}, \"{$c['header']}\"): $old -> $new\n"; $updateField('tt_content', $uid, 'vitec_primary_cta', $new); $pageSlug = $pdo->query("SELECT slug FROM pages WHERE uid = {$c['pid']}")->fetchColumn(); if ($pageSlug) { $purgePaths[$pageSlug] = true; } } // X-View card links to the page it sits on: no button. $ce->execute([12133]); $c = $ce->fetch(); if ($c && trim((string)$c['vitec_primary_cta']) === '/product/x-view-integrated-displays') { echo " CE #12133 (\"{$c['header']}\"): Selbst-Link entfernt (Button weg)\n"; $updateField('tt_content', 12133, 'vitec_primary_cta', ''); $updateField('tt_content', 12133, 'vitec_primary_cta_label', ''); $purgePaths['/products/x-view-integrated-displays'] = true; } // The two product redirects follow the cards. $addRedirect('/product/artiosign', '/products/aetria'); $addRedirect('/product/artioview', '/products/aetria'); } // --- B. placeholder news shells ----------------------------------------------------- if ($runs('B')) { echo "\nB. Platzhalter-News (InfoComm-Importtext)\n"; $placeholderMarker = 'VITEC will demonstrate its latest enterprise video technologies at InfoComm 2026'; $isPlaceholder = static function (int $uid) use ($pdo, $placeholderMarker): bool { $b = (string)$pdo->query("SELECT bodytext FROM tx_news_domain_model_news WHERE uid = $uid")->fetchColumn(); return strlen($b) === 1308 && str_contains($b, $placeholderMarker); }; // duplicate shell => real article foreach ([6 => 17, 7 => 18, 5 => 16] as $shell => $real) { $s = $newsRow($shell); $r = $newsRow($real); if (!$s || !$r) { echo " #$shell/#$real fehlt, uebersprungen\n"; continue; } if (!$isPlaceholder($shell)) { echo " #$shell ist kein Platzhalter mehr, uebersprungen\n"; continue; } echo " #$shell \"{$s['title']}\" (Platzhalter) -> #$real \"{$r['title']}\" ({$r['bl']} Zeichen)\n"; $softDelete('tx_news_domain_model_news', $shell); $addRedirect('/news/article/' . $s['path_segment'], '/news/article/' . $r['path_segment']); $retarget('/news/article/' . $s['path_segment'], '/news/article/' . $r['path_segment']); $purgePaths['/news/article/' . $r['path_segment']] = true; } // EKOM: text lives on the old page $s = $newsRow(4); if ($s && $isPlaceholder(4)) { $page = $pdo->query("SELECT uid, slug FROM pages WHERE deleted = 0 AND slug LIKE '%/news/vitec-partners-with-ekom%' ORDER BY uid LIMIT 1")->fetch(); $html = ''; if ($page) { foreach ($pdo->query("SELECT CType, bodytext FROM tt_content WHERE deleted = 0 AND hidden = 0 AND pid = {$page['uid']} AND CType IN ('text','textmedia','html') AND bodytext <> '' ORDER BY sorting") as $c) { $part = (string)$c['bodytext']; if (str_contains($part, 'vitec.com/ ')) { $part = str_replace('https://www.vitec.com/ ', 'www.vitec.com', $part); } $html .= $part . "\n"; } } $len = mb_strlen($plain($html)); if ($len >= 500) { echo " #4 \"{$s['title']}\": Text von Seite #{$page['uid']} ({$page['slug']}) uebernommen, $len Zeichen\n"; $updateField('tx_news_domain_model_news', 4, 'bodytext', trim($html)); $purgePaths['/news/article/' . $s['path_segment']] = true; } else { echo " #4: keine alte Seite mit Text gefunden ($len Zeichen) - bleibt\n"; } } // byte-identical Datapath pair: keep the one whose title matches the text $a = $newsRow(551); $b = $newsRow(550); if ($a && $b) { $ba = $pdo->query("SELECT bodytext FROM tx_news_domain_model_news WHERE uid = 551")->fetchColumn(); $bb = $pdo->query("SELECT bodytext FROM tx_news_domain_model_news WHERE uid = 550")->fetchColumn(); if ($ba === $bb) { echo " #551 \"{$a['title']}\" = Textkopie von #550 \"{$b['title']}\" -> weg\n"; $softDelete('tx_news_domain_model_news', 551); $addRedirect('/news/article/' . $a['path_segment'], '/news/article/' . $b['path_segment']); $retarget('/news/article/' . $a['path_segment'], '/news/article/' . $b['path_segment']); } } } // --- C. typos ------------------------------------------------------------------------ if ($runs('C')) { echo "\nC. Tippfehler\n"; $replaceIn('tx_news_domain_model_news', 'bodytext', [ ['/\bsponser\b/', 'sponsor', true, 'sponser'], ['/\bSponser\b/', 'Sponsor', true, 'Sponser'], ['/\breconnaisance\b/', 'reconnaissance', true, 'reconnaisance'], ['/\bReconnaisance\b/', 'Reconnaissance', true, 'Reconnaisance'], ['/\bauxillary\b/', 'auxiliary', true, 'auxillary'], ['/\bAuxillary\b/', 'Auxiliary', true, 'Auxillary'], ], 'Tippfehler'); } // --- D. dead vitec.com links ----------------------------------------------------------- if ($runs('D')) { echo "\nD. Tote vitec.com-Links\n"; $pairs = [ ['https://www.vitec.com/ ', 'www.vitec.com', false], ['~https?://www\.vitec\.com\.(?=[/"#\s<])/?~', '/', true, 'www.vitec.com.'], ['https://www.vitec.com/green-initiative', '/insights/sustainability', false], ['https://www.vitec.com/industries/military/government', '/markets/defense-and-security', false], ['https://www.vitec.com/industries', '/markets', false], ['~https?://www\.vitec\.com/fileadmin/downloads/Company_Downloads/Use_Cases/Northwestern_University_USE_CASE_WebVersion\.pdf~', '/success-stories/northwestern-university', true, 'Northwestern_University_USE_CASE'], ['~https?://datapathdocuments\.co\.uk/wp-content/uploads/Datapath-Fx4_datasheet_EN\.pdf~', '/product/fx4', true, 'Datapath-Fx4_datasheet'], ]; $n = $replaceIn('tx_news_domain_model_news', 'bodytext', $pairs, 'Link'); $n += $replaceIn('tt_content', 'bodytext', $pairs, 'Link'); echo " $n Datensaetze\n"; } // --- E. SEO titles for short top-level pages ------------------------------------------- if ($runs('E')) { echo "\nE. Seitentitel der Hauptseiten\n"; $titles = [ '/solutions' => 'Video Solutions for Control Rooms, Streaming & Signage', '/products' => 'Products: IPTV, Video Streaming, Video Walls & Capture', '/support' => 'Support, Training & Product Resources', '/datasheets' => 'Datasheets: Encoders, IPTV, Video Walls & Capture Cards', '/contact' => 'Contact VITEC: Sales, Support & Offices Worldwide', '/contact/locations' => 'VITEC Offices & Locations Worldwide', '/success-stories' => 'Success Stories from Broadcast, Defense & Enterprise', ]; $st = $pdo->prepare("SELECT uid, title, seo_title FROM pages WHERE deleted = 0 AND slug = ?"); foreach ($titles as $slug => $seo) { $st->execute([$slug]); $p = $st->fetch(); if (!$p) { continue; } if ($p['seo_title'] === $seo) { continue; } printf(" %-20s \"%s\" -> \"%s\"\n", $slug, $p['seo_title'], $seo); $updateField('pages', (int)$p['uid'], 'seo_title', $seo); $purgePaths[$slug] = true; } } // --- F. descriptions for the thinnest category pages ------------------------------------- if ($runs('F')) { echo "\nF. Meta-Descriptions der kuerzesten Kategorieseiten\n"; $desc = [ '/products/ez-tv' => 'EZ TV Platform by VITEC: enterprise IPTV and digital signage for live TV, video distribution and targeted content across screens and devices.', '/products/activesqx' => 'ActiveSQX2 by VITEC and Datapath: IP stream decoding card that brings network video sources onto video walls. Specifications, features and datasheets.', '/products/avedia' => 'Avedia Platform by VITEC: one platform for IP video, digital signage and interactive experiences across enterprise networks.', '/products/rf-gateway' => 'VITEC RF gateways for the Avedia Modular System: receive satellite, cable and terrestrial TV and distribute it as IPTV across the network.', '/products/prism-transcoder' => 'PRISM and PRISM SFF by VITEC: broadcast-grade video transcoders for converting, scaling and re-streaming live video, including a compact small-form-factor model.', '/products/channellink-ip-gateway' => 'ChannelLink IP Gateway by VITEC: brings IP video streams into IPTV and digital signage networks, with transcoding for reliable delivery to every screen.', '/products/aligo' => 'Aligo by VITEC: AV over IP and KVM encoders, receivers and workstations (QTX100, TX100, RX100, TX100 SQX) for control rooms and multi-display environments.', '/products/vms-plugins-and-integrations' => 'VMS plugins and integrations for VITEC and Datapath video walls: bring Genetec and Milestone video management systems onto operator displays.', '/products/image-graphics-cards' => 'Datapath Image graphics cards from VITEC (Image 2K, Image 4K, IQS4): multi-output graphics for video walls and multi-display workstations.', ]; $st = $pdo->prepare("SELECT uid, description FROM pages WHERE deleted = 0 AND slug = ?"); foreach ($desc as $slug => $d) { $st->execute([$slug]); $p = $st->fetch(); if (!$p || $p['description'] === $d) { continue; } printf(" %-42s (%d -> %d Zeichen)\n alt: %s\n neu: %s\n", $slug, strlen((string)$p['description']), strlen($d), $p['description'], $d); $updateField('pages', (int)$p['uid'], 'description', $d); $purgePaths[$slug] = true; } } echo "\nSSR-Pfade zum Purgen (" . count($purgePaths) . "):\n " . implode("\n ", array_keys($purgePaths)) . "\n"; echo $apply ? "\nFERTIG.\n" : "\n(Trockenlauf, nichts geschrieben)\n";