News: Canonical auf Success Story (Feld vitec_canonical_usecase), 13 Paare gesetzt

13 Pressemeldungen tragen denselben Text wie eine Success Story
(Seobility: doppelter Inhalt). Neues Feld am News-Datensatz (Tab
'VITEC SEO', Auswahl der Story); VitecMetaHandler ersetzt damit den
Canonical-Link durch /success-stories/<slug>. Die Meldung bleibt in der
Liste, Suchmaschinen werten die Story als Original. Migration
news_story_canonical_20261110 setzt die 13 Zuordnungen (angewendet).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
orascheandClaude Fable 5.1 committed 2026-11-10 18:20:03 +01:00
1 parent f7b1eacecb
commit 4b6731786d
4 files changed
+135

No files matched your search

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
/**
* News articles that carry the same text as a success story get the story
* as canonical (field tx_news_domain_model_news.vitec_canonical_usecase,
* evaluated by VitecMetaHandler). The article stays in the news list.
*
* Pairs from the Seobility duplicate-content report of 2026-11-10.
*
* Dry run: php migrations/news_story_canonical_20261110.php
* Write: php migrations/news_story_canonical_20261110.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";
// news uid => usecase (story) uid
$pairs = [
361 => 56, // Denver Sportsbook Bar & Grill
121 => 24, // European Space Agency
164 => 26, // Feature Story News
336 => 62, // Kyndryl Cyber Operations Center
388 => 74, // Leading Video Capture for the Medical Market
239 => 66, // RRS Sir David Attenborough
158 => 46, // Sacramento Kings
310 => 68, // SENTRY Video Walls – 2024 Olympic Games
290 => 72, // UK Mission Critical Control Room
389 => 59, // Intercity Istanbul Park
436 => 64, // NHS tribute onto Parliament
395 => 54, // Bahrain International Circuit
327 => 60, // ITV News Headquarters
];
$news = $pdo->prepare("SELECT uid, title, path_segment, vitec_canonical_usecase FROM tx_news_domain_model_news WHERE deleted = 0 AND uid = ?");
$story = $pdo->prepare("SELECT uid, title, slug, hidden FROM tx_vitec_domain_model_usecase WHERE deleted = 0 AND uid = ?");
$update = $pdo->prepare("UPDATE tx_news_domain_model_news SET vitec_canonical_usecase = ?, tstamp = UNIX_TIMESTAMP() WHERE uid = ?");
$set = 0;
$paths = [];
foreach ($pairs as $newsUid => $storyUid) {
$news->execute([$newsUid]); $n = $news->fetch();
$story->execute([$storyUid]); $s = $story->fetch();
if (!$n || !$s) { echo " #$newsUid -> #$storyUid: Datensatz fehlt, uebersprungen\n"; continue; }
if ((int)$s['hidden'] === 1) { echo " #$newsUid -> #$storyUid: Story versteckt, uebersprungen\n"; continue; }
$state = (int)$n['vitec_canonical_usecase'] === $storyUid ? 'schon gesetzt' : 'setzen';
printf(" news #%-4d %-70s -> story #%-3d /success-stories/%s [%s]\n", $newsUid, mb_substr($n['title'], 0, 70), $storyUid, $s['slug'], $state);
$paths[] = '/news/article/' . $n['path_segment'];
if ($state === 'setzen') {
$set++;
if ($apply) { $update->execute([$storyUid, $newsUid]); }
}
}
echo "\n$set Zuordnung(en) " . ($apply ? 'geschrieben' : 'wuerden geschrieben') . ".\n";
echo "SSR-Pfade:\n " . implode("\n ", $paths) . "\n";
@@ -67,6 +67,7 @@ class VitecMetaHandler extends MetaHandler
$title = trim((string)($content['seo']['title'] ?? ''));
$description = '';
$image = '';
$canonicalPath = '';
// --- record context -------------------------------------------------
$newsUid = (int)($args['tx_news_pi1']['news'] ?? 0);
@@ -90,6 +91,15 @@ class VitecMetaHandler extends MetaHandler
);
}
$image = $this->firstImage('tx_news_domain_model_news', 'fal_media', $newsUid);
// Same text as a success story: the story is the original.
$storyUid = (int)($news['vitec_canonical_usecase'] ?? 0);
if ($storyUid > 0) {
$story = $this->fetchRow('tx_vitec_domain_model_usecase', ['uid' => $storyUid]);
$storySlug = trim((string)($story['slug'] ?? ''));
if ($storySlug !== '') {
$canonicalPath = '/success-stories/' . $storySlug;
}
}
}
} elseif ($productArg !== '') {
$product = is_numeric($productArg)
@@ -227,6 +237,23 @@ class VitecMetaHandler extends MetaHandler
}
}
// --- news article whose canonical is a success story --------------------
if ($canonicalPath !== '') {
$href = $this->absolute($canonicalPath, $base);
$links = is_array($content['seo']['link'] ?? null) ? $content['seo']['link'] : [];
$replaced = false;
foreach ($links as $index => $link) {
if (($link['rel'] ?? '') === 'canonical') {
$links[$index]['href'] = $href;
$replaced = true;
}
}
if (!$replaced) {
$links[] = ['href' => $href, 'rel' => 'canonical'];
}
$content['seo']['link'] = $links;
}
return $content;
}
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
defined('TYPO3') or die();
// Canonical story for a news article: when a success story carries the same
// text as the article, the article stays in the news list but its canonical
// link points to the story (VitecMetaHandler reads this field).
(static function (): void {
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTCAcolumns('tx_news_domain_model_news', [
'vitec_canonical_usecase' => [
'label' => 'Canonical success story',
'description' => 'Set when a success story carries the same text as this article. The article stays in the news list; search engines count the story as the original.',
'config' => [
'type' => 'select',
'renderType' => 'selectSingle',
'foreign_table' => 'tx_vitec_domain_model_usecase',
'foreign_table_where' => 'AND {#tx_vitec_domain_model_usecase}.{#deleted} = 0 ORDER BY tx_vitec_domain_model_usecase.title',
'items' => [['label' => '— none —', 'value' => 0]],
'default' => 0,
'size' => 1,
'maxitems' => 1,
],
],
]);
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addToAllTCAtypes(
'tx_news_domain_model_news',
'--div--;VITEC SEO, vitec_canonical_usecase'
);
})();
+4
View File
@@ -518,3 +518,7 @@ CREATE TABLE tx_vitec_clienterrorlog (
PRIMARY KEY (uid),
KEY crdate (crdate)
);
CREATE TABLE tx_news_domain_model_news (
vitec_canonical_usecase int(11) unsigned DEFAULT '0' NOT NULL
);