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:
singleist ein Einzelstück: genau ein Eintrag. Beispiele: Öffnungszeiten, Impressum, Kontaktdaten.collectionist 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.
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:
| Einstellung | Gilt für | Bedeutung |
|---|---|---|
label | beide | Name des Bausteins im Editor. Pflicht. |
describe | beide | Kurze 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. |
fields | beide | Die Felder des Bausteins. Pflicht. |
source | beide | 'files' liest den Baustein aus Markdown-Dateien. Ohne Angabe kommt er von der Plattform. |
tasks | beide | Aufgaben für den Editor. Siehe unten. |
layout | beide | 'tabs' zeigt Feldgruppen als Tabs (siehe Gruppen und Tabs). |
max | Sammlung | Höchstzahl der Einträge, 1 bis 500. Pflicht. |
order | Sammlung | Reihenfolge der Einträge. Pflicht. |
route | Sammlung | Adresse der Detailseiten pro Sprache. |
visible | Sammlung | Wann ein Eintrag sichtbar ist. |
untranslated | Sammlung | Was passiert, wenn eine Übersetzung fehlt: 'hide' (Standard mit route) oder 'fallback' (Standard ohne route). Siehe Sprachen. |
plans | Sammlung | Einträge planen Wochen eines Einzelstücks voraus. |
status | beide | Eine 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: truemacht das Feld freiwillig. Ohne Angabe ist ein Feld Pflicht.hintist ein Hilfetext im Editor. Wie das Label ist er ein Text oder ein Text pro Sprache:{ de: '…', en: '…' }.showWhenzeigt 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 mitshowWhenist 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.askAgainWhenan einemchoices-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' }).defaultist 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.imageundf.filehaben keinen Startwert.translate: falseschaltet 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.
| Feld | Einstellungen | Wert im Bundle | Übersetzbar |
|---|---|---|---|
f.text | max (bis 500, mit format: 'url' bis 2000, Standard 200), format: 'url', 'email' oder 'tel' | String | ja, mit format nein |
f.longtext | max (bis 5000, Standard 2000) | String | ja |
f.markdown | profile: 'inline', 'basic', 'rich' oder 'page', max (Standard 50000). Kein default | Markdown-Baum | ja |
f.number | min, max, step (alle drei Pflicht), unit | Zahl | nein |
f.money | min, max (in der kleinsten Einheit, z. B. Cent; keine negativen Beträge), currency | { amount, currency } | nein |
f.boolean | keine | true oder false | nein |
f.date | keine | '2026-10-10' | nein |
f.time | after: ein anderes time-Feld des Bausteins, nach dem diese Zeit liegen muss | '18:30' | nein |
f.daterange | keine | { from, to }, beide als Datum | nein |
f.choice | options (Liste der Optionen, bis 50) | Der Wert der Option | nein |
f.choices | options, max | Liste der Werte | nein |
f.list | itemLabel, fields, max (1 bis 50, Pflicht), min, unique | Liste von Zeilen, jede mit _id | je Spalte |
f.image | aspect | { url, alt, width, height, focus?, aspect? } | nur alt |
f.file | accept: ['pdf'], maxBytes (bis 10 MB, 10 * 1024 * 1024) | { url, name, size } | nein |
f.slug | from (Pflicht) | String | ja |
f.ref | to: ein Sammlungs-Baustein (Pflicht) | die ID eines Eintrags | nein |
f.refs | to (Pflicht), max | Liste von Eintrags-IDs | nein |
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 nehmentitle,lang,dirundaria-*(**news**{lang="en"}), dazu, was das Element braucht, etwatarget="_blank"bei einem Link oderwidthundheightbei einem Bild. Eine Komponente nimmt die Props, die ihre Registry nennt. Alles andere, etwaclass,data-*,style,idoderonclick, 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. Ohnecurrencynimmt das Feld die Währung der Seite. MitformatMoneyzeigst du den Wert an;formatMoney(price, language, { wholeWithoutMinor: true })lässt bei ganzen Beträgen die Cent weg (390 €statt390,00 €). - Datum und Zeit sind Strings. Ein Datum ist
YYYY-MM-DD, eine Zeit istHH:MM. Ein Datum gilt in der Zeitzone der Seite. Mitf.time('Ende', { after: 'from' })muss das Ende später als der Beginnfromsein; eine gleiche Zeit lehnt die Plattform auch ab. - Land und Feiertage:
countryin 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) undSGkennt Ginko, ein anderes Land zählt keine Feiertage (ginko checkwarnt).calendarüberschreibt das:'AT','DE','SG','none'oder eine eigene Funktion(year) => Feiertage. Das Schema speichert das Ergebnis alsholidays, damit Studio und Erinnerungs-Mail dieselben Feiertage zählen wie die Seite. Seiten lesencountrymituseRuntimeConfig().public.ginko.country. - Auswahl:
f.choiceundf.choicesbekommen die Optionen alsoptionsim zweiten Argument, zum Beispielf.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 jedesLabel. Es steht nicht im Bundle, zeige in der Seite also eigene Texte pro Sprache. - Listen:
f.list(label, { itemLabel, fields: spalten, ...einstellungen }).itemLabelnennt eine Zeile, zum Beispiel „Tag“.minist die kleinste Zeilenzahl.uniquenennt 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 mitsrcsetan, 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 auchfocus: { 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
FieldundBrickhaben auchposundretired. Die Plattform setzt sie;f.*,singleundcollectionnehmen sie nicht an. Um ein Feld auszumustern, nimmretiredin der Seitenkonfiguration (siehe Plattform und CLI).
Ein Beispiel mit Liste und Auswahl:
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.
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]:
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
localeseinen 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 Optionfromnennt 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 Felduntilvorbei ist. Das Feld ist eindateoder eindaterange; bei einemdaterangezählt das Enddatum (to). Ein leeres Feld zeigt den Eintrag immer. Mitdays: 6bleibt 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'). Eindaterange-Feld sortiert nachfrom, dann nachto.
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.
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 ausfields. Mitpick: truewählt die Person erst einen Eintrag.kind: 'create'legt einen neuen Eintrag an. Mitfieldsfragt und schreibt die Aufgabe nur diese Felder (dazuprefillund Felder mit Standardwert); jedes Pflichtfeld ohne Standardwert muss infieldsoderprefillstehen. 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 infieldsab. Ohnefieldsfragt sie jedes Feld außer der Adresse und dem Plan einer geplanten Änderung.kind: 'remove'löscht einen Eintrag.needsApproval: truelässt eine Änderung von Mitarbeitern über diese Aufgabe erst online gehen, wenn Inhaber oder Admin sie freigeben.prefillbelegt 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 eindate-Feld; jeder andere Wert wird wie der Wert des Feldes geschrieben und geprüft, einmarkdown-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ürde-AT, Sonntag füren-US) und lässt sich mitfirstDayOfWeek(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
fieldvonplans, sonst das Feld, das eine Aufgabe mit'@workWeek'oder'@nextWorkWeek'vorbelegt), nimmt nur den ersten Tag einer Woche an. Dieedit-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.
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:
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:
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.