web: Entitaeten im JSON-LD verknuepft, News-Sitemap und llms.txt (Roadmap 2.1, 2.2, 2.7)

- App\Support\StructuredData: Portal-Organization als Publisher mit @id
  und Logo (ImageObject), Newsroom-@id; NewsArticle.author auf der
  Meldung und Organization auf der Newsroom-Seite beschreiben jetzt
  dieselbe Entitaet. Startseiten nutzen denselben Publisher.
- /sitemap-news.xml je Spoke (NewsSitemapController): im Archiv
  sichtbare Meldungen der letzten 48 h, max. 1.000, news:publication
  mit Portalname und Sprache; 15 min gecacht, mit den anderen Sitemaps
  geleert; in robots.txt der Spokes. Hub liefert leere Liste.
- /llms.txt je Host (LlmsTxtController): sachliche Beschreibung,
  URL-Aufbau, aktive Kategorien, Seiten fuer Unternehmen, Rechtliches;
  Hub verweist auf beide Portale. Keine Aussagen ueber KI-Nennungen.
- Protokoll Teil 5 (Block 2) inkl. Betreiber-Schritten fuer das
  Publisher Center und Befund zu 2.5/E5; Roadmap 2.1/2.7 abgehakt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-09-17 10:15:28 +00:00
parent 8cd76df0bb
commit b60a28dc9c
15 changed files with 521 additions and 23 deletions

View file

@ -1,10 +1,12 @@
@extends('web.layouts.web-master')
@php
use App\Support\StructuredData;
$brand = config('domains.domains.businessportal24.brand', []);
// SEO: WebSite-Markup mit Sitelinks-Searchbox (SearchAction auf /suche).
$portalBase = rtrim($domainUrl ?? config('app.url'), '/');
$portalBase = StructuredData::portalBase($domainUrl ?? null);
$brandFullName = ($brand['name'] ?? 'businessportal').($brand['accent'] ?? '24');
$jsonLd = [
@ -24,11 +26,7 @@
'query-input' => 'required name=search_term_string',
],
],
[
'@type' => 'Organization',
'name' => $brandFullName,
'url' => $portalBase.'/',
],
StructuredData::publisher($portalBase),
],
];
@endphp

View file

@ -1,6 +1,7 @@
@extends('web.layouts.web-master')
@php
use App\Support\StructuredData;
use Illuminate\Support\Str;
\Carbon\Carbon::setLocale(app()->getLocale());
@ -34,10 +35,11 @@
// SEO: Der Newsroom ist die kanonische Firmenseite im Portal —
// Organization-Markup verknüpft PMs (author) mit diesem Profil.
// Portal-Basis wie beim Canonical im Layout (unabhängig von forceRootUrl).
$newsroomUrl = rtrim($domainUrl ?? config('app.url'), '/').'/'.app()->getLocale().'/newsroom/'.$company->slug;
$newsroomUrl = StructuredData::newsroomUrl(StructuredData::portalBase($domainUrl ?? null), $company->slug);
$jsonLd = [
'@context' => 'https://schema.org',
'@type' => 'Organization',
'@id' => StructuredData::newsroomId($newsroomUrl),
'name' => $company->name,
'url' => $newsroomUrl,
...($company->websiteUrl() ? ['sameAs' => [$company->websiteUrl()]] : []),

View file

@ -1,10 +1,12 @@
@extends('web.layouts.web-master')
@php
use App\Support\StructuredData;
$brand = config('domains.domains.presseecho.brand', []);
// SEO: WebSite-Markup mit Sitelinks-Searchbox (SearchAction auf /suche).
$portalBase = rtrim($domainUrl ?? config('app.url'), '/');
$portalBase = StructuredData::portalBase($domainUrl ?? null);
$brandFullName = ($brand['name'] ?? 'presse').($brand['accent'] ?? 'echo');
$jsonLd = [
@ -24,11 +26,7 @@
'query-input' => 'required name=search_term_string',
],
],
[
'@type' => 'Organization',
'name' => $brandFullName,
'url' => $portalBase.'/',
],
StructuredData::publisher($portalBase),
],
];
@endphp

View file

@ -1,6 +1,7 @@
@extends('web.layouts.web-master')
@php
use App\Support\StructuredData;
use Illuminate\Support\Str;
// Private Vorschau über Magic-Link (/pm-vorschau/{token}): identisches
@ -93,7 +94,8 @@
$jsonLd = null;
if (! $isPreview) {
$portalBase = rtrim($domainUrl ?? config('app.url'), '/');
$portalBase = StructuredData::portalBase($domainUrl ?? null);
$authorNewsroomUrl = $company?->slug ? StructuredData::newsroomUrl($portalBase, $company->slug) : null;
// dateModified darf laut Google nicht vor datePublished liegen
// (updated_at kann bei terminierten PMs älter sein als published_at).
@ -125,12 +127,15 @@
'wordCount' => $wordCount ?: null,
'inLanguage' => app()->getLocale(),
'isAccessibleForFree' => true,
// author trägt die @id der Newsroom-Organization (2.1): Meldung
// und Newsroom-Seite beschreiben dieselbe Entität.
'author' => $companyName ? array_filter([
'@type' => 'Organization',
'@id' => $authorNewsroomUrl ? StructuredData::newsroomId($authorNewsroomUrl) : null,
'name' => $companyName,
'url' => $company?->slug ? $portalBase.'/'.app()->getLocale().'/newsroom/'.$company->slug : null,
'url' => $authorNewsroomUrl,
]) : null,
'publisher' => ['@type' => 'Organization', 'name' => $brandLabel, 'url' => $portalBase],
'publisher' => StructuredData::publisher($portalBase),
], fn ($value) => $value !== null),
[
'@type' => 'BreadcrumbList',

View file

@ -0,0 +1,16 @@
{!! '<?xml version="1.0" encoding="UTF-8"?>' !!}
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9">
@foreach ($entries as $entry)
<url>
<loc>{{ $entry['loc'] }}</loc>
<news:news>
<news:publication>
<news:name>{{ $entry['publication'] }}</news:name>
<news:language>{{ $entry['language'] }}</news:language>
</news:publication>
<news:publication_date>{{ $entry['published_at'] }}</news:publication_date>
<news:title>{{ $entry['title'] }}</news:title>
</news:news>
</url>
@endforeach
</urlset>