Tippe, um Seiten und Abschnitte zu finden.

Zum Inhalt
Ginko

Bausteine

Einzelstück und Sammlung, alle Feldtypen, Routen, Sichtbarkeit, Reihenfolge und Aufgaben.

Einzelstück und Sammlung

Ein Baustein beschreibt einen Inhaltstyp. Es gibt zwei Arten:

  • single ist ein Einzelstück: genau ein Eintrag. Beispiele: Öffnungszeiten, Impressum, Kontaktdaten.
  • collection ist eine Sammlung: viele Einträge mit denselben Feldern. Beispiele: Gerichte, Veranstaltungen, Hinweise.

Beide stehen unter bricks in defineSite. Der Schlüssel ist der Name des Bausteins. Er beginnt mit einem Kleinbuchstaben und enthält nur Buchstaben und Ziffern, zum Beispiel menu.

ts
import { collection, defineSite, f, single } from '@lupinum/ginko'

export default defineSite({
  site: 'restaurant',
  name: 'Gasthaus zur Linde',
  url: 'https://ginko-demo-restaurant.vercel.app',
  timeZone: 'Europe/Vienna',
  locales: ['de', 'en'],
  currency: 'EUR',
  pages: { home: { de: '/', en: '/en' }, menu: { de: '/speisekarte', en: '/en/menu' } },
  bricks: {
    contact: single({
      label: 'Adresse und Kontakt',
      fields: {
        street: f.text('Straße und Hausnummer', { max: 80, translate: false }),
        phone: f.text('Telefon', { format: 'tel' }),
      },
    }),
    menu: collection({
      label: 'Speisekarte',
      fields: {
        name: f.text('Name', { max: 120 }),
        price: f.money('Preis'),
      },
      max: 150,
      order: 'manual',
    }),
  },
})

Beide Funktionen nehmen diese Einstellungen:

EinstellungGilt fürBedeutung
labelbeideName des Bausteins im Editor. Pflicht.
describebeideKurze Erklärung im Editor, ein Text oder ein Text pro Sprache wie label. Bausteine und Komponenten sagen mit describe, wofür sie da sind; ein Feld hat hint für den Hilfetext darunter.
fieldsbeideDie Felder des Bausteins. Pflicht.
sourcebeide'files' liest den Baustein aus Markdown-Dateien. Ohne Angabe kommt er von der Plattform.
tasksbeideAufgaben für den Editor. Siehe unten.
layoutbeide'tabs' zeigt Feldgruppen als Tabs (siehe Gruppen und Tabs).
maxSammlungHöchstzahl der Einträge, 1 bis 500. Pflicht.
orderSammlungReihenfolge der Einträge. Pflicht.
routeSammlungAdresse der Detailseiten pro Sprache.
visibleSammlungWann ein Eintrag sichtbar ist.
untranslatedSammlungWas passiert, wenn eine Übersetzung fehlt: 'hide' (Standard mit route) oder 'fallback' (Standard ohne route). Siehe Sprachen.
plansSammlungEinträge planen Wochen eines Einzelstücks voraus.
statusbeideEine Statuszeile auf der Seite „Heute“ im Studio: { next: 'when' } (nächster Eintrag nach einem Datumsfeld), { count: true } (Einträge, die in den nächsten 7 Tagen enden; braucht visible), { missing: 'image' } (Einträge ohne ein optionales Feld), { age: 'date' } (neuester Eintrag und sein Alter) oder bei einem Einzelstück { hours: true } (heutige Öffnungszeiten). Eine Zeile, bei der etwas zu tun ist, kommt auch in die wöchentliche Erinnerung.

Grenzen einer Seite: höchstens 30 Bausteine und höchstens 60 Felder pro Baustein.

Feldtypen

Du baust Felder mit f.<typ>('Label', einstellungen). Das Label ist der Name im Editor. Die Einstellungen sind ein Objekt.

