web: Doppelveröffentlichung — Prüfsumme, kanonische Zuordnung (aus bis E9), Quellenauskunft, noindex im Portal (Anhang 2)
Über die API kommt praktisch jede Meldung zweimal, je Portal eine Zeile mit gleichem Text: 9.092 Paare, 53 % des Archivs, bisher jede Fassung self-canonical und in beiden Sitemaps — Google wählte. 1c.7: press_releases.content_hash (SHA-256 über Titel + Text ohne Markup), gesetzt beim Speichern, Bestand per press-releases:hash-content (Query-Builder, Blöcke). Migration in zwei Statements — ADD COLUMN INSTANT, ADD INDEX INPLACE, kein Rebuild. press-releases:duplicates-report zählt Paare je Hauptkategorie. 1c.8: canonical_press_release_id + CanonicalAssigner. Erstquelle folgt der Kategorie (config/canonical.php, Vorschlag zu E9), sonst die zuerst veröffentlichte Fassung. Zuordnung bei jeder Veröffentlichung und über den Bestand (press-releases:assign-canonicals). Die Kopie trägt rel=canonical und mainEntityOfPage auf die Erstquelle; Sitemap und News-Sitemap nennen nur sie; Leser sehen weiterhin beide. Alles hinter CANONICAL_ASSIGNMENT_ENABLED (false), bis E9 entschieden ist. 1c.9: user_canonical in den Index-Prüfungen, sourceVerdict() unterscheidet eigene Adresse / andere eigene Fassung / fremdes Portal; Anzeige im Block „Google-Index" hinter GSC_CUSTOMER_VISIBLE, ohne Aussage über das Warum. Dazu: Abschnitt zur einen Quelle auf /gute-pressemitteilung (Verteiler → viele Kopien, eigene Website zuerst, keine Garantie), noindex in Admin-, Kunden- und Auth-Layouts, UmamiSnippetTest von der lokalen .env gelöst. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
447dc4c61e
commit
ee267c622e
28 changed files with 1231 additions and 11 deletions
49
app/Console/Commands/AssignCanonicalPressReleases.php
Normal file
49
app/Console/Commands/AssignCanonicalPressReleases.php
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\PressRelease\CanonicalAssigner;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
/**
|
||||
* Kanonische Zuordnung über den Bestand (Anhang 2, 1c.8). Neue Paare
|
||||
* ordnet die Veröffentlichung selbst zu; dieser Lauf holt den Bestand nach
|
||||
* und stellt nach einer geänderten Kategorie-Zuordnung (E9) alles neu ein.
|
||||
*/
|
||||
class AssignCanonicalPressReleases extends Command
|
||||
{
|
||||
protected $signature = 'press-releases:assign-canonicals
|
||||
{--dry-run : Nur zählen, nichts schreiben}
|
||||
{--verbose-pairs : Jedes Paar ausgeben}';
|
||||
|
||||
protected $description = 'Ordnet bei Doppelveröffentlichung je Paar die Erstquelle zu (rel=canonical).';
|
||||
|
||||
public function handle(CanonicalAssigner $assigner): int
|
||||
{
|
||||
$dryRun = (bool) $this->option('dry-run');
|
||||
|
||||
if (! $assigner->enabled() && ! $dryRun) {
|
||||
$this->warn('Zuordnung ist aus (CANONICAL_ASSIGNMENT_ENABLED). Mit --dry-run lässt sich das Ergebnis trotzdem ansehen.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$verbose = (bool) $this->option('verbose-pairs');
|
||||
|
||||
$result = $assigner->assignAll($dryRun, function (int $canonicalId, int $duplicateId, bool $changed) use ($verbose): void {
|
||||
if ($verbose) {
|
||||
$this->line(sprintf(' %d ← %d%s', $canonicalId, $duplicateId, $changed ? '' : ' (unverändert)'));
|
||||
}
|
||||
});
|
||||
|
||||
$this->info(sprintf(
|
||||
'%s%d Paare, %d %s.',
|
||||
$dryRun ? '[Dry-Run] ' : '',
|
||||
$result['pairs'],
|
||||
$result['changed'],
|
||||
$dryRun ? 'würden geändert' : 'geändert',
|
||||
));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
70
app/Console/Commands/HashPressReleaseContent.php
Normal file
70
app/Console/Commands/HashPressReleaseContent.php
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\PressRelease;
|
||||
use App\Scopes\PortalScope;
|
||||
use App\Support\ContentFingerprint;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Prüfsumme für den Bestand nachtragen (Anhang 2, 1c.7).
|
||||
*
|
||||
* Neue und geänderte Meldungen bekommen die Summe beim Speichern; dieser
|
||||
* Lauf holt den Bestand nach. Schreibt in Blöcken über den Query-Builder
|
||||
* (keine Model-Events, kein Suchindex-Neuaufbau) — je Meldung ein kurzes
|
||||
* UPDATE auf die große Tabelle, nie ein Statement über alles.
|
||||
*/
|
||||
class HashPressReleaseContent extends Command
|
||||
{
|
||||
protected $signature = 'press-releases:hash-content
|
||||
{--all : Auch vorhandene Summen neu berechnen}
|
||||
{--chunk=500 : Meldungen je Durchgang}';
|
||||
|
||||
protected $description = 'Trägt die Inhalts-Prüfsumme (content_hash) für Pressemitteilungen nach.';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$chunk = max(50, (int) $this->option('chunk'));
|
||||
$all = (bool) $this->option('all');
|
||||
|
||||
$query = PressRelease::query()
|
||||
->withoutGlobalScope(PortalScope::class)
|
||||
->withTrashed()
|
||||
->select(['id', 'title', 'text', 'content_hash'])
|
||||
->when(! $all, fn (Builder $q) => $q->whereNull('content_hash'));
|
||||
|
||||
$total = (clone $query)->toBase()->getCountForPagination();
|
||||
|
||||
if ($total === 0) {
|
||||
$this->info('Alle Meldungen tragen eine Prüfsumme — nichts zu tun.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info(sprintf('%s: %d Meldungen.', $all ? 'Neu berechnet' : 'Ergänzt', $total));
|
||||
$bar = $this->output->createProgressBar($total);
|
||||
$done = 0;
|
||||
|
||||
$query->chunkById($chunk, function ($releases) use (&$done, $bar): void {
|
||||
foreach ($releases as $release) {
|
||||
$hash = ContentFingerprint::hash($release->title, $release->text);
|
||||
|
||||
if ($hash !== $release->content_hash) {
|
||||
DB::table('press_releases')->where('id', $release->id)->update(['content_hash' => $hash]);
|
||||
}
|
||||
}
|
||||
|
||||
$done += $releases->count();
|
||||
$bar->advance($releases->count());
|
||||
});
|
||||
|
||||
$bar->finish();
|
||||
$this->newLine(2);
|
||||
$this->info("Fertig: {$done} Meldungen geprüft.");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
73
app/Console/Commands/ReportDuplicatePressReleases.php
Normal file
73
app/Console/Commands/ReportDuplicatePressReleases.php
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Enums\Portal;
|
||||
use App\Models\PressRelease;
|
||||
use App\Scopes\PortalScope;
|
||||
use App\Services\PressRelease\CanonicalAssigner;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Doppelveröffentlichung erheben (Anhang 2, 1c.7): wie viele Meldungen
|
||||
* liegen mit identischem Text derselben Firma auf beiden Portalen — gesamt,
|
||||
* seit dem neuen System, je Hauptkategorie, mit und ohne Zuordnung. Dazu
|
||||
* die Meldungen mit portal=both, die dieselbe Frage auf anderem Weg stellen.
|
||||
*/
|
||||
class ReportDuplicatePressReleases extends Command
|
||||
{
|
||||
protected $signature = 'press-releases:duplicates-report';
|
||||
|
||||
protected $description = 'Zählt identische Pressemitteilungen auf beiden Portalen (Doppelveröffentlichung).';
|
||||
|
||||
public function handle(CanonicalAssigner $assigner): int
|
||||
{
|
||||
$pairs = $assigner->pairsQuery();
|
||||
|
||||
$total = (clone $pairs)->count();
|
||||
$recent = (clone $pairs)->where('a.created_at', '>=', '2026-07-01')->count();
|
||||
$assigned = (clone $pairs)->where(fn ($q) => $q->whereColumn('a.canonical_press_release_id', '=', 'b.id')->orWhereColumn('b.canonical_press_release_id', '=', 'a.id'))->count();
|
||||
$unhashed = PressRelease::query()->withoutGlobalScope(PortalScope::class)->whereNull('content_hash')->count();
|
||||
|
||||
$visible = PressRelease::query()->withoutGlobalScope(PortalScope::class)->visibleInArchive()
|
||||
->selectRaw('portal, count(*) as total')->groupBy('portal')->toBase()->pluck('total', 'portal');
|
||||
|
||||
$this->info('Doppelveröffentlichung — Stand '.now()->format('d.m.Y H:i'));
|
||||
$this->newLine();
|
||||
$this->line(sprintf('Sichtbare Meldungen: presseecho %s, businessportal24 %s, both %s',
|
||||
number_format((int) ($visible['presseecho'] ?? 0), 0, ',', '.'),
|
||||
number_format((int) ($visible['businessportal24'] ?? 0), 0, ',', '.'),
|
||||
number_format((int) ($visible['both'] ?? 0), 0, ',', '.')));
|
||||
$this->line(sprintf('Paare gleichen Inhalts auf beiden Portalen: %s (%s URLs)', number_format($total, 0, ',', '.'), number_format($total * 2, 0, ',', '.')));
|
||||
$this->line(sprintf(' davon seit 07/2026 (neues System): %s', number_format($recent, 0, ',', '.')));
|
||||
$this->line(sprintf(' davon mit kanonischer Zuordnung: %s', number_format($assigned, 0, ',', '.')));
|
||||
$this->line(sprintf('Meldungen ohne Prüfsumme: %s%s', number_format($unhashed, 0, ',', '.'), $unhashed > 0 ? ' — erst press-releases:hash-content laufen lassen' : ''));
|
||||
$this->line(sprintf('Zuordnung (config canonical.enabled): %s', $assigner->enabled() ? 'aktiv' : 'aus'));
|
||||
$this->newLine();
|
||||
|
||||
$byCategory = (clone $pairs)
|
||||
->leftJoin('categories as c', 'c.id', '=', 'a.category_id')
|
||||
->leftJoin('categories as root', 'root.id', '=', DB::raw('coalesce(c.parent_id, c.id)'))
|
||||
->leftJoin('category_translations as t', fn ($join) => $join->on('t.category_id', '=', 'root.id')->where('t.locale', '=', 'de'))
|
||||
->select(DB::raw('t.slug as slug, count(*) as pairs'))
|
||||
->groupBy('t.slug')
|
||||
->orderByDesc('pairs')
|
||||
->get();
|
||||
|
||||
$rows = $byCategory->map(function (object $row): array {
|
||||
$slug = (string) ($row->slug ?? '—');
|
||||
$portal = config('canonical.portal_by_category.'.$slug);
|
||||
|
||||
return [
|
||||
$slug,
|
||||
number_format((int) $row->pairs, 0, ',', '.'),
|
||||
is_string($portal) ? (Portal::tryFrom($portal)?->label() ?? $portal) : 'Fallback ('.config('canonical.fallback', 'first').')',
|
||||
];
|
||||
})->all();
|
||||
|
||||
$this->table(['Hauptkategorie', 'Paare', 'Erstquelle laut Zuordnung (E9)'], $rows);
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue