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 <noreply@anthropic.com>
This commit is contained in:
1 parent
dc9b5fce47
commit
be4bb44c40
3 files changed
+126
No files matched your search
@@ -0,0 +1,105 @@
|
||||
<?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());
|
||||
|
||||
$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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user