Diese Einstellungen gelten für jeden Typ:

  • optional: true macht das Feld freiwillig. Ohne Angabe ist ein Feld Pflicht.
  • hint ist ein Hilfetext im Editor. Wie das Label ist er ein Text oder ein Text pro Sprache: { de: '…', en: '…' }.
  • showWhen zeigt das Feld im Editor nur, solange ein Feld daneben einen Wert hat: { field: 'closed', is: true } oder { field: 'closed', not: true }. Ein fehlender Wert zählt als „nicht“. Der gespeicherte Wert und die Prüfungen bleiben; nur der Editor blendet das Feld aus. Ein Pflichtfeld mit showWhen ist nur Pflicht, solange es sichtbar ist: price: f.money('Betrag', { showWhen: { field: 'pricing', is: 'price' } }) braucht einen Betrag bei einem festen Preis und keinen sonst. Im Typ der Daten ist es darum optional.
  • askAgainWhen an einem choices-Feld nennt das Textfeld daneben, auf das sich die Auswahl bezieht (ein Feld desselben Bausteins oder eine Spalte derselben Listenzeile). Ändert sich dieser Text in der Hauptsprache, die Auswahl aber nicht, fragt das Studio „Noch richtig?“, und Speichern wartet auf die Antwort: allergens: f.choices('Allergene', { options, askAgainWhen: 'dish' }).
  • default ist der Startwert, so geschrieben wie der Wert des Feldes: ein Text, eine Zahl, { amount: 1250 } für Geld, ein Optionswert bei einer Auswahl. Bei einem übersetzbaren Feld ist ein einfacher Text der Text der Hauptsprache, und { de: 'Hallo', en: 'Hello' } setzt einen Text pro Sprache; Ginko behält nur die Sprachen der Seite. Bleibt kein Text in der Hauptsprache, entfällt der Startwert (etwa bei einem Preset für andere Sprachen). f.markdown, f.image und f.file haben keinen Startwert.
  • translate: false schaltet die Übersetzung eines Feldes aus. translate: 'manual' lässt keine automatischen Übersetzungsvorschläge zu.

Die Spalte „Wert im Bundle“ zeigt, was useGinko in der Seite liefert.

FeldEinstellungenWert im BundleÜbersetzbar
f.textmax (bis 500, mit format: 'url' bis 2000, Standard 200), format: 'url', 'email' oder 'tel'Stringja, mit format nein
f.longtextmax (bis 5000, Standard 2000)Stringja
f.markdownprofile: 'inline', 'basic', 'rich' oder 'page', max (Standard 50000). Kein defaultMarkdown-Baumja
f.numbermin, max, step (alle drei Pflicht), unitZahlnein
f.moneymin, max (in der kleinsten Einheit, z. B. Cent; keine negativen Beträge), currency{ amount, currency }nein
f.booleankeinetrue oder falsenein
f.datekeine'2026-10-10'nein
f.timeafter: ein anderes time-Feld des Bausteins, nach dem diese Zeit liegen muss'18:30'nein
f.daterangekeine{ from, to }, beide als Datumnein
f.choiceoptions (Liste der Optionen, bis 50)Der Wert der Optionnein
f.choicesoptions, maxListe der Wertenein
f.listitemLabel, fields, max (1 bis 50, Pflicht), min, uniqueListe von Zeilen, jede mit _idje Spalte
f.imageaspect{ url, alt, width, height, focus?, aspect? }nur alt
f.fileaccept: ['pdf'], maxBytes (bis 10 MB, 10 * 1024 * 1024){ url, name, size }nein
f.slugfrom (Pflicht)Stringja
f.refto: ein Sammlungs-Baustein (Pflicht)die ID eines Eintragsnein
f.refsto (Pflicht), maxListe von Eintrags-IDsnein

