An outside review (ChatGPT, from a screenshot) found the hero too generic and the copy full of agency words. Checked each point against the live page; the valid ones are done here: - Hero now says who it is for: "Webdesign für Betriebe vor Ort", "Websites, auf denen Ihre Kunden sofort finden, was sie suchen." This matches the 500 € offer and the three portfolio pieces, which were already about local businesses. - Second hero button "Arbeiten ansehen" (#arbeiten); every primary button now reads "Projekt besprechen". - "Was wir anders machen" replaces "Individuell, sorgfältig, technisch solide."; highlights I and III and the service texts are rewritten as concrete statements. - Mappe entries show "Die Aufgabe" / "Unsere Lösung". No invented results: all three are concept work. - Two FAQ answers filled where the facts are known (self-editing is part of the offer; no on-site meetings). - Two em dashes in body copy replaced. Not taken over: the "500+" figure and "Mehr erfahren" button the review mentions do not exist on the page. Admin schema gains hero.secondaryLabel and mappe.items[].aufgabe. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
344 lines
13 KiB
JavaScript
344 lines
13 KiB
JavaScript
const { FONT_NAMES } = require('./fonts');
|
||
|
||
/**
|
||
* Describes the editable shape of content/site.json (file: 'content', the
|
||
* default) and content/theme.json (file: 'theme') for the admin UI.
|
||
*
|
||
* Field types: text, textarea, richtext, email, boolean, image,
|
||
* list-string, list-richtext, list-object, group, select, size, color.
|
||
* "richtext"/"list-richtext" fields are edited with a WYSIWYG toolbar
|
||
* (bold/italic/underline + per-selection font) and stored as a small
|
||
* allowlisted HTML subset - see admin/lib/rich-sanitize.js for what's
|
||
* allowed and src/lib/content.ts for how it's rendered on the site.
|
||
* "group" is a single nested object (not repeatable) described by itemFields.
|
||
* "color" fields store { r, g, b, a }. "size" fields store a CSS length (e.g. "1.25rem").
|
||
*/
|
||
module.exports = [
|
||
{
|
||
key: 'meta',
|
||
label: 'SEO / Meta',
|
||
fields: [{ key: 'description', label: 'Meta-Beschreibung', type: 'textarea' }],
|
||
},
|
||
{
|
||
key: 'nav',
|
||
label: 'Navigation',
|
||
fields: [
|
||
{ key: 'logo', label: 'Logo', type: 'image' },
|
||
{ key: 'logoAlt', label: 'Logo Alt-Text', type: 'text' },
|
||
{ key: 'ctaLabel', label: 'Button-Text', type: 'text' },
|
||
{
|
||
key: 'links',
|
||
label: 'Menüpunkte',
|
||
type: 'list-object',
|
||
itemLabel: 'Menüpunkt',
|
||
itemFields: [
|
||
{ key: 'label', label: 'Text', type: 'text' },
|
||
{ key: 'anchor', label: 'Sprungziel (Abschnitt-ID, ohne #)', type: 'text' },
|
||
],
|
||
},
|
||
],
|
||
},
|
||
{
|
||
key: 'hero',
|
||
label: 'Hero (ganz oben)',
|
||
fields: [
|
||
{ key: 'eyebrow', label: 'Kicker-Zeile', type: 'text' },
|
||
{ key: 'titleBefore', label: 'Titel – Teil 1', type: 'text' },
|
||
{ key: 'titleEmphasis', label: 'Titel – hervorgehobenes Wort', type: 'text' },
|
||
{ key: 'titleAfter', label: 'Titel – Teil 2', type: 'text' },
|
||
{ key: 'lead', label: 'Einleitungstext', type: 'richtext' },
|
||
{ key: 'ctaLabel', label: 'Button-Text', type: 'text' },
|
||
{ key: 'secondaryLabel', label: 'Zweiter Button (zur Mappe), leer = ausblenden', type: 'text' },
|
||
{
|
||
key: 'visual',
|
||
label: 'Geräte-Ansicht (rechts neben dem Hero-Text)',
|
||
type: 'group',
|
||
itemFields: [
|
||
{
|
||
key: 'customImage',
|
||
label: 'Eigenes Bild statt Laptop und Handy (leer = Geräte anzeigen)',
|
||
type: 'image',
|
||
},
|
||
{ key: 'laptop', label: 'Bild im Laptop (Querformat 16:9)', type: 'image' },
|
||
{ key: 'phone', label: 'Bild im Handy (Hochformat)', type: 'image' },
|
||
],
|
||
},
|
||
],
|
||
},
|
||
{
|
||
key: 'warum',
|
||
label: 'Abschnitt 02 – Warum uns',
|
||
fields: [
|
||
{ key: 'eyebrow', label: 'Kicker-Zeile', type: 'text' },
|
||
{ key: 'title', label: 'Überschrift', type: 'text' },
|
||
{ key: 'lead', label: 'Text', type: 'richtext' },
|
||
{
|
||
key: 'readability',
|
||
label: 'PDF-vs-Tabelle-Vergleich',
|
||
type: 'group',
|
||
itemFields: [
|
||
{
|
||
key: 'customImage',
|
||
label: 'Eigenes Bild statt der Tabelle (leer = Öffnungszeiten-Tabelle anzeigen)',
|
||
type: 'image',
|
||
},
|
||
{ key: 'title', label: 'Überschrift', type: 'text' },
|
||
{ key: 'lead', label: 'Text', type: 'richtext' },
|
||
{ key: 'hoursTitle', label: 'Titel der Tabelle', type: 'text' },
|
||
{
|
||
key: 'hoursRows',
|
||
label: 'Tabellenzeilen',
|
||
type: 'list-object',
|
||
itemLabel: 'Zeile',
|
||
itemFields: [
|
||
{ key: 'day', label: 'Tag', type: 'text' },
|
||
{ key: 'time', label: 'Uhrzeit', type: 'text' },
|
||
],
|
||
},
|
||
],
|
||
},
|
||
],
|
||
},
|
||
{
|
||
key: 'preis',
|
||
label: 'Abschnitt 01 – Preis',
|
||
fields: [
|
||
{ key: 'eyebrow', label: 'Kicker-Zeile', type: 'text' },
|
||
{ key: 'title', label: 'Überschrift', type: 'text' },
|
||
{ key: 'amount', label: 'Preis', type: 'text' },
|
||
{ key: 'amountNote', label: 'Preis-Zusatz', type: 'text' },
|
||
{ key: 'lead', label: 'Einleitungstext', type: 'richtext' },
|
||
{ key: 'slotsNote', label: 'Plätze-Hinweis', type: 'text' },
|
||
{
|
||
key: 'features',
|
||
label: 'Leistungspunkte',
|
||
type: 'list-object',
|
||
itemLabel: 'Punkt',
|
||
itemFields: [
|
||
{ key: 'text', label: 'Text', type: 'text' },
|
||
{ key: 'excluded', label: 'Als "nicht enthalten" markieren', type: 'boolean' },
|
||
],
|
||
},
|
||
{ key: 'asideTitle', label: 'Kasten-Überschrift', type: 'text' },
|
||
{ key: 'asideParagraph1', label: 'Kasten-Absatz 1', type: 'richtext' },
|
||
{ key: 'asideParagraph2', label: 'Kasten-Absatz 2', type: 'richtext' },
|
||
{ key: 'ctaLabel', label: 'Button-Text', type: 'text' },
|
||
],
|
||
},
|
||
{
|
||
key: 'highlights',
|
||
label: 'Abschnitt 02 – Die drei Punkte',
|
||
type: 'array-root',
|
||
itemLabel: 'Punkt',
|
||
itemFields: [
|
||
{ key: 'num', label: 'Kennung (z. B. I, II, III)', type: 'text' },
|
||
{ key: 'title', label: 'Titel', type: 'text' },
|
||
{ key: 'text', label: 'Text', type: 'textarea' },
|
||
],
|
||
},
|
||
{
|
||
key: 'leistungen',
|
||
label: 'Abschnitt 03 – Leistungen',
|
||
fields: [
|
||
{ key: 'eyebrow', label: 'Kicker-Zeile', type: 'text' },
|
||
{ key: 'title', label: 'Überschrift', type: 'text' },
|
||
{ key: 'lead', label: 'Text', type: 'richtext' },
|
||
{
|
||
key: 'items',
|
||
label: 'Leistungen',
|
||
type: 'list-object',
|
||
itemLabel: 'Leistung',
|
||
itemFields: [
|
||
{ key: 'num', label: 'Nummer', type: 'text' },
|
||
{ key: 'title', label: 'Titel', type: 'text' },
|
||
{ key: 'text', label: 'Text', type: 'textarea' },
|
||
],
|
||
},
|
||
{ key: 'nichtMachenTitle', label: '"Was wir nicht machen" – Überschrift', type: 'text' },
|
||
{ key: 'nichtMachen', label: '"Was wir nicht machen" – Punkte', type: 'list-string' },
|
||
],
|
||
},
|
||
{
|
||
key: 'mappe',
|
||
label: 'Abschnitt 04 – Mappe',
|
||
fields: [
|
||
{ key: 'eyebrow', label: 'Kicker-Zeile', type: 'text' },
|
||
{ key: 'title', label: 'Überschrift', type: 'text' },
|
||
{
|
||
key: 'items',
|
||
label: 'Arbeiten',
|
||
type: 'list-object',
|
||
itemLabel: 'Arbeit',
|
||
itemFields: [
|
||
{ key: 'image', label: 'Bildschirmfoto', type: 'image' },
|
||
{ key: 'mobileImage', label: 'Handy-Ansicht (Hochformat, leer = kein Handy)', type: 'image' },
|
||
{ key: 'video', label: 'Video im Laptop (Pfad zu einer .mp4, leer = Standbild)', type: 'text' },
|
||
{ key: 'kategorie', label: 'Kategorie', type: 'text' },
|
||
{ key: 'titel', label: 'Titel / Name', type: 'text' },
|
||
{ key: 'aufgabe', label: 'Die Aufgabe (leer = nur Text zeigen)', type: 'textarea' },
|
||
{ key: 'text', label: 'Unsere Lösung / Text', type: 'textarea' },
|
||
{ key: 'nameHidden', label: 'Name noch verstecken (Platzhalter zeigen)', type: 'boolean' },
|
||
],
|
||
},
|
||
],
|
||
},
|
||
{
|
||
key: 'ablauf',
|
||
label: 'Abschnitt 05 – Ablauf',
|
||
fields: [
|
||
{ key: 'eyebrow', label: 'Kicker-Zeile', type: 'text' },
|
||
{ key: 'title', label: 'Überschrift', type: 'text' },
|
||
{
|
||
key: 'steps',
|
||
label: 'Schritte',
|
||
type: 'list-object',
|
||
itemLabel: 'Schritt',
|
||
itemFields: [
|
||
{ key: 'num', label: 'Nummer', type: 'text' },
|
||
{ key: 'title', label: 'Titel', type: 'text' },
|
||
{ key: 'text', label: 'Text', type: 'textarea' },
|
||
{ key: 'dauer', label: 'Dauer (leer = wird nicht angezeigt)', type: 'text' },
|
||
],
|
||
},
|
||
],
|
||
},
|
||
{
|
||
key: 'about',
|
||
label: 'Abschnitt 06 – Über uns',
|
||
fields: [
|
||
{ key: 'eyebrow', label: 'Kicker-Zeile', type: 'text' },
|
||
{ key: 'title', label: 'Überschrift', type: 'text' },
|
||
{ key: 'quote', label: 'Zitat', type: 'text' },
|
||
{ key: 'paragraphs', label: 'Absätze', type: 'list-richtext' },
|
||
{ key: 'photo', label: 'Foto', type: 'image' },
|
||
],
|
||
},
|
||
{
|
||
key: 'faq',
|
||
label: 'Abschnitt 07 – FAQ',
|
||
fields: [
|
||
{ key: 'eyebrow', label: 'Kicker-Zeile', type: 'text' },
|
||
{ key: 'title', label: 'Überschrift', type: 'text' },
|
||
{
|
||
key: 'items',
|
||
label: 'Fragen',
|
||
type: 'list-object',
|
||
itemLabel: 'Frage',
|
||
itemFields: [
|
||
{ key: 'question', label: 'Frage', type: 'text' },
|
||
{ key: 'answer', label: 'Antwort (leer = Platzhalter)', type: 'textarea' },
|
||
],
|
||
},
|
||
],
|
||
},
|
||
{
|
||
key: 'kontakt',
|
||
label: 'Abschnitt 08 – Kontakt',
|
||
fields: [
|
||
{ key: 'eyebrow', label: 'Kicker-Zeile', type: 'text' },
|
||
{ key: 'title', label: 'Überschrift', type: 'text' },
|
||
{ key: 'lead', label: 'Einleitungstext', type: 'richtext' },
|
||
{ key: 'phone', label: 'Telefonnummer (leer = Platzhalter)', type: 'text' },
|
||
{ key: 'hours', label: 'Erreichbarkeit (leer = Platzhalter)', type: 'text' },
|
||
{ key: 'email', label: 'E-Mail-Adresse', type: 'email' },
|
||
{ key: 'howWeWork', label: '"Wie wir arbeiten" (leer = Platzhalter)', type: 'textarea' },
|
||
],
|
||
},
|
||
{
|
||
key: 'footer',
|
||
label: 'Footer',
|
||
fields: [
|
||
{ key: 'laraNachname', label: 'Lara – Nachname (leer = Platzhalter)', type: 'text' },
|
||
{ key: 'pascalNachname', label: 'Pascal – Nachname (leer = Platzhalter)', type: 'text' },
|
||
],
|
||
},
|
||
{
|
||
key: 'impressum',
|
||
label: 'Impressum',
|
||
fields: [
|
||
{ key: 'anbieterAngaben', label: 'Name/Firma + Anschrift (leer = Platzhalter)', type: 'textarea' },
|
||
{ key: 'ustId', label: 'Umsatzsteuer-ID (leer = Platzhalter)', type: 'textarea' },
|
||
{ key: 'verantwortlich', label: 'Verantwortlich für den Inhalt (leer = Platzhalter)', type: 'textarea' },
|
||
{ key: 'streitschlichtung', label: 'Verbraucherstreitbeilegung (§ 36 VSBG, leer = Platzhalter)', type: 'textarea' },
|
||
{
|
||
key: 'haftungSections',
|
||
label: 'Haftungsausschluss – Abschnitte',
|
||
type: 'list-object',
|
||
itemLabel: 'Abschnitt',
|
||
itemFields: [
|
||
{ key: 'heading', label: 'Überschrift', type: 'text' },
|
||
{ key: 'text', label: 'Text', type: 'richtext' },
|
||
],
|
||
},
|
||
{ key: 'note', label: 'Hinweistext am Seitenende', type: 'textarea' },
|
||
],
|
||
},
|
||
{
|
||
key: 'datenschutz',
|
||
label: 'Datenschutz',
|
||
fields: [
|
||
{ key: 'intro', label: 'Einleitung (leer = Platzhalter)', type: 'textarea' },
|
||
{
|
||
key: 'sections',
|
||
label: 'Abschnitte',
|
||
type: 'list-object',
|
||
itemLabel: 'Abschnitt',
|
||
itemFields: [
|
||
{ key: 'heading', label: 'Überschrift', type: 'text' },
|
||
{
|
||
key: 'text',
|
||
label: 'Text (leer = Platzhalter; {email} wird durch die Kontakt-E-Mail ersetzt)',
|
||
type: 'richtext',
|
||
},
|
||
],
|
||
},
|
||
{ key: 'note', label: 'Hinweistext am Seitenende', type: 'textarea' },
|
||
],
|
||
},
|
||
{
|
||
key: 'fonts',
|
||
file: 'theme',
|
||
label: 'Design – Schriftarten',
|
||
fields: [
|
||
{ key: 'heading', label: 'Überschriften-Schrift', type: 'select', options: FONT_NAMES },
|
||
{ key: 'body', label: 'Fließtext-Schrift', type: 'select', options: FONT_NAMES },
|
||
],
|
||
},
|
||
{
|
||
key: 'fontSizes',
|
||
file: 'theme',
|
||
label: 'Design – Schriftgrößen',
|
||
fields: [
|
||
{ key: 'sm', label: 'Klein (Kicker-Zeilen, Feinschrift)', type: 'size', min: 0.6, max: 1.5, step: 0.025 },
|
||
{ key: 'base', label: 'Standard (Fließtext)', type: 'size', min: 0.75, max: 1.5, step: 0.025 },
|
||
{ key: 'md', label: 'Mittel (Einleitungstexte)', type: 'size', min: 0.9, max: 1.75, step: 0.025 },
|
||
{ key: 'lg', label: 'Groß (H3)', type: 'size', min: 1, max: 2.5, step: 0.025 },
|
||
{ key: 'xl', label: 'Größer (H2 mobil)', type: 'size', min: 1.2, max: 3, step: 0.025 },
|
||
{ key: '2xl', label: 'Sehr groß (H2)', type: 'size', min: 1.5, max: 4, step: 0.025 },
|
||
{ key: '3xl', label: 'Riesig (H1)', type: 'size', min: 1.75, max: 5, step: 0.025 },
|
||
],
|
||
},
|
||
{
|
||
key: 'colors',
|
||
file: 'theme',
|
||
label: 'Design – Farben',
|
||
fields: [
|
||
{ key: 'bg', label: 'Hintergrund', type: 'color' },
|
||
{ key: 'bgAlt', label: 'Hintergrund (Variante)', type: 'color' },
|
||
{ key: 'surface', label: 'Flächen / Karten', type: 'color' },
|
||
{ key: 'text', label: 'Text', type: 'color' },
|
||
{ key: 'textMuted', label: 'Text (gedämpft)', type: 'color' },
|
||
{ key: 'border', label: 'Rahmen', type: 'color' },
|
||
{ key: 'primary', label: 'Primärfarbe (Buttons, Links)', type: 'color' },
|
||
{ key: 'primaryDark', label: 'Primärfarbe (Hover)', type: 'color' },
|
||
{ key: 'primaryContrast', label: 'Text auf Primärfarbe', type: 'color' },
|
||
{ key: 'accent', label: 'Akzentfarbe', type: 'color' },
|
||
{ key: 'accentDark', label: 'Akzentfarbe (dunkel)', type: 'color' },
|
||
{ key: 'success', label: 'Erfolg', type: 'color' },
|
||
{ key: 'danger', label: 'Fehler/Warnung', type: 'color' },
|
||
{ key: 'flag', label: 'Platzhalter-Markierung', type: 'color' },
|
||
{ key: 'flagBg', label: 'Platzhalter-Markierung (Hintergrund)', type: 'color' },
|
||
{ key: 'googleBlue', label: 'Linkfarbe in der Google-Vorschau', type: 'color' },
|
||
],
|
||
},
|
||
];
|