Checkpoint: headless JSON architecture documented + cleanup (Stufe 1+2)

Restore point before Stufe 3 (central resolver-service refactoring).

Includes this session's work:
- Success Story (usecase) rebuild: new fields/tabs, UsecaseSerializer,
  thin List/Show renderers, MM relations, inline content elements.
- vitec_columns content block (two-column layout with per-item content)
  incl. unified header section; special-cased JSON resolution.
- Container per-column flex (items[].config align/justify) + gap on parent.
- Unified header section across CEs/plugins/containers; header fields in JSON.
- ISO-style architecture spec: Documentation/Headless-JSON-Architecture.md.
- Cleanup: removed local scratch + verified .bak backups.
- Fixes: B-6 (undefined thumbnail variable in Downloadcardcollection image
  resolver), B-7 (unsatisfiable legacy CType OR branch in Downloadcard/Datasheets).

Rollback: git reset --hard snapshot-2026-07-09-pre-stufe3

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
o-rascheandClaude Opus 4.8 committed 2026-07-09 10:01:56 +02:00
1 parent c6759186cb
commit bb7c03235d
237 files changed
+6698 -1749

No files matched your search

+142
View File
@@ -305,6 +305,148 @@ final class StructuredDataService
return $video;
}
/**
* schema.org FAQPage from a list of question/answer pairs.
* Answers may contain RTE HTML — tags are stripped for the JSON-LD text.
*
* @param list<array{question:string,answer:string}> $items
*/
public function buildFaqPage(array $items, string $base): ?array
{
$mainEntity = [];
foreach ($items as $item) {
$question = trim((string)($item['question'] ?? ''));
$answer = trim(strip_tags((string)($item['answer'] ?? '')));
if ($question === '' || $answer === '') {
continue;
}
$mainEntity[] = [
'@type' => 'Question',
'name' => $question,
'acceptedAnswer' => [
'@type' => 'Answer',
'text' => $answer,
],
];
}
if ($mainEntity === []) {
return null;
}
return [
'@type' => 'FAQPage',
'mainEntity' => $mainEntity,
];
}
/**
* schema.org ExhibitionEvent (trade-show) node from a
* tx_vitec_domain_model_event row.
*
* Google rich-result requirements: name, startDate, location.
* Recommended extras handled here: endDate, description, image, url,
* eventStatus, eventAttendanceMode, organizer.
*
* @param array<string,mixed> $e Event DB row
* @param list<string> $imageUrls Absolute or root-relative logo URLs
*/
public function buildEvent(array $e, string $base, array $imageUrls, bool $hasOrganization): ?array
{
$name = trim((string)($e['title'] ?? ''));
$start = (int)($e['eventstart'] ?? 0);
if ($name === '' || $start === 0) {
return null;
}
$event = [
'@type' => 'ExhibitionEvent',
'name' => $name,
'startDate' => date('Y-m-d', $start),
];
$end = (int)($e['eventend'] ?? 0);
if ($end > 0) {
$event['endDate'] = date('Y-m-d', $end);
}
$description = trim(strip_tags((string)($e['description'] ?? '')));
if ($description === '') {
$description = trim((string)($e['teaser'] ?? ''));
}
if ($description !== '') {
$event['description'] = $description;
}
// Location: Place with venue + city/country, or VirtualLocation for online.
$mode = (string)($e['attendancemode'] ?? 'offline');
$venue = trim((string)($e['venue'] ?? ''));
$city = trim((string)($e['city'] ?? ''));
$country = trim((string)($e['country'] ?? ''));
if ($mode === 'online') {
$url = trim((string)($e['eventurl'] ?? ''));
if ($url !== '') {
$event['location'] = [
'@type' => 'VirtualLocation',
'url' => $this->absUrl($url, $base),
];
}
} else {
$address = array_filter([
'@type' => 'PostalAddress',
'addressLocality' => $city !== '' ? $city : null,
'addressCountry' => $country !== '' ? $country : null,
]);
$place = array_filter([
'@type' => 'Place',
'name' => $venue !== '' ? $venue : ($city !== '' ? $city : null),
'address' => count($address) > 1 ? $address : null,
]);
if (count($place) > 1) {
$event['location'] = $place;
}
}
$statusMap = [
'scheduled' => 'https://schema.org/EventScheduled',
'cancelled' => 'https://schema.org/EventCancelled',
'postponed' => 'https://schema.org/EventPostponed',
'rescheduled' => 'https://schema.org/EventRescheduled',
'movedonline' => 'https://schema.org/EventMovedOnline',
];
$event['eventStatus'] = $statusMap[(string)($e['eventstatus'] ?? 'scheduled')] ?? $statusMap['scheduled'];
$modeMap = [
'offline' => 'https://schema.org/OfflineEventAttendanceMode',
'online' => 'https://schema.org/OnlineEventAttendanceMode',
'mixed' => 'https://schema.org/MixedEventAttendanceMode',
];
$event['eventAttendanceMode'] = $modeMap[$mode] ?? $modeMap['offline'];
$images = [];
foreach ($imageUrls as $imageUrl) {
$abs = $this->absUrl((string)$imageUrl, $base);
if ($abs !== '') {
$images[] = $abs;
}
}
if ($images !== []) {
$event['image'] = $images;
}
$eventUrl = trim((string)($e['eventurl'] ?? ''));
if ($eventUrl !== '' && !str_starts_with($eventUrl, 't3://')) {
$event['url'] = $this->absUrl($eventUrl, $base);
}
if ($hasOrganization) {
$event['organizer'] = ['@id' => $base . '/#organization'];
}
return $event;
}
/**
* Wrap one or more schema nodes into a single @graph document string.
*