Hinweise zu einzelnen Typen:

  • Markdown-Profile legen fest, was ein Text enthalten darf. inline: ein Absatz mit fett, kursiv, durchgestrichen, Code und Links. basic: dazu Überschriften h2–h4, Listen und Checklisten (- [ ] Punkt), Zitate, Trennlinien und Fußnoten. rich: dazu Tabellen, Bilder, Codeblöcke, Formeln und Mermaid-Diagramme. page: dazu h1, h5, h6 und Komponenten (::callout). Rohes HTML bleibt immer Text. Attribute in geschweiften Klammern nehmen title, lang, dir und aria-* (**news**{lang="en"}), dazu, was das Element braucht, etwa target="_blank" bei einem Link oder width und height bei einem Bild. Eine Komponente nimmt die Props, die ihre Registry nennt. Alles andere, etwa class, data-*, style, id oder onclick, lehnt der Parser ab. Klassen setzen nur Markdown und seine Plugins (Checklisten, Fußnoten, Code): Mit einer eigenen Klasse könnte ein Text eine CSS-Regel der Website übernehmen und etwa einen Link unsichtbar über die ganze Seite legen.
  • Geld liegt in der kleinsten Einheit vor, z. B. als ganze Cent (eine Währung ohne Untereinheit wie JPY in ganzen Einheiten). Der Betrag 12,50 € ist { amount: 1250, currency: 'EUR' }. Beträge können nicht negativ sein. Ohne currency nimmt das Feld die Währung der Seite. Mit formatMoney zeigst du den Wert an; formatMoney(price, language, { wholeWithoutMinor: true }) lässt bei ganzen Beträgen die Cent weg (390 € statt 390,00 €).
  • Datum und Zeit sind Strings. Ein Datum ist YYYY-MM-DD, eine Zeit ist HH:MM. Ein Datum gilt in der Zeitzone der Seite. Mit f.time('Ende', { after: 'from' }) muss das Ende später als der Beginn from sein; eine gleiche Zeit lehnt die Plattform auch ab.
  • Land und Feiertage: country in der Seitenkonfiguration ist das Land der Seite (ISO 3166-1, z. B. 'AT'). Es ist das Land in JSON-LD-Adressen ohne eigenes Land und wählt die Feiertage der Öffnungszeiten: AT, DE (nur bundesweite Feiertage) und SG kennt Ginko, ein anderes Land zählt keine Feiertage (ginko check warnt). calendar überschreibt das: 'AT', 'DE', 'SG', 'none' oder eine eigene Funktion (year) => Feiertage. Das Schema speichert das Ergebnis als holidays, damit Studio und Erinnerungs-Mail dieselben Feiertage zählen wie die Seite. Seiten lesen country mit useRuntimeConfig().public.ginko.country.
  • Auswahl: f.choice und f.choices bekommen die Optionen als options im zweiten Argument, zum Beispiel f.choice(label, { options: ['a', 'b'], default: 'a' }). Eine Option ist ein String oder { value, label }. Das Bundle enthält nur den Wert. Das Label ist Editor-Text: ein Text oder einer pro Sprache wie jedes Label. Es steht nicht im Bundle, zeige in der Seite also eigene Texte pro Sprache.
  • Listen: f.list(label, { itemLabel, fields: spalten, ...einstellungen }). itemLabel nennt eine Zeile, zum Beispiel „Tag“. min ist die kleinste Zeilenzahl. unique nennt eine Pflicht-Spalte, deren Wert in jeder Zeile verschieden sein muss. Eine Liste darf keine Liste enthalten.
  • Bilder und Dateien lädt der Editor auf der Plattform hoch. Das Bundle liefert die URL. Von Bildern macht Ginko zusätzlich WebP-Kopien mit 480, 960 und 1600 px Breite (nur die, die kleiner als das Bild sind): beim Hochladen auf der Plattform und beim Build für Dateien. <GinkoImage> bietet sie dem Browser mit srcset an, so laden Handys kleine Dateien.
  • Bildbeschreibung (alt) ist in der Hauptsprache Pflicht. Ein Feld kann einen eigenen Text haben; ohne ihn gilt die Beschreibung aus der Dateien-Bibliothek. Das Bundle liefert das Ergebnis. Hat die Inhaberin einen Fokuspunkt gesetzt, hat der Bildwert auch focus: { x, y } (0 bis 1), und <GinkoImage> hält diesen Punkt im Ausschnitt.
  • Rahmen (aspect) sagt, in welcher Form die Website das Foto zeigt: ein Seitenverhältnis (f.image('Foto', { aspect: '3:4' })) oder benannte Rahmen, wenn das Foto an mehreren Stellen steht ({ aspect: { card: '3:2', header: '21:9' } }). Das Studio zeigt das Foto in jedem Rahmen, und die Inhaberin setzt den Fokuspunkt direkt im Feld. Es warnt, wenn ein Rahmen viel abschneidet oder das Foto dafür zu klein ist. <GinkoImage> schneidet genauso zu (frame="card" wählt einen benannten Rahmen). Ein Rahmen ändert keinen gespeicherten Wert: Er darf jederzeit kommen, sich ändern oder gehen.
  • Slug ist die Adresse eines Eintrags. Siehe unten.
  • Von der Plattform gesetzte Eigenschaften. Die gespeicherten Typen Field und Brick haben auch pos und retired. Die Plattform setzt sie; f.*, single und collection nehmen sie nicht an. Um ein Feld auszumustern, nimm retired in der Seitenkonfiguration (siehe Plattform und CLI).

