Seobility-Crawl 2: Karten-Links, Platzhalter-News, Titel-Kontext ueber Ordner, Descriptions 155 Zeichen

Migration crawl2_fixes_20261110 (angewendet): sechs Karten-Buttons
zeigten auf versteckte/geloeschte Produkt-URLs (ArtioSign/ArtioView ->
/products/aetria, Redirects folgen; X-View-Selbstlink entfernt;
Chassis-Karte auf das verbliebene Produkt). Drei weitere News trugen
noch den InfoComm-Importplatzhalter und doppelten echte Artikel
(JODDB, Bluefin, NAB 2026) -> geloescht + 301; EKOM-Artikel bekommt
seinen Text von der alten /en/news-Seite; byte-identische Datapath-
Kopie (#551) -> 301. Drei Tippfehler. Tote vitec.com-Links in 14
Datensaetzen auf interne Ziele (/markets, /insights/sustainability,
Story Northwestern, /product/fx4, 'vitec.com./' und ' '-Links).
SEO-Titel fuer sieben Hauptseiten, Descriptions fuer neun
Kategorieseiten mit < 60 Zeichen.

VitecMetaHandler: Titel-Kontext ueberspringt Ordner-/Shortcut-Ebenen
('End-Points | Products | VITEC' statt 'End-Points | VITEC'); kurze
Story-Titel bekommen '| Success Story'; Meta-Description auf 155
Zeichen gekappt, auch redaktionelle (Seobility: 527 'zu lang').

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
orascheandClaude Fable 5.1 committed 2026-11-10 19:08:34 +01:00
1 parent 4b6731786d
commit e357f42b81
2 files changed
+326 -13

No files matched your search

+296
View File
@@ -0,0 +1,296 @@
<?php
declare(strict_types=1);
/**
* Fixes from the second Seobility crawl of 2026-11-10 (after the duplicate clean-up).
*
* A. Card CTAs pointing at hidden/deleted product URLs (internal 301 hops):
* ArtioSign/ArtioView cards -> /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/&nbsp;",
* 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/&nbsp;')) { $part = str_replace('https://www.vitec.com/&nbsp;', '<a href="/">www.vitec.com</a>', $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/&nbsp;', '<a href="/">www.vitec.com</a>', 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";