Umut: "Kannst du bitte fuer die Erstseite bzw. nach der Installation eine schoene Erstseite erstellen. Under Construction oder so ohne Details mit melde dich an usw. Am besten auch noch Logo von KantSites." starterPage() shipped two sections of instructions for the owner: "Melde dich an, oeffne sie im Editor ..." and "Die naechsten Schritte" with design, menus and settings. That stood on the only page a fresh installation serves publicly, so the visitor read it, not the owner - who gets the same steps on the last installer screen and in the admin area anyway. It is now the page such a site actually needs: the KantSites mark, the label "In Arbeit" in the accent colour, the site name as the single H1, "Hier entsteht eine neue Webpraesenz.", a short rule and the line "Erstellt mit KantSites". The section is a flex container with min-height 70vh and a gradient from @paper to @mint, so the little that is there sits in the middle of the screen; the colours are palette references and follow a design change. Everything is an ordinary block with ordinary style values and can be changed or removed in the editor. The page description is a real sentence now instead of "NOCH AUSZUFUELLEN", so a freshly installed site does not carry a placeholder into search results. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
292 lines
19 KiB
PHP
292 lines
19 KiB
PHP
<?php
|
||
|
||
namespace App\Content;
|
||
|
||
final class DemoContent
|
||
{
|
||
public static function pages(): array
|
||
{
|
||
return [
|
||
'home' => [
|
||
'schemaVersion' => 1,
|
||
'title' => 'KantSites · Raum für deine Website',
|
||
'description' => 'Ein eigenes CMS für individuelle Kundenwebsites. Entdecke die technische Grundlage für strukturierte Inhalte, schnelle Seiten und einen visuellen Editor.',
|
||
'blocks' => [
|
||
self::section('hero', [
|
||
self::text('eyebrow', 'KANTSITES / DER ANFANG IST GEMACHT'),
|
||
self::heading('headline', 'Deine Inhalte.\nDein Gestaltungsspielraum.', 1),
|
||
self::text('intro', 'Eine Website sollte zu dir passen. Wir entwickeln die Grundlage dafür: klar strukturierte Inhalte, unabhängiges Hosting und einen Editor, der Gestaltung einfach macht.'),
|
||
self::button('discover', 'Die Grundlage entdecken', '/grundlagen'),
|
||
]),
|
||
self::section('principles', [
|
||
self::text('principles-label', '01 / WAS UNS WICHTIG IST'),
|
||
self::heading('principles-heading', 'Ein guter Auftritt beginnt\nunter der Oberfläche.', 2),
|
||
['id' => 'principles-grid', 'type' => 'row', 'children' => [
|
||
self::card('content', '01 — INHALTE', 'Klar aufgebaut.', 'Texte und Gestaltung bleiben getrennt. Ein versionierter Blockbaum bildet die Grundlage, damit sich Inhalte später weiterentwickeln lassen.'),
|
||
self::card('delivery', '02 — AUSGABE', 'Direkt lesbar.', 'Der Hauptinhalt kommt als vollständiges HTML beim Browser an. Zum Lesen braucht diese Seite kein JavaScript und keine Editor-Bibliothek.'),
|
||
self::card('control', '03 — KONTROLLE', 'Bewusst veröffentlicht.', 'Entwurf und Veröffentlichung haben eigene Stände. Eine gespeicherte Änderung erscheint erst dann öffentlich, wenn sie veröffentlicht wird.'),
|
||
]],
|
||
], 'mint'),
|
||
self::section('next', [
|
||
self::text('next-label', '02 / VON DER GRUNDLAGE ZUM WERKZEUG'),
|
||
self::heading('next-heading', 'Schritt für Schritt.\nMit einem klaren Ziel.', 2),
|
||
self::text('next-text', 'Diese Seite ist der erste technische Prototyp von KantSites. Als Nächstes entsteht der visuelle Editor mit Strukturbaum, Vorschau und Eigenschaften. Das fertige CMS wächst an einer echten Pilotwebsite.'),
|
||
self::button('progress', 'Den Projektstand ansehen', '/projektstand'),
|
||
], 'ink'),
|
||
],
|
||
],
|
||
'grundlagen' => self::article('Die Grundlage · KantSites', 'Wie KantSites Inhalte als Blöcke speichert und Seiten als HTML ausliefert.', 'Ein Fundament für\nveränderbare Websites.', [
|
||
['Inhalte mit Struktur', 'Eine Seite besteht aus einem versionierten Dokument. Sektionen, Zeilen und Spalten ordnen Überschriften, Texte, Bilder und Links. Jeder Block hat eine stabile Kennung. Text wird bei der Ausgabe automatisch maskiert; frei ausführbares HTML ist kein Inhaltsformat.'],
|
||
['Ein Entwurf bleibt ein Entwurf', 'Speichern erzeugt einen eigenen Stand. Die öffentliche Route liest ausschließlich die veröffentlichte Revision. Veraltete Schreibstände werden abgewiesen, damit eine ältere Bearbeitung neue Inhalte nicht unbemerkt überschreibt.'],
|
||
['PHP-Webhosting als Ziel', 'Der Prototyp nutzt Symfony, Twig und Doctrine. Die lokale Installation verwendet SQLite. Für den späteren Hostingbetrieb ist MySQL oder MariaDB vorgesehen. Der öffentlich erreichbare Dokumentenstamm ist ausschließlich das Verzeichnis public.'],
|
||
['Qualität von Anfang an', 'Semantische Elemente, sichtbarer Tastaturfokus und ein Sprunglink sind bereits Teil der Ausgabe. Automatische Prüfungen begleiten die Entwicklung. Belastbare Qualitätswerte für Kundenwebsites benötigen repräsentative Inhalte und Messungen auf dem tatsächlichen Hosting.'],
|
||
]),
|
||
'projektstand' => self::article('Projektstand · KantSites', 'Der aktuelle Entwicklungsstand von KantSites und die nächsten Schritte zum visuellen Editor.', 'Der erste Schritt\nist eine solide Basis.', [
|
||
['Jetzt: technische Grundlage', 'Projektregeln und Entwicklungsplan sind abgelegt. Das Grundgerüst speichert Seiten in einer Datenbank und liefert veröffentlichte Blockdokumente als HTML aus. Diese Demonstrationsinhalte gehören zum Prototyp; sie sind keine Kundenwebsite.'],
|
||
['Als Nächstes: visuell bearbeiten', 'Geplant sind eine Elementbibliothek, ein synchronisierter Strukturbaum, Live-Vorschau und ein Eigenschaftenpanel. Verschieben, Duplizieren, Löschen und Undo/Redo werden zusammen mit dem Speichern und Laden geprüft.'],
|
||
['Noch zu konkretisieren', 'Die Pilotwebsite, konkrete UX-Builder-Referenzen und die Ausstattung des PHP-Webhostings stehen noch aus. Anmeldung, Medienverwaltung, Formulare, Templates und Plugins folgen in den dafür vorgesehenen Ausbauschritten.'],
|
||
]),
|
||
];
|
||
}
|
||
|
||
/** The first draft of a newly created page: valid, empty enough to replace, with exactly one H1. */
|
||
/**
|
||
* The two pages a German business website must have. Not demo content and not optional: they
|
||
* are created with a skeleton whose gaps are marked, because the text is a legal statement
|
||
* about a real company - the CMS can prepare the structure, not write the declaration.
|
||
*/
|
||
public const LEGAL = [
|
||
'datenschutz' => 'Datenschutzerklärung',
|
||
'impressum' => 'Impressum',
|
||
];
|
||
|
||
public static function legalPage(string $kind): array
|
||
{
|
||
$sections = 'impressum' === $kind
|
||
? [
|
||
['Angaben gemäß § 5 DDG', "Firmenname\nStraße und Hausnummer\nPostleitzahl und Ort\n\nNOCH AUSZUFÜLLEN: Rechtsform, Vertretungsberechtigte, Registergericht und Registernummer, Umsatzsteuer-Identifikationsnummer."],
|
||
['Kontakt', "Telefon: \nE-Mail: \n\nNOCH AUSZUFÜLLEN: eine Adresse, unter der eine Anfrage tatsächlich beantwortet wird."],
|
||
['Verantwortlich für den Inhalt', 'NOCH AUSZUFÜLLEN: Name und Adresse der verantwortlichen Person, falls von oben abweichend. Bei redaktionellen Inhalten nach § 18 Abs. 2 MStV erforderlich.'],
|
||
['Streitschlichtung', 'NOCH AUSZUFÜLLEN oder entfernen: Hinweis auf eine Verbraucherschlichtungsstelle, sofern eine Teilnahmepflicht besteht.'],
|
||
]
|
||
: [
|
||
['Verantwortliche Stelle', "Firmenname\nStraße und Hausnummer\nPostleitzahl und Ort\nE-Mail: \n\nNOCH AUSZUFÜLLEN: gegebenenfalls Datenschutzbeauftragte oder Datenschutzbeauftragter mit Kontaktdaten."],
|
||
['Welche Daten beim Besuch anfallen', "Beim Aufruf dieser Website verarbeitet der Server technisch notwendige Daten: abgerufene Adresse, Zeitpunkt, übertragene Datenmenge, Browser- und Betriebssystemangaben sowie die IP-Adresse.\n\nNOCH AUSZUFÜLLEN: Speicherdauer der Serverprotokolle und Name des Hosters, mit dem ein Vertrag über die Auftragsverarbeitung besteht."],
|
||
['Cookies und Einwilligung', "Diese Website setzt von sich aus keine Cookies zur Analyse oder Werbung. Wird eine Einwilligung abgefragt, speichert ein einzelnes Cookie dieser Website ausschließlich die Entscheidung selbst – ohne Kennung, für ein halbes Jahr. Die Entscheidung lässt sich jederzeit unter /einwilligung ändern.\n\nNOCH PRÜFEN: Dieser Abschnitt muss zu den tatsächlich eingebundenen Inhalten passen."],
|
||
['Eingebettete Inhalte anderer Anbieter', 'NOCH AUSZUFÜLLEN: Für jeden eingebetteten Inhalt Anbieter, Zweck, Rechtsgrundlage und Empfängerland angeben. Ohne Einwilligung wird ein solcher Inhalt nicht geladen und es entsteht keine Verbindung zum Anbieter.'],
|
||
['Kontaktformular', 'NOCH AUSZUFÜLLEN: Welche Felder erhoben werden, wofür die Angaben verwendet werden, wie lange eine Anfrage gespeichert bleibt und auf welcher Rechtsgrundlage.'],
|
||
['Deine Rechte', 'Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung, Datenübertragbarkeit und Widerspruch. Eine erteilte Einwilligung kann jederzeit widerrufen werden. Außerdem besteht ein Beschwerderecht bei einer Datenschutzaufsichtsbehörde.'],
|
||
];
|
||
|
||
$blocks = [
|
||
self::section('legal-hero', [
|
||
self::heading('legal-heading', self::LEGAL[$kind], 1),
|
||
self::text('legal-intro', 'Diese Seite wurde von KantSites angelegt und muss noch ausgefüllt werden. Die mit „NOCH AUSZUFÜLLEN“ markierten Stellen sind Pflichtangaben; der Text ist keine Rechtsberatung.'),
|
||
], 'mint'),
|
||
];
|
||
foreach ($sections as $index => [$title, $text]) {
|
||
$blocks[] = self::section('legal-'.$index, [
|
||
self::heading('legal-title-'.$index, $title, 2),
|
||
self::text('legal-text-'.$index, $text),
|
||
], 'paper', 'narrow');
|
||
}
|
||
|
||
return [
|
||
'schemaVersion' => 3,
|
||
'title' => self::LEGAL[$kind],
|
||
'description' => 'impressum' === $kind
|
||
? 'Anbieterkennzeichnung und Kontaktangaben.'
|
||
: 'Wie diese Website mit personenbezogenen Daten umgeht.',
|
||
'blocks' => $blocks,
|
||
];
|
||
}
|
||
|
||
public static function starter(string $title, string $type = 'page'): array
|
||
{
|
||
return [
|
||
'schemaVersion' => 3,
|
||
'title' => $title,
|
||
'description' => 'post' === $type
|
||
? 'Ein neuer Beitrag. Ersetze diese Beschreibung durch eine kurze Zusammenfassung.'
|
||
: 'Eine neue Seite. Ersetze diese Beschreibung durch eine kurze Zusammenfassung fuer Suchmaschinen.',
|
||
'blocks' => [
|
||
self::section('start-hero', [
|
||
self::heading('start-heading', $title, 1),
|
||
self::text('start-intro', 'Schreibe hier den ersten Absatz. Waehle links ein Element aus, um es zu bearbeiten, oder fuege ueber die Elementbibliothek weitere Bloecke hinzu.'),
|
||
], 'mint'),
|
||
self::section('start-body', [
|
||
self::heading('start-subheading', 'Ein erster Abschnitt', 2),
|
||
self::text('start-text', 'Dieser Text ist ein Platzhalter. Ersetze ihn durch deinen eigenen Inhalt.'),
|
||
], 'paper', 'narrow'),
|
||
],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* The start page of a fresh installation - not demo content.
|
||
*
|
||
* A website that is already reachable but not yet written needs a page that says exactly that:
|
||
* the name of the site, the notice that something is being built here, and nothing else. No
|
||
* instructions for the owner - those stand on the last screen of the installer and in the
|
||
* admin area, not on a page every visitor sees. Exactly one H1, as every page requires.
|
||
*/
|
||
public static function starterPage(string $siteName): array
|
||
{
|
||
return [
|
||
'schemaVersion' => 24,
|
||
'title' => $siteName,
|
||
'description' => 'Hier entsteht eine neue Webpräsenz. Diese Seite wird gerade eingerichtet.',
|
||
'blocks' => [
|
||
[
|
||
'id' => 'start-hero',
|
||
'type' => 'section',
|
||
'layout' => ['tone' => 'mint', 'width' => 'narrow'],
|
||
'style' => [
|
||
// Eine Seite ohne Inhalt: der Abschnitt füllt den Bildschirm und stellt das
|
||
// Wenige, das dasteht, in die Mitte, statt es oben in eine Ecke zu setzen.
|
||
'desktop' => [
|
||
'display' => 'flex', 'alignItems' => 'center', 'justifyContent' => 'center',
|
||
'minHeight' => '70vh', 'textAlign' => 'center',
|
||
'padding' => ['top' => '96px', 'bottom' => '96px'],
|
||
'gradientFrom' => '@paper', 'gradientTo' => '@mint', 'gradientAngle' => 165,
|
||
],
|
||
'mobile' => ['minHeight' => 'auto', 'padding' => ['top' => '64px', 'bottom' => '64px']],
|
||
],
|
||
'children' => [
|
||
[
|
||
'id' => 'start-logo',
|
||
'type' => 'image',
|
||
'props' => ['src' => '/assets/kantsites-logo.svg', 'alt' => 'KantSites', 'decorative' => false, 'width' => 128, 'height' => 128],
|
||
'style' => ['desktop' => ['maxWidth' => '86px', 'margin' => ['left' => 'auto', 'right' => 'auto', 'bottom' => '30px']]],
|
||
],
|
||
[
|
||
'id' => 'start-label',
|
||
'type' => 'text',
|
||
'props' => ['text' => 'In Arbeit'],
|
||
'style' => ['desktop' => [
|
||
'fontSize' => '13px', 'fontWeight' => '700', 'letterSpacing' => '2px',
|
||
'textTransform' => 'uppercase', 'color' => '@accent', 'margin' => ['bottom' => '16px'],
|
||
]],
|
||
],
|
||
self::heading('start-heading', $siteName, 1),
|
||
[
|
||
'id' => 'start-intro',
|
||
'type' => 'text',
|
||
'props' => ['text' => 'Hier entsteht eine neue Webpräsenz.'],
|
||
'style' => ['desktop' => [
|
||
'fontSize' => '21px', 'maxWidth' => '38ch',
|
||
'margin' => ['left' => 'auto', 'right' => 'auto', 'bottom' => '0px'],
|
||
]],
|
||
],
|
||
[
|
||
'id' => 'start-rule',
|
||
'type' => 'divider',
|
||
'style' => ['desktop' => ['width' => '72px', 'margin' => ['top' => '40px', 'right' => 'auto', 'bottom' => '22px', 'left' => 'auto']]],
|
||
],
|
||
[
|
||
'id' => 'start-note',
|
||
'type' => 'text',
|
||
'props' => ['text' => 'Erstellt mit KantSites'],
|
||
'style' => ['desktop' => ['fontSize' => '14px', 'letterSpacing' => '0.4px', 'margin' => ['bottom' => '0px']]],
|
||
],
|
||
],
|
||
],
|
||
],
|
||
];
|
||
}
|
||
|
||
/** Header and footer of a fresh installation, with the site name instead of a wordmark. */
|
||
public static function starterAreas(string $siteName): array
|
||
{
|
||
$areas = self::areas();
|
||
$areas['header']['document']['blocks'][0]['children'][0]['children'][0]['children'][0]['props']['text'] = $siteName;
|
||
$areas['footer']['document']['blocks'][0]['children'][0]['children'][0]['children'][0]['props']['text'] = '© '.date('Y').' '.$siteName;
|
||
|
||
return $areas;
|
||
}
|
||
|
||
/**
|
||
* The header and footer a fresh installation starts with. They are ordinary block documents:
|
||
* everything in them can be changed, moved or removed in the editor. No main heading, because
|
||
* every page brings its own.
|
||
*
|
||
* @return array<string, array{name: string, document: array}>
|
||
*/
|
||
public static function areas(): array
|
||
{
|
||
$row = static fn (string $id, array $children): array => [
|
||
'id' => $id, 'type' => 'row',
|
||
'layout' => ['design' => ['desktop' => ['columns' => 'two', 'gap' => 'medium'], 'mobile' => ['columns' => 'one']]],
|
||
'children' => $children,
|
||
];
|
||
$column = static fn (string $id, array $children): array => ['id' => $id, 'type' => 'column', 'children' => $children];
|
||
|
||
return [
|
||
'header' => ['name' => 'Kopfbereich', 'document' => [
|
||
'schemaVersion' => 11,
|
||
'title' => 'Kopfbereich',
|
||
'description' => 'Erscheint auf jeder Seite über dem Inhalt.',
|
||
'blocks' => [
|
||
self::section('area-header', [
|
||
$row('area-header-row', [
|
||
$column('area-header-brand', [self::text('area-header-name', 'kantsites')]),
|
||
$column('area-header-nav', [['id' => 'area-header-menu', 'type' => 'menu', 'props' => ['slot' => 'header']]]),
|
||
]),
|
||
]),
|
||
],
|
||
]],
|
||
'footer' => ['name' => 'Fußbereich', 'document' => [
|
||
'schemaVersion' => 11,
|
||
'title' => 'Fußbereich',
|
||
'description' => 'Erscheint auf jeder Seite unter dem Inhalt.',
|
||
'blocks' => [
|
||
self::section('area-footer', [
|
||
$row('area-footer-row', [
|
||
$column('area-footer-text', [self::text('area-footer-claim', 'Eine Grundlage. Viele Möglichkeiten.')]),
|
||
$column('area-footer-nav', [['id' => 'area-footer-menu', 'type' => 'menu', 'props' => ['slot' => 'footer']]]),
|
||
]),
|
||
], 'ink'),
|
||
],
|
||
]],
|
||
];
|
||
}
|
||
|
||
private static function article(string $title, string $description, string $heading, array $sections): array
|
||
{
|
||
$blocks = [self::section('article-hero', [self::text('article-label', 'KANTSITES / ENTWICKLUNG'), self::heading('article-heading', $heading, 1)], 'mint')];
|
||
$children = [];
|
||
foreach ($sections as $i => [$label, $text]) {
|
||
$children[] = self::heading('heading-'.$i, $label, 2);
|
||
$children[] = self::text('text-'.$i, $text);
|
||
}
|
||
$blocks[] = self::section('article-body', $children, 'paper', 'narrow');
|
||
|
||
return ['schemaVersion' => 1, 'title' => $title, 'description' => $description, 'blocks' => $blocks];
|
||
}
|
||
|
||
private static function section(string $id, array $children, string $tone = 'paper', string $width = 'normal'): array
|
||
{
|
||
return ['id' => $id, 'type' => 'section', 'layout' => ['tone' => $tone, 'width' => $width], 'children' => $children];
|
||
}
|
||
|
||
private static function heading(string $id, string $text, int $level): array
|
||
{
|
||
return ['id' => $id, 'type' => 'heading', 'props' => ['text' => str_replace('\\n', ' ', $text), 'level' => $level]];
|
||
}
|
||
|
||
private static function text(string $id, string $text): array
|
||
{
|
||
return ['id' => $id, 'type' => 'text', 'props' => ['text' => $text]];
|
||
}
|
||
|
||
private static function button(string $id, string $text, string $href): array
|
||
{
|
||
return ['id' => $id, 'type' => 'button', 'props' => ['text' => $text, 'href' => $href]];
|
||
}
|
||
|
||
private static function card(string $id, string $label, string $title, string $text): array
|
||
{
|
||
return ['id' => $id, 'type' => 'column', 'children' => [self::text($id.'-label', $label), self::heading($id.'-heading', $title, 3), self::text($id.'-text', $text)]];
|
||
}
|
||
}
|