Serve every font from this server and surface open tasks in 1.14.0
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

Fonts were still reachable from Google in two places. The editor and area
previews were handed `googleFonts` and no `localFonts`, so the screen used
most of the day fetched Lato from fonts.googleapis.com on every open; the
public page fell back to the same link whenever the files were missing.
Neither path remains: the template has no Google branch, `googleFontsUrl()`
is gone, and the policies name no foreign font host - `font-src 'self'`
publicly, `style-src 'self'; font-src 'self'` in administration. Without
local files a site now shows the theme's fallback and says so.

Preloading took the first two weights per family. The Kant-IT theme names
Lato 400, 700 and 900 and sets its headings on 800/900, so the one weight
that carried them loaded after the stylesheet and `font-display:swap`
swapped the heading in plain sight. Every weight of the theme is preloaded
now, latin subset, and the links sit before the stylesheet: measured cold,
all three files finish at 168 ms against a first paint at 292 ms.

Open tasks no longer hide on the screen that owns them. A strip above every
management screen names what needs a button and carries it: a new version
leads to System, missing fonts to Design. Neither asks the network - the
version comes from the daily update-check task, the fonts from a look in
the folder.

System and update was unusable on a phone: `.stack` is a grid whose
automatic column grows to its widest content, and one long database path
pushed the page sideways. It gets `minmax(0, 1fr)`, the backup table stacks
below 620px, and the prose is 45% shorter with the fine print folded away.

The device chips drew the current device with an outline, which lies outside
the box, ran under its neighbour and was clipped by the sidebar - it read as
a cut-off red frame. Used devices are filled in `#9d4e0d` now (5.6:1), the
current one carries a border inside the chip.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
kantarciandClaude Opus 5 committed 2026-09-15 00:16:16 +02:00
1 parent 858b8c6de4
commit a434e0cefc
19 files changed
+345 -126

No files matched your search

