Arrow icons, cascading own CSS, a preload for the hero image, and EB Solar

Built while setting up the redesign of ebsolar.de as the sixth local
installation (scripts/seed-ebsolar.php, reset-ebsolar.sh, ports 8096/8097).
The rebuild surfaced four things in the core:

- The icon set knows three arrows now: arrow-right, arrow-left,
  arrow-up-right. The button block and the post navigation render them as
  inline SVG inside the same aria-hidden span as before, so a template that
  hides the arrow with "& span{display:none}" keeps working. Until now the
  arrow was a text glyph that looked different in every font. Umut: "bei
  alle Projekte bzw bei allen möchte ich dieses emoji icon weg haben".

- Own CSS with the same nested selector twice kept only the last rule.
  "& .post-card{border:0}" followed by "& .post-card{transform:none}" lost
  the border - silently, which brought the core's card frame back on the
  project grid. The parser now joins the declarations in source order, as
  CSS does; the same for "&{…}" next to the element's own properties.

- The eager image over the fold was not preloaded. The background image of
  the first section has had a preload link for a while; a plain <img> was
  found only after the stylesheet was parsed, and that round trip went
  straight into LCP. PageController::heroImage() now writes the same src,
  srcset and sizes the block template writes, and page.html.twig emits the
  link - unless a background image is already preloaded.

- Measured on the way: an entrance effect on the LCP element costs about
  900 ms of render delay. The EB Solar hero image therefore has none.

Tests: 581 green, the icon test accepts quoted keys, two cases for the CSS
cascade, the preload asserted in the big-picture test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
kantarciandClaude Fable 5.1 committed 2026-09-17 12:56:57 +02:00
1 parent ab9e6a7ea0
commit d0019c80af
15 files changed
+1472 -12

No files matched your search

+13 -3
View File
@@ -1398,8 +1398,11 @@ final class CmsTest extends TestCase
$this->pages->publish('page-1', $this->pages->saveDraft('page-1', $document, 0), 1);
$html = $this->request('/')->getContent();
// Genau ein Bild wird vorgezogen, und zwar das erste im ersten Abschnitt.
self::assertSame(1, substr_count($html, 'fetchpriority="high"'));
// Genau ein Bild wird vorgezogen, und zwar das erste im ersten Abschnitt. Es wird
// zweimal genannt: einmal als Vorabladung im Kopf, einmal als das Bild selbst.
self::assertSame(1, substr_count($html, '<link rel="preload" as="image"'));
self::assertMatchesRegularExpression('#<link rel="preload" as="image" href="[^"]+" imagesrcset="[^"]*480w[^"]*" imagesizes="\(max-width: 650px\) 100vw[^"]*" fetchpriority="high">#', $html);
self::assertSame(2, substr_count($html, 'fetchpriority="high"'));
self::assertMatchesRegularExpression('#<figure id="gross".{0,400}fetchpriority="high"#s', $html);
self::assertMatchesRegularExpression('#<figure id="weiter-unten".{0,400}loading="lazy"#s', $html);
// Ein Block mit fester Breite fragt nach genau dieser Breite.
@@ -1944,7 +1947,8 @@ final class CmsTest extends TestCase
$template = (string) file_get_contents(__DIR__.'/../templates/blocks/icon.html.twig');
foreach (DocumentValidator::ICONS as $icon) {
self::assertMatchesRegularExpression(
'/\R\s{4}'.preg_quote($icon, '/').": '[^']{4,}',?\R/",
// Ein Name mit Bindestrich braucht in Twig Anfuehrungszeichen um den Schluessel.
'/\R\s{4}\'?'.preg_quote($icon, '/')."'?: '[^']{4,}',?\R/",
$template,
sprintf('Das Symbol „%s“ steht in der Liste, hat aber keine Zeichnung.', $icon),
);
@@ -2983,6 +2987,12 @@ final class CmsTest extends TestCase
yield 'descendant keeps its space' => ['& .block-text { font-size: 20px }', '#b1 .block-text{font-size:20px}'];
yield 'child combinator' => ['& > p { margin: 0 }', '#b1 > p{margin:0}'];
yield 'local image' => ['background: url(/bild.png)', '#b1{background:url(/bild.png)}'];
// Zweimal derselbe Selektor ist eine Kaskade, kein Widerruf: vorher gewann die letzte
// Regel, und was nur in der ersten stand, verschwand ohne Meldung.
yield 'same selector twice' => ['& .card { border: 0 }
& .card { transform: none }', '#b1 .card{border:0;transform:none}'];
yield 'element rule after a nested one' => ['& { color: red }
display: flex', '#b1{display:flex;color:red}'];
}
#[DataProvider('ownCssCases')]