Files
VITEC-website/packages/vitec/Classes/Headless/ProcessedFileReferenceToArray.php
T
orascheandClaude Fable 5.1 8a652063c5 Seobility-Report 2026-11-10: Backend-Befunde umgesetzt
Crawl nach dem Footer-Fix: 1.094 Seiten (vorher 9). Backend-Anteil:

- Migration seo_report_fixes_20261110: Platzhalter-SEO-Titel auf /solutions
  geleert; News mit verdoppeltem Titel/Slug und Slugs mit Slash bereinigt
  (+Redirects); 8 Tippfehler; zweite Welle toter Alt-Links (datapath.co.uk-
  Absolutlinks, utm-Varianten, /resource-center, /services, /contact-sales,
  /aetria-use-cases, /video-wall-solutions ...) in 48 News ersetzt + 101
  Redirects; Record-Shells /product, /news/article, /success-stories/story
  leiten auf ihre Uebersichten; Meta-Description /datasheets; WordPress-
  Reste in den 17 SEO-Artikeln entfernt; 53 News-Zeiger-Records (type=1
  auf alte /en/news-Seiten, ohne eigenen Text) bekommen den Seiteninhalt
  (Text + Bilder) in den Record und werden normale Artikel - die alten
  Seiten leiten per 301 auf die Artikel.
- VitecMetaHandler: kurze Seitentitel ohne SEO-Titel erhalten ihren
  Bereich als Kontext ('Aligo | AV over IP & KVM | VITEC').
- ProcessedFileReferenceToArray: Alt-Text-Fallback (Referenz-Titel, dann
  Dateititel) fuer Content-Block-Bilder.
- UsecaseListJsonRenderer: Testimonial-Baender liefern nur noch Stories
  mit Zitat, aktueller Markt zuerst, max. 12 (vorher alle 70 = 226 kB auf
  jeder Markt-/Solution-Unterseite, identische Bloecke auf ~90 Seiten).
- NewsJsonRenderer: paginierte Listen-Payloads 15 Min. im hash-Cache
  (USER_INT rendert sonst jede Variante neu; 50 langsame URLs im Report).
- robots.txt host-abhaengig: dev bekommt robots-dev.txt mit der dev-Sitemap
  (Seobility fand '0 Sitemaps', weil die Sitemap-Zeile auf www zeigte).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-10 17:47:55 +01:00

119 lines
5.0 KiB
PHP

<?php
declare(strict_types=1);
namespace Evomedien\Vitec\Headless;
use Netzbewegung\NbHeadlessContentBlocks\DataProcessing\ToArray\FileReferenceToArray;
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Service\ImageService;
/**
* XCLASS of the headless-content-blocks file serializer: delivers
* processed, responsive images instead of the original upload.
*
* Why (Lighthouse 2026-11-10, home page score 40 / LCP 25 s / 42 MB): the
* stock serializer emits `publicUrl` = the ORIGINAL file for every
* content-block image field (hero_bgimage, intro_image, card images …).
* Editors upload 4-13 MB JPEGs, so that is what every visitor downloads -
* while news/product images already go through the ImageService.
*
* Output per image (additive to the stock keys id/alt/title):
* - publicUrl: now the processed default variant (WebP, max. 1920 px wide,
* editor crop applied) - the frontend needs no change to benefit.
* - originalUrl: the untouched upload, should anyone need it.
* - srcset: WebP steps 480-1920 px (steps that would fall back to the
* original are skipped), as {url,width,descriptor} like elsewhere.
* - width/height: pixel size of the default variant (aspect ratio for
* CLS-free layout) - mimeType alongside.
* Non-images (video, pdf …), SVG and GIF keep the stock behaviour and only
* gain mimeType/size (GIF processing would drop animation; SVG scales).
*
* Exception-safe: any processing failure falls back to the stock output.
*/
class ProcessedFileReferenceToArray extends FileReferenceToArray
{
private const DEFAULT_WIDTH = 1920;
private const SRCSET_WIDTHS = [480, 768, 1024, 1440, 1920];
private const SKIP_EXTENSIONS = ['svg', 'gif'];
public function toArray(): array
{
$data = parent::toArray();
try {
$file = $this->fileReference;
$mimeType = (string)$file->getMimeType();
$extension = strtolower((string)$file->getExtension());
// Alt-text fallback (Seobility: 215 pages with empty alt): the
// reference title, then the file's title property. Editors can
// still override per reference.
if (trim((string)($data['alt'] ?? '')) === '') {
$fallbackAlt = trim((string)($file->getTitle() ?? ''));
if ($fallbackAlt === '') {
$fallbackAlt = trim((string)($file->getOriginalFile()->getProperty('title') ?? ''));
}
if ($fallbackAlt !== '') {
$data['alt'] = $fallbackAlt;
}
}
$data['mimeType'] = $mimeType;
$data['size'] = (int)$file->getSize();
if (!str_starts_with($mimeType, 'image/') || in_array($extension, self::SKIP_EXTENSIONS, true)) {
if (str_starts_with($mimeType, 'image/')) {
$data['width'] = (int)$file->getProperty('width');
$data['height'] = (int)$file->getProperty('height');
}
return $data;
}
$crop = null;
if ($file->hasProperty('crop') && $file->getProperty('crop')) {
$cropArea = CropVariantCollection::create((string)$file->getProperty('crop'))->getCropArea('default');
if (!$cropArea->isEmpty()) {
$crop = $cropArea->makeAbsoluteBasedOnFile($file);
}
}
$imageService = GeneralUtility::makeInstance(ImageService::class);
$instructions = static fn (int $width): array => array_filter([
'width' => $width,
'crop' => $crop,
'fileExtension' => 'webp',
], static fn ($v) => $v !== null);
$default = $imageService->applyProcessingInstructions($file, $instructions(self::DEFAULT_WIDTH));
$srcset = [];
foreach (self::SRCSET_WIDTHS as $width) {
$variant = $imageService->applyProcessingInstructions($file, $instructions($width));
if ($variant->usesOriginalFile()) {
continue;
}
$srcset[] = [
'url' => $imageService->getImageUri($variant, true),
'width' => (int)$variant->getProperty('width'),
'descriptor' => (int)$variant->getProperty('width') . 'w',
];
}
$data['originalUrl'] = (string)($data['publicUrl'] ?? '');
$data['originalSize'] = (int)$file->getSize();
$data['publicUrl'] = $imageService->getImageUri($default, true);
$data['width'] = (int)$default->getProperty('width');
$data['height'] = (int)$default->getProperty('height');
$data['size'] = (int)$default->getSize();
$data['srcset'] = $srcset;
$data['mimeType'] = (string)($default->getMimeType() ?: 'image/webp');
} catch (\Throwable $e) {
// keep the stock payload
}
return $data;
}
}