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:
1 parent
5824981e2b
commit
07f74e372b
4 files changed
+189
-3
No files matched your search
@@ -266,6 +266,32 @@ class VitecMetaHandler extends MetaHandler
|
||||
$content['seo']['link'] = $links;
|
||||
}
|
||||
|
||||
// --- paginated / filtered list pages: self-referencing canonical --------
|
||||
// /insights/news?page=2 pointed at page 1, which marks every further
|
||||
// page as a duplicate and weakens the links to its articles (0100-FE).
|
||||
if ($canonicalPath === '' && $newsUid === 0 && $productArg === '' && $usecaseUid === 0) {
|
||||
$query = $request->getQueryParams();
|
||||
$keep = [];
|
||||
foreach (['category', 'page'] as $key) {
|
||||
$value = $query[$key] ?? null;
|
||||
if (!is_scalar($value) || trim((string)$value) === '') {
|
||||
continue;
|
||||
}
|
||||
if ($key === 'page' && (int)$value <= 1) {
|
||||
continue;
|
||||
}
|
||||
$keep[$key] = trim((string)$value);
|
||||
}
|
||||
if ($keep !== [] && is_array($content['seo']['link'] ?? null)) {
|
||||
foreach ($content['seo']['link'] as $index => $link) {
|
||||
if (($link['rel'] ?? '') === 'canonical' && is_string($link['href'] ?? null)) {
|
||||
$content['seo']['link'][$index]['href'] =
|
||||
strtok($link['href'], '?') . '?' . http_build_query($keep);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user