+32 -15
View File
@@ -1393,27 +1393,37 @@ final class CmsTest extends TestCase
self::assertSame('https://fonts.gstatic.com/s/lato/latein.woff2', $faces[0]['url']);
self::assertStringContainsString('U+0131', $faces[0]['range']);
// Ohne heruntergeladene Dateien bleibt es beim Verweis auf Google - und das Theme weiß es.
// Ohne heruntergeladene Dateien bleibt es bei der Ersatzschrift - verlinkt wird Google
// nie, denn das gäbe die IP jedes Besuchers an einen Dritten weiter.
$theme = new Theme($this->db);
$gespeichert = $theme->load();
$theme->save($gespeichert['colors'], [
['key' => 'body', 'name' => 'Fließtext', 'family' => 'Lato', 'source' => 'google', 'weights' => [400, 700], 'fallback' => 'system-ui'],
['key' => 'heading', 'name' => 'Überschriften', 'family' => 'Lato', 'source' => 'google', 'weights' => [700], 'fallback' => 'system-ui'],
['key' => 'heading', 'name' => 'Überschriften', 'family' => 'Lato', 'source' => 'google', 'weights' => [700, 900], 'fallback' => 'system-ui'],
], $gespeichert['base']);
self::assertFalse($fonts->hasLocal($theme->load()));
self::assertSame(['Lato 400', 'Lato 700'], $fonts->missing($theme->load()));
self::assertSame(['Lato 400', 'Lato 700', 'Lato 900'], $fonts->missing($theme->load()));
$this->pages->publish('page-1', $this->pages->saveDraft('page-1', DemoContent::starter('Startseite'), 0), 1);
self::assertStringContainsString('fonts.googleapis.com', $this->request('/')->getContent());
$ohne = $this->request('/');
self::assertStringNotContainsString('fonts.googleapis.com', $ohne->getContent());
self::assertStringNotContainsString('fonts.gstatic.com', $ohne->getContent());
self::assertStringNotContainsString('rel="preload"'." href=\"/assets/fonts", $ohne->getContent());
// Mit Dateien: kein Wort mehr von Google, dafür die eigenen Dateien und ein Vorabholen.
// Mit Dateien: die eigenen Dateien, und zwar **jeder** Schnitt vorab. Fehlte einer, läge
// er erst nach dem Stylesheet vor, und die Überschrift spränge sichtbar von der
// Ersatzschrift auf die richtige.
mkdir($this->path.'-fonts', 0o775, true);
foreach (['lato-400-latin.woff2', 'lato-700-latin.woff2'] as $datei) {
foreach (['lato-400-latin.woff2', 'lato-700-latin.woff2', 'lato-900-latin.woff2'] as $datei) {
file_put_contents($this->path.'-fonts/'.$datei, 'wOF2test');
}
file_put_contents($this->path.'-fonts/fonts.css', "@font-face{font-family:'Lato'}");
self::assertTrue($fonts->hasLocal($theme->load()));
self::assertSame(['/assets/fonts/lato-400-latin.woff2', '/assets/fonts/lato-700-latin.woff2'], $fonts->preload($theme->load()));
self::assertSame([
'/assets/fonts/lato-400-latin.woff2',
'/assets/fonts/lato-700-latin.woff2',
'/assets/fonts/lato-900-latin.woff2',
], $fonts->preload($theme->load()));
}
public function testTheStylesheetsGoOutAsOneFileWithoutComments(): void
@@ -1900,9 +1910,7 @@ final class CmsTest extends TestCase
$template = (string) file_get_contents(__DIR__.'/../templates/blocks/icon.html.twig');
foreach (DocumentValidator::ICONS as $icon) {
self::assertMatchesRegularExpression(
'/
\s{4}'.preg_quote($icon, '/').": '[^']{4,}',?
/",
'/\R\s{4}'.preg_quote($icon, '/').": '[^']{4,}',?\R/",
$template,
sprintf('Das Symbol „%s“ steht in der Liste, hat aber keine Zeichnung.', $icon),
);
@@ -2352,7 +2360,7 @@ final class CmsTest extends TestCase
$this->pages->saveDraft('page-1', $document, 0);
}
public function testThemeDrivesEveryPageAndGoogleFontsOpenTheirOwnPolicy(): void
public function testThemeDrivesEveryPageAndNoFontIsEverFetchedFromGoogle(): void
{
$this->pages->publish('page-1', $this->pages->saveDraft('page-1', DemoContent::pages()['home'], 0), 1);
// Without a stored theme the defaults apply and no third party is contacted.
@@ -2373,10 +2381,14 @@ final class CmsTest extends TestCase
self::assertStringContainsString('--kf-body:"Inter",sans-serif', $html);
self::assertStringContainsString('font-size:var(--k-size)', $html);
self::assertStringContainsString('--k-container:1100px', $html);
self::assertStringContainsString('fonts.googleapis.com/css2?family=Inter:wght@400;700', $html);
// Auch eine Google-Familie im Theme verlinkt nichts nach draußen: Solange die Dateien
// nicht hier liegen, greift die Ersatzschrift, und die Kopfzeile lässt keinen fremden
// Schrifthost zu.
self::assertStringNotContainsString('fonts.googleapis.com', $html);
$policy = (string) $response->headers->get('Content-Security-Policy');
self::assertStringContainsString('https://fonts.googleapis.com', $policy);
self::assertStringContainsString("font-src 'self' https://fonts.gstatic.com", $policy);
self::assertStringNotContainsString('googleapis.com', $policy);
self::assertStringContainsString("font-src 'self';", $policy);
self::assertStringContainsString("style-src 'self' 'sha256-", $policy);
}
/**
@@ -2591,7 +2603,12 @@ final class CmsTest extends TestCase
$this->menus->save('header', 'Hauptnavigation', [
['label' => 'Start', 'type' => 'page', 'pageId' => $start, 'anchor' => '', 'children' => []],
], 0);
self::assertStringContainsString('<a href="/" aria-current="page">Start</a>', $this->request('/')->getContent());
// Seit den Menuesymbolen (1.13.0) steht die Beschriftung in einer eigenen Huelle. Geprueft
// wird, was die Aussage traegt: Adresse, Kennzeichnung der aktuellen Seite, Beschriftung.
self::assertMatchesRegularExpression(
'#<a href="/" aria-current="page">.*?Start.*?</a>#s',
(string) $this->request('/')->getContent(),
);
// The root is protected from being renamed or binned while it is the root.
foreach ([fn () => $this->pages->movePage($start, 'anders', null, $this->pages->findForEditor($start)['version'], $this->redirects),