web: RSS-Feeds je Branche und je Portal (Roadmap 3.1); dev: Sichtung des Ordners, Protokoll-Teile umnummeriert

- /{edition}/feed und /{edition}/category/{slug}/feed (FeedController,
  RSS 2.0 mit atom:link self, dc:creator, category; jüngste 50 der Ausgabe,
  Branche inkl. aktiver Unterkategorien, nur visibleInArchive).
- Cache über WebPageCache (Key-Gruppe feed), Hub leitet per 301 aufs Portal
  (canonical.portal_only_content_routes).
- Autodiscovery über @yield('feed_links') auf Start-, Branchen- und
  Archivseiten, sichtbarer Link in der Archiv-Leiste, llms.txt nennt die
  Feed-Muster, Block „RSS-Feeds" im Panel „Auslieferung"
  (VisibilityStats::feedLinks).
- Übersetzungen unter web.rss (web.feed ist die Startseiten-Liste).
- Tests: FeedTest (7), VisibilityStatsTest erweitert.
- dev/2026-09-01: Sichtung 18.09. (Teil 11), zweite Teil-Serie des
  Protokolls jetzt 7–10 mit nachgezogenen Verweisen, Anhang 1 abgeglichen,
  E6 abgehakt, 2.6 extern, 1c.5/E8 wartet auf Daten, 3.1 abgehakt.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-09-18 13:57:09 +00:00
parent b423650d80
commit d0c7194491
20 changed files with 547 additions and 28 deletions

View file

@ -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<string, int>, shares_total: int, in_sitemap_since: Carbon|null, crawlers: list<array{service: string, label: string, kind: string, fetches: int, last_at: Carbon}>, crawlers_since: Carbon|null}
* @return array{hits: int, first_view_at: Carbon|null, last_view_at: Carbon|null, shares: array<string, int>, shares_total: int, in_sitemap_since: Carbon|null, crawlers: list<array{service: string, label: string, kind: string, fetches: int, last_at: Carbon}>, crawlers_since: Carbon|null, feeds: list<array{label: string, url: string}>}
*/
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<array{portal: Portal, label: string, url: string}> $links
* @return list<array{label: string, url: string}>
*/
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