web: RSS-Feed je Newsroom, RSS-Übersichtsseite, „RSS" in der Topleiste (Roadmap 3.1, Nachtrag)

- /{edition}/newsroom/{slug}/feed: Sichtbarkeit wie die Newsroom-Seite
  (aktiv, nicht entfernt, mindestens eine Meldung), Prüfung vor dem Cache,
  301 bei zusammengeführter Firma; Autodiscovery und Link „RSS-Feed" auf
  der Newsroom-Seite; Hub leitet auf das Portal der Firma.
- /{edition}/rss (RssPageController, static.rss DE/EN): Was RSS ist, wie
  ein Reader funktioniert, Hauptfeed, alle Branchen-Feeds mit Adresse,
  Muster des Newsroom-Feeds, Hinweise. Portalseite mit Portal-Kontext,
  Hub → presseecho, in Sitemap und llms.txt.
- Topleiste, Burger-Menü und Footer verlinken RSS neben FAQ/Hilfe/API.
- FeedController in portal/category/newsroom aufgeteilt; Panel
  „Auslieferung" nennt zusätzlich den Newsroom-Feed.
- Tests: FeedTest (+3), RssPageTest (4), StaticPagesTest, VisibilityStatsTest.
- dev/2026-09-01: Nachtrag in Protokoll Teil 11 und Roadmap 3.1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-09-21 08:27:14 +00:00
parent 9d5e5a0b54
commit ff75159b8f
22 changed files with 577 additions and 55 deletions

View file

@ -59,6 +59,7 @@
<li><a href="{{ route('veroeffentlichen') }}" class="cursor-pointer hover:text-white transition-colors">{{ __('web.footer.publish') }}</a></li>
<li><a href="{{ route('preise') }}" class="cursor-pointer hover:text-white transition-colors">{{ __('web.footer.pricing') }}</a></li>
<li><a href="{{ route('api') }}" class="cursor-pointer hover:text-white transition-colors">API</a></li>
<li><a href="{{ route('rss') }}" class="cursor-pointer hover:text-white transition-colors">RSS</a></li>
</ul>
</div>

View file

@ -94,6 +94,7 @@
<a href="{{ route('faq') }}" class="hidden lg:inline cursor-pointer hover:text-ink-on-dark transition-colors">FAQ</a>
<a href="{{ route('hilfe') }}" class="hidden lg:inline cursor-pointer hover:text-ink-on-dark transition-colors">{{ __('web.header.help') }}</a>
<a href="{{ route('api') }}" class="hidden lg:inline cursor-pointer hover:text-ink-on-dark transition-colors">API</a>
<a href="{{ route('rss') }}" class="hidden lg:inline cursor-pointer hover:text-ink-on-dark transition-colors">RSS</a>
</span>
</div>
</div>
@ -441,7 +442,7 @@
{{-- Service-Links: auf Mobile sonst nur im Footer erreichbar (FAQ/Hilfe/API liegen in der Topbar hinter lg:). --}}
<div class="mt-1 pt-1">
@foreach ([['label' => __('web.header.pricing'), 'href' => route('preise')], ['label' => 'FAQ', 'href' => route('faq')], ['label' => __('web.header.help'), 'href' => route('hilfe')], ['label' => 'API', 'href' => route('api')]] as $service)
@foreach ([['label' => __('web.header.pricing'), 'href' => route('preise')], ['label' => 'FAQ', 'href' => route('faq')], ['label' => __('web.header.help'), 'href' => route('hilfe')], ['label' => 'API', 'href' => route('api')], ['label' => 'RSS', 'href' => route('rss')]] as $service)
<a href="{{ $service['href'] }}"
class="flex items-center justify-between px-3 py-3 text-[13.5px] border-b border-bg-rule last:border-b-0 font-medium text-ink-3 hover:text-ink hover:bg-bg cursor-pointer transition-colors">
<span>{{ $service['label'] }}</span>

View file

@ -36,6 +36,9 @@
// Organization-Markup verknüpft PMs (author) mit diesem Profil.
// Portal-Basis wie beim Canonical im Layout (unabhängig von forceRootUrl).
$newsroomUrl = StructuredData::newsroomUrl(StructuredData::portalBase($domainUrl ?? null), $company->slug);
// RSS-Feed des Newsrooms (Roadmap 3.1) — Adresse aus der Portal-Basis.
$newsroomFeedUrl = StructuredData::portalBase($domainUrl ?? null).'/'.app()->getLocale().'/newsroom/'.$company->slug.'/feed';
$portalFeedUrl = StructuredData::portalBase($domainUrl ?? null).'/'.app()->getLocale().'/feed';
$jsonLd = [
'@context' => 'https://schema.org',
'@type' => 'Organization',
@ -58,6 +61,11 @@
<link rel="alternate" hreflang="x-default" href="{{ url('/de/newsroom/'.$company->slug) }}">
@endsection
@section('feed_links')
<link rel="alternate" type="application/rss+xml" title="{{ __('web.rss.newsroom_link_title', ['company' => $company->name, 'brand' => $brandName.$brandAccent]) }}" href="{{ $newsroomFeedUrl }}">
<link rel="alternate" type="application/rss+xml" title="{{ __('web.rss.portal_link_title', ['brand' => $brandName.$brandAccent]) }}" href="{{ $portalFeedUrl }}">
@endsection
@section('structured_data')
<x-web.structured-data :data="$jsonLd" />
@endsection
@ -115,6 +123,11 @@
<span class="text-ink-4" aria-hidden="true">·</span>
<span>{{ __('web.newsroom_page.since', ['year' => $newsroomStats['sinceYear']]) }}</span>
@endif
<span class="text-ink-4" aria-hidden="true">·</span>
<a href="{{ $newsroomFeedUrl }}" type="application/rss+xml" class="inline-flex items-center gap-1.5 font-medium hover:text-ink transition-colors" aria-label="{{ __('web.rss.newsroom_link_aria', ['company' => $company->name]) }}">
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M2 12.5A1.5 1.5 0 1 1 3.5 14 1.5 1.5 0 0 1 2 12.5ZM2 8a6 6 0 0 1 6 6h2a8 8 0 0 0-8-8Zm0-4a10 10 0 0 1 10 10h2A12 12 0 0 0 2 2Z"/></svg>
{{ __('web.newsroom_page.feed_link') }}
</a>
</div>
</div>
</div>

View file

@ -0,0 +1,104 @@
@extends('web.layouts.web-master')
@php
// Roadmap 3.1: Übersicht der Feeds dieses Portals. Adressen aus der
// Portal-Basis (wie Canonical und Autodiscovery), nicht aus url()/route(),
// die auf dem Hub den falschen Host trügen. Kontext und Kategorien liefert
// RssPageController.
$edition = app()->getLocale();
$themeKey = $portalContext->domainKey;
$portalBase = rtrim((string) ($portalContext->domainConfig['url'] ?? config('app.url')), '/');
$brand = (array) ($portalContext->domainConfig['brand'] ?? []);
$brandLabel = ($brand['name'] ?? $themeKey).($brand['accent'] ?? '');
$otherKey = $themeKey === 'presseecho' ? 'businessportal24' : 'presseecho';
$otherBrand = (array) config('domains.domains.'.$otherKey.'.brand', []);
$otherLabel = ($otherBrand['name'] ?? $otherKey).($otherBrand['accent'] ?? '');
$otherBase = rtrim((string) config('domains.domains.'.$otherKey.'.url'), '/');
$portalFeed = $portalBase.'/'.$edition.'/feed';
$categories = $feedCategories;
$maxItems = \App\Http\Controllers\Web\FeedController::MAX_ITEMS;
@endphp
@section('title', __('static.rss.meta_title', ['brand' => $brandLabel]))
@section('meta_description', __('static.rss.meta_description'))
@section('feed_links')
<link rel="alternate" type="application/rss+xml" title="{{ __('web.rss.portal_link_title', ['brand' => $brandLabel]) }}" href="{{ $portalFeed }}">
@endsection
@section('content')
<x-web.static-page
:eyebrow="__('static.rss.eyebrow')"
:title="__('static.rss.title')"
:lead="__('static.rss.lead', ['brand' => $brandLabel])"
:breadcrumb="__('static.rss.breadcrumb')"
>
<div class="pm-body max-w-[760px]">
<h2 id="was-ist-rss">{{ __('static.rss.what_heading') }}</h2>
<p>{{ __('static.rss.what_p1') }}</p>
<p>{{ __('static.rss.what_p2', ['brand' => $brandLabel]) }}</p>
<h2 id="so-funktioniert-es">{{ __('static.rss.how_heading') }}</h2>
<ol>
<li><strong>{{ __('static.rss.how_step1') }}</strong> — {{ __('static.rss.how_step1_text') }}</li>
<li><strong>{{ __('static.rss.how_step2') }}</strong> — {{ __('static.rss.how_step2_text') }}</li>
<li><strong>{{ __('static.rss.how_step3') }}</strong> — {{ __('static.rss.how_step3_text') }}</li>
</ol>
<p>{{ __('static.rss.how_p1', ['count' => $maxItems]) }}</p>
<p>{{ __('static.rss.how_p2') }}</p>
</div>
{{-- Portal-Feed --}}
<h2 id="feeds" class="font-serif text-[22px] font-semibold tracking-[-0.3px] text-ink m-0 mt-12 mb-4">{{ __('static.rss.feeds_heading', ['brand' => $brandLabel]) }}</h2>
<div class="border border-bg-rule bg-bg-elev p-6 sm:p-8">
<div class="eyebrow mb-2">{{ __('static.rss.main_eyebrow') }}</div>
<h3 class="font-serif text-[20px] font-semibold tracking-[-0.3px] text-ink m-0">{{ __('static.rss.main_title', ['brand' => $brandLabel]) }}</h3>
<p class="text-[13px] text-ink-2 mt-1.5 mb-4">{{ __('static.rss.main_text') }}</p>
<a href="{{ $portalFeed }}" type="application/rss+xml" class="inline-flex items-center gap-2 font-mono text-[13px] text-ink underline underline-offset-2 hover:text-brand transition-colors break-all">
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M2 12.5A1.5 1.5 0 1 1 3.5 14 1.5 1.5 0 0 1 2 12.5ZM2 8a6 6 0 0 1 6 6h2a8 8 0 0 0-8-8Zm0-4a10 10 0 0 1 10 10h2A12 12 0 0 0 2 2Z"/></svg>
{{ $portalFeed }}
</a>
</div>
{{-- Branchen-Feeds --}}
<h2 id="branchen" class="font-serif text-[22px] font-semibold tracking-[-0.3px] text-ink m-0 mt-12 mb-1.5">{{ __('static.rss.categories_heading') }}</h2>
<p class="text-[13px] text-ink-2 m-0 mb-4">{{ __('static.rss.categories_text') }}</p>
@if ($categories->isNotEmpty())
<ul class="list-none p-0 m-0 grid gap-px bg-bg-rule border border-bg-rule grid-cols-1 md:grid-cols-2">
@foreach ($categories as $category)
@php $categoryFeed = $portalBase.'/'.$edition.'/category/'.$category['slug'].'/feed'; @endphp
<li class="bg-bg p-4 sm:p-5">
<div class="flex items-baseline justify-between gap-3 flex-wrap">
<a href="{{ $category['href'] }}" class="text-[14px] font-semibold text-ink hover:text-brand transition-colors">{{ $category['label'] }}</a>
<a href="{{ $categoryFeed }}" type="application/rss+xml" class="inline-flex items-center gap-1.5 text-[12px] font-semibold text-ink-3 hover:text-brand transition-colors" aria-label="{{ __('web.rss.link_aria', ['category' => $category['label']]) }}">
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M2 12.5A1.5 1.5 0 1 1 3.5 14 1.5 1.5 0 0 1 2 12.5ZM2 8a6 6 0 0 1 6 6h2a8 8 0 0 0-8-8Zm0-4a10 10 0 0 1 10 10h2A12 12 0 0 0 2 2Z"/></svg>
{{ __('web.rss.link') }}
</a>
</div>
<div class="font-mono text-[11.5px] text-ink-3 mt-1 break-all">{{ $categoryFeed }}</div>
</li>
@endforeach
</ul>
@else
<p class="text-[13px] text-ink-3 m-0">{{ __('static.rss.categories_empty') }}</p>
@endif
{{-- Newsroom-Feeds --}}
<h2 id="newsrooms" class="font-serif text-[22px] font-semibold tracking-[-0.3px] text-ink m-0 mt-12 mb-1.5">{{ __('static.rss.newsrooms_heading') }}</h2>
<div class="pm-body max-w-[760px]">
<p>{{ __('static.rss.newsrooms_p1') }}</p>
<p>{{ __('static.rss.newsrooms_p2') }} <code>{{ $portalBase }}/{{ $edition }}/newsroom/{{ __('static.rss.newsrooms_slug_placeholder') }}/feed</code></p>
<p><a href="{{ route('newsrooms') }}">{{ __('static.rss.newsrooms_link') }}</a></p>
<h2 id="hinweise">{{ __('static.rss.notes_heading') }}</h2>
<ul>
<li>{{ __('static.rss.note_editions', ['de' => $portalBase.'/de/feed', 'en' => $portalBase.'/en/feed']) }}</li>
<li>{{ __('static.rss.note_other_portal', ['portal' => $otherLabel]) }} <a href="{{ $otherBase }}/{{ $edition }}/rss">{{ $otherBase }}/{{ $edition }}/rss</a></li>
<li>{{ __('static.rss.note_content') }}</li>
<li>{{ __('static.rss.note_free') }}</li>
</ul>
</div>
</x-web.static-page>
@endsection