diff --git a/app/Http/Controllers/Web/FeedController.php b/app/Http/Controllers/Web/FeedController.php new file mode 100644 index 00000000..2a47bcca --- /dev/null +++ b/app/Http/Controllers/Web/FeedController.php @@ -0,0 +1,125 @@ +getSchemeAndHttpHost(), '/'); + + $xml = WebPageCache::remember( + WebPageCache::key('feed', [$context->portal->value, $context->language, $slug ?? 'all']), + fn (): string => $this->render($context, $base, $slug), + ); + + return response($xml, 200, ['Content-Type' => 'application/rss+xml; charset=UTF-8']); + } + + private function render(WebPortalContext $context, string $base, ?string $slug): string + { + $language = $context->language; + $publisher = (string) StructuredData::publisher($base)['name']; + $brand = (array) ($context->domainConfig['brand'] ?? []); + + $categoryIds = null; + $category = null; + + if ($slug !== null) { + $category = Category::query() + ->whereIn('portal', $context->portalValues) + ->where('is_active', true) + ->whereHas('translations', fn (Builder $query) => $query->where('locale', $language)->where('slug', $slug)) + ->with(['translations' => fn ($query) => $query->where('locale', $language)]) + ->first(); + + if (! $category) { + abort(404); + } + + $categoryIds = Category::query() + ->where('parent_id', $category->id) + ->where('is_active', true) + ->pluck('id') + ->prepend($category->id) + ->unique() + ->values() + ->all(); + } + + $releases = PressRelease::query() + ->withoutGlobalScope(PortalScope::class) + ->forPortalValues($context->portalValues) + ->forLanguage($language) + ->visibleInArchive() + ->when($categoryIds !== null, fn (Builder $query) => $query->whereIn('category_id', $categoryIds)) + ->with([ + 'company:id,name', + 'category.translations' => fn ($query) => $query->where('locale', $language), + ]) + ->orderByDesc('published_at') + ->limit(self::MAX_ITEMS) + ->get(['id', 'slug', 'title', 'text', 'portal', 'language', 'category_id', 'company_id', 'published_at']); + + $items = $releases->map(fn (PressRelease $release): array => [ + 'title' => (string) $release->title, + 'link' => $base.'/'.$language.'/press-release/'.$release->slug, + 'published_at' => $release->published_at?->toRfc2822String() ?? '', + 'description' => $release->teaserText(self::TEASER_LENGTH), + 'category' => $release->localizedCategoryName($language), + 'creator' => $release->company?->name, + ])->all(); + + $translation = $category?->translations->first(); + $categoryName = $translation?->name; + + $channel = [ + 'title' => $categoryName !== null + ? __('web.rss.category_title', ['category' => $categoryName, 'brand' => $publisher]) + : __('web.rss.portal_title', ['brand' => $publisher]), + 'link' => $categoryName !== null + ? $base.'/'.$language.'/category/'.$slug + : $base.'/'.$language, + 'self' => $categoryName !== null + ? $base.'/'.$language.'/category/'.$slug.'/feed' + : $base.'/'.$language.'/feed', + 'description' => filled($translation?->description) + ? (string) $translation->description + : (string) ($brand['tagline_long'] ?? __('web.rss.default_description', ['brand' => $publisher])), + 'language' => $language, + 'last_build' => ($releases->first()->published_at ?? Carbon::now())->toRfc2822String(), + ]; + + return view('web.feed', ['channel' => $channel, 'items' => $items])->render(); + } +} diff --git a/app/Http/Controllers/Web/LlmsTxtController.php b/app/Http/Controllers/Web/LlmsTxtController.php index a8e3d595..abfc632c 100644 --- a/app/Http/Controllers/Web/LlmsTxtController.php +++ b/app/Http/Controllers/Web/LlmsTxtController.php @@ -106,6 +106,7 @@ class LlmsTxtController extends Controller '- Newsroom eines Unternehmens: '.$base.'/de/newsroom/{slug} — Übersicht unter '.$base.'/de/newsrooms', '- Kategorie: '.$base.'/de/category/{slug}', '- Sitemap: '.$base.'/sitemap.xml — News-Sitemap (Meldungen der letzten 48 Stunden): '.$base.'/sitemap-news.xml', + '- RSS-Feed aller Meldungen: '.$base.'/de/feed — je Kategorie: '.$base.'/de/category/{slug}/feed', '', '## Kategorien', '', diff --git a/app/Services/PressRelease/VisibilityStats.php b/app/Services/PressRelease/VisibilityStats.php index 839e3718..746cf990 100644 --- a/app/Services/PressRelease/VisibilityStats.php +++ b/app/Services/PressRelease/VisibilityStats.php @@ -2,9 +2,11 @@ namespace App\Services\PressRelease; +use App\Enums\Portal; use App\Enums\PressReleaseEventType; use App\Enums\PressReleaseShareChannel; use App\Enums\PressReleaseStatus; +use App\Http\Middleware\SetEdition; use App\Models\Company; use App\Models\PressRelease; use App\Models\PressReleaseEvent; @@ -133,7 +135,7 @@ class VisibilityStats /** * Einzelwerte einer Meldung für das Panel „Auslieferung". * - * @return array{hits: int, first_view_at: Carbon|null, last_view_at: Carbon|null, shares: array, shares_total: int, in_sitemap_since: Carbon|null, crawlers: list, crawlers_since: Carbon|null} + * @return array{hits: int, first_view_at: Carbon|null, last_view_at: Carbon|null, shares: array, shares_total: int, in_sitemap_since: Carbon|null, crawlers: list, crawlers_since: Carbon|null, feeds: list} */ public function releaseSummary(PressRelease $pressRelease): array { @@ -167,9 +169,49 @@ class VisibilityStats 'in_sitemap_since' => $links !== [] && $pressRelease->published_at !== null ? Carbon::instance($pressRelease->published_at) : null, 'crawlers' => $this->crawlerFetches($pressRelease), 'crawlers_since' => $this->crawlerHitsSince(), + 'feeds' => $this->feedLinks($pressRelease, $links), ]; } + /** + * RSS-Feeds, in denen die Meldung erscheint (Roadmap 3.1): je Portal der + * Feed der Branche und der Portal-Feed. Beide führen die jüngsten + * Meldungen — eine Aussage über uns, nicht über Abonnenten. + * + * @param list $links + * @return list + */ + private function feedLinks(PressRelease $pressRelease, array $links): array + { + if ($links === []) { + return []; + } + + $edition = in_array($pressRelease->language, SetEdition::EDITIONS, true) ? $pressRelease->language : SetEdition::DEFAULT_EDITION; + $categorySlug = $pressRelease->category?->translations()->where('locale', $edition)->value('slug'); + $categoryName = $pressRelease->localizedCategoryName($edition); + + $feeds = []; + + foreach ($links as $link) { + $base = (string) parse_url($link['url'], PHP_URL_SCHEME).'://'.(string) parse_url($link['url'], PHP_URL_HOST); + + if ($categorySlug !== null) { + $feeds[] = [ + 'label' => $link['label'].' · '.$categoryName, + 'url' => $base.'/'.$edition.'/category/'.$categorySlug.'/feed', + ]; + } + + $feeds[] = [ + 'label' => $link['label'], + 'url' => $base.'/'.$edition.'/feed', + ]; + } + + return $feeds; + } + /** * Abrufe durch Such- und KI-Dienste je Meldung (Roadmap 1c.6), zuletzt * abgerufener Dienst zuerst. Belegt nur den Abruf durch den Crawler des diff --git a/config/canonical.php b/config/canonical.php index 22d23c40..d50ff395 100644 --- a/config/canonical.php +++ b/config/canonical.php @@ -93,7 +93,7 @@ return [ | sonst `portal_only_host`. */ - 'portal_only_content_routes' => ['kategorie', 'kategorie.archiv', 'release.detail', 'newsroom', 'newsroom.claim'], + 'portal_only_content_routes' => ['kategorie', 'kategorie.archiv', 'kategorie.feed', 'feed', 'release.detail', 'newsroom', 'newsroom.claim'], 'portal_only_host' => 'presseecho', diff --git a/dev/2026-09-01/roadmap-2026.md b/dev/2026-09-01/roadmap-2026.md index e4aa96f5..9a5e67ad 100644 --- a/dev/2026-09-01/roadmap-2026.md +++ b/dev/2026-09-01/roadmap-2026.md @@ -187,11 +187,11 @@ Verbreitert den Trichter oben. Sinnvoll erst, wenn Block 1 unten geschlossen ist | Nr. | Arbeitspaket | Aufwand | Abhängigkeit | |---|---|---|---| | ~~2.1~~ | NewsArticle-Schema auf allen Meldungen, Organization-Schema auf Newsrooms — **Grundgerüst seit Juli (379688f), 17.09.2026 ergänzt:** gemeinsame `@id` zwischen `NewsArticle.author` und Newsroom-Organization, Publisher mit `@id` und Logo (`App\Support\StructuredData`) | M | 1b.1 | -| 2.2 | Aufnahme in Google News und Publisher Center prüfen und beantragen — **technischer Teil 17.09.2026:** News-Sitemap `/sitemap-news.xml` je Spoke (48 h, ≤ 1.000), in `robots.txt`; Publisher-Center-Anmeldung liegt beim Betreiber (Protokoll Teil 5) | M | 2.1 | +| 2.2 | Aufnahme in Google News und Publisher Center prüfen und beantragen — **technischer Teil 17.09.2026:** News-Sitemap `/sitemap-news.xml` je Spoke (48 h, ≤ 1.000), in `robots.txt`; Publisher-Center-Anmeldung liegt beim Betreiber (Protokoll Teil 7) | M | 2.1 | | ~~2.3~~ | Branchen- und Kategorie-Landingpages aus dem Archivbestand — **17.09.2026:** Jahresarchiv je Branche `/category/{slug}/archiv/{jahr}` (34.600 Meldungen intern verlinkt, in der Sitemap), Jahresleiste auf der Branchenseite, Beschreibungen für alle 14 Kategorien (`categories:seed-descriptions`), Portalname im Titel | M | keine | | ~~2.4~~ | Transparenzseiten — **17.09.2026 nach Briefing, drei Seiten:** `was-kostet-eine-pressemitteilung` (Modellvergleich ohne Namen, Prüfliste Folgekosten, eigene Zusagen, Rechenbeispiel mit gemischtem Ausgang), `pressemitteilung-veroeffentlichen-ohne-abo`, `pressemitteilung-loeschen-oder-aendern-lassen` (Meldeverfahren + Absatz für fremde Portale); keine fremden Zahlen | S | keine — **Nachtrag 17.09.2026 (Anhang 1):** vierte Seite „kostenlos veröffentlichen" | | ~~2.5~~ | Konsolidierung der Ratgeberseiten unter `/de/presse/` — **geschlossen 17.09.2026:** Strecke existiert seit R3.2 nicht mehr, 301 auf `/faq` bleibt; Hilfe-Center trägt die Inhalte, jetzt auch in der Topleiste neben FAQ | S | 1d.1 | -| 2.6 | Outreach an Betreiber von Presseportal-Listen und Vergleichsartikeln | M | 2.4 | +| 2.6 | Outreach an Betreiber von Presseportal-Listen und Vergleichsartikeln — **extern, in Arbeit (Betreiber, Stand 18.09.2026)**; Arbeitsregel, Ziele und Mustertext in Anhang 1, Abschnitt 5; nicht Teil der Umsetzung | M | 2.4 | | ~~2.7~~ | Auffindbarkeit für KI-Systeme: strukturierte Daten, klare Entitätszuordnung, `llms.txt` — **17.09.2026:** `/llms.txt` je Host (Hub verweist auf beide Portale), Entitätszuordnung über 2.1 | M | 2.1 | **Formulierungsgrenze bei 2.7:** Die technischen Voraussetzungen zu beschreiben ist haltbar. Eine Nennung in KI-Antworten zu versprechen ist es nicht. Vor Veröffentlichung auf einer Preisseite sauber formulieren. @@ -206,7 +206,7 @@ Braucht Betreuungszeit und ist teilweise von Dritten abhängig. | Nr. | Arbeitspaket | Aufwand | Abhängigkeit | |---|---|---|---| -| 3.1 | RSS-Feeds je Branche | S | 2.3 | +| ~~3.1~~ | RSS-Feeds je Branche — **gebaut 18.09.2026**: `/{edition}/feed` und `/{edition}/category/{slug}/feed` (RSS 2.0, jüngste 50, inkl. Unterkategorien), Autodiscovery auf Start-, Branchen- und Archivseiten, Link in der Archiv-Leiste, Block im Panel „Auslieferung", Hub leitet aufs Portal (Protokoll Teil 11) | S | 2.3 | | 3.2 | Opt-in E-Mail-Digest je Branche, Double-Opt-in | M | 3.1, Bounce-Suppression live | | 3.3 | Agenturmodell: mehrere Firmen unter einem Konto, aus den Sammeladressen aus 1a.2 abgeleitet | L | 1a.2 | | 3.4 | Gezielte Ansprache kleiner PR-Agenturen und freier Texter | L | 3.3 | @@ -237,12 +237,12 @@ Braucht Betreuungszeit und ist teilweise von Dritten abhängig. | ~~E2~~ | **Empfehlung 01.09.2026: Rohzahlen** — kein Bodensatz, Verteilung symmetrisch, Einordnung gegen den Schnitt wäre wegen der Wachstumskurve falsch (siehe `basiswerte-block0.md`). | 1c.1 | | ~~E3~~ | **Entschieden 16.09.2026: keine Credits.** Prüfung ist Teil der Freigabe, nicht ein Werkzeug; N automatische Läufe je Meldung (einstellbar, 3), danach Mensch, Automatik für die Meldung aus. Gelb geht weiter automatisch online, `yellow_to_manual_queue` bleibt als Schalter (Vorgabe aus). | 1d.4 | | ~~E4~~ | **Entschieden 15.09.2026: der Magic-Link-Hinweis, am Ende der Mail, als Hinweis, nicht als Angebot.** Umgesetzt in 1b.4. Der Link auf die Infoseite (1d.2) bekommt damit keinen Platz in der Veröffentlichungsmail — ein Element pro Mail. | 1b.4, 1d.2 | -| ~~E5~~ | **Geschlossen 17.09.2026: bleibt bei FAQ, kein Slug-Mapping.** Werden die bestehenden Ratgeberseiten konsolidiert oder ergänzt — **Befund 17.09.2026:** `/de/presse/*` leitet seit R3.2 auf `/faq`; offen ist nur, ob Schreib-Slugs stattdessen auf `/gute-pressemitteilung` zeigen sollen (Protokoll Teil 5) | 2.5 | -| E6 | Bleibt die Ablehnungsstrecke dauerhaft frei von kommerziellen Angeboten | 1d.1, 1d.6 | +| ~~E5~~ | **Geschlossen 17.09.2026: bleibt bei FAQ, kein Slug-Mapping.** Werden die bestehenden Ratgeberseiten konsolidiert oder ergänzt — **Befund 17.09.2026:** `/de/presse/*` leitet seit R3.2 auf `/faq`; offen ist nur, ob Schreib-Slugs stattdessen auf `/gute-pressemitteilung` zeigen sollen (Protokoll Teil 7) | 2.5 | +| ~~E6~~ | **Entschieden (umgesetzt 16.09.2026, abgehakt 18.09.2026): ja, dauerhaft.** Hilfeseite und Ablehnungsmail ohne kommerziellen Anteil; Partnerbereich nur als BALD am Seitenende. | 1d.1, 1d.6 | | ~~E7~~ | **Entschieden 17.09.2026 (Anhang 1):** keine kostenfreie Einreichung, dauerhaft | 2.4, 2.6 | -| E8 | Kundenansicht des Indexierungsstatus — gebaut, zu (`GSC_CUSTOMER_VISIBLE`), Entscheidung nach Verteilung aus 1c.4 (Anhang 1) | 1c.5 | -| ~~E9~~ | **Entschieden 18.09.2026:** Zuordnung je Kategorie wie vorgeschlagen, Mechanismus aktiv (Anhang 2, Protokoll Teil 8) | 1c.8 | -| ~~E10~~ | **Entschieden 18.09.2026: Portale führen** (presseecho), API-Doku beim Hub, Rechtstexte je Host (Anhang 2, Protokoll Teil 8) | 2.4 | +| E8 | Kundenansicht des Indexierungsstatus — gebaut, zu (`GSC_CUSTOMER_VISIBLE`); **Prüfung läuft seit 17.09.2026 täglich, Entscheidung, sobald die Verteilung belastbar ist** (Anhang 1) | 1c.5 | +| ~~E9~~ | **Entschieden 18.09.2026:** Zuordnung je Kategorie wie vorgeschlagen, Mechanismus aktiv (Anhang 2, Protokoll Teil 10) | 1c.8 | +| ~~E10~~ | **Entschieden 18.09.2026: Portale führen** (presseecho), API-Doku beim Hub, Rechtstexte je Host (Anhang 2, Protokoll Teil 10) | 2.4 | --- diff --git a/dev/2026-09-01/roadmap-anhang1-2026.md b/dev/2026-09-01/roadmap-anhang1-2026.md index d3ee3cf6..4dd0be07 100644 --- a/dev/2026-09-01/roadmap-anhang1-2026.md +++ b/dev/2026-09-01/roadmap-anhang1-2026.md @@ -9,7 +9,7 @@ | Änderung | Betrifft | Art | |---|---|---| | Crawler-Abruf-Protokoll als eigenes Arbeitspaket | 1c.0 | neu | -| Absenderansicht in Stufen zerlegt, Indexierungsstatus zunächst intern | 1c.1, 1c.4 bis 1c.N9A59YXJai6 | ersetzt | +| Absenderansicht in Stufen zerlegt, Indexierungsstatus zunächst intern | 1c.1, 1c.4 bis 1c.6 | ersetzt | | Erstquelle gegen Kopie als Argument für die Direktveröffentlichung | 1d | neu | | Transparenzseiten ausformuliert, kein Wettbewerbervergleich | 2.4 | ersetzt | | Zuschriften an Portalübersichten ausformuliert, zwei konkrete Ziele | 2.6 | ersetzt | @@ -28,10 +28,10 @@ |---|---|---|---| | ~~1c.0~~ | Crawler-Abruf-Protokoll — **gebaut 17.09.2026**: `press_release_crawler_hits`, 15 Dienste in `config/crawlers.php`, keine IP, Bereinigung mit dem Ereignis-Log; `CRAWLER_HITS_RECORDED_SINCE` beim Deploy setzen | S | keine | | ~~1c.1~~ | Absenderansicht Stufe 1 — **umgebaut 17.09.2026**: Panel heißt „Auslieferung", Google-Suchlink entfernt, Sitemap-Datum bleibt; keine Feeds vorhanden, daher kein Feed-Eintrag | M | 0.1 | -| 1c.2 | Zugang zur Ansicht über Magic Link, auch ohne Konto | S | 1b.2 | -| 1c.3 | Newsroom-Übersicht mit allen Meldungen der Firma und Summenwerten | M | 1a.3, 1c.1 | +| ~~1c.2~~ | Zugang zur Ansicht über Magic Link, auch ohne Konto — **16.09.2026** (Hauptdokument) | S | 1b.2 | +| ~~1c.3~~ | Newsroom-Übersicht mit allen Meldungen der Firma und Summenwerten — **16.09.2026** (Hauptdokument) | M | 1a.3, 1c.1 | | ~~1c.4~~ | Indexierungsstatus — **gebaut 17.09.2026**: `press-releases:check-index-status` (täglich, Kontingent nach Alter gestaffelt), `press_release_index_checks` als Momentaufnahmen, Admin ▸ Reports ▸ Indexierung getrennt nach direkt/API; **live seit 17.09.2026** (Service-Konto eingerichtet, täglicher Lauf 05:10, Abfrage auf `en-US`) | M | GSC-Property je Portal verifiziert | -| 1c.5 | Entscheidung über die Kundenansicht des Indexierungsstatus, auf Basis der Verteilung aus 1c.4 — Schalter `GSC_CUSTOMER_VISIBLE` ist vorbereitet; Altbestand ohne Status-Log zählt in der Verteilung als „direkt" | S | 1c.4 | +| 1c.5 | Entscheidung über die Kundenansicht des Indexierungsstatus, auf Basis der Verteilung aus 1c.4 — Schalter `GSC_CUSTOMER_VISIBLE` ist vorbereitet; Altbestand ohne Status-Log zählt in der Verteilung als „direkt". **Wartet auf Daten (Stand 18.09.2026): der tägliche Lauf sammelt, Bericht unter Admin ▸ Reports ▸ Indexierung** | S | 1c.4 | | ~~1c.6~~ | Abrufe durch KI-Dienste — **gebaut 17.09.2026**: Block im Panel je Dienst mit letztem Abruf und Anzahl, Satz „Ein Abruf ist kein Zitat" | S | 1c.0, 1c.1 | **Stufe 1 verspricht nichts, was von Dritten abhängt.** Überschrift "Auslieferung", nicht "Sichtbarkeit bei Google". Keine Schaltfläche, die eine `site:`-Suche öffnet — sie liefert dem Kunden ein unvollständiges Ergebnis ohne Einordnung und ist schlechter als keine Angabe. @@ -62,8 +62,8 @@ | Nr. | Arbeitspaket | Aufwand | Abhängigkeit | |---|---|---|---| | ~~2.4~~ | Transparenzseiten — vier Seiten, die vierte (`/pressemitteilung-kostenlos-veroeffentlichen`) **gebaut 17.09.2026**; Liste kostenloser Portale in `config/transparency.php` vorbereitet, leer bis geprüfte Einträge vorliegen | S | keine | -| 2.6 | Zuschriften an Betreiber von Portalübersichten — übernimmt der Betreiber, nicht Teil der Umsetzung | S | 2.4 | -| 2.7 | Auffindbarkeit für KI-Systeme: strukturierte Daten, klare Entitätszuordnung, `llms.txt` | M | 2.1 | +| 2.6 | Zuschriften an Betreiber von Portalübersichten — **extern, in Arbeit (Betreiber, Stand 18.09.2026)**; nicht Teil der Umsetzung | S | 2.4 | +| ~~2.7~~ | Auffindbarkeit für KI-Systeme: strukturierte Daten, klare Entitätszuordnung, `llms.txt` — **17.09.2026** (`/llms.txt` je Host, Entitäten über 2.1; Hauptdokument) | M | 2.1 | **2.7 rückt vor.** Das Feld ist nicht mehr leer: seit Ende August 2026 besetzt mit Leitquelle ein kuratiertes Portal genau diese Positionierung, betrieben von einer Berliner PR-Agentur, 89 Euro je Meldung. Der Bestand dort ist zum Stichtag klein und die Aufbereitung Handarbeit; die These wird damit bestätigt, nicht der Markt besetzt. Der Vorteil liegt bei Archiv, Zulauf und Automatisierung — nicht beim Vokabular. Begriffe wie "KI-lesbar" oder "Referenzversion" sind belegt und werden nicht übernommen. diff --git a/dev/2026-09-01/roadmap-anhang2-2026.md b/dev/2026-09-01/roadmap-anhang2-2026.md index 174cefc1..903bdf5e 100644 --- a/dev/2026-09-01/roadmap-anhang2-2026.md +++ b/dev/2026-09-01/roadmap-anhang2-2026.md @@ -40,7 +40,7 @@ Schneller als über die API geht es zunächst über eine Bestandsabfrage nach id | Nr. | Arbeitspaket | Aufwand | Abhängigkeit | |---|---|---|---| -| ~~1c.7~~ | Doppelveröffentlichung erheben — **gebaut 17.09.2026**: `content_hash`, `press-releases:hash-content`, `press-releases:duplicates-report`. Befund: 9.092 Paare = 53 % des Archivs (Protokoll Teil 7) | S | keine | +| ~~1c.7~~ | Doppelveröffentlichung erheben — **gebaut 17.09.2026**: `content_hash`, `press-releases:hash-content`, `press-releases:duplicates-report`. Befund: 9.092 Paare = 53 % des Archivs (Protokoll Teil 9) | S | keine | | ~~1c.8~~ | Kanonische Zuordnung — **gebaut 17.09.2026, ausgeschaltet bis E9**: `CanonicalAssigner`, `canonical_press_release_id`, Zuordnung bei Veröffentlichung + `press-releases:assign-canonicals`; rel=canonical, mainEntityOfPage, Sitemaps nur Erstquelle. Schalter `CANONICAL_ASSIGNMENT_ENABLED` | M | 1c.7 | | ~~1c.9~~ | Quellenauskunft — **gebaut 17.09.2026, hinter `GSC_CUSTOMER_VISIBLE`**: `user_canonical` gespeichert, `sourceVerdict()` (eigene / andere eigene Fassung / fremdes Portal), Text im Block „Google-Index" | S | 1c.4, 1c.8 | @@ -99,7 +99,7 @@ Zwei Portale bedeuten zwei Fundstellen für Leser, nicht zwei Fundstellen in der Die Property ist angelegt, wird für die Indexierungsprüfung aus 1c.4 aber nicht gebraucht — das Tageskontingent geht an die beiden Portale. Zwei Dinge sind dort trotzdem zu prüfen: - ~~Stehen Konto- und Login-Bereiche auf `noindex`?~~ **17.09.2026: ja, jetzt per Meta-Tag in allen Portal-Layouts.** Die Indexierungsübersicht der neuen Property zeigt, was Google von dort erfasst hat. -- ~~**Offene Entscheidung E10**~~ **Entschieden 18.09.2026: Portale führen, presseecho als Standard; die API-Doku bleibt beim Hub, Rechtstexte je Host.** Dabei aufgefallen und behoben: `/de/preise`, `/de/veroeffentlichen`, `/de/newsrooms` und `/de/suche` wurden unter pressekonto.com mit dem Branding von businessportal24 ausgeliefert — jetzt 301 auf presseecho. Offen: Kategorie-, Archiv- und Meldungsadressen sind auf dem Hub weiterhin erreichbar (Canonical zeigt aufs Portal, keine Sitemap-Einträge) — Empfehlung 301 auf businessportal24, ~34.000 Adressen, wartet auf Freigabe. +- ~~**Offene Entscheidung E10**~~ **Entschieden 18.09.2026: Portale führen, presseecho als Standard; die API-Doku bleibt beim Hub, Rechtstexte je Host.** Dabei aufgefallen und behoben: `/de/preise`, `/de/veroeffentlichen`, `/de/newsrooms` und `/de/suche` wurden unter pressekonto.com mit dem Branding von businessportal24 ausgeliefert — jetzt 301 auf presseecho. ~~Offen: Kategorie-, Archiv- und Meldungsadressen sind auf dem Hub weiterhin erreichbar~~ — **erledigt 18.09.2026 nach Freigabe (Commit 24c1bbf):** 301 aufs Portal des Eintrags, bei gleichem Slug auf die Erstquelle, unbekannter Slug bleibt 404, nur GET (Protokoll Teil 10). Feeds (3.1) leiten genauso. --- @@ -108,4 +108,4 @@ Die Property ist angelegt, wird für die Indexierungsprüfung aus 1c.4 aber nich | Nr. | Frage | Betrifft | |---|---|---| | ~~E9~~ | **Entschieden 18.09.2026:** Zuordnung wie vorgeschlagen (businessportal24: finanzen, handel, it, recht, wissenschaft-forschung, motor-verkehr; presseecho: alle übrigen inkl. sonstiges). Zuordnung ist aktiv, `CANONICAL_ASSIGNMENT_ENABLED` steht standardmäßig auf true | 1c.8 | -| ~~E10~~ | **Entschieden 18.09.2026: Portale führen** — presseecho für faq, hilfe, gute-pressemitteilung und die vier Transparenzseiten, pressekonto für die API-Doku. Rechtstexte bleiben bewusst je Host (eigene Anbieterkennzeichnung, eigener Meldeweg). Sitemap und llms.txt nennen nur die führende Adresse (Protokoll Teil 8) | 2.4 | \ No newline at end of file +| ~~E10~~ | **Entschieden 18.09.2026: Portale führen** — presseecho für faq, hilfe, gute-pressemitteilung und die vier Transparenzseiten, pressekonto für die API-Doku. Rechtstexte bleiben bewusst je Host (eigene Anbieterkennzeichnung, eigener Meldeweg). Sitemap und llms.txt nennen nur die führende Adresse (Protokoll Teil 10) | 2.4 | \ No newline at end of file diff --git a/dev/2026-09-01/umsetzung-protokoll.md b/dev/2026-09-01/umsetzung-protokoll.md index b4d7ad1b..49d04475 100644 --- a/dev/2026-09-01/umsetzung-protokoll.md +++ b/dev/2026-09-01/umsetzung-protokoll.md @@ -1540,7 +1540,7 @@ Test: `RetireLinkSpamCompaniesTest`. --- -## Teil 5 — Block 2: Organische Sichtbarkeit (ab 17.09.2026) +# Teil 7 — Block 2: Organische Sichtbarkeit (ab 17.09.2026) Block 1 ist komplett und live (17.09.2026). Block 2 beginnt mit dem technischen Teil (2.1, 2.2, 2.7); die Inhalts- und Marketing-Pakete (2.3, @@ -1739,11 +1739,11 @@ neue Tabellen-Styles), `view:cache`/`route:cache` wie üblich, dann eine Meldung im Rich-Results-Test (search.google.com/test/rich-results) prüfen. -## Teil 6 — Anhang 1 zur Roadmap (17.09.2026) +# Teil 8 — Anhang 1 zur Roadmap (17.09.2026) Grundlage: `roadmap-anhang1-2026.md`. 2.6 (Zuschriften an Betreiber) bewusst ausgelassen — übernimmt der Betreiber an anderer Stelle. 2.7 war -mit Teil 5 bereits gebaut; die Formulierungsgrenze (Abruf belegbar, +mit Teil 7 bereits gebaut; die Formulierungsgrenze (Abruf belegbar, Nennung nicht) gilt in allen neuen Texten. ### 1c.0 Crawler-Abruf-Protokoll @@ -1891,7 +1891,7 @@ Inhalte trägt das Hilfe-Center. Dafür steht **Hilfe** jetzt in der Topleiste der Portale neben FAQ (Desktop) und im Burger-Menü unter den Service-Links (Mobile); der Hub hatte den Link bereits. -## Teil 7 — Anhang 2: Doppelveröffentlichung und Quellenauskunft (17.09.2026) +# Teil 9 — Anhang 2: Doppelveröffentlichung und Quellenauskunft (17.09.2026) Grundlage: `roadmap-anhang2-2026.md`. Commits siehe unten. **Der Mechanismus ist gebaut und getestet, aber ausgeschaltet** @@ -1999,7 +1999,7 @@ Hintergrund), dann `press-releases:duplicates-report`. Erst nach E9: `CANONICAL_ASSIGNMENT_ENABLED=true`, `press-releases:assign-canonicals --dry-run`, dann ohne Flag, `view:cache`, Sitemaps prüfen. -## Teil 8 — E9 und E10 entschieden, Hub-Erreichbarkeit geprüft (18.09.2026) +# Teil 10 — E9 und E10 entschieden, Hub-Erreichbarkeit geprüft (18.09.2026) ### E9 — kanonische Zuordnung aktiv @@ -2122,3 +2122,80 @@ Pfade werden jetzt ab `base_path()` aufgelöst. Dev-Verteilung presseecho (Bestand): direkt 8.845 Erstquellen / 2.126 Kopien, API 1.308 / 308. + +# Teil 11 — Abschluss Block 0–2, Block 3.1 RSS-Feeds (18.09.2026) + +## Sichtung des Ordners (18.09.2026) + +Alle Dokumente in `dev/2026-09-01/` gegen Code und Git-Stand abgeglichen. +Befunde und was daraus wurde: + +| Befund | Erledigt | +|---|---| +| `GSC_PROPERTY_BUSINESSPORTAL24` hatte als Vorgabe `sc-domain:businessportal24.de`, die Property ist `.com` | Vorgabe in `config/search_console.php` und `.env.example` korrigiert (live ist der Wert explizit gesetzt) | +| Anhang 1: Tippfehler in der Übersicht, 1c.2/1c.3/2.7 nicht abgehakt | angeglichen | +| Protokoll: „Teil 5" und „Teil 6" doppelt vergeben | zweite Serie ist jetzt Teil 7–10, Verweise in Roadmap und Anhang 2 nachgezogen | +| Log-Aufteilung je Thema lag uncommittet auf dem Branch | Commit 2ec6169 (billing, ki, monitoring, market als Tagesdateien; Suite loggt nur nach `testing-*.log`) | +| `docs/weiteres`: Decision-Update Duplicate-Content gelöscht, Verlinkung & Backlinks wieder da | Duplicate-Content ist durch Anhang 2 überholt (entfernt, README-Index nachgezogen); Verlinkung bleibt als Referenz für die Link-Policy | +| E6 stand offen, ist aber in 1d.1/1d.2 umgesetzt | als entschieden geführt | +| 2.6 Zuschriften | extern, in Arbeit beim Betreiber — nicht Teil der Umsetzung | +| 1c.5/E8 | Indexierungsprüfung läuft täglich; Entscheidung, sobald die Verteilung belastbar ist | + +**Bleibt offen, bewusst nicht aus dem Code heraus erledigt:** + +- 0.7: Absatz zur Ereignistabelle (Zweck, Umfang, 395 Tage, Rechtsgrundlage) + in der Datenschutzerklärung — Rechtstext, Betreiber. +- 0.1: Kohortenwerte „28–55 Tage" vom Live-System (Fenster 01.06.–01.09.) + einmal erheben und ablegen: `php artisan press-releases:view-distribution --from=2026-06-01 --to=2026-09-01 --json > dev/2026-09-01/basiswert-0.1.json`. +- Abbruchbedingungen der Blöcke 1–3 tragen weiter Platzhalter für Zeitraum + und Schwellwert; Block 1 läuft seit 17.09. ohne festen Vergleichspunkt. +- 2.2 Publisher Center (Betreiber), Ergebnis nach 4–6 Wochen in der Search + Console. + +## 3.1 — RSS-Feeds je Branche + +**Stand vorher:** keine Feeds (siehe 1c.1: „keine Feeds vorhanden, daher +kein Feed-Eintrag"). + +**Umsetzung** (`FeedController`, `web.feed`-View): + +- `/{edition}/feed` — alle Meldungen der Ausgabe (Portal × Sprache); + `/{edition}/category/{slug}/feed` — Branche inklusive aktiver + Unterkategorien, wie die Branchenseite. Jeweils die jüngsten 50, neueste + zuerst, nur `visibleInArchive` (kein Termin in der Zukunft, kein 410). +- RSS 2.0 mit `atom:link rel="self"`, `dc:creator` = Firmenname, + `category` = lokalisierter Branchenname, `guid` = Permalink, Teaser 300 + Zeichen ohne Markup. Kanaltitel „Pressemitteilungen {Branche} – {Portal}", + Beschreibung aus der Kategoriebeschreibung (2.3), sonst Tagline. +- Bei Doppelveröffentlichung führt jedes Portal seine eigene Fassung — Leser + und Feeds sehen beide (Anhang 2, Abschnitt 1); nur die Suchmaschine + bekommt die Erstquelle. Kein Canonical-Filter im Feed. +- Cache wie die Seitendaten (`WebPageCache`, 5 min frisch, bis 1 h stale + mit Neuaufbau nach der Antwort); `WebCaches::forgetPublicPages()` leert + ihn mit. Eine Korrektur (nur `forgetSitemaps()`) erscheint im Feed nach + spätestens fünf Minuten — hingenommen. +- Hub: `feed` und `kategorie.feed` in `canonical.portal_only_content_routes` + → 301 auf presseecho, wie Kategorie und Archiv. +- Autodiscovery: `@yield('feed_links')` im Web-Layout; Startseiten nennen + den Portal-Feed, Branchenseite Branchen- und Portal-Feed, Jahresarchiv + den Branchen-Feed — Adressen aus der Portal-Basis, nicht aus `url()` + (das liefert wegen `forceRootUrl` den Hub). Sichtbarer Link „RSS-Feed" + in der Archiv-Leiste der Branchenseite; `llms.txt` nennt beide Muster. +- Absenderansicht: Block „RSS-Feeds" im Panel „Auslieferung" mit Branchen- + und Portal-Feed je Portal der Meldung, plus dem Satz, dass Abonnenten uns + nicht bekannt sind (`VisibilityStats::feedLinks()`). +- Übersetzungen unter `web.rss` — `web.feed` war schon die Meldungsliste der + Startseite. + +Nicht gebaut: Feeds je Newsroom (kein Arbeitspaket), Bild-Enclosures +(zusätzliche Queries je Item, kein Bedarf benannt), Feed-Einträge in der +Sitemap (Feeds gehören nicht hinein). + +Tests: `FeedTest` (7 — Branche mit Kindern und Fremdportal, Portal-Feed, +404, englische Ausgabe, Autodiscovery + llms.txt, Hub-301, Cache), +`VisibilityStatsTest` (+Feed-Links im Summary und im Panel). Pint, Larastan +ohne Befund. + +**Deploy:** `view:cache`/`route:cache` neu; kein `migrate`, kein +`cache:clear` (neue Cache-Keys). + diff --git a/lang/de/web.php b/lang/de/web.php index 29f130af..5ed7ff9e 100644 --- a/lang/de/web.php +++ b/lang/de/web.php @@ -322,6 +322,16 @@ return [ 'cta_pricing' => 'Tarife & Branchen-Pakete ansehen', ], + 'rss' => [ + 'portal_title' => 'Pressemitteilungen – :brand', + 'category_title' => 'Pressemitteilungen :category – :brand', + 'default_description' => 'Aktuelle Pressemitteilungen auf :brand.', + 'portal_link_title' => 'RSS-Feed: alle Pressemitteilungen – :brand', + 'category_link_title' => 'RSS-Feed: Pressemitteilungen :category – :brand', + 'link' => 'RSS-Feed', + 'link_aria' => 'RSS-Feed der Branche :category', + ], + 'category_archive' => [ 'eyebrow' => 'Archiv', 'breadcrumb' => 'Archiv :year', diff --git a/lang/en/web.php b/lang/en/web.php index c5cee636..1e01e31a 100644 --- a/lang/en/web.php +++ b/lang/en/web.php @@ -317,6 +317,16 @@ return [ 'cta_pricing' => 'View plans & industry packages', ], + 'rss' => [ + 'portal_title' => 'Press releases – :brand', + 'category_title' => ':category press releases – :brand', + 'default_description' => 'Latest press releases on :brand.', + 'portal_link_title' => 'RSS feed: all press releases – :brand', + 'category_link_title' => 'RSS feed: :category press releases – :brand', + 'link' => 'RSS feed', + 'link_aria' => 'RSS feed for :category', + ], + 'category_archive' => [ 'eyebrow' => 'Archive', 'breadcrumb' => 'Archive :year', diff --git a/resources/views/livewire/customer/press-releases/show.blade.php b/resources/views/livewire/customer/press-releases/show.blade.php index 04a76c76..b3332af9 100644 --- a/resources/views/livewire/customer/press-releases/show.blade.php +++ b/resources/views/livewire/customer/press-releases/show.blade.php @@ -1598,6 +1598,21 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends + {{-- RSS-Feeds (Roadmap 3.1): Branchen- und Portal-Feed je Portal. --}} +
+
{{ __('RSS-Feeds') }}
+ @if (($visibility['feeds'] ?? []) !== []) + +

{{ __('Die Feeds führen die jüngsten :n Meldungen — wer sie abonniert hat, ist uns nicht bekannt.', ['n' => \App\Http\Controllers\Web\FeedController::MAX_ITEMS]) }}

+ @else +

{{ __('Derzeit in keinem Feed — die Meldung ist nicht öffentlich.') }}

+ @endif +
+ {{-- Abrufe durch Such- und KI-Dienste (Roadmap 1c.6). Ein Abruf ist kein Zitat — die Anzeige belegt nur, dass der Crawler des Dienstes die Seite geholt hat. --}} diff --git a/resources/views/web/businessportal24.blade.php b/resources/views/web/businessportal24.blade.php index aef6fa8f..48ac05dc 100644 --- a/resources/views/web/businessportal24.blade.php +++ b/resources/views/web/businessportal24.blade.php @@ -49,6 +49,10 @@ @endsection +@section('feed_links') + +@endsection + @section('content') diff --git a/resources/views/web/feed.blade.php b/resources/views/web/feed.blade.php new file mode 100644 index 00000000..9f4d1aee --- /dev/null +++ b/resources/views/web/feed.blade.php @@ -0,0 +1,27 @@ +{!! '' !!} + + + {{ $channel['title'] }} + {{ $channel['link'] }} + + {{ $channel['description'] }} + {{ $channel['language'] }} + {{ $channel['last_build'] }} + 15 +@foreach ($items as $item) + + {{ $item['title'] }} + {{ $item['link'] }} + {{ $item['link'] }} + {{ $item['published_at'] }} + {{ $item['description'] }} +@if ($item['category']) + {{ $item['category'] }} +@endif +@if ($item['creator']) + {{ $item['creator'] }} +@endif + +@endforeach + + diff --git a/resources/views/web/kategorie-archiv.blade.php b/resources/views/web/kategorie-archiv.blade.php index c7231a1a..c23b4dc4 100644 --- a/resources/views/web/kategorie-archiv.blade.php +++ b/resources/views/web/kategorie-archiv.blade.php @@ -75,6 +75,12 @@ @endsection @endif +@section('feed_links') + @if ($catSlug) + + @endif +@endsection + @section('structured_data') @endsection diff --git a/resources/views/web/kategorie.blade.php b/resources/views/web/kategorie.blade.php index f04d6740..f51279e8 100644 --- a/resources/views/web/kategorie.blade.php +++ b/resources/views/web/kategorie.blade.php @@ -72,6 +72,13 @@ @endsection @endif +@section('feed_links') + @if ($catSlug) + + @endif + +@endsection + @section('structured_data') @endsection @@ -270,7 +277,10 @@