Ein Beispiel mit Liste und Auswahl:

ts
const weekdays = [
  { value: 'mo', label: 'Montag' },
  { value: 'tu', label: 'Dienstag' },
  { value: 'we', label: 'Mittwoch' },
] as const

const lunch = single({
  label: 'Mittagsmenü',
  fields: {
    weekOf: f.date('Woche ab'),
    days: f.list('Tage', {
      itemLabel: 'Tag',
      fields: {
        day: f.choice('Tag', { options: weekdays }),
        dish: f.text('Gericht', { max: 200 }),
        price: f.money('Preis', { optional: true }),
      },
      min: 3, max: 3, unique: 'day',
    }),
  },
})

Das Feld day hat dabei in der Seite den Typ 'mo' | 'tu' | 'we' | UnknownChoice. UnknownChoice steht für einen Wert, den ein späteres Release hinzufügt. Behandle ihn mit einem Ersatztext.

Gruppen und Tabs

Ein langes Formular füllt man leichter in Teilen aus. Gib Feldern eine group; das Studio zeigt jede Gruppe unter ihrer Überschrift. Mit layout: 'tabs' am Baustein werden die Gruppen auf breiten Bildschirmen zu Tabs. Handys zeigen immer Überschriften. Felder ohne Gruppe kommen zuerst. Gruppen ändern nur das Formular: Die Daten bleiben gleich.

ts
legal: single({
  label: 'Rechtliches',
  layout: 'tabs',
  fields: {
    imprint: f.markdown('Impressum', { group: 'Impressum' }),
    privacy: f.markdown('Datenschutz', { group: 'Datenschutz' }),
  },
}),

Nach einem abgelehnten Speichern zeigt ein Tab mit Fehler einen roten Punkt und öffnet sich zuerst.

Routen und Slugs

Eine Sammlung bekommt Detailseiten, wenn sie eine route hat. Die Route nennt für jede Sprache einen Pfad mit [slug]:

ts
events: collection({
  label: 'Veranstaltungen',
  fields: {
    title: f.text('Titel', { max: 120 }),
    slug: f.slug('Adresse', { from: 'title' }),
    when: f.daterange('Wann'),
  },
  max: 30,
  order: { field: 'when', dir: 'asc' },
  route: { de: '/veranstaltungen/[slug]', en: '/en/events/[slug]' },
})

Für die Route gelten diese Regeln:

  • Du musst für jede Sprache aus locales einen Pfad angeben. Jeder Pfad beginnt mit / und enthält [slug].
  • Der Pfad enthält schon das Sprachpräfix, zum Beispiel /en/events/[slug].
  • Der Baustein braucht ein f.slug-Feld. Seine Option from nennt ein Text-Feld, aus dem der Slug entsteht.
  • Nur Sammlungen haben Slugs. Ein Einzelstück mit Slug-Feld ist ein Fehler.

Der Slug entsteht einmal beim Anlegen eines Eintrags. Ginko schreibt ä, ö, ü und ß als ae, oe, ue und ss, nimmt Kleinbuchstaben und trennt Wörter mit -. Ein gleicher Slug bekommt -2, -3 und so weiter. Danach bleibt der Slug fest. Jede Sprache hat ihren eigenen Slug. Bei Datei-Bausteinen kommt der Slug aus dem Frontmatter oder dem Dateinamen (siehe „Inhalte aus Dateien“).

