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:
1 parent
c6759186cb
commit
bb7c03235d
237 files changed
+6698
-1749
No files matched your search
Executable → Regular
+142
@@ -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.
|
||||
*
|
||||
|
||||
Reference in new issue
Block a user