presseportale/app/Http/Controllers/Web/LlmsTxtController.php
Kevin Adametz ff75159b8f 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>
2026-09-21 08:27:14 +00:00

137 lines
7.4 KiB
PHP

<?php
namespace App\Http\Controllers\Web;
use App\Http\Controllers\Controller;
use App\Models\Category;
use App\Support\StaticPageCanonical;
use App\Support\WebPortalContext;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Cache;
/**
* `/llms.txt` (Roadmap 2.7): eine kurze, sachliche Beschreibung des Hosts
* für KI-Systeme — was die Seite ist, wie die URLs aufgebaut sind, wo die
* strukturierten Daten und Sitemaps liegen. Keine Werbeaussagen, keine
* Versprechen über Nennungen in KI-Antworten (Formulierungsgrenze 2.7).
* Hub und Spokes bekommen je eine eigene Fassung; pro Host eine Stunde
* gecacht.
*/
class LlmsTxtController extends Controller
{
public function __invoke(Request $request): Response
{
$host = $request->getHost();
$base = rtrim($request->getSchemeAndHttpHost(), '/');
$isHub = str_contains($host, 'pressekonto');
$text = Cache::remember('web.llms.'.$host, 3600, fn (): string => $isHub ? $this->hub($base) : $this->portal($base));
return response($text, 200, ['Content-Type' => 'text/plain; charset=UTF-8']);
}
private function hub(string $base): string
{
// Seiten, die auf allen drei Hosts liegen, werden mit der führenden
// Adresse genannt (Anhang 2, E10) — eine Adresse je Text.
$shared = static fn (string $route): string => StaticPageCanonical::baseFor($route) ?? $base;
$presseecho = rtrim((string) config('domains.domains.presseecho.url'), '/');
$businessportal = rtrim((string) config('domains.domains.businessportal24.url'), '/');
return implode("\n", [
'# pressekonto',
'',
'> Publisher-Bereich der Presseportale presseecho und businessportal24: Unternehmen schreiben hier Pressemitteilungen, lassen sie automatisiert prüfen und veröffentlichen sie auf beiden Portalen.',
'',
'Die Meldungen selbst erscheinen nicht auf dieser Domain, sondern auf den beiden Portalen. Jede Meldung trägt dort NewsArticle-Markup (schema.org), jeder Newsroom Organization-Markup; Urheber einer Meldung ist immer das genannte Unternehmen, nicht das Portal.',
'',
'## Informationen',
'',
'- [Hilfe]('.$base.'/de/hilfe): Ablauf von der Registrierung bis zur Veröffentlichung',
'- [Was eine gute Pressemitteilung ausmacht]('.$base.'/de/gute-pressemitteilung): Anlass, Aufbau, Sprache, Prüfablauf',
'- [FAQ]('.$base.'/de/faq)',
'- [Richtlinien]('.$base.'/de/richtlinien): Was veröffentlicht wird und was nicht',
'- [Was kostet eine Pressemitteilung?]('.$base.'/de/was-kostet-eine-pressemitteilung): Abrechnungsmodelle, Prüfliste für Folgekosten, Rechenbeispiel',
'- [Pressemitteilung löschen oder ändern lassen]('.$base.'/de/pressemitteilung-loeschen-oder-aendern-lassen)',
'- [Pressemitteilung kostenlos veröffentlichen?]('.$base.'/de/pressemitteilung-kostenlos-veroeffentlichen): Warum es hier keine kostenlose Einreichung gibt, was kostenlose Portale leisten',
'- [API]('.$base.'/de/api): Einlieferung per REST-API; OpenAPI-Spezifikation unter '.$base.'/docs/api/v1',
'',
'## Portale',
'',
'- [presseecho]('.$presseecho.'/llms.txt): Pressemitteilungen aus Wirtschaft und Industrie',
'- [businessportal24]('.$businessportal.'/llms.txt): Pressemitteilungen aus Deutschland, Österreich und der Schweiz',
'',
'## Rechtliches',
'',
'- [Impressum]('.$base.'/de/impressum)',
'- [Datenschutz]('.$base.'/de/datenschutz)',
'- [AGB]('.$base.'/de/agb)',
'- [Inhalte melden]('.$base.'/de/melden)',
'',
]);
}
private function portal(string $base): string
{
// Wie im Hub: gemeinsame Seiten mit der führenden Adresse nennen.
$shared = static fn (string $route): string => StaticPageCanonical::baseFor($route) ?? $base;
$context = WebPortalContext::fromRequest();
$brand = (array) ($context->domainConfig['brand'] ?? []);
$name = ($brand['name'] ?? '').($brand['accent'] ?? '');
$hub = rtrim((string) config('domains.domains.pressekonto.url'), '/');
$categories = Category::query()
->where('is_active', true)
->whereIn('portal', $context->portalValues)
->with('translations')
->get()
->map(fn (Category $category) => $category->translations->firstWhere('locale', 'de'))
->filter()
->sortBy('name')
->map(fn ($translation) => '- ['.$translation->name.']('.$base.'/de/category/'.$translation->slug.')')
->values()
->all();
return implode("\n", [
'# '.$name,
'',
'> '.($brand['tagline_long'] ?? 'Presseportal für Pressemitteilungen.'),
'',
'Pressemitteilungen werden von Unternehmen eingereicht und vor der Veröffentlichung geprüft. Urheber einer Meldung ist das im Kopf genannte Unternehmen; das Portal ist Herausgeber. Meldungen tragen NewsArticle-Markup (schema.org) mit dem Newsroom des Unternehmens als author, Newsrooms tragen Organization-Markup.',
'',
'## Aufbau',
'',
'- Meldung: '.$base.'/de/press-release/{slug}',
'- 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-Feeds: alle Meldungen '.$base.'/de/feed — je Kategorie '.$base.'/de/category/{slug}/feed — je Newsroom '.$base.'/de/newsroom/{slug}/feed — Übersicht: '.$base.'/de/rss',
'',
'## Kategorien',
'',
...$categories,
'',
'## Für Unternehmen',
'',
'- [Veröffentlichen]('.$base.'/de/veroeffentlichen): Wie eine Meldung auf das Portal kommt',
'- [Preise]('.$base.'/de/preise)',
'- [Was kostet eine Pressemitteilung?]('.$base.'/de/was-kostet-eine-pressemitteilung): Abrechnungsmodelle, Prüfliste für Folgekosten, Rechenbeispiel',
'- [Pressemitteilung veröffentlichen ohne Abo]('.$base.'/de/pressemitteilung-veroeffentlichen-ohne-abo)',
'- [Pressemitteilung löschen oder ändern lassen]('.$base.'/de/pressemitteilung-loeschen-oder-aendern-lassen): Korrektur, Änderung, DSGVO, Depublikation',
'- [Pressemitteilung kostenlos veröffentlichen?]('.$base.'/de/pressemitteilung-kostenlos-veroeffentlichen): Warum es hier keine kostenlose Einreichung gibt, was kostenlose Portale leisten',
'- [Was eine gute Pressemitteilung ausmacht]('.$base.'/de/gute-pressemitteilung)',
'- [Hilfe]('.$base.'/de/hilfe)',
'- [API]('.$base.'/de/api): Einlieferung per REST-API über den Publisher-Bereich '.$hub,
'',
'## Rechtliches',
'',
'- [Impressum]('.$base.'/de/impressum)',
'- [Datenschutz]('.$base.'/de/datenschutz)',
'- [AGB]('.$base.'/de/agb)',
'- [Richtlinien]('.$base.'/de/richtlinien)',
'- [Inhalte melden]('.$base.'/de/melden)',
'',
]);
}
}