Die Seite für die Route schreibst du selbst. Wie sie den Eintrag liest, steht auf der Seite „Seiten lesen“. Wie die Pfade pro Sprache zusammenpassen, steht auf der Seite „Sprachen“.

Sichtbarkeit

Mit visible blendest du Einträge nach Datum ein und aus:

  • { mode: 'always' } zeigt alles. Das ist der Standard.
  • { mode: 'until', field: 'until' } zeigt einen Eintrag, bis das Datum im Feld until vorbei ist. Das Feld ist ein date oder ein daterange; bei einem daterange zählt das Enddatum (to). Ein leeres Feld zeigt den Eintrag immer. Mit days: 6 bleibt er noch sechs Tage länger, etwa eine Woche ab ihrem ersten Tag.

Ein Datum gilt den ganzen Tag, in der Zeitzone der Seite. useGinko filtert nach dieser Regel. Eine Detailseite eines nicht sichtbaren Eintrags liefert 404. Auch in der Sitemap fehlt dieser Eintrag.

Mit deleteExpired: true in visible löscht die Plattform nicht sichtbare Einträge, wenn für einen neuen Eintrag der Platz fehlt. Außerdem löscht ein täglicher Lauf Einträge, die seit 30 Tagen nicht sichtbar sind. Gelöschte Einträge bleiben 30 Tage wiederherstellbar.

Mit plans: { brick: 'lunch', field: 'weekOf' } planen die Einträge einer Sammlung Wochen des Einzelstücks lunch voraus: weekOf (ein date-Feld der Sammlung) hält den ersten Tag der Woche, und eine geplante Woche gilt für die Wochen-Erinnerung als eingetragen.

Reihenfolge

order bestimmt die Reihenfolge der Einträge im Bundle:

  • 'manual': Inhaber ordnen die Einträge auf der Plattform per Hand. Bei Dateien gibt die Zahl im Dateinamen die Reihenfolge vor.
  • { field: 'when', dir: 'asc' } sortiert nach einem Feld, aufsteigend ('asc') oder absteigend ('desc'). Ein daterange-Feld sortiert nach from, dann nach to.

Aufgaben

Aufgaben (tasks) sind Kurzwege im Editor für einen häufigen Handgriff. Eine Aufgabe ändert nicht, was in den Daten steht. Sie bestimmt, welche Felder ein Mitarbeiter sieht und darf.

ts
tasks: [
  { id: 'closedToday', label: 'Heute geschlossen', kind: 'edit', fields: ['closedOn', 'closedNote'], prefill: { closedOn: '@today' } },
  { id: 'add', label: 'Gericht hinzufügen', kind: 'create', fields: ['name', 'price'] },
  { id: 'remove', label: 'Gericht entfernen', kind: 'remove' },
]
  • kind: 'edit' ändert die Felder aus fields. Mit pick: true wählt die Person erst einen Eintrag.
  • kind: 'create' legt einen neuen Eintrag an. Mit fields fragt und schreibt die Aufgabe nur diese Felder (dazu prefill und Felder mit Standardwert); jedes Pflichtfeld ohne Standardwert muss in fields oder prefill stehen. Ein Feld mit Standardwert, das die Aufgabe nicht nennt, bekommt nur seinen Standardwert. Die Prüfung vor dem Veröffentlichen lehnt die Adresse (slug), ein geplantes „nächster“/„ab“-Feld und ein stillgelegtes Feld in fields ab. Ohne fields fragt sie jedes Feld außer der Adresse und dem Plan einer geplanten Änderung.
  • kind: 'remove' löscht einen Eintrag.
  • needsApproval: true lässt eine Änderung von Mitarbeitern über diese Aufgabe erst online gehen, wenn Inhaber oder Admin sie freigeben.
  • prefill belegt Felder des Bausteins im Formular vor. Die Tokens '@today', '@workWeek' und '@nextWorkWeek' löst die Plattform in der Zeitzone der Seite auf. Ein Token braucht ein date-Feld; jeder andere Wert wird wie der Wert des Feldes geschrieben und geprüft, ein markdown-Feld bekommt also Markdown-Text. '@workWeek' ist der erste Tag der Arbeitswoche, '@nextWorkWeek' der erste Tag der Woche danach. Der erste Tag der Woche kommt aus der Hauptsprache der Seite (languages, z. B. Montag für de-AT, Sonntag für en-US) und lässt sich mit firstDayOfWeek (1 bis 7, 1 = Montag) in der Seitenkonfiguration überschreiben. Am Wochenende (Tage nach dem letzten Arbeitstag) ist die Arbeitswoche schon die kommende Woche: Ein Wochenplan, den man am Samstag macht, gilt für die Woche danach.
  • Ein Datumsfeld, das einen Wochenanfang hält (das field von plans, sonst das Feld, das eine Aufgabe mit '@workWeek' oder '@nextWorkWeek' vorbelegt), nimmt nur den ersten Tag einer Woche an. Die edit-Aufgabe, die ein Feld mit '@workWeek' vorbelegt (die Wochenaufgabe), gilt immer für die laufende Woche: Die Plattform lehnt dort eine andere Woche ab, auch beim Freigeben eines Vorschlags aus einer früheren Woche.

