Meldungen, die über die API eingegangen sind, bekommen in der Veröffentlichungsmail einen Absatz zum Unterschied Erstquelle/Kopie — technische Tatsache, ohne Preis und Angebot; direkt eingereichte nicht. /pressemitteilung-kostenlos-veroeffentlichen beantwortet die Suche nach „kostenlos" ehrlich: kein kostenloser Weg (E7), was kostenlose Portale leisten, warum Links aus Presseportalen kaum tragen (auch bei uns sponsored nofollow), warum Annahme ohne Gegenwert und Prüfung sich ausschließen, wer am Ende zahlt. Die Liste kostenloser Portale ist in config/transparency.php vorbereitet und bleibt leer, bis geprüfte Einträge mit Prüfdatum vorliegen. Unter-Navigation, Hilfe, Sitemaps, llms.txt und die Modelltabelle der Kostenseite verlinken die neue Seite. Anhang 1 und Protokoll Teil 6. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
140 lines
5.2 KiB
PHP
140 lines
5.2 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Web;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Category;
|
|
use App\Models\Company;
|
|
use App\Models\PressRelease;
|
|
use App\Scopes\PortalScope;
|
|
use App\Services\Web\CategoryArchivePageData;
|
|
use App\Support\WebPortalContext;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\Response;
|
|
use Illuminate\Support\Facades\Cache;
|
|
|
|
/**
|
|
* Host-aware XML-Sitemap: Der Hub listet Landing- und Rechts-/Info-Seiten,
|
|
* die Spokes (presseecho, businessportal24) zusätzlich Kategorien, Newsrooms
|
|
* und alle im Archiv sichtbaren Pressemitteilungen ihres Portals. Marketing-
|
|
* Platzhalterseiten (team, presse, karriere, partner, kontakt, ueber-uns)
|
|
* und die Suche bleiben bewusst draußen (R2.4/R3.2). Pro Host eine Stunde
|
|
* gecacht.
|
|
*/
|
|
class SitemapController extends Controller
|
|
{
|
|
/** @var list<string> */
|
|
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<string> */
|
|
private const PORTAL_PAGES = [
|
|
'preise', 'veroeffentlichen', 'newsrooms', 'faq', 'hilfe', 'gute-pressemitteilung', 'api',
|
|
'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']);
|
|
}
|
|
|
|
/**
|
|
* @return list<array{loc: string, lastmod: ?string}>
|
|
*/
|
|
private function hubUrls(string $base): array
|
|
{
|
|
$urls = [['loc' => $base.'/', 'lastmod' => null]];
|
|
|
|
foreach (self::HUB_PAGES as $page) {
|
|
$urls[] = ['loc' => $base.'/de/'.$page, 'lastmod' => null];
|
|
}
|
|
|
|
return $urls;
|
|
}
|
|
|
|
/**
|
|
* @return list<array{loc: string, lastmod: ?string}>
|
|
*/
|
|
private function portalUrls(string $base): array
|
|
{
|
|
$context = WebPortalContext::fromRequest();
|
|
|
|
$urls = [['loc' => $base.'/de', 'lastmod' => null]];
|
|
|
|
foreach (self::PORTAL_PAGES as $page) {
|
|
$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.
|
|
$releases = PressRelease::query()
|
|
->withoutGlobalScope(PortalScope::class)
|
|
->visibleInArchive()
|
|
->forPortalValues($context->portalValues)
|
|
->orderByDesc('published_at')
|
|
->get(['slug', 'published_at', 'updated_at']);
|
|
|
|
foreach ($releases as $release) {
|
|
$urls[] = [
|
|
'loc' => $base.'/de/press-release/'.$release->slug,
|
|
'lastmod' => ($release->updated_at ?? $release->published_at)?->toAtomString(),
|
|
];
|
|
}
|
|
|
|
return $urls;
|
|
}
|
|
}
|