Build Kantarci Markt as an eighth installation, allow content in own CSS and keep other fonts when fetching

- Coming-soon page "Kantarci Markt – Neueröffnung" rebuilt from the delivered package with
  imprint, privacy and 404 (scripts/reset-kantarci-markt.sh, ports 8100/8101).
- Own CSS accepts a short quoted string for `content`, so ::before/::after work.
- cms:fonts:fetch replaces only the rules of the fetched families in fonts.css.
- A variable font is linked and preloaded once with a weight range instead of once per weight.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
kantarciandClaude Opus 5.5 committed 2026-09-23 10:37:54 +02:00
1 parent 6fb6d30e8d
commit 5e5d380fd3
19 files changed
+994 -8

No files matched your search

+8
View File
@@ -239,6 +239,14 @@ final class CustomCode
if (1 !== preg_match('/^-{0,2}[a-z][a-z0-9-]{0,40}$/D', $property)) {
throw new InvalidDocument(sprintf('„%s“ ist kein CSS-Eigenschaftsname.', mb_substr($property, 0, 40)));
}
// `content` braucht einen Text in Anfuehrungszeichen, sonst gibt es kein ::before und
// ::after fuer Striche, Verlaeufe oder ein Wasserzeichen. Erlaubt ist ein kurzer Text
// aus Buchstaben, Ziffern und wenigen Satzzeichen: kein <, kein Backslash, kein
// weiteres Anfuehrungszeichen, also kein Weg aus dem String oder dem <style>.
if ('content' === $property && 1 === preg_match('/^(["\'])[\p{L}\p{N} .,:!?·•–-]{0,40}\1$/uD', $value)) {
$out[] = $property.':'.$value;
continue;
}
if ('' === $value || 1 !== preg_match('/^[a-zA-Z0-9 ,.%#()\/:_+*-]{1,300}$/D', $value)) {
throw new InvalidDocument(sprintf('Der Wert von „%s“ enthält Zeichen, die in eigenem CSS nicht vorgesehen sind.', $property));
}
+61 -7
View File
@@ -84,21 +84,41 @@ final class FontLibrary
$bytes = 0;
foreach ($wanted as $family => $weights) {
$css = $this->stylesheet($family, $weights);
// Eine variable Schrift (etwa DM Sans) liefert Google als eine Datei für alle
// Schnitte, mit einer @font-face-Regel je Schnitt. Die Datei liegt trotzdem je Schnitt
// im Ordner, damit missing() weiter nach Schnitten fragen kann, aber das Stylesheet
// nennt sie nur einmal, mit einem Gewichtsbereich. Sonst lädt der Browser dieselben
// Bytes je Schnitt einmal, und preload() holt sie dreimal vorab.
$groups = [];
foreach ($this->parse($css, $family) as $face) {
$name = $this->slug($family).'-'.$face['weight'].'-'.$face['subset'].'.woff2';
$path = $this->directory.'/'.$name;
if (!is_file($path)) {
file_put_contents($path, $this->download($face['url']));
++$files;
if (isset($groups[$face['url']])) {
copy($this->directory.'/'.$groups[$face['url']]['name'], $path);
} else {
file_put_contents($path, $this->download($face['url']));
++$files;
}
}
if (isset($groups[$face['url']])) {
$groups[$face['url']]['weights'][] = $face['weight'];
continue;
}
$bytes += (int) filesize($path);
$groups[$face['url']] = ['name' => $name, 'weights' => [$face['weight']], 'face' => $face];
}
foreach ($groups as $group) {
$face = $group['face'];
$low = min($group['weights']);
$high = max($group['weights']);
$rules[] = sprintf(
"/* %s */\n@font-face{font-family:'%s';font-style:%s;font-weight:%d;font-display:swap;src:url(/assets/fonts/%s) format('woff2');unicode-range:%s}",
"/* %s */\n@font-face{font-family:'%s';font-style:%s;font-weight:%s;font-display:swap;src:url(/assets/fonts/%s) format('woff2');unicode-range:%s}",
$face['subset'],
$family,
$face['style'],
$face['weight'],
$name,
$low === $high ? (string) $low : $low.' '.$high,
$group['name'],
$face['range'],
);
}
@@ -106,11 +126,40 @@ final class FontLibrary
if ([] === $rules) {
throw new \RuntimeException('Google Fonts hat keine passende Schriftdatei geliefert.');
}
file_put_contents($this->directory.'/fonts.css', implode("\n", $rules)."\n");
$stylesheet = $this->directory.'/fonts.css';
$existing = is_file($stylesheet) ? (string) file_get_contents($stylesheet) : '';
file_put_contents($stylesheet, $this->combine($existing, $rules, array_keys($wanted)));
return ['files' => $files, 'bytes' => $bytes, 'families' => array_keys($wanted)];
}
/**
* Setzt fonts.css aus den neuen Regeln und den Regeln anderer Familien zusammen.
*
* Bisher schrieb jeder Abruf die Datei neu und nur mit den Familien des aktuellen Designs.
* Teilen sich mehrere Websites den Ordner (lokal alle Installationen) oder wechselt ein Design
* die Schrift und wieder zurück, verschwanden damit Schriften, deren Dateien noch daliegen.
* Regeln für Familien, die gerade neu geholt wurden, werden ersetzt; die übrigen bleiben.
* Eine unbenutzte @font-face-Regel kostet nichts: der Browser lädt die Datei erst, wenn Text
* in dieser Familie steht.
*
* @param list<string> $rules je Regel ein Kommentar mit dem Bereich und die @font-face
* @param list<string> $families die Familien, zu denen $rules gehören
*/
public function combine(string $existing, array $rules, array $families): string
{
$kept = [];
preg_match_all('~(?:/\*[^*]*\*/\s*)?@font-face\s*\{[^}]*\}~', $existing, $matches);
foreach ($matches[0] as $rule) {
if (1 !== preg_match("~font-family:\\s*'([^']+)'~", $rule, $family) || in_array($family[1], $families, true)) {
continue;
}
$kept[] = trim($rule);
}
return implode("\n", [...$kept, ...$rules])."\n";
}
/**
* Die Dateien, die für den ersten Eindruck vorab geholt werden sollen.
*
@@ -124,11 +173,16 @@ final class FontLibrary
*/
public function preload(array $theme): array
{
// Nur Dateien, die das Stylesheet auch nennt: bei einer variablen Schrift steht dort eine
// Datei für alle Schnitte, die Kopien je Schnitt holt der Browser nie.
$stylesheet = is_file($this->directory.'/fonts.css') ? (string) file_get_contents($this->directory.'/fonts.css') : null;
$preload = [];
foreach ($this->wanted($theme) as $family => $weights) {
foreach ($weights as $weight) {
$name = $this->slug($family).'-'.$weight.'-latin.woff2';
if (is_file($this->directory.'/'.$name)) {
$genannt = null === $stylesheet || !str_contains($stylesheet, '/assets/fonts/'.$this->slug($family).'-')
|| str_contains($stylesheet, '/assets/fonts/'.$name.')');
if (is_file($this->directory.'/'.$name) && $genannt) {
$preload[] = '/assets/fonts/'.$name;
}
}