Mitarbeiter (Rolle staff) dürfen nur, was zu einer Aufgabe passt, die der Inhaber ihnen freigegeben hat. Du darfst Aufgaben jederzeit ändern. Sie sind kein Datenvertrag.

Aufgaben haben nur bei Plattform-Bausteinen einen Zweck. Die Plattform sieht Bausteine mit source: 'files' nie, denn niemand bearbeitet sie im Editor.

Geplante Änderung

Mit planned plant ein Baustein eine Änderung eines Feldes voraus, etwa einen Preis, der ab dem 1. November steigt:

ts
planned: { field: 'price', next: 'nextPrice', from: 'priceFrom' }

Ab dem Datum in priceFrom gilt der Wert in nextPrice statt price. next hat den Typ von field, from ist ein date-Feld, und beide sind optional: Ohne Plan bleiben sie leer. ginko check meldet eine Aufgabe, die field ändert, aber next und from nicht zeigt. So ändert niemand einen Preis, ohne den geplanten zu sehen.

Die Plattform speichert einen Plan nur mit Wert und Datum zusammen. Ab dem Datum öffnet eine Aufgabe mit diesen Feldern den neuen Wert, und Speichern löscht den abgelaufenen Plan. Eine Aufgabe, die einen neuen Eintrag anlegt, fragt nicht nach einem Plan. Wer nur den Plan speichert, liest in der Meldung den Tag, ab dem er gilt, nicht „in etwa einer Minute“. Welcher Wert an einem Tag gilt, liest deine Seite mit valueOn(brick, data, 'price', today) (im Gastro-Preset mit priceOn); eine Angabe (siehe unten) rechnet den Plan selbst ein.

Angaben im Text

Preis, Telefonnummer oder Öffnungszeiten stehen oft auch im Fließtext. Damit sie dort nicht veralten, nennt facts die Felder von Einzelstücken, die ein Text zitieren darf:

ts
facts: ['praxis.price', 'praxis.times', 'site.phone']

Ginko fügt dafür die Inline-Komponente fact hinzu. Im Text steht :fact{name="praxis.price"}, im Studio ein Chip mit dem Namen des Feldes („Preis pro Sitzung“). Die Seite, die Suche, der Markdown-Zwilling und llms-full.txt zeigen den Wert, der heute gilt: eine geplante Änderung ab ihrem Datum, Geld als „€ 98“, ein Datum lang, Mehrfachauswahl und Listen mit einer Spalte als Aufzählung („11:00, 12:15 und 13:30“), eine E-Mail-Adresse oder Telefonnummer (format: 'email' oder 'tel') als Link.

Erlaubt sind Text, Zahl (mit unit), Geld, Datum, Uhrzeit, Auswahl, Mehrfachauswahl und Listen mit genau einer Spalte. ginko check meldet einen Pfad, der kein solches Feld eines Einzelstücks nennt. Im eigenen Code liest useGinkoFact('praxis.price') denselben Text; eine Seite, die fact anders zeigen will, gibt <GinkoMarkdown> unter components eine eigene Komponente.