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:
parent
e2ef4dee31
commit
9d5e5a0b54
20 changed files with 547 additions and 28 deletions
125
app/Http/Controllers/Web/FeedController.php
Normal file
125
app/Http/Controllers/Web/FeedController.php
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Web;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Category;
|
||||
use App\Models\PressRelease;
|
||||
use App\Scopes\PortalScope;
|
||||
use App\Support\StructuredData;
|
||||
use App\Support\WebPageCache;
|
||||
use App\Support\WebPortalContext;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* RSS-Feeds je Branche und je Portal (Roadmap 3.1): `/{edition}/feed` und
|
||||
* `/{edition}/category/{slug}/feed`. Die jüngsten Meldungen der Ausgabe
|
||||
* (Portal × Sprache), bei einer Branche inklusive Unterkategorien — wie die
|
||||
* Branchenseite selbst. Bei Doppelveröffentlichung erscheint je Portal die
|
||||
* eigene Fassung; Leser und Feeds sehen beide (Anhang 2), nur die
|
||||
* Suchmaschine bekommt die Erstquelle.
|
||||
*
|
||||
* Gecacht wie die Seitendaten (WebPageCache: 5 min frisch, danach stale und
|
||||
* Neuaufbau nach der Antwort); Entfernungen leeren ihn mit den übrigen
|
||||
* Seiten-Caches. Der Hub hat keine Meldungen — die Routen leiten dort auf
|
||||
* das Portal (config/canonical.php, portal_only_content_routes).
|
||||
*/
|
||||
class FeedController extends Controller
|
||||
{
|
||||
public const MAX_ITEMS = 50;
|
||||
|
||||
public const TEASER_LENGTH = 300;
|
||||
|
||||
public function __invoke(Request $request, string $edition, ?string $slug = null): Response
|
||||
{
|
||||
$context = WebPortalContext::fromRequest();
|
||||
$base = rtrim($request->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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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',
|
||||
'',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue