Presets
Fertige Bausteine für Inhalte, die fast jede Seite hat, und für Restaurant-Seiten. Eine Zeile pro Inhaltstyp statt handgeschriebener Sammlungen.
Was ein Preset ist
Ein Preset ist ein Paket mit fertigen Bausteinen. Jede Fabrik (hours(), menu() und so weiter) gibt einen Baustein zurück, den du in bricks einträgst. Felder, Beschriftungen, Aufgaben und Routen sind schon da. Die Komponenten bleiben in deiner Seite, weil das Aussehen zu deiner Seite gehört.
Grundbausteine: @lupinum/ginko/basics
Bausteine, die fast jede Seite braucht, ohne eigenes Paket: Sie kommen mit @lupinum/ginko.
| Fabrik | Art | Inhalt | JSON-LD |
|---|---|---|---|
site() | einzeln | offizieller Name, Anschrift, E-Mail, Telefon, Register- und Steuernummer (alles optional) | siteJsonLd: Organization, LocalBusiness oder NGO |
legal() | Sammlung | ein Rechtstext pro Eintrag, legalType: privacy, customer-privacy, imprint, terms, cookies, accessibility | – |
people() | Sammlung | Name, Rolle, Kurzbiografie, E-Mail, Telefon, Foto | personJsonLd |
posts(), authors() | Sammlungen | Beiträge mit Datum, Autoren und Schlagwörtern | postJsonLd: BlogPosting |
changelog() | Sammlung | ein Eintrag pro Version | – |
jobs() | Sammlung | Stellen; validUntil ist Pflicht, danach verschwindet die Stelle von selbst | jobJsonLd: JobPosting |
faq() | Sammlung | Frage und Antwort | faqJsonLd: FAQPage |
downloads() | Sammlung | Titel, Kurzbeschreibung, Datum, PDF, Sprache des Dokuments (optional) | – |
// ginko.config.ts
import { defineSite } from '@lupinum/ginko'
import { faq, jobs, legal, site } from '@lupinum/ginko/basics'
export default defineSite({
// site, name, url, timeZone, locales: ['de', 'en'], currency, pages wie bisher
bricks: {
site: site({ source: 'files' }),
legal: legal({ source: 'files', route: { de: '/[slug]', en: '/en/[slug]' } }),
jobs: jobs({ source: 'files', route: { de: '/jobs/[slug]', en: '/en/jobs/[slug]' } }),
faq: faq({ source: 'files' }),
},
})Jede Fabrik nimmt source und label; legal(), posts() und jobs() dazu route. Ohne source bearbeitet die Inhaberin den Baustein auf der Ginko-Plattform. Die Beschriftungen im Studio gibt es auf Deutsch, Englisch, Spanisch und Französisch. Die gespeicherten Feldnamen ändern sich nie. posts(), authors() und changelog() speichern dasselbe wie der Blog und das Changelog von @lupinum/ginko-docs.
Das Land kennt der Baustein site() nicht: Es kommt aus country in ginko.config.ts (useRuntimeConfig().public.ginko.country). useGinkoSeo setzt es als addressCountry in siteJsonLd(data, { name, url, type: 'LocalBusiness', ...seo.organization }) und jobJsonLd ein, wo du kein eigenes country mitgibst. Eine Seite, deren JSON-LD diesen Knoten hat, bekommt aus seo.organization keine zweite Organization: Darum gehören logo und sameAs in siteJsonLd. withoutFiles(config) gibt die Konfiguration ohne source zurück: So prüfst du, dass eine Seite aus Dateien später auf die Plattform umziehen kann.
offerJsonLd(price, currency, { valueAddedTaxIncluded, unitCode }) ist das schema.org-Offer eines f.money-Werts für eigenes JSON-LD (eine Leistung, ein Produkt): price in der Hauptwährungseinheit ("390.00") und priceCurrency. Mit valueAddedTaxIncluded (false für Nettopreise) kommt eine PriceSpecification dazu, die das sagt; lass es weg, wenn die Preise keinen Hinweis zur Mehrwertsteuer tragen. Mit unitCode (MON pro Monat, ANN pro Jahr) wird der Preis wiederkehrend: Das Offer bekommt dann eine UnitPriceSpecification für eine solche Einheit.
releaseAnchor(version) gibt den stabilen Anker eines Releases zurück (4.6.0 wird zu v4-6-0), den die Changelog-Seite und ihr Feed nutzen.
rssXml(feed) schreibt einen RSS-2.0-Feed (Titel, Link, Beschreibung, Sprache und Einträge mit Titel, Link, Datum, Zusammenfassung und Autorinnen und Autoren), zum Beispiel in einer Server-Route mit getGinkoItems (Seiten lesen).
authors() und people() beschreiben beide Menschen, in zwei gespeicherten Formen: authors() behält die Form des Blogs von @lupinum/ginko-docs (avatar, ein Name pro Sprache), people() ist für eine Team-Seite (photo, ein Name, Rolle und Kurzbiografie). Eine Seite mit Blog und Team-Seite nimmt posts({ authors: 'people' }): Die Beiträge nennen ihre Autorinnen und Autoren dann in people, und die Seite braucht kein authors(). Docs-Seiten behalten authors().
Restaurant: @lupinum/ginko-preset-gastro
Für Gasthäuser: hours(), hourPlans(), notices(), lunch(), lunchPlans(), menu(), events() und gallery(), mit den Aufgaben der Wirtin („Heute geschlossen“, „Heute ausgebucht“, „Gericht heute aus“, Preis ändern). Dazu die 14 EU-Allergene (allergensEU) und die Öffnungszeiten-Regeln hoursOn und todayState. Mit hourPlans() gibst du hours() den Schlüssel der Öffnungszeiten ab Datum mit, hours({ plans: 'hourPlans' }): Dann folgt die Zeile „Heute“ im Studio dem Plan, der an dem Tag gilt.
Eine neue Seite mit diesem Preset startest du so:
npm create ginko@latest mein-gasthaus -- --preset gastro --locale deDer Starter zeigt Öffnungszeiten und eine Speisekarte, beide aus Dateien. Ohne --locale de ist der Starter englisch.
Bausteine eintragen
// ginko.config.ts
import { defineSite } from '@lupinum/ginko'
import { hours, menu, notices } from '@lupinum/ginko-preset-gastro'
export default defineSite({
site: 'mein-gasthaus',
name: 'Mein Gasthaus',
url: 'https://example.com',
timeZone: 'Europe/Vienna',
locales: ['de'],
currency: 'EUR',
pages: { home: '/' },
bricks: {
hours: hours({ source: 'files' }),
menu: menu({ allergens: 'none', source: 'files' }),
notices: notices(),
},
})Optionen
Jede Fabrik nimmt die Option source. Ohne sie bearbeitet die Wirtin den Baustein auf der Ginko-Plattform. 'files' liest ihn aus Markdown in content/, dann läuft die Seite ohne Plattform.
Beschriftungen im Studio und vorbelegte Texte, etwa der Platzhalter im Mittagsmenü oder „Heute sind wir ausgebucht.“, gibt es auf Deutsch und Englisch. Ist die Hauptsprache der Seite de oder en, behält sie die Texte ihrer eigenen Sprachen; sonst startet sie ohne die vorbelegten Texte. Ein lunch() auf der Plattform kann nicht ohne sie starten: Seine Tage brauchen das Platzhalter-Gericht, darum lehnt ginko check das neue Einzelstück bei einer anderen Hauptsprache ab. Nimm dort lunch({ source: 'files' }). Ein Code mit Region wie de-CH zählt nicht als de.
Manche Fabriken haben mehr Optionen: allergens bei menu(), lunch() und lunchPlans() ('EU', 'none' oder eine eigene Liste), weekOf bei lunch() (Pflicht für ein lunch() auf der Plattform: die Woche, in der das erste Menü beginnt; ein lunch() auf der Plattform braucht außerdem die Hauptsprache de oder en, siehe oben), lunchBrick bei lunchPlans() und route bei events() (Pflicht).
Die Öffnungszeiten-Regeln zählen österreichische Feiertage. Für ein anderes Land gib jeder Regel, die du aufrufst, calendar mit: todayState({ hours, calendar: 'DE' }, today) (oder 'SG', oder eine eigene Funktion); 'none' schaltet Feiertage ab. Statt calendar geht auch country (aus useRuntimeConfig().public.ginko.country). Setze country oder calendar auch in ginko.config.ts (siehe Bausteine), damit Studio und Erinnerungs-Mail dieselben Feiertage zählen. Ein Tag hat höchstens zwei Öffnungszeiten (Mittag und Abend).
eventJsonLd braucht timeZone (die Zeitzone der Seite, aus useGinkoSite()): start und end stehen dann mit dem Versatz dieses Tages in startDate und endDate, etwa 2026-12-03T19:00:00+01:00.
Öffnungszeiten anzeigen
import { todayState } from '@lupinum/ginko-preset-gastro'
const state = todayState({ hours: { week: [], exceptions: [], holidays: 'closed' } }, '2026-10-26')
// { kind: 'holiday', holiday: { de: 'Nationalfeiertag', en: 'Austrian National Day' } }todayState gibt open (mit den Zeiten), closed (mit dem Grund der Wirtin), holiday oder restDay zurück. Die Zeiten verbindest du in deiner Sprache mit spansText(state.times, 'und').
Ein vollständiges Beispiel ist sites/restaurant im Ginko-Repository. Es läuft im Repository gegen eine nachgebaute Plattform.