Bilder in vier Breiten, Releasearchiv fuer Hosting ohne Composer
CMS quality / php (8.2) (push) Canceled after 0s
CMS quality / php (8.4) (push) Canceled after 0s
CMS quality / browser (push) Canceled after 0s
CMS quality / mysql (push) Canceled after 0s

Uploads werden weiterhin nach WebP umgewandelt, jetzt in vier statt drei
Breiten (480/960/1600/2400) und ohne Hochskalieren. Hintergrundbilder
waehlen ihre Variante je Geraet, weil CSS kein srcset kennt.

Dabei kam ein Fehler heraus, den erst der Browsertest zeigte: der
Dokumentpruefer erlaubte drei sources je Bild, also war mit der vierten
Variante jedes Dokument mit einem Bild ungueltig - ohne sichtbare Meldung.
Die Grenze haengt jetzt an MediaLibrary::WIDTHS.

Geteiltes Hosting hat meistens keinen Composer. scripts/build-release.php
baut deshalb auf einem Build-Rechner ein Archiv mit fertigem vendor/ ohne
Entwicklungspakete, samt SHA-256. Der Updater bricht ab, wenn ein Update
composer.lock aendert und Composer auf dem Server nicht aufrufbar ist,
statt mit einem unpassenden vendor/ weiterzulaufen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
kantarciandClaude Opus 5 committed 2026-09-12 22:39:08 +02:00
1 parent c046681302
commit c3bff7d936
17 files changed
+414 -26

No files matched your search

+4 -1
View File
@@ -267,7 +267,10 @@ final class DocumentValidator
$this->require(is_string($props['mediaId']) && 1 === preg_match('/^[a-f0-9]{32}$/D', $props['mediaId']), 'Invalid media ID.');
}
if (isset($props['sources'])) {
$this->require(is_array($props['sources']) && array_is_list($props['sources']) && count($props['sources']) <= 3, 'Invalid image variants.');
// As many entries as the media library produces widths, not one more: the list ends
// up in a srcset, and the number has to move with the pipeline instead of standing
// here as a number nobody connects to it.
$this->require(is_array($props['sources']) && array_is_list($props['sources']) && count($props['sources']) <= count(MediaLibrary::WIDTHS), 'Invalid image variants.');
$widths = [];
foreach ($props['sources'] as $source) {
$this->require(is_array($source) && is_int($source['width'] ?? null) && $source['width'] > 0 && $source['width'] <= 8000 && !in_array($source['width'], $widths, true), 'Invalid image variant width.');
+7 -1
View File
@@ -11,6 +11,9 @@ final class MediaLibrary
/** Enough to fill a screen, few enough that the browser does not decode a hundred images. */
public const PER_PAGE = 24;
/** The widths every upload is rendered to, in WebP. A source narrower than a step is skipped. */
public const WIDTHS = [480, 960, 1600, 2400];
public function __construct(private Connection $db, private string $directory)
{
}
@@ -109,7 +112,10 @@ final class MediaLibrary
$variants = [];
$width = imagesx($source);
$height = imagesy($source);
foreach (array_unique([min(480, $width), min(960, $width), min(1600, $width)]) as $targetWidth) {
// Four widths, and never larger than the original: 480 for a phone, 960 for a tablet
// or a column, 1600 for a full width section, 2400 for a full bleed image on a large
// display. A smaller source simply produces fewer variants.
foreach (array_unique(array_map(static fn (int $step): int => min($step, $width), self::WIDTHS)) as $targetWidth) {
$targetHeight = max(1, (int) round($height * $targetWidth / $width));
$target = imagecreatetruecolor($targetWidth, $targetHeight);
try {
+28 -7
View File
@@ -107,17 +107,17 @@ final class StyleCompiler
continue;
}
$rules = '';
$base = $this->declarations($values);
$base = $this->declarations($values, 'self', $device);
if ('' !== $base) {
$rules .= sprintf('#%s{%s}', $id, $base);
}
// Some properties belong on the images inside the block, not on the block: object-fit
// on a figure does nothing, and a radius on the figure leaves the image square.
$images = $this->declarations($values, 'img');
$images = $this->declarations($values, 'img', $device);
if ('' !== $images) {
$rules .= sprintf('#%s img{%s}', $id, $images);
}
$hover = is_array($values['hover'] ?? null) ? $this->declarations($values['hover']) : '';
$hover = is_array($values['hover'] ?? null) ? $this->declarations($values['hover'], 'self', $device) : '';
if ('' !== $hover) {
$rules .= sprintf('#%s:hover,#%s:focus-within{%s}', $id, $id, $hover);
}
@@ -138,7 +138,7 @@ final class StyleCompiler
*
* @param string $target 'self' for the block, 'img' for the images inside it
*/
private function declarations(array $values, string $target = 'self'): string
private function declarations(array $values, string $target = 'self', string $device = 'desktop'): string
{
$properties = StyleSchema::properties();
$out = [];
@@ -173,7 +173,7 @@ final class StyleCompiler
// The composed declarations describe the block itself; none of them has an image variant.
if ('self' === $target) {
foreach ([
$this->background($values),
$this->background($values, $device),
$this->shadow($values),
$this->textShadow($values),
$this->filter($values),
@@ -203,7 +203,7 @@ final class StyleCompiler
}
/** Overlay, gradient and image share one background-image declaration, layered in that order. */
private function background(array $values): string
private function background(array $values, string $device = 'desktop'): string
{
$layers = [];
$overlay = $values['overlayColor'] ?? null;
@@ -223,7 +223,9 @@ final class StyleCompiler
if (isset($values['backgroundImage']) && null !== $this->media) {
$item = $this->media->find((string) $values['backgroundImage']);
if (null !== $item && [] !== $item['variants']) {
$largest = end($item['variants']);
// A phone should not download the 2400 pixel variant of a background. CSS has no
// srcset, but the rules are already written per device, so the device decides.
$largest = $this->variantFor($item['variants'], $device);
// The URL comes from the media library, never from the document.
$layers[] = sprintf('url("%s")', $largest['src']);
}
@@ -314,6 +316,25 @@ final class StyleCompiler
return [] === $parts ? '' : 'transform:'.implode(' ', $parts);
}
/**
* The variant a device should load for a background.
*
* Mobile takes the smallest that is at least 960 wide, tablet the one around 1600, desktop the
* largest. A picture that only has small variants simply gives what it has.
*/
private function variantFor(array $variants, string $device): array
{
$wanted = ['mobile' => 960, 'tablet' => 1600, 'desktop' => 4000][$device] ?? 4000;
$chosen = $variants[0];
foreach ($variants as $variant) {
if ($variant['width'] <= $wanted || $chosen['width'] < $wanted) {
$chosen = $variant;
}
}
return $chosen;
}
private function grid(array $values): string
{
return isset($values['gridColumns'])