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
87558aba4f
commit
4edb915a9a
28 changed files with 1231 additions and 11 deletions
|
|
@ -37,6 +37,8 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}" />
|
||||
{{-- Login/Registrierung gehören nicht in den Index (Anhang 2, Abschnitt 7). --}}
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
|
||||
{{-- Der Auth-Bereich ist bewusst immer im Hellmodus; Dark/Light gibt es nur
|
||||
im eingeloggten Portal. Eine aus dem Portal übernommene .dark-Klasse
|
||||
|
|
|
|||
|
|
@ -1631,15 +1631,27 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
@if ($indexChecks !== [])
|
||||
<div class="rounded-[5px] border border-[color:var(--color-bg-rule)] bg-[color:var(--color-bg-elev)] p-3">
|
||||
<div class="text-[11px] uppercase tracking-[0.6px] text-[color:var(--color-ink-3)] font-semibold">{{ __('Google-Index') }}</div>
|
||||
<ul class="mt-1 m-0 p-0 list-none space-y-1 text-[12.5px]">
|
||||
<ul class="mt-1 m-0 p-0 list-none space-y-1.5 text-[12.5px]">
|
||||
@foreach ($indexChecks as $check)
|
||||
<li class="flex flex-wrap items-baseline justify-between gap-x-3">
|
||||
<span class="text-[color:var(--color-ink)]">{{ $check->portal->label() }}: {{ $check->state->label() }}</span>
|
||||
<span class="text-[11px] tabular-nums text-[color:var(--color-ink-4)]">{{ __('Stand :date', ['date' => $check->checked_at->format('d.m.Y')]) }}@if ($check->last_crawl_time) · {{ __('zuletzt gecrawlt :date', ['date' => $check->last_crawl_time->format('d.m.Y')]) }}@endif</span>
|
||||
@php $source = app(IndexStatusChecker::class)->sourceVerdict($check); @endphp
|
||||
<li>
|
||||
<div class="flex flex-wrap items-baseline justify-between gap-x-3">
|
||||
<span class="text-[color:var(--color-ink)]">{{ $check->portal->label() }}: {{ $check->state->label() }}</span>
|
||||
<span class="text-[11px] tabular-nums text-[color:var(--color-ink-4)]">{{ __('Stand :date', ['date' => $check->checked_at->format('d.m.Y')]) }}@if ($check->last_crawl_time) · {{ __('zuletzt gecrawlt :date', ['date' => $check->last_crawl_time->format('d.m.Y')]) }}@endif</span>
|
||||
</div>
|
||||
{{-- Quellenauskunft (Anhang 2, 1c.9): welche Adresse Google als
|
||||
maßgeblich führt — ohne Aussage darüber, warum. --}}
|
||||
@if ($source['kind'] === 'self')
|
||||
<div class="text-[11.5px] text-[color:var(--color-ink-3)]">{{ __('Google führt diese Adresse als maßgebliche Quelle.') }}</div>
|
||||
@elseif ($source['kind'] === 'own_other')
|
||||
<div class="text-[11.5px] text-[color:var(--color-ink-3)]">{{ __('Google führt die Fassung auf :host als maßgebliche Quelle; diese Adresse gilt als Kopie.', ['host' => $source['host']]) }}</div>
|
||||
@elseif ($source['kind'] === 'foreign')
|
||||
<div class="text-[11.5px] text-[color:var(--color-ink-3)]">{{ __('Google führt eine Fassung auf :host als maßgebliche Quelle — ein anderes Portal mit demselben Text. Diese Adresse gilt dort als Kopie.', ['host' => $source['host']]) }}</div>
|
||||
@endif
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
<p class="mt-2 mb-0 text-[11px] text-[color:var(--color-ink-4)]">{{ __('Momentaufnahme aus der Google Search Console. Ob und wann Google eine Seite aufnimmt, entscheidet Google.') }}</p>
|
||||
<p class="mt-2 mb-0 text-[11px] text-[color:var(--color-ink-4)]">{{ __('Momentaufnahme aus der Google Search Console. Ob und wann Google eine Seite aufnimmt und welche Fassung sie als Quelle führt, entscheidet Google — warum, ist nicht bekannt.') }}</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
{{-- Adminbereich gehört nicht in den Index (Anhang 2, Abschnitt 7). --}}
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
|
||||
<title>{{ $title ?? config('app.name') }}</title>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
{{-- Konto- und Adminbereich gehören nicht in den Index (Anhang 2, Abschnitt 7). --}}
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
|
||||
<title>{{ $title ?? config('app.name') }}</title>
|
||||
|
||||
|
|
|
|||
|
|
@ -94,6 +94,14 @@
|
|||
<li><strong>{{ __('static.good_release.images_format') }}</strong> — {{ __('static.good_release.images_format_text') }}</li>
|
||||
</ul>
|
||||
|
||||
{{-- Anhang 2, Abschnitt 6: warum eine Meldung mehrfach im Netz steht
|
||||
und nur einmal in den Ergebnissen — ohne Zusage, ohne Verkauf. --}}
|
||||
<h2 id="quelle">{{ __('static.good_release.source_heading') }}</h2>
|
||||
<p>{{ __('static.good_release.source_p1') }}</p>
|
||||
<p>{{ __('static.good_release.source_p2') }}@if (config('canonical.enabled')) {{ __('static.good_release.source_p2_assigned') }}@endif</p>
|
||||
<p>{{ __('static.good_release.source_p3') }}</p>
|
||||
<p>{{ __('static.good_release.source_p4') }}</p>
|
||||
|
||||
<h2 id="weiter">{{ __('static.good_release.more_heading') }}</h2>
|
||||
<ul>
|
||||
<li><a href="{{ route('richtlinien') }}">{{ __('static.good_release.more_guidelines') }}</a></li>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
@extends('web.layouts.web-master')
|
||||
|
||||
@php
|
||||
use App\Enums\Portal;
|
||||
use App\Services\PressRelease\CanonicalAssigner;
|
||||
use App\Support\StructuredData;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
|
|
@ -89,6 +91,14 @@
|
|||
$shareUrl = $isPreview ? request()->fullUrl() : route('release.detail', ['slug' => $release->slug]);
|
||||
$brandLabel = config('app.theme') === 'presseecho' ? 'presseecho' : 'businessportal24';
|
||||
|
||||
// Doppelveröffentlichung (Anhang 2, 1c.8): rel=canonical zeigt auf die
|
||||
// Erstquelle — die eigene Adresse, oder bei zweiter Fassung/portal=both
|
||||
// das Portal, das laut Kategorie-Zuordnung führt. Geteilt wird weiterhin
|
||||
// die Adresse, auf der der Leser gerade ist.
|
||||
$canonicalUrl = $isPreview
|
||||
? $shareUrl
|
||||
: app(CanonicalAssigner::class)->canonicalUrlFor($release, $brandLabel === 'presseecho' ? Portal::Presseecho : Portal::Businessportal24);
|
||||
|
||||
// SEO: NewsArticle + BreadcrumbList als JSON-LD — nur für die öffentliche
|
||||
// Seite (die Vorschau ist noindex und hat keine stabile URL).
|
||||
$jsonLd = null;
|
||||
|
|
@ -116,7 +126,7 @@
|
|||
'@graph' => [
|
||||
array_filter([
|
||||
'@type' => 'NewsArticle',
|
||||
'mainEntityOfPage' => ['@type' => 'WebPage', '@id' => $shareUrl],
|
||||
'mainEntityOfPage' => ['@type' => 'WebPage', '@id' => $canonicalUrl],
|
||||
'headline' => Str::limit($release->title, 110),
|
||||
'description' => filled($lead) ? $lead : $teaser,
|
||||
'image' => $imageUrl ? [$imageUrl] : null,
|
||||
|
|
@ -160,7 +170,7 @@
|
|||
@if ($isPreview)
|
||||
@section('robots', 'noindex,nofollow')
|
||||
@else
|
||||
@section('canonical', $shareUrl)
|
||||
@section('canonical', $canonicalUrl)
|
||||
|
||||
@section('meta_extra')
|
||||
@if ($publishedAt)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue