Antworten auf 0100/0102/0104/0108-FE: Canonical paginierter Listen, JSON-LD-Breadcrumb auf den Datensatz, Story-Liste ohne crops, Startseiten-Daten

VitecMetaHandler: /insights/news?page=2 (und ?category=) bekommt einen
selbstreferenzierenden Canonical statt Seite 1 (0100-FE 5.1).
PageJsonLdRenderer: auf Produkt-, News- und Story-Detailseiten ersetzt
der Datensatz (Titel + URL) die Shell-Seite als letztes Breadcrumb-
Element (0108-FE F11). UsecaseListJsonRenderer: cardImage.crops aus den
Listen-Karten entfernt, ~20 kB pro Seite mit Testimonial-Band
(0102-FE 5). Migration fe_answers_fixes_20261111 (angewendet):
Platzhalter 'H1 Header' (/solutions) und 'Featured Product' (Startseite,
Lorem ipsum, CTA auf /product) versteckt; Karte 'Government' auf
/markets/government; Button in #699 'Explore our solutions'; Story CDC
h4 'Product Highlight' -> h3 (0104-FE 4.1).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
orascheandClaude Fable 5.1 committed 2026-11-11 10:14:49 +01:00
1 parent 5824981e2b
commit 07f74e372b
4 files changed
+189 -3

No files matched your search

@@ -78,8 +78,17 @@ final class PageJsonLdRenderer
$nodes[] = $service->buildWebSite($base, $siteName, $hasOrganization);
}
// BreadcrumbList from the rootline.
$breadcrumb = $service->buildBreadcrumbList($this->breadcrumbItems($rootLine), $base);
// BreadcrumbList from the rootline. On record detail pages the last
// crumb is the generic shell page ("Story", "Product", "Article")
// and is swapped for the record itself (0108-FE, F11).
$args = $this->routeArguments($request);
$crumbs = $this->breadcrumbItems($rootLine);
$recordCrumb = $this->recordCrumb($args);
if ($recordCrumb !== null && $crumbs !== []) {
array_pop($crumbs);
$crumbs[] = $recordCrumb;
}
$breadcrumb = $service->buildBreadcrumbList($crumbs, $base);
if ($breadcrumb !== null) {
$nodes[] = $breadcrumb;
}
@@ -113,7 +122,6 @@ final class PageJsonLdRenderer
// Product on product detail pages (routed record, SEO audit 2026-11-09).
$args = $this->routeArguments($request);
$productArg = (string)($args['tx_vitec_productshow']['product'] ?? '');
if ($productArg !== '') {
$productRow = $this->fetchDetailRecord(
@@ -220,6 +228,40 @@ final class PageJsonLdRenderer
];
}
/**
* Breadcrumb item for the routed detail record (product, news, story), or
* null on plain pages.
*
* @param array<string,mixed> $args
* @return array{name:string, url:string}|null
*/
private function recordCrumb(array $args): ?array
{
$productArg = (string)($args['tx_vitec_productshow']['product'] ?? '');
if ($productArg !== '') {
$row = $this->fetchDetailRecord('tx_vitec_domain_model_product', is_numeric($productArg) ? 'uid' : 'slug', $productArg);
if ($row !== null && (string)($row['slug'] ?? '') !== '') {
return ['name' => (string)($row['title'] ?? ''), 'url' => '/product/' . $row['slug']];
}
}
$newsUid = (int)($args['tx_news_pi1']['news'] ?? 0);
if ($newsUid > 0) {
$row = $this->fetchDetailRecord('tx_news_domain_model_news', 'uid', (string)$newsUid);
if ($row !== null && (string)($row['path_segment'] ?? '') !== '') {
$name = trim((string)($row['alternative_title'] ?? '')) ?: (string)($row['title'] ?? '');
return ['name' => $name, 'url' => '/news/article/' . $row['path_segment']];
}
}
$usecaseUid = (int)($args['tx_vitec_usecaseshow']['usecase'] ?? 0);
if ($usecaseUid > 0) {
$row = $this->fetchDetailRecord('tx_vitec_domain_model_usecase', 'uid', (string)$usecaseUid);
if ($row !== null && (string)($row['slug'] ?? '') !== '') {
return ['name' => (string)($row['title'] ?? ''), 'url' => '/success-stories/' . $row['slug']];
}
}
return null;
}
/**
* Build ordered breadcrumb items (root → current) from the rootline.
* Skips spacers, folders and recyclers; uses nav_title with title fallback.
@@ -178,6 +178,11 @@ class UsecaseListJsonRenderer
$usecases = array_map(
static function (array $u) use ($serializer, $detailBase): array {
$item = $serializer->serializeListItem($u);
// The list cards read url/srcset/alternative only; the card
// crop variants cost about 1 kB per story (0102-FE, F8).
if (is_array($item['cardImage'] ?? null)) {
unset($item['cardImage']['crops']);
}
// A story with its own detail page wins; only the others
// fall back to the Single PID plus the slug, so nothing
// changes for the stories that have no page assigned.