getQueryParams()['uid'] ?? 0); if ($uid <= 0) { return new JsonResponse([ 'success' => false, 'message' => 'Bitte das Produkt zuerst speichern, dann erneut generieren.', ]); } $product = $this->loadProduct($uid); if ($product === null) { return new JsonResponse([ 'success' => false, 'message' => 'Produkt nicht gefunden.', ]); } $base = $this->resolveCanonicalBase((int)($product['pid'] ?? 0), $request); $data = [ 'uid' => $uid, 'title' => (string)($product['title'] ?? ''), 'slug' => (string)($product['slug'] ?? ''), 'teaser' => (string)($product['teaser'] ?? ''), 'description' => (string)($product['description'] ?? ''), 'video' => (string)($product['video'] ?? ''), 'categories' => $this->categories($uid), 'videofile' => $this->videoFile($uid, $base), ]; $images = $this->imageUrls($uid, $base); // Self-contained snippet → no @id reference to the page-level Organization. $productNode = $this->structuredDataService->buildProduct($data, $base, $images, false); $videoNode = $this->structuredDataService->buildVideoObject( $data, $base, $images, (int)($product['crdate'] ?? $product['tstamp'] ?? 0) ); $nodes = array_values(array_filter([$productNode, $videoNode])); // One node → standalone object with @context; multiple → @graph. if (count($nodes) === 1) { $document = array_merge(['@context' => 'https://schema.org'], $nodes[0]); } else { $document = ['@context' => 'https://schema.org', '@graph' => $nodes]; } $json = (string)json_encode( $document, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); return new JsonResponse([ 'success' => true, 'jsonLd' => $json, ]); } /** * @return array|null */ private function loadProduct(int $uid): ?array { $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable('tx_vitec_domain_model_product'); $row = $queryBuilder ->select('*') ->from('tx_vitec_domain_model_product') ->where( $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, ParameterType::INTEGER)), $queryBuilder->expr()->eq('deleted', 0) ) ->executeQuery() ->fetchAssociative(); return $row ?: null; } /** * @return list */ private function categories(int $productUid): array { $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable('sys_category'); $rows = $queryBuilder ->select('c.title') ->from('sys_category', 'c') ->join( 'c', 'sys_category_record_mm', 'mm', 'mm.uid_local = c.uid AND mm.tablenames = ' . $queryBuilder->createNamedParameter('tx_vitec_domain_model_product', ParameterType::STRING) . ' AND mm.fieldname = ' . $queryBuilder->createNamedParameter('categories', ParameterType::STRING) ) ->where( $queryBuilder->expr()->eq('mm.uid_foreign', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), $queryBuilder->expr()->eq('c.deleted', 0), $queryBuilder->expr()->eq('c.hidden', 0) ) ->orderBy('mm.sorting', 'ASC') ->executeQuery() ->fetchAllAssociative(); return array_map(static fn ($r) => ['title' => (string)($r['title'] ?? '')], $rows); } /** * Absolute URLs of the product images (original file public URLs). * Falls back to the OG image when no product images exist. * * @return string[] */ private function imageUrls(int $productUid, string $base): array { $urls = $this->publicUrlsForField($productUid, 'productimage', $base); if ($urls === []) { $urls = $this->publicUrlsForField($productUid, 'ogimage', $base); } return $urls; } /** * @return string[] */ private function publicUrlsForField(int $productUid, string $fieldName, string $base): array { $rows = $this->fileReferences($productUid, $fieldName); if ($rows === []) { return []; } $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); $urls = []; foreach ($rows as $row) { try { $fileReference = $resourceFactory->getFileReferenceObject((int)$row['uid']); $publicUrl = (string)$fileReference->getOriginalFile()->getPublicUrl(); if ($publicUrl !== '') { $urls[] = $this->structuredDataService->absUrl($publicUrl, $base); } } catch (\Throwable $e) { continue; } } return $urls; } /** * @return array{url:string}|null */ private function videoFile(int $productUid, string $base): ?array { $rows = $this->fileReferences($productUid, 'videofile'); if ($rows === []) { return null; } $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class); try { $fileReference = $resourceFactory->getFileReferenceObject((int)$rows[0]['uid']); $publicUrl = (string)$fileReference->getOriginalFile()->getPublicUrl(); if ($publicUrl === '') { return null; } return ['url' => $this->structuredDataService->absUrl($publicUrl, $base)]; } catch (\Throwable $e) { return null; } } /** * @return array> */ private function fileReferences(int $productUid, string $fieldName): array { $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) ->getQueryBuilderForTable('sys_file_reference'); return $queryBuilder ->select('uid') ->from('sys_file_reference') ->where( $queryBuilder->expr()->eq('uid_foreign', $queryBuilder->createNamedParameter($productUid, ParameterType::INTEGER)), $queryBuilder->expr()->eq('tablenames', $queryBuilder->createNamedParameter('tx_vitec_domain_model_product', ParameterType::STRING)), $queryBuilder->expr()->eq('fieldname', $queryBuilder->createNamedParameter($fieldName, ParameterType::STRING)), $queryBuilder->expr()->eq('deleted', 0), $queryBuilder->expr()->eq('hidden', 0) ) ->orderBy('sorting_foreign', 'ASC') ->executeQuery() ->fetchAllAssociative(); } private function resolveCanonicalBase(int $pid, ServerRequestInterface $request): string { if ($pid > 0) { try { $site = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId($pid); $configured = trim((string)($site->getSettings()->get('seo.site.canonicalBase') ?? '')); if ($configured !== '') { return rtrim($configured, '/'); } } catch (\Throwable $e) { // fall through to request host } } $normalizedParams = $request->getAttribute('normalizedParams'); if ($normalizedParams !== null) { return rtrim($normalizedParams->getSiteUrl(), '/'); } return ''; } }