DEV CommiDEV Committ
This commit is contained in:
1 parent
4541c58fe7
commit
c6759186cb
145 files changed
+2742
-579
No files matched your search
@@ -217,6 +217,20 @@ class DownloadcardJsonRenderer
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
$thumbnailUrl = null;
|
||||
try {
|
||||
$fileObj = GeneralUtility::makeInstance(TYPO3CMSCoreResourceResourceFactory::class)
|
||||
->getFileObject((int)$row["file_uid"]);
|
||||
$processed = $fileObj->process(
|
||||
TYPO3CMSCoreResourceProcessedFile::CONTEXT_IMAGEPREVIEW,
|
||||
["width" => 400, "height" => 566]
|
||||
);
|
||||
$thumbnailUrl = $processed->getPublicUrl();
|
||||
} catch (Throwable $e) {
|
||||
// ignore — keep null
|
||||
}
|
||||
|
||||
return [
|
||||
'uid' => (int)$row['uid'],
|
||||
'title' => (string)($row['title'] ?? ''),
|
||||
@@ -284,11 +298,26 @@ class DownloadcardJsonRenderer
|
||||
return null;
|
||||
}
|
||||
|
||||
$thumbnailUrl = null;
|
||||
try {
|
||||
$fileObj = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Resource\ResourceFactory::class)
|
||||
->getFileObject((int)$row['file_uid']);
|
||||
$processed = $fileObj->process(
|
||||
\TYPO3\CMS\Core\Resource\ProcessedFile::CONTEXT_IMAGEPREVIEW,
|
||||
['width' => 400, 'height' => 566]
|
||||
);
|
||||
$thumbnailUrl = $processed->getPublicUrl();
|
||||
} catch (\Throwable $e) {
|
||||
// ignore — keep null
|
||||
}
|
||||
|
||||
return [
|
||||
|
||||
'uid' => (int)$row['file_uid'],
|
||||
'name' => (string)($row['name'] ?? ''),
|
||||
'url' => '/fileadmin' . ($row['identifier'] ?? ''),
|
||||
'size' => (int)($row['size'] ?? 0),
|
||||
'thumbnail' => $thumbnailUrl,
|
||||
'size' => (int)($row['size'] ?? 0),
|
||||
'extension' => (string)($row['extension'] ?? ''),
|
||||
'mimeType' => (string)($row['mime_type'] ?? ''),
|
||||
'title' => (string)($row['title'] ?? ''),
|
||||
|
||||
@@ -212,6 +212,7 @@ class DownloadcardcollectionJsonRenderer
|
||||
);
|
||||
$srcset[] = [
|
||||
'url' => $imageService->getImageUri($variant),
|
||||
'thumbnail' => $thumbnailUrl,
|
||||
'width' => $width,
|
||||
'descriptor' => $width . 'w',
|
||||
];
|
||||
@@ -225,6 +226,7 @@ class DownloadcardcollectionJsonRenderer
|
||||
return [
|
||||
'uid' => (int)$fileRefData['uid'],
|
||||
'url' => $imageService->getImageUri($default),
|
||||
'thumbnail' => $thumbnailUrl,
|
||||
'title' => $fileRefData['title'] ?? '',
|
||||
'alternative' => $fileRefData['alternative'] ?? '',
|
||||
'description' => $fileRefData['description'] ?? '',
|
||||
@@ -297,10 +299,25 @@ class DownloadcardcollectionJsonRenderer
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
$thumbnailUrl = null;
|
||||
try {
|
||||
$fileObj = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Resource\ResourceFactory::class)
|
||||
->getFileObject((int)$row['file_uid']);
|
||||
$processed = $fileObj->process(
|
||||
\TYPO3\CMS\Core\Resource\ProcessedFile::CONTEXT_IMAGEPREVIEW,
|
||||
['width' => 400, 'height' => 566]
|
||||
);
|
||||
$thumbnailUrl = $processed->getPublicUrl();
|
||||
} catch (\Throwable $e) {
|
||||
// ignore — keep null
|
||||
}
|
||||
|
||||
return [
|
||||
'uid' => (int)$row['file_uid'],
|
||||
'name' => (string)($row['name'] ?? ''),
|
||||
'url' => '/fileadmin' . ($row['identifier'] ?? ''),
|
||||
'thumbnail' => $thumbnailUrl,
|
||||
'size' => (int)($row['size'] ?? 0),
|
||||
'extension' => (string)($row['extension'] ?? ''),
|
||||
'mimeType' => (string)($row['mime_type'] ?? ''),
|
||||
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\UserFunc;
|
||||
|
||||
use Evomedien\Vitec\Service\StructuredDataService;
|
||||
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||
use TYPO3\CMS\Extbase\Service\ImageService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use Doctrine\DBAL\ParameterType;
|
||||
|
||||
/**
|
||||
* Emits the page-level schema.org @graph (Organization, WebSite,
|
||||
* BreadcrumbList, NewsArticle) as a JSON-LD string for the headless page
|
||||
* object. Wired in setup.typoscript as:
|
||||
*
|
||||
* page.10.fields.jsonLd = USER
|
||||
* page.10.fields.jsonLd.userFunc = Evomedien\Vitec\UserFunc\PageJsonLdRenderer->render
|
||||
*
|
||||
* The JS frontend wraps the returned string in
|
||||
* `<script type="application/ld+json">…</script>` inside the page <head>.
|
||||
*
|
||||
* Organization data is pulled from site settings (category "seo"); the
|
||||
* canonical absolute base URL likewise (with a request-host fallback) because
|
||||
* the headless site base is "/".
|
||||
*
|
||||
* Exception-safe: returns '' on any failure, never breaks the page JSON.
|
||||
*/
|
||||
final class PageJsonLdRenderer
|
||||
{
|
||||
/** News-detail backend layouts (see BackendLayoutDataProvider::LAYOUT_MAP). */
|
||||
private const NEWS_DETAIL_LAYOUTS = [13, 14, 15];
|
||||
|
||||
#[AsAllowedCallable]
|
||||
public function render(string $content, array $conf): string
|
||||
{
|
||||
try {
|
||||
$request = $GLOBALS['TYPO3_REQUEST'] ?? null;
|
||||
if ($request === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$pageInfo = $request->getAttribute('frontend.page.information');
|
||||
if ($pageInfo === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$pageId = (int)$pageInfo->getId();
|
||||
$pageRecord = $pageInfo->getPageRecord();
|
||||
$rootLine = $pageInfo->getRootLine();
|
||||
|
||||
$site = $request->getAttribute('site');
|
||||
$settings = $site?->getSettings();
|
||||
$base = $this->resolveCanonicalBase($request, $settings);
|
||||
|
||||
$service = GeneralUtility::makeInstance(StructuredDataService::class);
|
||||
|
||||
$orgCfg = $this->organizationConfig($settings, $base);
|
||||
$hasOrganization = ($orgCfg['name'] ?? '') !== '';
|
||||
|
||||
$nodes = [];
|
||||
|
||||
// Organization on every page (anchors @id references).
|
||||
if ($hasOrganization) {
|
||||
$nodes[] = $service->buildOrganization($orgCfg);
|
||||
}
|
||||
|
||||
// WebSite only on the site root / home page.
|
||||
$isHome = $site !== null && $pageId === (int)$site->getRootPageId();
|
||||
if ($isHome) {
|
||||
$siteName = (string)($settings?->get('seo.site.name') ?? '');
|
||||
if ($siteName === '') {
|
||||
$siteName = (string)($orgCfg['name'] ?? '');
|
||||
}
|
||||
$nodes[] = $service->buildWebSite($base, $siteName, $hasOrganization);
|
||||
}
|
||||
|
||||
// BreadcrumbList from the rootline.
|
||||
$breadcrumb = $service->buildBreadcrumbList($this->breadcrumbItems($rootLine), $base);
|
||||
if ($breadcrumb !== null) {
|
||||
$nodes[] = $breadcrumb;
|
||||
}
|
||||
|
||||
// NewsArticle on news-detail pages.
|
||||
if (in_array((int)($pageRecord['layout'] ?? 0), self::NEWS_DETAIL_LAYOUTS, true)) {
|
||||
$nodes[] = $service->buildNewsArticle(
|
||||
$pageRecord,
|
||||
$base,
|
||||
$this->pageMediaUrls($pageId, $base, $service),
|
||||
$hasOrganization
|
||||
);
|
||||
}
|
||||
|
||||
return $service->encodeGraph($nodes);
|
||||
} catch (\Throwable $e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Canonical absolute base, e.g. "https://www.vitec.com".
|
||||
* Prefers the explicit site setting, falls back to the request host.
|
||||
*/
|
||||
private function resolveCanonicalBase($request, $settings): string
|
||||
{
|
||||
$configured = trim((string)($settings?->get('seo.site.canonicalBase') ?? ''));
|
||||
if ($configured !== '') {
|
||||
return rtrim($configured, '/');
|
||||
}
|
||||
|
||||
$normalizedParams = $request->getAttribute('normalizedParams');
|
||||
if ($normalizedParams !== null) {
|
||||
return rtrim($normalizedParams->getSiteUrl(), '/');
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Assemble the Organization config from site settings.
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function organizationConfig($settings, string $base): array
|
||||
{
|
||||
$get = static fn (string $key): string => trim((string)($settings?->get($key) ?? ''));
|
||||
|
||||
$sameAsRaw = $get('seo.organization.sameAs');
|
||||
$sameAs = $sameAsRaw !== ''
|
||||
? array_filter(array_map('trim', preg_split('/[\r\n,]+/', $sameAsRaw) ?: []))
|
||||
: [];
|
||||
|
||||
return [
|
||||
'name' => $get('seo.organization.name'),
|
||||
'legalName' => $get('seo.organization.legalName'),
|
||||
'base' => $base,
|
||||
'logoUrl' => $get('seo.organization.logoUrl'),
|
||||
'sameAs' => array_values($sameAs),
|
||||
'email' => $get('seo.organization.email'),
|
||||
'phone' => $get('seo.organization.phone'),
|
||||
'street' => $get('seo.organization.street'),
|
||||
'postalCode' => $get('seo.organization.postalCode'),
|
||||
'locality' => $get('seo.organization.locality'),
|
||||
'country' => $get('seo.organization.country'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build ordered breadcrumb items (root → current) from the rootline.
|
||||
* Skips spacers, folders and recyclers; uses nav_title with title fallback.
|
||||
*
|
||||
* @param array<int,array<string,mixed>> $rootLine
|
||||
* @return list<array{name:string, url:string}>
|
||||
*/
|
||||
private function breadcrumbItems(array $rootLine): array
|
||||
{
|
||||
$skipDoktypes = [199, 254, 255];
|
||||
$items = [];
|
||||
|
||||
// Rootline is ordered current → root; reverse for breadcrumb order.
|
||||
foreach (array_reverse($rootLine) as $page) {
|
||||
$doktype = (int)($page['doktype'] ?? 1);
|
||||
if (in_array($doktype, $skipDoktypes, true)) {
|
||||
continue;
|
||||
}
|
||||
// Matches the visible headless breadcrumb (HMENU special=rootline),
|
||||
// which keeps nav_hide pages for path integrity.
|
||||
$name = trim((string)($page['nav_title'] ?? '')) ?: trim((string)($page['title'] ?? ''));
|
||||
if ($name === '') {
|
||||
continue;
|
||||
}
|
||||
$items[] = [
|
||||
'name' => $name,
|
||||
'url' => (string)($page['slug'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Absolute URLs of the page's `media` images (used as NewsArticle image).
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
private function pageMediaUrls(int $pageId, string $base, StructuredDataService $service): array
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable('sys_file_reference');
|
||||
|
||||
$rows = $queryBuilder
|
||||
->select('uid')
|
||||
->from('sys_file_reference')
|
||||
->where(
|
||||
$queryBuilder->expr()->eq('uid_foreign', $queryBuilder->createNamedParameter($pageId, ParameterType::INTEGER)),
|
||||
$queryBuilder->expr()->eq('tablenames', $queryBuilder->createNamedParameter('pages', ParameterType::STRING)),
|
||||
$queryBuilder->expr()->eq('fieldname', $queryBuilder->createNamedParameter('media', ParameterType::STRING)),
|
||||
$queryBuilder->expr()->eq('deleted', 0),
|
||||
$queryBuilder->expr()->eq('hidden', 0)
|
||||
)
|
||||
->orderBy('sorting_foreign', 'ASC')
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
|
||||
if ($rows === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
|
||||
$imageService = GeneralUtility::makeInstance(ImageService::class);
|
||||
$urls = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
try {
|
||||
$fileReference = $resourceFactory->getFileReferenceObject((int)$row['uid']);
|
||||
$processed = $imageService->applyProcessingInstructions($fileReference, ['width' => 1200]);
|
||||
$urls[] = $service->absUrl($imageService->getImageUri($processed), $base);
|
||||
} catch (\Throwable $e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return $urls;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Evomedien\Vitec\UserFunc;
|
||||
|
||||
|
||||
use Evomedien\Vitec\Service\StructuredDataService;
|
||||
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Service\FlexFormService;
|
||||
@@ -12,7 +13,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
|
||||
use TYPO3\CMS\Core\Resource\FileReference;
|
||||
use TYPO3\CMS\Core\Imaging\ImageService;
|
||||
use TYPO3\CMS\Extbase\Service\ImageService;
|
||||
use Doctrine\DBAL\ParameterType;
|
||||
|
||||
/**
|
||||
@@ -176,7 +177,7 @@ class ProductShowJsonRenderer
|
||||
{
|
||||
$uid = (int)$product['uid'];
|
||||
|
||||
return [
|
||||
$data = [
|
||||
'uid' => $uid,
|
||||
|
||||
'title' => (string)($product['title'] ?? ''),
|
||||
@@ -215,6 +216,79 @@ class ProductShowJsonRenderer
|
||||
'videofile' => $this->getProductVideoFile($uid),
|
||||
'relatedprodukt' => $this->getRelatedProducts($uid),
|
||||
];
|
||||
|
||||
// schema.org JSON-LD (Product + optional VideoObject) as a ready-to-emit
|
||||
// @graph string. The frontend injects it as <script type="application/ld+json">.
|
||||
// Supersedes the manual `structureddata` field above (kept for backwards
|
||||
// compatibility until the frontend has switched over).
|
||||
$data['jsonLd'] = $this->buildJsonLd($product, $data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the product's structured-data @graph from the already-serialized
|
||||
* product array. Exception-safe: returns '' on any failure.
|
||||
*
|
||||
* @param array<string,mixed> $rawProduct raw DB row (for crdate/tstamp)
|
||||
* @param array<string,mixed> $data serialized product
|
||||
*/
|
||||
protected function buildJsonLd(array $rawProduct, array $data): string
|
||||
{
|
||||
try {
|
||||
$service = GeneralUtility::makeInstance(StructuredDataService::class);
|
||||
$base = $this->resolveCanonicalBase();
|
||||
|
||||
$imageUrls = [];
|
||||
foreach (($data['images'] ?? []) as $img) {
|
||||
if (!empty($img['url'])) {
|
||||
$imageUrls[] = $service->absUrl((string)$img['url'], $base);
|
||||
}
|
||||
}
|
||||
if ($imageUrls === [] && !empty($data['ogimage']['url'])) {
|
||||
$imageUrls[] = $service->absUrl((string)$data['ogimage']['url'], $base);
|
||||
}
|
||||
|
||||
$hasOrganization = trim((string)($this->siteSettings()?->get('seo.organization.name') ?? '')) !== '';
|
||||
|
||||
$product = $service->buildProduct($data, $base, $imageUrls, $hasOrganization);
|
||||
|
||||
$uploadTs = (int)($rawProduct['crdate'] ?? $rawProduct['tstamp'] ?? 0);
|
||||
$video = $service->buildVideoObject($data, $base, $imageUrls, $uploadTs);
|
||||
|
||||
return $service->encodeGraph([$product, $video]);
|
||||
} catch (\Throwable $e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Site settings of the current request, or null outside a site context.
|
||||
*/
|
||||
protected function siteSettings(): ?\TYPO3\CMS\Core\Site\Entity\SiteSettings
|
||||
{
|
||||
$request = $GLOBALS['TYPO3_REQUEST'] ?? null;
|
||||
return $request?->getAttribute('site')?->getSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Canonical absolute base (e.g. "https://www.vitec.com"). Prefers the
|
||||
* explicit site setting, falls back to the request host.
|
||||
*/
|
||||
protected function resolveCanonicalBase(): string
|
||||
{
|
||||
$configured = trim((string)($this->siteSettings()?->get('seo.site.canonicalBase') ?? ''));
|
||||
if ($configured !== '') {
|
||||
return rtrim($configured, '/');
|
||||
}
|
||||
|
||||
$request = $GLOBALS['TYPO3_REQUEST'] ?? null;
|
||||
$normalizedParams = $request?->getAttribute('normalizedParams');
|
||||
if ($normalizedParams !== null) {
|
||||
return rtrim($normalizedParams->getSiteUrl(), '/');
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
protected function getProductImages(int $productUid): array
|
||||
|
||||
Reference in new issue
Block a user