*/ private const HUB_PAGES = [ 'faq', 'hilfe', 'gute-pressemitteilung', 'api', 'kuendigung', 'was-kostet-eine-pressemitteilung', 'pressemitteilung-veroeffentlichen-ohne-abo', 'pressemitteilung-loeschen-oder-aendern-lassen', 'pressemitteilung-kostenlos-veroeffentlichen', 'impressum', 'datenschutz', 'agb', 'richtlinien', 'melden', ]; /** @var list */ private const PORTAL_PAGES = [ 'preise', 'veroeffentlichen', 'newsrooms', 'faq', 'hilfe', 'gute-pressemitteilung', 'api', 'rss', 'was-kostet-eine-pressemitteilung', 'pressemitteilung-veroeffentlichen-ohne-abo', 'pressemitteilung-loeschen-oder-aendern-lassen', 'pressemitteilung-kostenlos-veroeffentlichen', 'impressum', 'datenschutz', 'agb', 'richtlinien', 'melden', ]; public function __invoke(Request $request): Response { $host = $request->getHost(); $base = rtrim($request->getSchemeAndHttpHost(), '/'); $isHub = str_contains($host, 'pressekonto'); $xml = Cache::remember('web.sitemap.'.$host, 3600, function () use ($base, $isHub): string { $urls = $isHub ? $this->hubUrls($base) : $this->portalUrls($base); return view('web.sitemap', ['urls' => $urls])->render(); }); return response($xml, 200, ['Content-Type' => 'application/xml; charset=UTF-8']); } /** * Ob dieser Host die Seite führt (Anhang 2, E10): gemeinsame * Informationsseiten liegen auf allen drei Hosts, in die Sitemap gehört * nur die kanonische Adresse. Rechtstexte bleiben je Host gelistet. */ private function leadsPage(string $base, string $page): bool { $canonicalBase = StaticPageCanonical::baseFor($page); return $canonicalBase === null || parse_url($canonicalBase, PHP_URL_HOST) === parse_url($base, PHP_URL_HOST); } /** * @return list */ private function hubUrls(string $base): array { $urls = [['loc' => $base.'/', 'lastmod' => null]]; foreach (self::HUB_PAGES as $page) { if (! $this->leadsPage($base, $page)) { continue; } $urls[] = ['loc' => $base.'/de/'.$page, 'lastmod' => null]; } return $urls; } /** * @return list */ private function portalUrls(string $base): array { $context = WebPortalContext::fromRequest(); $urls = [['loc' => $base.'/de', 'lastmod' => null]]; foreach (self::PORTAL_PAGES as $page) { if (! $this->leadsPage($base, $page)) { continue; } $urls[] = ['loc' => $base.'/de/'.$page, 'lastmod' => null]; } // Kategorien über die lokalisierten Slugs (CategoryTranslation), // dazu je Kategorie die Jahresarchive (2.3). $categories = Category::query() ->where('is_active', true) ->whereIn('portal', $context->portalValues) ->with(['translations', 'children' => fn ($query) => $query->where('is_active', true)]) ->get(); foreach ($categories as $category) { $slug = $category->translations->firstWhere('locale', 'de')?->slug; if (! $slug) { continue; } $urls[] = ['loc' => $base.'/de/category/'.$slug, 'lastmod' => null]; $categoryIds = $category->children->pluck('id')->prepend($category->id)->unique()->values()->all(); foreach (array_keys(CategoryArchivePageData::yearsWithCounts($categoryIds, $context->portalValues, 'de')) as $year) { $urls[] = ['loc' => $base.'/de/category/'.$slug.'/archiv/'.$year, 'lastmod' => null]; } } // Öffentliche Newsrooms: aktive Firmen mit mindestens einer im Archiv // sichtbaren PM des Portals (gleiche Regel wie NewsroomsPageData). $companies = Company::query() ->where('is_active', true) ->whereHas('pressReleases', fn ($query) => $query ->withoutGlobalScope(PortalScope::class) ->visibleInArchive() ->forPortalValues($context->portalValues)) ->get(['slug', 'updated_at']); foreach ($companies as $company) { $urls[] = [ 'loc' => $base.'/de/newsroom/'.$company->slug, 'lastmod' => $company->updated_at?->toAtomString(), ]; } // Alle im Archiv sichtbaren Pressemitteilungen des Portals — bei // Doppelveröffentlichung nur die Erstquelle (Anhang 2, 1c.8): eine // Sitemap soll nur kanonische Adressen nennen. $assigner = app(CanonicalAssigner::class); $releases = PressRelease::query() ->withoutGlobalScope(PortalScope::class) ->visibleInArchive() ->forPortalValues($context->portalValues) ->when($assigner->enabled(), fn ($query) => $query->whereNull('canonical_press_release_id')) ->orderByDesc('published_at') ->get(['id', 'slug', 'portal', 'language', 'category_id', 'canonical_press_release_id', 'published_at', 'content_changed_at']); foreach ($releases as $release) { if (! $assigner->isCanonicalOn($release, $context->portal)) { continue; } // Edition aus der Sprache (wie News-Sitemap und Canonical): die // Detailseite filtert nach Sprache, eine EN-Meldung unter /de/ // wäre ein 404 (Umsetzungsaudit 29.09.2026, R-03). lastmod ist // die letzte inhaltliche Änderung, nicht updated_at (R-02). $edition = in_array($release->language, SetEdition::EDITIONS, true) ? $release->language : SetEdition::DEFAULT_EDITION; $urls[] = [ 'loc' => $base.'/'.$edition.'/press-release/'.$release->slug, 'lastmod' => $release->contentModifiedAt()?->toAtomString(), ]; } return $urls; } }