From be4bb44c4091dc1780d8b1a73b26899b9384ca56 Mon Sep 17 00:00:00 2001 From: Oliver Rasche Date: Tue, 10 Nov 2026 15:53:20 +0100 Subject: [PATCH] Content-Block-Bilder als verarbeitete WebP mit srcset ausliefern Lighthouse (Mobile) auf der Startseite: Score 40, LCP 25 s, 42 MB. Ursache: nb-headless-content-blocks serialisiert Bildfelder (hero_bgimage, intro_image, Karten, Banner) als publicUrl des ORIGINALS - 3,7 und 13,4 MB JPEGs gingen unveraendert an den Browser, waehrend News-/Produktbilder laengst durch den ImageService laufen. - XCLASS Headless\ProcessedFileReferenceToArray (das Paket instanziiert seinen Serializer per makeInstance): publicUrl = WebP max. 1920 px mit Redaktions-Crop, dazu originalUrl/originalSize, srcset 480-1920 (Stufen, die aufs Original zurueckfielen, entfallen), width/height, mimeType, size. SVG/GIF unveraendert (+Metadaten), Videos/PDFs bekommen mimeType/size. Exception-safe: Fallback auf Stock-Payload. - .htaccess: fileadmin/_processed_/* mit Cache-Control max-age=1 Jahr, immutable (Hash im Dateinamen). Effekt Startseite (gleiche Messbedingungen): LCP 25,1 -> 3,8 s, Bilder 18,7 -> 1,1 MB, gesamt 42 -> 22,6 MB; der Rest ist das 20-MB-Testvideo im Hero (redaktionell) und Frontend-Themen (Ticket 0085-BE). Co-Authored-By: Claude Fable 5.1 --- .../ProcessedFileReferenceToArray.php | 105 ++++++++++++++++++ packages/vitec/ext_localconf.php | 8 ++ public/.htaccess | 13 +++ 3 files changed, 126 insertions(+) create mode 100644 packages/vitec/Classes/Headless/ProcessedFileReferenceToArray.php diff --git a/packages/vitec/Classes/Headless/ProcessedFileReferenceToArray.php b/packages/vitec/Classes/Headless/ProcessedFileReferenceToArray.php new file mode 100644 index 0000000..8771ea4 --- /dev/null +++ b/packages/vitec/Classes/Headless/ProcessedFileReferenceToArray.php @@ -0,0 +1,105 @@ +fileReference; + $mimeType = (string)$file->getMimeType(); + $extension = strtolower((string)$file->getExtension()); + + $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; + } +} diff --git a/packages/vitec/ext_localconf.php b/packages/vitec/ext_localconf.php index aa49d07..90ad5be 100755 --- a/packages/vitec/ext_localconf.php +++ b/packages/vitec/ext_localconf.php @@ -20,6 +20,14 @@ $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['proc // XClass BackendLayoutView so the page module always reflects pages.layout // without the editor having to set backend_layout manually. +// Content-block images as processed, responsive WebP instead of the raw +// upload (Lighthouse 2026-11-10: 4-13 MB hero originals). The headless +// content-blocks package instantiates its serializer via makeInstance, so +// XCLASS is the supported hook. See Classes/Headless/ProcessedFileReferenceToArray. +$GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'][\Netzbewegung\NbHeadlessContentBlocks\DataProcessing\ToArray\FileReferenceToArray::class] = [ + 'className' => \Evomedien\Vitec\Headless\ProcessedFileReferenceToArray::class, +]; + $GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'][\TYPO3\CMS\Backend\View\BackendLayoutView::class] = [ 'className' => \Evomedien\Vitec\View\VitecBackendLayoutView::class, ]; diff --git a/public/.htaccess b/public/.htaccess index bfb124d..3b7dc13 100644 --- a/public/.htaccess +++ b/public/.htaccess @@ -106,6 +106,19 @@ +### Begin: Immutable processed images (SEO/Lighthouse 2026-11-10) ### + +# Files below fileadmin/_processed_/ carry a content hash in their name and +# never change in place - a year of immutable caching is safe and removes the +# "uses-long-cache-ttl" findings (mod_expires below sets only one month). + + + Header set Cache-Control "max-age=31536000, immutable" + + + +### End: Immutable processed images ### + ### Begin: Browser caching of resource files ### # This affects Frontend and Backend and increases performance.