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:
1 parent
6fb6d30e8d
commit
5e5d380fd3
19 files changed
+994
-8
No files matched your search
+21
-1
@@ -128,6 +128,26 @@
|
||||
"$env:APP_ENV='prod'; $env:APP_DEBUG='0'; $env:DATABASE_URL='sqlite:///C:/Django Project/KantSites/var/kantarci-immobilien.db'; $env:MEDIA_DIRECTORY='C:/Django Project/KantSites/var/kantarci-immobilien-media'; $env:SITE_ORIGIN='http://127.0.0.1:8099'; & C:/php84/php.exe -d upload_max_filesize=8M -d post_max_size=10M -d memory_limit=256M -S 127.0.0.1:8099 -t public bin/dev-router.php"
|
||||
],
|
||||
"port": 8099
|
||||
},
|
||||
{
|
||||
"name": "kantarci-markt",
|
||||
"runtimeExecutable": "powershell",
|
||||
"runtimeArgs": [
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
"$env:APP_ENV='dev'; $env:DATABASE_URL='sqlite:///C:/Django Project/KantSites/var/kantarci-markt.db'; $env:MEDIA_DIRECTORY='C:/Django Project/KantSites/var/kantarci-markt-media'; $env:SITE_ORIGIN='http://127.0.0.1:8100'; & C:/php84/php.exe -d upload_max_filesize=8M -d post_max_size=10M -d memory_limit=256M -S 127.0.0.1:8100 -t public bin/dev-router.php"
|
||||
],
|
||||
"port": 8100
|
||||
},
|
||||
{
|
||||
"name": "kantarci-markt-prod",
|
||||
"runtimeExecutable": "powershell",
|
||||
"runtimeArgs": [
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
"$env:APP_ENV='prod'; $env:APP_DEBUG='0'; $env:DATABASE_URL='sqlite:///C:/Django Project/KantSites/var/kantarci-markt.db'; $env:MEDIA_DIRECTORY='C:/Django Project/KantSites/var/kantarci-markt-media'; $env:SITE_ORIGIN='http://127.0.0.1:8101'; & C:/php84/php.exe -d upload_max_filesize=8M -d post_max_size=10M -d memory_limit=256M -S 127.0.0.1:8101 -t public bin/dev-router.php"
|
||||
],
|
||||
"port": 8101
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -385,6 +385,10 @@ Der Redesign-Entwurf von ebsolar.de (Elektro Bernhard Regenerative Energie GmbH,
|
||||
|
||||
Die Landingpage „KANTARCI Immobilien – Mietanfrage“ (KANTARCI Immobilien GmbH, Baden-Baden) liegt in `var/kantarci-immobilien.db` mit Medien in `var/kantarci-immobilien-media`; die Startkonfiguration `kantarci-immobilien` in `.claude/launch.json` nutzt Port 8098, `kantarci-immobilien-prod` denselben Bestand im Produktionsmodus auf Port 8099 zum Messen. `sh scripts/reset-kantarci-immobilien.sh [Bilderordner]` (Standard `var/kantarci-immobilien-assets` mit `kantarci-banner.jpeg` aus dem gelieferten Paket) schneidet Logo, Hausmotiv und Seitensymbol aus dem Banner (`scripts/build-kantarci-assets.php`), zeichnet die Bildkarten der Mietanfrage (`node scripts/build-kantarci-options.mjs`), wirft die Installation weg und baut mit `scripts/seed-kantarci-immobilien.php` Gestaltung, Kopf- und Fußbereich, Startseite, Impressum, Datenschutz, 404, Einstellungen und das Formular `mietanfrage` (drei Schritte mit Bildkarten und Zusammenfassung) auf. Schriften sind Arial und Georgia, es gibt nichts zu holen. Die bewussten Abweichungen vom Paket stehen im Kopf des Skripts.
|
||||
|
||||
### Achte lokale Referenzwebsite: Kantarci Markt (Vorab-Seite zur Neueröffnung)
|
||||
|
||||
Die Seite „Kantarci Markt – Neueröffnung“ (Wilhelm-Drapp-Straße, Baden-Baden) liegt in `var/kantarci-markt.db` mit Medien in `var/kantarci-markt-media`; die Startkonfiguration `kantarci-markt` in `.claude/launch.json` nutzt Port 8100, `kantarci-markt-prod` denselben Bestand im Produktionsmodus auf Port 8101 zum Messen. `sh scripts/reset-kantarci-markt.sh [Bilderordner]` (Standard `var/kantarci-markt-assets` mit `kantarci-markt-logo.png`, `market.jpg` und `vegetables.jpg` aus dem gelieferten Paket) schneidet das Logo zu und rastert Seitensymbol und Stempel (`node scripts/build-kantarci-markt-assets.mjs`), wirft die Installation weg, baut mit `scripts/seed-kantarci-markt.php` Gestaltung, Kopf- und Fußbereich, Startseite, Impressum, Datenschutz, 404 und Einstellungen auf und holt DM Sans auf den eigenen Server (`cms:fonts:fetch`, ersetzt in `fonts.css` nur die Regeln für DM Sans). Gemessen mit `MSYS_NO_PATHCONV=1 CMS_URL=http://127.0.0.1:8101 CMS_PATHS=/,/impressum,/datenschutz CMS_AUDIT_DIR=reports/lighthouse-kantarci-markt node scripts/audit.mjs` (in Git Bash verhindert `MSYS_NO_PATHCONV`, dass `/impressum` zu einem Windows-Pfad wird). Die bewussten Abweichungen vom Paket stehen im Kopf des Skripts.
|
||||
|
||||
Zusätzliche Prüfung mit den lokalen Kant-IT-Inhalten: `node scripts/admin-quality-check.mjs` (PHP über `PHP_BINARY` wählbar). Das Skript benötigt `var/kantit.db` und `var/kantit-media`, erstellt eine eigene Datenbankkopie und ein zufälliges Testkonto darin; es ändert die Pilotdatenbank nicht. Screenshots und Bericht landen in `reports/admin-quality/`.
|
||||
|
||||
Die Livesuche prüft `npm run test:search` mit derselben isolierten Pilotkopie: Eingabe ohne Enter, langsame Antworten, Fehler/Wiederholen, Filter, Browserhistorie, Mediathek, Tastaturfokus, mobile Ansicht und axe. Ergebnisse stehen in `reports/live-search/`. Die allgemeine Verwaltungsprüfung ohne erneuten öffentlichen Bewegungstest startet `npm run test:admin -- --admin-only`.
|
||||
@@ -711,6 +711,8 @@ Umut wollte an jedem Element eigenes CSS **und** eigenes Skript setzen können.
|
||||
|
||||
Zwei echte Löcher kamen beim Testen heraus. `& .block-text` wurde zu `#id.block-text`, weil das führende Leerzeichen wegtrimmt wurde – aus dem Nachfahren wurde derselbe Knoten, die Regel traf das falsche Element. Und `url(//fremd.example/x.png)` kam durch, weil die Prüfung jedes `/` erlaubte: eine protokollrelative Adresse hätte über das Schema der Seite von Dritten geladen. Beides ist behoben und durch Tests abgedeckt.
|
||||
|
||||
**Nachtrag 23.09.2026: `content` mit Text.** Werte durften bisher keine Anführungszeichen enthalten, damit gab es kein `::before` oder `::after` – beim Nachbau von Kantarci Markt fehlten dadurch der Verlauf über einem Foto, ein Goldstrich und ein Buchstabe als Wasserzeichen. Jetzt nimmt `content` einen Text in einfachen oder doppelten Anführungszeichen an, höchstens 40 Zeichen aus Buchstaben, Ziffern, Leerzeichen und `.,:!?·•–-`. Kein `<`, kein Backslash, kein zweites Anführungszeichen: Der Wert kann weder den String noch das `<style>` verlassen. Jede andere Eigenschaft bleibt ohne Anführungszeichen (also auch kein `font-family:'…'`). Folge: Dekoration, die Vorlesesoftware nicht vorlesen soll, gehört in ein Pseudo-Element statt in einen Textbaustein.
|
||||
|
||||
## ADR 029 – Verwaltung: ein Rahmen statt sieben Kopfzeilen (12.09.2026)
|
||||
|
||||
Umuts Auftrag war „backend komplett neugestalten, einfacher schöner Bedienbarkeit komplett einfach“, mit ausdrücklicher Gestaltungsfreiheit. Das Problem war weniger die Optik als die Orientierung: jede Verwaltungsseite trug ihre eigene Kopfzeile und ihre eigene, unvollständige Liste von Links auf die anderen Seiten – die Seitenübersicht verlinkte sieben Bereiche, das Menü fünf, die Fassungen drei. Wo man war, stand nirgends.
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
# Projektstatus
|
||||
|
||||
## Achte Installation: Vorab-Seite „Kantarci Markt – Neueröffnung“ (23.09.2026)
|
||||
|
||||
**Anlass:** Umut: „erstellst du bitte bald verfügbar site nach dem prinzip kannst gerne dazu implementieren“ – dazu das Paket `Kantarci-Markt-Landingpage.zip` (statische Seite: Logo-Kopf, Hero „Frische bekommt eine neue Adresse.“ mit Marktfoto, Tomatenkarte und Stempel, Versprechen auf Waldgrün, Abschluss „Frische. Qualität. Faire Preise.“, Fuß).
|
||||
|
||||
**Neu.** Datenbank `var/kantarci-markt.db`, Medien `var/kantarci-markt-media`, Bildquellen `var/kantarci-markt-assets`, Startkonfigurationen `kantarci-markt` (Port 8100, dev) und `kantarci-markt-prod` (Port 8101). `sh scripts/reset-kantarci-markt.sh` baut alles neu: `build-kantarci-markt-assets.mjs` (Logo zugeschnitten auf 600 px, Seitensymbol und Stempel als PNG über Chromium), `seed-kantarci-markt.php` (Startseite, Impressum, Datenschutz, 404, Kopf, Fuß, Einstellungen), dann DM Sans auf den eigenen Server. Alles aus gewöhnlichen Bausteinen, kein Plugin.
|
||||
|
||||
- **Abweichungen:** Manrope entfällt (DM Sans an seiner Stelle, eine Schrift weniger). Die Bildzeile „Vorfreude auf frische Vielfalt.“ steht höher als in der Vorlage, weil Stempel und Tomatenkarte sie dort verdeckten. Das große „K“ im Abschluss ist ein Pseudo-Element, kein Text. Keine LocalBusiness-Daten, solange der Markt nicht geöffnet hat.
|
||||
- **Impressum und Datenschutz** sind Pflicht und angelegt. Offen, im Text als „NOCH ZU PRÜFEN“: Betreiber/Rechtsform, Hausnummer, Vertretung, Register, Telefon, USt-IdNr., ob `info@kantarci-markt.de` existiert (die Domain hat Mailserver), Quelle der beiden Fotos, Hosting.
|
||||
|
||||
**Im Kern dazu gebaut:**
|
||||
|
||||
- **Eigenes CSS: `content` mit kurzem Text** (`CustomCode`), damit `::before`/`::after` möglich sind. Eng begrenzt, siehe Nachtrag zu ADR 028.
|
||||
- **`fonts.css` wird ergänzt statt überschrieben** (`FontLibrary::combine`): Ein Abruf ersetzt nur die Regeln der geholten Familien. Vorher hätte das Holen von DM Sans Lato und Cormorant der anderen lokalen Installationen aus der gemeinsamen Datei gelöscht.
|
||||
- **Variable Schriften einmal statt je Schnitt:** DM Sans liefert Google als eine Datei für alle Schnitte. Der Kern verlinkte und lud sie dreimal vor (111 KB statt 37 KB), das kostete am Telefon den LCP. Jetzt eine `@font-face` mit Gewichtsbereich `400 700`, und `preload()` nennt nur Dateien, die `fonts.css` tatsächlich verlinkt.
|
||||
- Bekannt, nicht geändert: Zeilen sind im Kern immer durchsichtig (`.block-row{background:transparent!important}`). Die grüne Karte „Wir öffnen bald“ ist deshalb eine Spalte.
|
||||
|
||||
**Geprüft:** Lighthouse im Produktionsmodus (Port 8101, je drei Läufe, Median): Startseite mobil 97 / Desktop 100, Impressum und Datenschutz 100 / 100; Barrierefreiheit, Best Practices, SEO und agentisches Browsing überall 100. Vorher 87 mobil wegen CLS 0,19 (Logo im Kopf bekam seine Breite erst nach dem Laden) und der dreifachen Schrift. Bildschirmfotos 1440 px und 375 px ohne waagerechtes Scrollen und ohne Konsolenfehler, Impressum und 404 angesehen. Neue Tests in `CmsTest` (`content` erlaubt/abgewiesen, `combine`, Preload bei variabler Schrift); volle PHPUnit-Suite (596) und `npm test` (36) grün. Keine Felddaten, kein INP.
|
||||
|
||||
**Nächster Schritt:** Die offenen Angaben im Impressum von Umut bekommen; auf Wunsch ein Formular „Zur Eröffnung benachrichtigen“ (braucht dann Einwilligungstext und Datenschutzabsatz).
|
||||
|
||||
## Fassung 1.22.5: alte Browserchecks wieder grün, Kopfbereich von kant-it.de wieder bearbeitbar (22.09.2026)
|
||||
|
||||
**Anlass:** Umut: „räum den Strukturbaum-Fehler auch auf und Release 1.22.5“.
|
||||
|
||||
@@ -23,6 +23,11 @@ die Verwendung des reservierten Namens für eine geänderte Fassung.
|
||||
unter der SIL Open Font License 1.1, Text in `cormorant-garamond-OFL.txt`. Copyright 2015 The
|
||||
Cormorant Project Authors (github.com/CatharsisFonts/Cormorant), Reserved Font Name „Cormorant".
|
||||
|
||||
**DM Sans** (Fließtext der Seite „Kantarci Markt") steht unter der SIL Open Font License 1.1, Text
|
||||
in `dm-sans-OFL.txt`. Copyright 2014 The DM Sans Project Authors (github.com/googlefonts/dm-fonts).
|
||||
DM Sans ist eine variable Schrift: `dm-sans-400-…`, `-500-…` und `-700-…` sind dieselbe Datei,
|
||||
`fonts.css` nennt nur die erste, mit dem Gewichtsbereich 400–700.
|
||||
|
||||
Wer hier andere Schriften ablegt, prüft deren Lizenz selbst: nicht jede Schrift, die Google Fonts
|
||||
ausliefert, steht unter der OFL – einige stehen unter der Apache-Lizenz 2.0, und eine gekaufte
|
||||
Schrift eines Schriftenhauses darf meist gar nicht weitergegeben werden. Die Lizenzdatei gehört in
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,93 @@
|
||||
Copyright 2014 The DM Sans Project Authors (https://github.com/googlefonts/dm-fonts)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -14,3 +14,7 @@
|
||||
@font-face{font-family:'Cormorant Garamond';font-style:normal;font-weight:700;font-display:swap;src:url(/assets/fonts/cormorant-garamond-700-latin-ext.woff2) format('woff2');unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF}
|
||||
/* latin */
|
||||
@font-face{font-family:'Cormorant Garamond';font-style:normal;font-weight:700;font-display:swap;src:url(/assets/fonts/cormorant-garamond-700-latin.woff2) format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
|
||||
/* latin-ext */
|
||||
@font-face{font-family:'DM Sans';font-style:normal;font-weight:400 700;font-display:swap;src:url(/assets/fonts/dm-sans-400-latin-ext.woff2) format('woff2');unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF}
|
||||
/* latin */
|
||||
@font-face{font-family:'DM Sans';font-style:normal;font-weight:400 700;font-display:swap;src:url(/assets/fonts/dm-sans-400-latin.woff2) format('woff2');unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Bereitet die Bilder der Seite „Kantarci Markt – Neueröffnung“ für die Mediathek vor.
|
||||
*
|
||||
* Die Mediathek nimmt kein SVG. Das Seitensymbol und der runde Stempel „Frische • Qualität •
|
||||
* Vertrauen“ der Vorlage sind aber SVG (der Stempel mit Text auf einem Kreisbogen, das kann kein
|
||||
* Baustein). Chromium rastert beide zu PNG. Das Logo der Vorlage (1536 × 1024, 1,3 MB) wird auf
|
||||
* seinen sichtbaren Teil zugeschnitten und auf 600 px Breite verkleinert (doppelte Anzeigebreite),
|
||||
* angezeigt wird es mit höchstens 300 px.
|
||||
*
|
||||
* node scripts/build-kantarci-markt-assets.mjs [ordner]
|
||||
* → logo.png, symbol.png, stempel.png im Ordner (Standard var/kantarci-markt-assets)
|
||||
*/
|
||||
import { chromium } from 'playwright';
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
|
||||
const ordner = process.argv[2] || 'var/kantarci-markt-assets';
|
||||
const FORST = '#123b26', GOLD = '#a88332', GOLD_HELL = '#d2b978';
|
||||
|
||||
const browser = await chromium.launch();
|
||||
const seite = await browser.newPage();
|
||||
|
||||
// Logo: sichtbaren Bereich über den Alphakanal bestimmen, zuschneiden, verkleinern.
|
||||
const logo = (await readFile(`${ordner}/kantarci-markt-logo.png`)).toString('base64');
|
||||
const logoPng = await seite.evaluate(async (daten) => {
|
||||
const bild = new Image();
|
||||
bild.src = `data:image/png;base64,${daten}`;
|
||||
await bild.decode();
|
||||
const quelle = new OffscreenCanvas(bild.width, bild.height);
|
||||
const q = quelle.getContext('2d');
|
||||
q.drawImage(bild, 0, 0);
|
||||
const { data } = q.getImageData(0, 0, bild.width, bild.height);
|
||||
let [x0, y0, x1, y1] = [bild.width, bild.height, 0, 0];
|
||||
for (let y = 0; y < bild.height; y++) {
|
||||
for (let x = 0; x < bild.width; x++) {
|
||||
// Der Schein um die Waage läuft sehr flach aus; erst ab etwa 8 % Deckkraft zählt er.
|
||||
if (data[(y * bild.width + x) * 4 + 3] > 20) {
|
||||
x0 = Math.min(x0, x); y0 = Math.min(y0, y); x1 = Math.max(x1, x); y1 = Math.max(y1, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
const rand = 8;
|
||||
x0 = Math.max(0, x0 - rand); y0 = Math.max(0, y0 - rand);
|
||||
x1 = Math.min(bild.width - 1, x1 + rand); y1 = Math.min(bild.height - 1, y1 + rand);
|
||||
const breite = x1 - x0 + 1, hoehe = y1 - y0 + 1;
|
||||
const ziel = Math.min(600, breite);
|
||||
const leinwand = new OffscreenCanvas(ziel, Math.round(hoehe * ziel / breite));
|
||||
const z = leinwand.getContext('2d');
|
||||
z.imageSmoothingQuality = 'high';
|
||||
z.drawImage(bild, x0, y0, breite, hoehe, 0, 0, leinwand.width, leinwand.height);
|
||||
const blob = await leinwand.convertToBlob({ type: 'image/png' });
|
||||
const bytes = new Uint8Array(await blob.arrayBuffer());
|
||||
let binaer = '';
|
||||
for (const b of bytes) binaer += String.fromCharCode(b);
|
||||
return { png: btoa(binaer), breite: leinwand.width, hoehe: leinwand.height, ausschnitt: [x0, y0, breite, hoehe] };
|
||||
}, logo);
|
||||
await writeFile(`${ordner}/logo.png`, Buffer.from(logoPng.png, 'base64'));
|
||||
console.log(`logo.png ${logoPng.breite} × ${logoPng.hoehe} (Ausschnitt ${logoPng.ausschnitt.join(', ')})`);
|
||||
|
||||
const rastern = async (svg, groesse, datei) => {
|
||||
await seite.setViewportSize({ width: groesse, height: groesse });
|
||||
await seite.setContent(`<!doctype html><style>html,body{margin:0;background:transparent}svg{display:block}</style>${svg}`);
|
||||
await seite.screenshot({ path: `${ordner}/${datei}`, omitBackground: true });
|
||||
console.log(`${datei} ${groesse} × ${groesse}`);
|
||||
};
|
||||
|
||||
// Seitensymbol: das K mit Blatt aus dem Favicon der Vorlage.
|
||||
await rastern(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="256" height="256">
|
||||
<rect width="64" height="64" rx="14" fill="#062f24"/>
|
||||
<path d="M19 43V18h7v10l10-10h9L33 30l13 13h-9L26 32v11z" fill="${GOLD}"/>
|
||||
<path d="M41 10c-7 2-10 6-9 13 6-1 10-5 9-13Z" fill="${GOLD}"/></svg>`, 256, 'symbol.png');
|
||||
|
||||
// Stempel: goldene Scheibe, Umschrift auf dem Kreis, Blatt in der Mitte. Doppelte Größe für
|
||||
// scharfe Kanten auf hochauflösenden Bildschirmen (angezeigt mit 122 px).
|
||||
await rastern(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 122 122" width="244" height="244">
|
||||
<circle cx="61" cy="61" r="61" fill="${GOLD_HELL}"/>
|
||||
<g transform="translate(10 10) scale(1.1333)">
|
||||
<defs><path id="kreis" d="M45,45 m-31,0 a31,31 0 1,1 62,0 a31,31 0 1,1 -62,0"/></defs>
|
||||
<text font-family="Arial, sans-serif" font-weight="700" font-size="8" letter-spacing="1.4" fill="${FORST}"><textPath href="#kreis">FRISCHE • QUALITÄT • VERTRAUEN • </textPath></text>
|
||||
<path d="M45 52c-2-16 6-25 22-26-1 16-8 24-22 26Zm0 0c-8-11-17-13-27-7 7 12 16 14 27 7Z" fill="${FORST}"/>
|
||||
</g></svg>`, 244, 'stempel.png');
|
||||
|
||||
await browser.close();
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/bin/sh
|
||||
# Wirft die Kantarci-Markt-Installation weg und baut sie neu auf. Nur fuer den Aufbau gedacht.
|
||||
set -e
|
||||
root="C:/Django Project/KantSites"
|
||||
php="${PHP_BINARY:-C:/php84/php.exe}"
|
||||
export DATABASE_URL="sqlite:///$root/var/kantarci-markt.db"
|
||||
export MEDIA_DIRECTORY="$root/var/kantarci-markt-media"
|
||||
rm -f "$root/var/kantarci-markt.db"
|
||||
rm -rf "$root/var/kantarci-markt-media"
|
||||
"$php" vendor/bin/doctrine-migrations migrate --no-interaction -q
|
||||
assets="${1:-$root/var/kantarci-markt-assets}"
|
||||
# Logo zuschneiden, Seitensymbol und Stempel rastern (Chromium ueber Playwright).
|
||||
node scripts/build-kantarci-markt-assets.mjs "$assets"
|
||||
"$php" -d memory_limit=512M scripts/seed-kantarci-markt.php "$assets"
|
||||
# DM Sans vom eigenen Server. Der Abruf ersetzt in fonts.css nur die Regeln fuer DM Sans; die
|
||||
# Schriften der anderen Installationen bleiben stehen. Das PHP 8.4 hier kennt keine
|
||||
# Stammzertifikate; mit var/cacert.pem (https://curl.se/ca/cacert.pem) klappt der Abruf.
|
||||
if [ -f "$root/var/cacert.pem" ]; then
|
||||
"$php" -d curl.cainfo="$root/var/cacert.pem" -d openssl.cafile="$root/var/cacert.pem" bin/console cms:fonts:fetch
|
||||
else
|
||||
"$php" bin/console cms:fonts:fetch
|
||||
fi
|
||||
@@ -0,0 +1,651 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Baut die Vorab-Seite „Kantarci Markt – Neueröffnung“ als Inhalt dieses CMS auf.
|
||||
*
|
||||
* Vorlage ist das gelieferte Paket „Kantarci-Markt-Landingpage.zip“ (23.09.2026): eine statische
|
||||
* Seite (index.html, styles.css) mit Logo-Kopf, Hero „Frische bekommt eine neue Adresse.“ mit zwei
|
||||
* Fotos und Stempel, dem Versprechen (Frisch, Qualität, Vielfältig) auf Waldgrün, dem Abschluss
|
||||
* „Frische. Qualität. Faire Preise.“ und einem schmalen Fuß. Wortlaut und Farben (Waldgrün
|
||||
* #123b26, Gold #ad893b, Creme #fcf9f2) sind daraus übernommen. Gebaut ist alles aus gewöhnlichen
|
||||
* Bausteinen – kein eigener Baustein, kein Plugin.
|
||||
*
|
||||
* Bewusste Abweichungen:
|
||||
* - DM Sans kommt vom eigenen Server (cms:fonts:fetch), nicht von Google. Manrope der Vorlage
|
||||
* (nur Kleinzeilen, Nummern und die Bildzeile) entfällt, dort steht DM Sans: eine Schrift
|
||||
* weniger zu laden. Überschriften in Georgia wie in der letzten Fassung der Vorlage.
|
||||
* - Der runde Stempel ist ein PNG (scripts/build-kantarci-markt-assets.mjs): Text auf einem
|
||||
* Kreisbogen kann kein Baustein, und die Mediathek nimmt kein SVG.
|
||||
* - Das große „K“ im Abschluss und der Goldstrich sind Pseudo-Elemente im eigenen CSS der
|
||||
* Sektion, damit Vorlesesoftware und KI-Leser kein einzelnes „K“ als Inhalt bekommen.
|
||||
* - Neu sind Impressum und Datenschutz, beide Pflicht für einen geschäftlichen Auftritt (§ 5 DDG,
|
||||
* Art. 13 DSGVO). Betreiber, Anschrift mit Hausnummer und Kontakt fehlen in der Vorlage; sie
|
||||
* stehen als „NOCH ZU PRÜFEN“ im Text.
|
||||
*
|
||||
* Das Skript schreibt ausschließlich in die Datenbank aus DATABASE_URL und in MEDIA_DIRECTORY,
|
||||
* beide müssen auf eine frische, migrierte Installation zeigen (siehe reset-kantarci-markt.sh).
|
||||
* Erstes Argument: der Ordner mit logo.png, symbol.png, stempel.png, market.jpg, vegetables.jpg.
|
||||
*/
|
||||
|
||||
require dirname(__DIR__).'/config/bootstrap.php';
|
||||
|
||||
use App\Content\DocumentValidator;
|
||||
use App\Content\MediaLibrary;
|
||||
use App\Content\PageRepository;
|
||||
use App\Content\PartialRepository;
|
||||
use App\Content\SiteSettings;
|
||||
use App\Content\Theme;
|
||||
use App\Infrastructure\ConnectionFactory;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
$root = dirname(__DIR__);
|
||||
$assets = $argv[1] ?? null;
|
||||
if (null === $assets || !is_dir($assets)) {
|
||||
fwrite(STDERR, "Bitte den Ordner mit den Bildern als Argument angeben.\n");
|
||||
exit(1);
|
||||
}
|
||||
$url = str_replace('%kernel.project_dir%', str_replace('\\', '/', $root), (string) $_SERVER['DATABASE_URL']);
|
||||
$mediaDirectory = str_replace('%kernel.project_dir%', $root, (string) $_SERVER['MEDIA_DIRECTORY']);
|
||||
if (!str_contains($url, 'kantarci-markt')) {
|
||||
fwrite(STDERR, "Sicherung: DATABASE_URL muss auf die Kantarci-Markt-Datenbank zeigen.\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$db = ConnectionFactory::create($url);
|
||||
if (false !== $db->fetchOne('SELECT id FROM page LIMIT 1')) {
|
||||
fwrite(STDERR, "Diese Datenbank enthält bereits Seiten. Abbruch, damit nichts überschrieben wird.\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$media = new MediaLibrary($db, $mediaDirectory);
|
||||
$settings = new SiteSettings($db, $media);
|
||||
$pages = new PageRepository($db, new DocumentValidator(), $settings);
|
||||
$partials = new PartialRepository($db, new DocumentValidator());
|
||||
$theme = new Theme($db);
|
||||
|
||||
$name = 'Kantarci Markt';
|
||||
// Die Domain kantarci-markt.de hat Mailserver (23.09.2026). Ob es das Postfach info@ gibt, ist
|
||||
// nicht bestätigt – deshalb steht es nur im Impressum und dort als „NOCH ZU PRÜFEN“.
|
||||
$mail = 'info@kantarci-markt.de';
|
||||
$strasse = 'Wilhelm-Drapp-Straße';
|
||||
$plz = '76532';
|
||||
$ort = 'Baden-Baden';
|
||||
$SCHEMA = 24;
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Bausteine
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
$seiten = static fn (string $w): array => ['top' => $w, 'right' => $w, 'bottom' => $w, 'left' => $w];
|
||||
$nurOben = static fn (string $w): array => ['top' => $w, 'right' => '0px', 'bottom' => '0px', 'left' => '0px'];
|
||||
$nurLinks = static fn (string $w): array => ['top' => '0px', 'right' => '0px', 'bottom' => '0px', 'left' => $w];
|
||||
$nurUnten = static fn (string $w): array => ['top' => '0px', 'right' => '0px', 'bottom' => $w, 'left' => '0px'];
|
||||
$rund = static fn (string $w): array => ['topLeft' => $w, 'topRight' => $w, 'bottomRight' => $w, 'bottomLeft' => $w];
|
||||
|
||||
$section = static function (string $id, array $children, array $style = [], ?string $anchor = null, string $css = ''): array {
|
||||
$block = ['id' => $id, 'type' => 'section', 'children' => $children, 'style' => $style];
|
||||
if (null !== $anchor) {
|
||||
$block['anchor'] = $anchor;
|
||||
}
|
||||
if ('' !== $css) {
|
||||
$block['css'] = $css;
|
||||
}
|
||||
|
||||
return $block;
|
||||
};
|
||||
|
||||
$row = static function (string $id, array $children, array $design = [], array $style = []): array {
|
||||
$block = ['id' => $id, 'type' => 'row', 'children' => $children];
|
||||
if ([] !== $design) {
|
||||
$block['layout'] = ['design' => $design];
|
||||
}
|
||||
if ([] !== $style) {
|
||||
$block['style'] = $style;
|
||||
}
|
||||
|
||||
return $block;
|
||||
};
|
||||
|
||||
/** Eine Zeile, die nebeneinander stellt statt zu rastern. */
|
||||
$zeile = static function (string $id, array $children, string $gap, array $extra = [], array $mobil = [], array $tablet = []) use ($row): array {
|
||||
$style = ['desktop' => ['display' => 'flex', 'alignItems' => 'center', 'gap' => $gap, 'margin' => ['top' => '0px'], ...$extra]];
|
||||
if ([] !== $tablet) {
|
||||
$style['tablet'] = $tablet;
|
||||
}
|
||||
if ([] !== $mobil) {
|
||||
$style['mobile'] = $mobil;
|
||||
}
|
||||
|
||||
return $row($id, $children, [], $style);
|
||||
};
|
||||
|
||||
$column = static function (string $id, array $children, array $style = [], string $css = ''): array {
|
||||
$block = ['id' => $id, 'type' => 'column', 'children' => $children];
|
||||
if ([] !== $style) {
|
||||
$block['style'] = $style;
|
||||
}
|
||||
if ('' !== $css) {
|
||||
$block['css'] = $css;
|
||||
}
|
||||
|
||||
return $block;
|
||||
};
|
||||
|
||||
$heading = static function (string $id, string $text, int $level, array $style = [], string $css = ''): array {
|
||||
$block = ['id' => $id, 'type' => 'heading', 'props' => ['text' => $text, 'level' => $level]];
|
||||
if ([] !== $style) {
|
||||
$block['style'] = $style;
|
||||
}
|
||||
if ('' !== $css) {
|
||||
$block['css'] = $css;
|
||||
}
|
||||
|
||||
return $block;
|
||||
};
|
||||
|
||||
$text = static function (string $id, string $inhalt, array $style = [], string $css = ''): array {
|
||||
$block = ['id' => $id, 'type' => 'text', 'props' => ['text' => $inhalt]];
|
||||
if ([] !== $style) {
|
||||
$block['style'] = $style;
|
||||
}
|
||||
if ('' !== $css) {
|
||||
$block['css'] = $css;
|
||||
}
|
||||
|
||||
return $block;
|
||||
};
|
||||
|
||||
/** Eine reine Fläche: der kurze Goldstrich vor der Kleinzeile. */
|
||||
$flaeche = static function (string $id, array $style, string $css = ''): array {
|
||||
$block = ['id' => $id, 'type' => 'spacer', 'props' => ['size' => 'small'], 'style' => $style];
|
||||
if ('' !== $css) {
|
||||
$block['css'] = $css;
|
||||
}
|
||||
|
||||
return $block;
|
||||
};
|
||||
|
||||
/** Ein Button ohne den Pfeil des Bausteins. */
|
||||
$button = static function (string $id, string $text, string $href, array $style = []): array {
|
||||
return ['id' => $id, 'type' => 'button', 'props' => ['text' => $text, 'href' => $href], 'style' => $style, 'css' => '& span{display:none}'];
|
||||
};
|
||||
|
||||
$bild = static function (string $id, array $eintrag, string $alt, array $style = [], ?string $href = null, string $css = '', string $bildzeile = ''): array {
|
||||
$gross = end($eintrag['variants']);
|
||||
$props = [
|
||||
'src' => $gross['src'], 'alt' => $alt, 'decorative' => '' === $alt, 'width' => $gross['width'], 'height' => $gross['height'],
|
||||
'mediaId' => $eintrag['id'],
|
||||
'sources' => array_map(static fn (array $v): array => ['src' => $v['src'], 'width' => $v['width']], $eintrag['variants']),
|
||||
];
|
||||
if (null !== $href) {
|
||||
$props['href'] = $href;
|
||||
$props['click'] = 'link';
|
||||
}
|
||||
if ('' !== $bildzeile) {
|
||||
$props['caption'] = $bildzeile;
|
||||
}
|
||||
|
||||
$block = ['id' => $id, 'type' => 'image', 'props' => $props, 'style' => $style];
|
||||
if ('' !== $css) {
|
||||
$block['css'] = $css;
|
||||
}
|
||||
|
||||
return $block;
|
||||
};
|
||||
|
||||
$symbol = static function (string $id, string $name, int $groesse, array $style = []): array {
|
||||
return ['id' => $id, 'type' => 'icon', 'props' => ['icon' => $name, 'size' => $groesse, 'decorative' => true, 'label' => ''], 'style' => $style];
|
||||
};
|
||||
|
||||
/**
|
||||
* Setzt die Einblendungen nach einer Liste, statt sie über das ganze Dokument zu streuen.
|
||||
*
|
||||
* @param array<string, array{0: string, 1?: string}> $plan Block-ID => [Art, Verzögerung]
|
||||
*/
|
||||
$einblenden = static function (array $dokument, array $plan): array {
|
||||
$gehen = function (array $bloecke) use (&$gehen, $plan): array {
|
||||
foreach ($bloecke as $i => $block) {
|
||||
if (isset($plan[$block['id'] ?? ''])) {
|
||||
[$art, $verzug] = [...$plan[$block['id']], 'none'];
|
||||
$bloecke[$i]['layout'] = [...($block['layout'] ?? []), 'effects' => ['entrance' => $art, 'delay' => $verzug]];
|
||||
}
|
||||
if (isset($block['children'])) {
|
||||
$bloecke[$i]['children'] = $gehen($block['children']);
|
||||
}
|
||||
}
|
||||
|
||||
return $bloecke;
|
||||
};
|
||||
$dokument['blocks'] = $gehen($dokument['blocks']);
|
||||
|
||||
return $dokument;
|
||||
};
|
||||
|
||||
// Wiederkehrende Gestaltungswerte der Vorlage.
|
||||
$kleinStil = ['fontSize' => '14px', 'fontWeight' => '700', 'letterSpacing' => '0.2em', 'lineHeight' => 1.2, 'textTransform' => 'uppercase'];
|
||||
$linkCss = "& a{color:#123b26;text-decoration:underline;text-underline-offset:3px}\n& a:hover{color:#806222}";
|
||||
|
||||
/** Die Kleinzeile in Versalien mit dem kurzen Goldstrich davor („— NEUERÖFFNUNG“). */
|
||||
$kleinzeile = static function (string $id, string $inhalt, string $farbe, string $strichFarbe = '@gold') use ($text, $zeile, $flaeche, $kleinStil): array {
|
||||
return $zeile($id, [
|
||||
$flaeche($id.'-strich', ['desktop' => ['width' => '38px', 'height' => '2px', 'backgroundColor' => $strichFarbe, 'flexShrink' => 0]], 'min-height:0'),
|
||||
$text($id.'-text', $inhalt, ['desktop' => [...$kleinStil, 'color' => $farbe, 'margin' => ['bottom' => '0px']]]),
|
||||
], '12px', ['margin' => ['top' => '0px', 'bottom' => '24px']]);
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Website, Gestaltung, Medien
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
$db->insert('website', ['id' => 'default', 'name' => $name, 'default_locale' => 'de', 'theme' => '{}']);
|
||||
|
||||
$theme->save(
|
||||
[
|
||||
['key' => 'forst', 'name' => 'Waldgrün', 'value' => '#123b26'],
|
||||
['key' => 'tief', 'name' => 'Tiefgrün (Fuß)', 'value' => '#0b281a'],
|
||||
['key' => 'gold', 'name' => 'Gold (Linien)', 'value' => '#ad893b'],
|
||||
['key' => 'gold-hell', 'name' => 'Gold hell (Flächen, auf Grün)', 'value' => '#d2b978'],
|
||||
['key' => 'gold-text', 'name' => 'Gold als Schrift auf Creme', 'value' => '#806222'],
|
||||
['key' => 'gold-titel', 'name' => 'Gold in großen Überschriften', 'value' => '#8c6b27'],
|
||||
['key' => 'creme', 'name' => 'Creme', 'value' => '#fcf9f2'],
|
||||
['key' => 'sand', 'name' => 'Sand (Abschluss)', 'value' => '#f4efe3'],
|
||||
['key' => 'text', 'name' => 'Fließtext', 'value' => '#58685e'],
|
||||
['key' => 'text-gruen', 'name' => 'Fließtext auf Grün', 'value' => '#b7c8c1'],
|
||||
['key' => 'klein-gruen', 'name' => 'Kleinzeile auf Grün', 'value' => '#a9bdb4'],
|
||||
['key' => 'fuss-text', 'name' => 'Schrift im Fuß', 'value' => '#a9bab2'],
|
||||
['key' => 'papier', 'name' => 'Weiß', 'value' => '#ffffff'],
|
||||
],
|
||||
[
|
||||
['key' => 'body', 'name' => 'Fließtext', 'family' => 'DM Sans', 'source' => 'google', 'weights' => [400, 500, 700], 'fallback' => 'sans-serif'],
|
||||
['key' => 'heading', 'name' => 'Überschriften', 'family' => 'Georgia', 'source' => 'system', 'weights' => [400], 'fallback' => 'serif'],
|
||||
],
|
||||
['fontFamily' => 'body', 'headingFontFamily' => 'heading', 'fontSize' => '16px', 'lineHeight' => 1.6, 'color' => '@forst', 'backgroundColor' => '@creme', 'linkColor' => '@forst', 'containerWidth' => '1280px', 'headingScale' => 1.25, 'linkUnderline' => false, 'imageRadius' => '0px'],
|
||||
['shape' => 'square', 'appearance' => 'solid', 'size' => 'normal', 'fontFamily' => 'inherit', 'fontSize' => 15, 'fontWeight' => 700, 'letterSpacing' => 0, 'textTransform' => 'none', 'backgroundColor' => '@forst', 'color' => '@papier', 'hoverBackgroundColor' => '#1d5236', 'hoverColor' => '@papier'],
|
||||
);
|
||||
echo "Farben, Schriften und Buttons gesetzt\n";
|
||||
|
||||
$medien = [];
|
||||
foreach ([
|
||||
'logo' => ['logo.png', 'Kantarci Markt – grünes K mit goldener Waage'],
|
||||
'symbol' => ['symbol.png', 'Bildmarke von Kantarci Markt'],
|
||||
'stempel' => ['stempel.png', ''],
|
||||
'markt' => ['market.jpg', 'Frisches Blattgemüse an einem Marktstand – Beispielbild'],
|
||||
'tomaten' => ['vegetables.jpg', 'Frische Strauchtomaten – Beispielbild'],
|
||||
] as $schluessel => [$datei, $alt]) {
|
||||
$pfad = rtrim($assets, '/\\').'/'.$datei;
|
||||
if (!is_file($pfad)) {
|
||||
fwrite(STDERR, "Bild fehlt: $pfad\n");
|
||||
exit(1);
|
||||
}
|
||||
$eintrag = $media->upload(new UploadedFile($pfad, basename($datei), null, null, true));
|
||||
$media->update($eintrag['id'], ['title' => pathinfo($datei, PATHINFO_FILENAME), 'alt' => $alt, 'archived' => false, 'version' => $eintrag['version']]);
|
||||
$medien[$schluessel] = $media->find($eintrag['id']);
|
||||
echo "Bild übernommen: $datei\n";
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Kopf- und Fußbereich
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
$kopf = [
|
||||
'schemaVersion' => $SCHEMA, 'title' => 'Kopfbereich', 'description' => 'Erscheint auf jeder Seite über dem Inhalt.',
|
||||
'blocks' => [
|
||||
$section('kopf', [
|
||||
$zeile('kopf-zeile', [
|
||||
$bild('kopf-logo', $medien['logo'], 'Kantarci Markt – zur Startseite', [
|
||||
// Volle Breite bis 300 px: sonst hängt die Breite in der zentrierten Zeile am
|
||||
// geladenen Bild, und der Kopf springt beim Laden (CLS).
|
||||
'desktop' => ['width' => '100%', 'maxWidth' => '300px', 'margin' => $seiten('0px')],
|
||||
'mobile' => ['maxWidth' => '230px'],
|
||||
], '/', '& img{display:block;width:100%;height:auto}'),
|
||||
], '0px', ['justifyContent' => 'center']),
|
||||
], [
|
||||
'desktop' => ['backgroundColor' => '@creme', 'padding' => ['top' => '18px', 'bottom' => '18px'], 'borderWidth' => $nurUnten('1px'), 'borderStyle' => 'solid', 'borderColor' => '@gold'],
|
||||
'mobile' => ['padding' => ['top' => '14px', 'bottom' => '14px']],
|
||||
]),
|
||||
],
|
||||
];
|
||||
|
||||
$fussLink = "& a{color:#a9bab2}\n& a:hover{color:#fff;text-decoration:underline}";
|
||||
$fuss = [
|
||||
'schemaVersion' => $SCHEMA, 'title' => 'Fußbereich', 'description' => 'Erscheint auf jeder Seite unter dem Inhalt.',
|
||||
'blocks' => [
|
||||
$section('fuss', [
|
||||
$zeile('fuss-zeile', [
|
||||
$text('fuss-copyright', '© 2026 '.$name, ['desktop' => ['fontSize' => '14px', 'letterSpacing' => '0.08em', 'margin' => ['bottom' => '0px']]]),
|
||||
$text('fuss-adresse', $strasse.' · '.$ort, ['desktop' => ['fontSize' => '14px', 'letterSpacing' => '0.08em', 'margin' => ['bottom' => '0px']]]),
|
||||
$zeile('fuss-links', [
|
||||
// Impressum und Datenschutz kommen aus den Einstellungen, nicht aus einem Menü.
|
||||
['id' => 'fuss-rechtliches', 'type' => 'legal', 'props' => ['label' => 'Rechtliches'],
|
||||
'style' => ['desktop' => ['fontSize' => '14px', 'gap' => '4px', 'color' => '@fuss-text']],
|
||||
'css' => $fussLink],
|
||||
$text('fuss-agentur', '<a href="https://kant-it.de">Website by Kant-IT Solutions</a>', ['desktop' => ['fontSize' => '14px', 'margin' => ['bottom' => '0px']]], $fussLink),
|
||||
], '22px', ['flexWrap' => 'wrap'], ['rowGap' => '10px', 'columnGap' => '18px']),
|
||||
], '20px', ['justifyContent' => 'space-between', 'flexWrap' => 'wrap'],
|
||||
['flexDirection' => 'column', 'alignItems' => 'flex-start', 'gap' => '12px']),
|
||||
], [
|
||||
'desktop' => ['backgroundColor' => '@tief', 'color' => '@fuss-text', 'padding' => ['top' => '28px', 'bottom' => '28px']],
|
||||
]),
|
||||
],
|
||||
];
|
||||
|
||||
$partials->create('header', 'default', 'Kopfbereich', $kopf);
|
||||
$partials->create('footer', 'default', 'Fußbereich', $fuss);
|
||||
echo "Kopf- und Fußbereich angelegt\n";
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Startseite
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
/** Ein Versprechen: Nummer, Titel, Satz – auf dem Telefon rückt der Satz unter den Titel. */
|
||||
$versprechen = static fn (string $n, string $titel, string $satz): array => $zeile('versprechen-'.$n, [
|
||||
$text('versprechen-'.$n.'-nummer', $n, ['desktop' => ['fontSize' => '13px', 'fontWeight' => '700', 'color' => '@gold-hell', 'flexBasis' => '50px', 'flexShrink' => 0, 'margin' => ['bottom' => '0px']], 'mobile' => ['flexBasis' => '34px']]),
|
||||
$zeile('versprechen-'.$n.'-inhalt', [
|
||||
$heading('versprechen-'.$n.'-titel', $titel, 3, [
|
||||
'desktop' => ['fontSize' => '34px', 'fontWeight' => '400', 'lineHeight' => 1.2, 'color' => '@papier', 'flexGrow' => 3, 'flexBasis' => '0', 'margin' => ['bottom' => '0px']],
|
||||
'tablet' => ['fontSize' => '28px'],
|
||||
'mobile' => ['fontSize' => '28px'],
|
||||
]),
|
||||
$text('versprechen-'.$n.'-satz', $satz, ['desktop' => ['fontSize' => '16px', 'lineHeight' => 1.6, 'color' => '@text-gruen', 'maxWidth' => '390px', 'flexGrow' => 5, 'flexBasis' => '0', 'margin' => ['bottom' => '0px']]]),
|
||||
], '20px', ['alignItems' => 'baseline', 'flexGrow' => 1, 'flexBasis' => '0', 'minWidth' => '0'],
|
||||
['flexDirection' => 'column', 'alignItems' => 'flex-start', 'gap' => '8px']),
|
||||
], '20px', ['alignItems' => 'baseline', 'padding' => ['top' => '31px', 'bottom' => '31px'], 'borderWidth' => $nurUnten('1px'), 'borderStyle' => 'solid', 'borderColor' => '#cfb57359']);
|
||||
|
||||
$startseite = [
|
||||
'schemaVersion' => $SCHEMA,
|
||||
'title' => 'Neueröffnung',
|
||||
'description' => 'Kantarci Markt eröffnet bald: Frische, Qualität und faire Preise in der Wilhelm-Drapp-Straße in Baden-Baden.',
|
||||
'blocks' => [
|
||||
// 1 – Hero: Text links, rechts das Marktfoto mit Bildzeile, die gedrehte Tomatenkarte und
|
||||
// der Stempel.
|
||||
$section('hero', [
|
||||
$row('hero-zeile', [
|
||||
$column('hero-text', [
|
||||
$kleinzeile('hero-label', 'Neueröffnung', '@gold-text'),
|
||||
$heading('hero-titel', 'Frische bekommt<br><em>eine neue Adresse.</em>', 1, [
|
||||
'desktop' => ['fontSize' => '80px', 'fontWeight' => '400', 'lineHeight' => 1.04, 'letterSpacing' => '-0.045em', 'color' => '@forst', 'margin' => ['bottom' => '0px']],
|
||||
'tablet' => ['fontSize' => '64px'],
|
||||
'mobile' => ['fontSize' => '44px', 'lineHeight' => 1.07],
|
||||
], '& em{font-style:italic;color:#8c6b27}'),
|
||||
$text('hero-intro', 'Freuen Sie sich auf sorgfältig ausgewählte Produkte, echte Vielfalt und einen Markt, der Nachbarschaft neu denkt.', [
|
||||
'desktop' => ['maxWidth' => '600px', 'fontSize' => '20px', 'lineHeight' => 1.65, 'color' => '@text', 'margin' => ['top' => '32px', 'bottom' => '38px']],
|
||||
'mobile' => ['fontSize' => '17px', 'margin' => ['top' => '24px', 'bottom' => '30px']],
|
||||
]),
|
||||
// Eine Spalte statt einer Zeile: Zeilen sind im Kern immer durchsichtig.
|
||||
$column('hero-termin', [
|
||||
$column('hero-termin-symbol', [
|
||||
$symbol('hero-termin-icon', 'shop', 24, ['desktop' => ['color' => '@forst']]),
|
||||
], ['desktop' => ['display' => 'flex', 'alignItems' => 'center', 'justifyContent' => 'center', 'width' => '50px', 'height' => '50px', 'flexShrink' => 0, 'backgroundColor' => '@gold-hell', 'borderRadius' => $rund('50%')]]),
|
||||
$column('hero-termin-text', [
|
||||
$text('hero-termin-klein', 'Wir öffnen bald', ['desktop' => ['fontSize' => '14px', 'letterSpacing' => '0.08em', 'textTransform' => 'uppercase', 'lineHeight' => 1.3, 'color' => '#bfd1c9', 'margin' => ['bottom' => '3px']]]),
|
||||
$text('hero-termin-satz', 'Eröffnungstermin folgt', ['desktop' => ['fontSize' => '16px', 'fontWeight' => '700', 'lineHeight' => 1.2, 'color' => '@papier', 'margin' => ['bottom' => '0px']]]),
|
||||
]),
|
||||
], [
|
||||
'desktop' => ['display' => 'flex', 'alignItems' => 'center', 'gap' => '17px', 'backgroundColor' => '@forst', 'borderWidth' => $seiten('1px'), 'borderStyle' => 'solid', 'borderColor' => '@gold',
|
||||
'borderRadius' => $rund('3px'), 'padding' => ['top' => '15px', 'right' => '24px', 'bottom' => '15px', 'left' => '15px'],
|
||||
'shadowY' => '18px', 'shadowBlur' => '50px', 'shadowColor' => '#062f2429'],
|
||||
'mobile' => ['gap' => '12px', 'padding' => ['top' => '14px', 'right' => '16px', 'bottom' => '14px', 'left' => '14px']],
|
||||
]),
|
||||
$text('hero-adresse', 'Hier finden Sie uns bald<br><strong>'.$strasse.'</strong><br>'.$ort, [
|
||||
'desktop' => ['fontSize' => '16px', 'lineHeight' => 1.55, 'color' => '@text', 'borderWidth' => $nurLinks('3px'), 'borderStyle' => 'solid', 'borderColor' => '@gold', 'padding' => ['left' => '18px'], 'margin' => ['top' => '28px', 'bottom' => '0px']],
|
||||
], "& strong{color:#123b26;font-weight:700}"),
|
||||
], [
|
||||
'desktop' => ['display' => 'flex', 'flexDirection' => 'column', 'alignItems' => 'flex-start', 'justifyContent' => 'center'],
|
||||
]),
|
||||
$column('hero-visual', [
|
||||
$bild('hero-markt', $medien['markt'], 'Frisches Blattgemüse an einem Marktstand – Beispielbild', [
|
||||
'desktop' => ['position' => 'absolute', 'top' => '0px', 'right' => '4%', 'bottom' => '8%', 'left' => '5%', 'margin' => $seiten('0px'), 'overflow' => 'hidden', 'backgroundColor' => '@forst',
|
||||
'outlineWidth' => '1px', 'outlineStyle' => 'solid', 'outlineColor' => '@gold', 'outlineOffset' => '8px',
|
||||
'shadowY' => '24px', 'shadowBlur' => '60px', 'shadowColor' => '#021d162e'],
|
||||
'mobile' => ['right' => '0px', 'bottom' => '10%', 'left' => '0px'],
|
||||
], null,
|
||||
"& img{display:block;width:100%;height:100%;object-fit:cover}\n"
|
||||
."&::after{content:'';position:absolute;top:25%;right:0;bottom:0;left:0;background:linear-gradient(transparent,rgba(2,29,22,.85) 65%)}\n"
|
||||
."& figcaption{position:absolute;z-index:1;bottom:150px;left:28px;right:28px;max-width:260px;margin:0;color:#fff;font-size:26px;font-weight:700;line-height:1.2;text-shadow:0 2px 14px rgba(2,29,22,.7)}",
|
||||
'Vorfreude auf frische Vielfalt.'),
|
||||
$bild('hero-tomaten', $medien['tomaten'], 'Frische Strauchtomaten – Beispielbild', [
|
||||
'desktop' => ['position' => 'absolute', 'width' => '49%', 'height' => '33%', 'right' => '-3%', 'bottom' => '-1%', 'margin' => $seiten('0px'), 'overflow' => 'hidden', 'rotate' => 4,
|
||||
'borderWidth' => $seiten('8px'), 'borderStyle' => 'solid', 'borderColor' => '@creme', 'shadowY' => '24px', 'shadowBlur' => '60px', 'shadowColor' => '#021d162e', 'zIndex' => 2],
|
||||
'mobile' => ['width' => '63%', 'height' => '39%', 'right' => '0px', 'borderWidth' => $seiten('7px')],
|
||||
], null, '& img{display:block;width:100%;height:100%;object-fit:cover}'),
|
||||
$bild('hero-stempel', $medien['stempel'], '', [
|
||||
'desktop' => ['position' => 'absolute', 'maxWidth' => '122px', 'width' => '122px', 'left' => '-1%', 'bottom' => '9%', 'margin' => $seiten('0px'), 'rotate' => -12, 'zIndex' => 3,
|
||||
'borderRadius' => $rund('50%'), 'shadowY' => '8px', 'shadowBlur' => '24px', 'shadowColor' => '#123b2626'],
|
||||
'mobile' => ['maxWidth' => '96px', 'width' => '96px', 'left' => '-2%', 'bottom' => '5%'],
|
||||
], null, '& img{display:block;width:100%;height:auto}'),
|
||||
], [
|
||||
'desktop' => ['position' => 'relative', 'minHeight' => '600px'],
|
||||
'tablet' => ['minHeight' => '520px'],
|
||||
'mobile' => ['minHeight' => '390px'],
|
||||
]),
|
||||
], ['desktop' => ['columns' => 'two'], 'tablet' => ['columns' => 'one'], 'mobile' => ['columns' => 'one']], [
|
||||
'desktop' => ['gap' => '80px', 'alignItems' => 'center', 'margin' => ['top' => '0px'], 'columnWidths' => '51 49'],
|
||||
'tablet' => ['gap' => '56px'],
|
||||
'mobile' => ['gap' => '40px'],
|
||||
]),
|
||||
], [
|
||||
'desktop' => ['backgroundColor' => '@creme', 'padding' => ['top' => '72px', 'bottom' => '84px'], 'overflow' => 'hidden'],
|
||||
'tablet' => ['padding' => ['top' => '56px', 'bottom' => '72px']],
|
||||
'mobile' => ['padding' => ['top' => '40px', 'bottom' => '64px']],
|
||||
], 'start'),
|
||||
|
||||
// 2 – Das Versprechen auf Waldgrün.
|
||||
$section('versprechen', [
|
||||
$row('versprechen-zeile', [
|
||||
$zeile('versprechen-intro', [
|
||||
$text('versprechen-nummer', '01', ['desktop' => ['fontSize' => '13px', 'fontWeight' => '700', 'color' => '@gold-hell', 'flexBasis' => '52px', 'flexShrink' => 0, 'margin' => ['top' => '4px', 'bottom' => '0px']], 'mobile' => ['flexBasis' => '38px']]),
|
||||
$column('versprechen-titel', [
|
||||
$text('versprechen-klein', 'Unser Versprechen', ['desktop' => [...$kleinStil, 'letterSpacing' => '0.18em', 'color' => '@klein-gruen', 'margin' => ['bottom' => '20px']]]),
|
||||
$heading('versprechen-h2', 'Mehr als<br>einkaufen.', 2, [
|
||||
'desktop' => ['fontSize' => '80px', 'fontWeight' => '400', 'lineHeight' => 1.04, 'letterSpacing' => '-0.035em', 'color' => '@papier', 'margin' => ['bottom' => '0px']],
|
||||
'tablet' => ['fontSize' => '64px'],
|
||||
'mobile' => ['fontSize' => '48px'],
|
||||
]),
|
||||
]),
|
||||
], '0px', ['alignItems' => 'flex-start']),
|
||||
$column('versprechen-liste', [
|
||||
$versprechen('01', 'Frisch', 'Ausgewählte Produkte, die jeden Einkauf ein bisschen besser machen.'),
|
||||
$versprechen('02', 'Qualität', 'Sorgfältig ausgewählt, mit Vertrauen gekauft. Gute Produkte zu fairen Preisen.'),
|
||||
$versprechen('03', 'Vielfältig', 'Ein Sortiment für den Alltag, besondere Momente und jeden Geschmack.'),
|
||||
], ['desktop' => ['borderWidth' => $nurOben('1px'), 'borderStyle' => 'solid', 'borderColor' => '#cfb573a6']]),
|
||||
], ['desktop' => ['columns' => 'two'], 'tablet' => ['columns' => 'one'], 'mobile' => ['columns' => 'one']], [
|
||||
'desktop' => ['gap' => '120px', 'alignItems' => 'flex-start', 'margin' => ['top' => '0px'], 'columnWidths' => '3 5'],
|
||||
'tablet' => ['gap' => '56px'],
|
||||
'mobile' => ['gap' => '48px'],
|
||||
]),
|
||||
], [
|
||||
'desktop' => ['backgroundColor' => '@forst', 'color' => '@papier', 'padding' => ['top' => '140px', 'bottom' => '140px'], 'borderWidth' => $nurOben('4px'), 'borderStyle' => 'solid', 'borderColor' => '@gold'],
|
||||
'tablet' => ['padding' => ['top' => '100px', 'bottom' => '100px']],
|
||||
'mobile' => ['padding' => ['top' => '80px', 'bottom' => '80px']],
|
||||
], 'unser-versprechen'),
|
||||
|
||||
// 3 – Abschluss: Handschrift links, Gruß rechts, das große K als Wasserzeichen.
|
||||
$section('abschluss', [
|
||||
$row('abschluss-zeile', [
|
||||
$text('abschluss-spruch', 'Frische. Qualität.<br>Faire Preise.', [
|
||||
'desktop' => ['fontFamily' => 'heading', 'fontStyle' => 'italic', 'fontSize' => '84px', 'lineHeight' => 1.12, 'letterSpacing' => '-0.05em', 'color' => '@forst', 'margin' => ['bottom' => '0px']],
|
||||
'tablet' => ['fontSize' => '64px'],
|
||||
'mobile' => ['fontSize' => '48px'],
|
||||
]),
|
||||
$column('abschluss-gruss', [
|
||||
$text('abschluss-gruss-text', 'Wir bereiten gerade alles für Sie vor.', ['desktop' => ['fontSize' => '17px', 'color' => '@text', 'margin' => ['bottom' => '10px']]]),
|
||||
$text('abschluss-gruss-satz', 'Bis ganz bald im Kantarci Markt.', ['desktop' => ['fontFamily' => 'heading', 'fontSize' => '34px', 'lineHeight' => 1.25, 'color' => '@forst', 'margin' => ['bottom' => '0px']], 'mobile' => ['fontSize' => '26px']]),
|
||||
], ['desktop' => ['maxWidth' => '390px', 'alignSelf' => 'center']]),
|
||||
], ['desktop' => ['columns' => 'two'], 'tablet' => ['columns' => 'one'], 'mobile' => ['columns' => 'one']], [
|
||||
'desktop' => ['gap' => '40px', 'alignItems' => 'center', 'margin' => ['top' => '0px'], 'position' => 'relative', 'zIndex' => 2],
|
||||
'tablet' => ['gap' => '56px'],
|
||||
]),
|
||||
], [
|
||||
'desktop' => ['backgroundColor' => '@sand', 'position' => 'relative', 'overflow' => 'hidden', 'minHeight' => '450px', 'padding' => ['top' => '130px', 'bottom' => '130px'], 'borderWidth' => $nurUnten('1px'), 'borderStyle' => 'solid', 'borderColor' => '@gold'],
|
||||
'tablet' => ['padding' => ['top' => '100px', 'bottom' => '110px']],
|
||||
'mobile' => ['padding' => ['top' => '80px', 'bottom' => '96px']],
|
||||
], 'bis-bald', "&::before{content:'K';position:absolute;right:3%;top:4%;font-family:Georgia,serif;font-size:min(30vw,480px);line-height:1;color:rgba(6,47,36,.045);pointer-events:none}\n"
|
||||
."&::after{content:'';position:absolute;right:7%;bottom:58px;width:180px;height:2px;background:#ad893b}"),
|
||||
],
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Seiten anlegen und veröffentlichen
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
$anlegen = static function (string $slug, array $dokument, string $seoTitel = '', string $teilbild = '') use ($pages, $db): string {
|
||||
$id = $pages->createPage($slug, null, 'page', $dokument);
|
||||
if ('' !== $seoTitel) {
|
||||
$db->executeStatement('UPDATE page SET seo_title = ? WHERE id = ?', [$seoTitel, $id]);
|
||||
}
|
||||
if ('' !== $teilbild) {
|
||||
$db->executeStatement('UPDATE page SET og_media_id = ? WHERE id = ?', [$teilbild, $id]);
|
||||
}
|
||||
$seite = $db->fetchAssociative('SELECT draft_revision_id, version FROM page WHERE id = ?', [$id]);
|
||||
$pages->publish($id, $seite['draft_revision_id'], (int) $seite['version']);
|
||||
echo 'Seite angelegt: /'.$db->fetchOne('SELECT path FROM page WHERE id = ?', [$id])."\n";
|
||||
|
||||
return $id;
|
||||
};
|
||||
|
||||
// Der Text des Hero spielt beim Laden; die Fotos bleiben ohne Einblendung (LCP).
|
||||
$startseite = $einblenden($startseite, [
|
||||
'hero-label' => ['fade'],
|
||||
'hero-titel' => ['rise', 'short'],
|
||||
'hero-intro' => ['rise', 'medium'],
|
||||
'hero-termin' => ['rise', 'long'],
|
||||
'hero-adresse' => ['fade', 'long'],
|
||||
'versprechen-intro' => ['rise'],
|
||||
'versprechen-liste' => ['rise', 'short'],
|
||||
'abschluss-spruch' => ['rise'],
|
||||
'abschluss-gruss' => ['rise', 'short'],
|
||||
]);
|
||||
|
||||
$anlegen('home', $startseite, 'Kantarci Markt – Neueröffnung in Baden-Baden', $medien['markt']['id']);
|
||||
|
||||
/** Eine Rechtsseite: Kleinzeile, Titel und Vorspann auf Waldgrün, dann Absätze mit Zwischenüberschriften. */
|
||||
$rechtsseite = static function (string $praefix, string $label, string $titel, string $vorspann, array $absaetze) use ($section, $row, $column, $kleinzeile, $heading, $text, $nurOben, $linkCss): array {
|
||||
$kopf = $section($praefix.'-kopf', [
|
||||
$kleinzeile($praefix.'-label', $label, '@gold-hell', '@gold-hell'),
|
||||
$heading($praefix.'-h1', $titel, 1, [
|
||||
'desktop' => ['fontSize' => '56px', 'fontWeight' => '400', 'lineHeight' => 1.1, 'letterSpacing' => '-0.03em', 'color' => '@papier', 'margin' => ['bottom' => '20px']],
|
||||
'mobile' => ['fontSize' => '40px'],
|
||||
]),
|
||||
$text($praefix.'-vorspann', $vorspann, [
|
||||
'desktop' => ['maxWidth' => '640px', 'fontSize' => '18px', 'lineHeight' => 1.7, 'color' => '@text-gruen', 'margin' => ['bottom' => '0px']],
|
||||
'mobile' => ['fontSize' => '16px'],
|
||||
]),
|
||||
], [
|
||||
'desktop' => ['backgroundColor' => '@forst', 'color' => '@papier', 'padding' => ['top' => '76px', 'bottom' => '68px'], 'borderWidth' => $nurOben('4px'), 'borderStyle' => 'solid', 'borderColor' => '@gold'],
|
||||
'mobile' => ['padding' => ['top' => '48px', 'bottom' => '44px']],
|
||||
]);
|
||||
|
||||
$punkte = [];
|
||||
foreach ($absaetze as $i => [$zwischentitel, $inhalt]) {
|
||||
$punkte[] = $row($praefix.'-punkt-'.$i, [
|
||||
$column($praefix.'-punkt-'.$i.'-titel', '' === $zwischentitel ? [] : [
|
||||
$heading($praefix.'-h2-'.$i, $zwischentitel, 2, [
|
||||
'desktop' => ['fontFamily' => 'body', 'fontSize' => '16px', 'fontWeight' => '700', 'lineHeight' => 1.5, 'letterSpacing' => '0', 'color' => '@forst', 'margin' => ['bottom' => '0px']],
|
||||
]),
|
||||
], [
|
||||
'desktop' => ['flexBasis' => '260px', 'flexShrink' => 0],
|
||||
'tablet' => ['flexBasis' => '200px'],
|
||||
'mobile' => ['flexBasis' => 'auto'],
|
||||
]),
|
||||
$column($praefix.'-punkt-'.$i.'-text', [
|
||||
$text($praefix.'-p-'.$i, $inhalt, ['desktop' => ['fontSize' => '16px', 'lineHeight' => 1.75, 'color' => '@text', 'margin' => ['bottom' => '0px']]], $linkCss),
|
||||
], ['desktop' => ['flexGrow' => 1, 'flexBasis' => '0', 'minWidth' => '0'], 'mobile' => ['flexBasis' => 'auto']]),
|
||||
], [], [
|
||||
'desktop' => ['display' => 'flex', 'alignItems' => 'flex-start', 'gap' => '44px', 'margin' => ['top' => '0px'],
|
||||
'borderWidth' => $nurOben('1px'), 'borderStyle' => 'solid', 'borderColor' => '#e4dcc8', 'padding' => ['top' => '26px', 'bottom' => '26px']],
|
||||
'tablet' => ['gap' => '28px'],
|
||||
'mobile' => ['flexDirection' => 'column', 'gap' => '8px', 'padding' => ['top' => '20px', 'bottom' => '20px']],
|
||||
]);
|
||||
}
|
||||
|
||||
$inhalt = $section($praefix, [
|
||||
$column($praefix.'-inhalt', $punkte, ['desktop' => ['maxWidth' => '920px']]),
|
||||
], ['desktop' => ['padding' => ['top' => '48px', 'bottom' => '90px']], 'mobile' => ['padding' => ['top' => '24px', 'bottom' => '56px']]]);
|
||||
|
||||
return [$kopf, $inhalt];
|
||||
};
|
||||
|
||||
$pruefen = static fn (string $was): string => '<strong>NOCH ZU PRÜFEN:</strong> '.$was;
|
||||
|
||||
$anlegen('impressum', [
|
||||
'schemaVersion' => $SCHEMA, 'title' => 'Impressum',
|
||||
'description' => 'Anbieterangaben von Kantarci Markt, Baden-Baden.',
|
||||
'blocks' => [
|
||||
...$rechtsseite('impressum', 'Rechtliche Angaben', 'Impressum', 'Angaben gemäß § 5 DDG und § 18 Abs. 2 MStV.', [
|
||||
['Anbieter', $name.'<br>'.$pruefen('Inhaber bzw. Firma mit Rechtsform (etwa „… GmbH“ oder „Inhaber …, e. K.“).').'<br>'.$strasse.' '.$pruefen('Hausnummer').'<br>'.$plz.' '.$ort],
|
||||
['Vertreten durch', $pruefen('Geschäftsführer bzw. Inhaber.')],
|
||||
['Registereintrag', $pruefen('Handelsregisternummer und Registergericht, falls eingetragen.')],
|
||||
['Kontakt', "E-Mail: <a href=\"mailto:$mail\">$mail</a><br>".$pruefen('Ob das Postfach existiert, und eine Telefonnummer als zweiten schnellen Kontaktweg ergänzen.')],
|
||||
['Umsatzsteuer', $pruefen('Umsatzsteuer-Identifikationsnummer nach § 27a UStG, falls vorhanden.')],
|
||||
['Verantwortlich für den Inhalt', $pruefen('Name der verantwortlichen Person, Anschrift wie oben.')],
|
||||
['Verbraucherstreitbeilegung', 'Wir sind nicht bereit und nicht verpflichtet, an einem Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.'],
|
||||
['Haftung für Inhalte', 'Als Diensteanbieter sind wir für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Wir sind jedoch nicht verpflichtet, übermittelte oder gespeicherte fremde Informationen zu überwachen oder nach Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen. Bei Bekanntwerden von Rechtsverletzungen entfernen wir diese Inhalte umgehend.'],
|
||||
['Urheberrecht', 'Die von uns erstellten Inhalte und Werke auf diesen Seiten unterliegen dem deutschen Urheberrecht. Vervielfältigung, Bearbeitung, Verbreitung und jede Art der Verwertung außerhalb der Grenzen des Urheberrechts bedürfen der schriftlichen Zustimmung.'],
|
||||
['Bildnachweis', 'Logo: '.$name.'. Die Fotos von Marktstand und Tomaten sind Beispielbilder. '.$pruefen('Quelle und Lizenz der beiden Fotos eintragen.')],
|
||||
]),
|
||||
],
|
||||
], 'Impressum | '.$name);
|
||||
|
||||
$anlegen('datenschutz', [
|
||||
'schemaVersion' => $SCHEMA, 'title' => 'Datenschutz',
|
||||
'description' => 'Welche Daten beim Besuch der Website von Kantarci Markt verarbeitet werden.',
|
||||
'blocks' => [
|
||||
...$rechtsseite('datenschutz', 'Ihre Daten', 'Datenschutzhinweise', 'Hier erfahren Sie, welche Daten beim Besuch dieser Website verarbeitet werden – und welche Rechte Sie haben.', [
|
||||
['1. Verantwortlicher', $name.', '.$strasse.', '.$plz.' '.$ort.". E-Mail <a href=\"mailto:$mail\">$mail</a>.<br><br>".$pruefen('Inhaber bzw. Firma wie im Impressum.')],
|
||||
['2. Besuch dieser Website', 'Beim Aufruf verarbeitet der Hostingdienst technisch erforderliche Verbindungsdaten: IP-Adresse, Zeitpunkt, angeforderte Adresse, Browserangaben und HTTP-Status. Das dient der sicheren Bereitstellung und der Fehlersuche. Rechtsgrundlage ist unser berechtigtes Interesse an einem störungsfreien Betrieb, Art. 6 Abs. 1 lit. f DSGVO.<br><br>'.$pruefen('Hostinganbieter benennen, Auftragsverarbeitungsvertrag abschließen und die Speicherdauer der Serverprotokolle eintragen.')],
|
||||
['3. Cookies und externe Dienste', 'Diese Website setzt keine Analyse- oder Werbecookies. Bilder, Schriften und Gestaltung kommen vom eigenen Server; es werden keine Karten, Videos oder Schriften von Dritten nachgeladen. Deshalb gibt es auch keinen Einwilligungsdialog.'],
|
||||
['4. Kontakt per E-Mail', 'Wenn Sie uns schreiben, verarbeiten wir Ihre Adresse und Ihre Nachricht, um Ihr Anliegen zu beantworten (Art. 6 Abs. 1 lit. b oder f DSGVO). Wir löschen die Nachricht, wenn sie erledigt ist und keine gesetzliche Aufbewahrungspflicht besteht.'],
|
||||
['5. Ihre Rechte', 'Sie haben unter den gesetzlichen Voraussetzungen das Recht auf Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung und Datenübertragbarkeit. Einer Verarbeitung auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO können Sie widersprechen.'],
|
||||
['6. Beschwerderecht', 'Sie können sich bei einer Datenschutz-Aufsichtsbehörde beschweren, für uns beim Landesbeauftragten für den Datenschutz und die Informationsfreiheit Baden-Württemberg.'],
|
||||
['', "Fragen zum Datenschutz beantworten wir gern unter <a href=\"mailto:$mail\">$mail</a>."],
|
||||
]),
|
||||
],
|
||||
], 'Datenschutz | '.$name);
|
||||
|
||||
$fehlerId = $anlegen('404', [
|
||||
'schemaVersion' => $SCHEMA, 'title' => 'Seite nicht gefunden',
|
||||
'description' => 'Die gesuchte Seite gibt es nicht oder sie wurde verschoben.',
|
||||
'blocks' => [
|
||||
$section('fehler', [
|
||||
$kleinzeile('fehler-label', 'Fehler 404', '@gold-text'),
|
||||
$heading('fehler-titel', 'Diese Seite<br><em>gibt es nicht.</em>', 1, [
|
||||
'desktop' => ['fontSize' => '64px', 'fontWeight' => '400', 'lineHeight' => 1.05, 'letterSpacing' => '-0.04em', 'color' => '@forst', 'margin' => ['bottom' => '22px']],
|
||||
'mobile' => ['fontSize' => '42px'],
|
||||
], '& em{font-style:italic;color:#8c6b27}'),
|
||||
$text('fehler-text', 'Vielleicht hat sich ein Tippfehler in die Adresse geschlichen. Über die Startseite finden Sie zurück.', [
|
||||
'desktop' => ['maxWidth' => '460px', 'fontSize' => '18px', 'lineHeight' => 1.7, 'color' => '@text', 'margin' => ['bottom' => '28px']],
|
||||
]),
|
||||
$button('fehler-knopf', 'Zur Startseite', '/', ['desktop' => ['margin' => ['top' => '0px']]]),
|
||||
], [
|
||||
'desktop' => ['backgroundColor' => '@creme', 'padding' => ['top' => '110px', 'bottom' => '130px']],
|
||||
'mobile' => ['padding' => ['top' => '64px', 'bottom' => '80px']],
|
||||
]),
|
||||
],
|
||||
]);
|
||||
$db->executeStatement('UPDATE page SET robots = ? WHERE id = ?', ['noindex', $fehlerId]);
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Einstellungen, Konto
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
$settings->assignNotFoundPage($fehlerId, '');
|
||||
$settings->save([
|
||||
'legalName' => $name,
|
||||
'description' => 'Kantarci Markt eröffnet bald in der Wilhelm-Drapp-Straße in Baden-Baden: frische Produkte, echte Vielfalt und faire Preise.',
|
||||
'email' => '',
|
||||
'street' => $strasse, 'postalCode' => $plz, 'city' => $ort, 'country' => 'DE',
|
||||
// Noch kein Geschäft mit Öffnungszeiten: keine LocalBusiness-Angaben, bis der Markt öffnet.
|
||||
'localBusiness' => false, 'llmsTxt' => true,
|
||||
'faviconMediaId' => $medien['symbol']['id'],
|
||||
'stickyHeader' => false, 'headerOverlay' => false,
|
||||
'motion' => 'always',
|
||||
'homePath' => 'home', 'imprintPath' => 'impressum', 'privacyPath' => 'datenschutz',
|
||||
// Die Seite lädt nichts von Dritten; deshalb keine Einwilligungsleiste.
|
||||
'consentBanner' => false, 'scrollTop' => false, 'postMetaHeader' => false,
|
||||
]);
|
||||
echo "Einstellungen gespeichert\n";
|
||||
|
||||
$passwort = bin2hex(random_bytes(12));
|
||||
$db->insert('cms_user', ['id' => bin2hex(random_bytes(16)), 'username' => 'umut', 'password_hash' => password_hash($passwort, PASSWORD_BCRYPT), 'roles' => '["ROLE_ADMIN"]']);
|
||||
|
||||
echo "\nFertig.\nVerwaltung: Benutzer „umut“, Passwort: $passwort\n";
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1516,6 +1516,16 @@ 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']);
|
||||
|
||||
// Ein neuer Abruf ersetzt nur die Regeln der geholten Familien; andere Familien, deren
|
||||
// Dateien noch im Ordner liegen, bleiben im Stylesheet.
|
||||
$vorher = "/* latin */\n@font-face{font-family:'Lato';font-weight:400;src:url(/assets/fonts/lato-400-latin.woff2)}\n"
|
||||
."/* latin */\n@font-face{font-family:'DM Sans';font-weight:400;src:url(/assets/fonts/alt.woff2)}\n";
|
||||
$neu = $fonts->combine($vorher, ["/* latin */\n@font-face{font-family:'DM Sans';font-weight:400;src:url(/assets/fonts/dm-sans-400-latin.woff2)}"], ['DM Sans']);
|
||||
self::assertStringContainsString('lato-400-latin.woff2', $neu);
|
||||
self::assertStringContainsString('dm-sans-400-latin.woff2', $neu);
|
||||
self::assertStringNotContainsString('alt.woff2', $neu);
|
||||
self::assertSame(2, substr_count($neu, '@font-face'));
|
||||
|
||||
// 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);
|
||||
@@ -1547,6 +1557,11 @@ final class CmsTest extends TestCase
|
||||
'/assets/fonts/lato-700-latin.woff2',
|
||||
'/assets/fonts/lato-900-latin.woff2',
|
||||
], $fonts->preload($theme->load()));
|
||||
|
||||
// Eine variable Schrift ist eine Datei für alle Schnitte: das Stylesheet nennt sie einmal
|
||||
// mit Gewichtsbereich, und vorgeladen wird nur diese eine Datei statt dreimal dieselbe.
|
||||
file_put_contents($this->path.'-fonts/fonts.css', "@font-face{font-family:'Lato';font-weight:400 900;src:url(/assets/fonts/lato-400-latin.woff2) format('woff2')}");
|
||||
self::assertSame(['/assets/fonts/lato-400-latin.woff2'], $fonts->preload($theme->load()));
|
||||
}
|
||||
|
||||
public function testTheStylesheetsGoOutAsOneFileWithoutComments(): void
|
||||
@@ -3138,6 +3153,9 @@ final class CmsTest extends TestCase
|
||||
& .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}'];
|
||||
// Ein Pseudo-Element braucht content: leer fuer Striche und Verlaeufe, ein Zeichen fuer ein Wasserzeichen.
|
||||
yield 'empty content' => ["&::after { content: ''; height: 2px }", "#b1::after{content:'';height:2px}"];
|
||||
yield 'short content' => ['&::before { content: "K" }', '#b1::before{content:"K"}'];
|
||||
}
|
||||
|
||||
#[DataProvider('ownCssCases')]
|
||||
@@ -3161,6 +3179,9 @@ display: flex', '#b1{display:flex;color:red}'];
|
||||
yield 'old expression syntax' => ['color: expression(alert(1))'];
|
||||
yield 'no value' => ['color'];
|
||||
yield 'nonsense property' => ['co lor: red'];
|
||||
yield 'content leaving the style tag' => ['content: "</style><script>"'];
|
||||
yield 'content with a second quote' => ["content: 'a' 'b'"];
|
||||
yield 'quotes outside content' => ["font-family: 'Evil'"];
|
||||
}
|
||||
|
||||
#[DataProvider('refusedOwnCss')]
|
||||
|
||||
Reference in new issue
Block a user