Type to search pages and sections.

Skip to content
Ginko

Presets

Ready-made bricks for the content most sites have, and for restaurant sites. One line per content type instead of hand-written collections.

What a preset is

A preset is a package of ready-made bricks. Each factory (hours(), menu() and so on) returns a brick that you add to bricks. Fields, labels, tasks and routes are already there. The components stay in your site, because their look belongs to your site.

Basics: @lupinum/ginko/basics

Bricks that almost every site needs, without an extra package: they come with @lupinum/ginko.

FactoryKindContentJSON-LD
site()singlelegal name, address, email, phone, registration and VAT number (all optional)siteJsonLd: Organization, LocalBusiness or NGO
legal()collectionone legal text per entry, legalType: privacy, customer-privacy, imprint, terms, cookies, accessibility–
people()collectionname, role, short bio, email, phone, photopersonJsonLd
posts(), authors()collectionsposts with date, authors and tagspostJsonLd: BlogPosting
changelog()collectionone entry per version–
jobs()collectionjob ads; validUntil is required, after it the job leaves the site by itselfjobJsonLd: JobPosting
faq()collectionquestion and answerfaqJsonLd: FAQPage
downloads()collectiontitle, summary, date, PDF, the document's language (optional)–
ts
// 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: ['en', 'de'], currency, pages as before
  bricks: {
    site: site({ source: 'files' }),
    legal: legal({ source: 'files', route: { en: '/[slug]', de: '/de/[slug]' } }),
    jobs: jobs({ source: 'files', route: { en: '/jobs/[slug]', de: '/de/jobs/[slug]' } }),
    faq: faq({ source: 'files' }),
  },
})

Every factory takes source and label; legal(), posts() and jobs() also take route. Without source, the owner edits the brick on the Ginko platform. Studio labels come in German, English, Spanish and French. The stored field names never change. posts(), authors() and changelog() store the same data as the blog and changelog of @lupinum/ginko-docs.

The site() brick has no country: it comes from country in ginko.config.ts (useRuntimeConfig().public.ginko.country). useGinkoSeo writes it as addressCountry into siteJsonLd(data, { name, url, type: 'LocalBusiness', ...seo.organization }) and jobJsonLd where you pass no country of your own. A page whose JSON-LD has this node gets no second Organization from seo.organization: that is why logo and sameAs go into siteJsonLd. withoutFiles(config) returns the config without source: use it to check that a site that reads files can move to the platform later.

offerJsonLd(price, currency, { valueAddedTaxIncluded, unitCode }) is the schema.org Offer of an f.money value for your own JSON-LD (a service, a product): price in major units of its currency ("390.00") and priceCurrency. With valueAddedTaxIncluded (false for net prices) it adds a PriceSpecification that says so; leave it out when the prices carry no VAT note. unitCode (MON per month, ANN per year) makes the price a recurring one: the Offer then gets a UnitPriceSpecification for one such unit.

releaseAnchor(version) gives the stable anchor of a release (4.6.0 becomes v4-6-0), which the changelog page and its feed use.

rssXml(feed) writes an RSS 2.0 feed (title, link, description, language and items with title, link, date, summary and authors), for example in a server route with getGinkoItems (Reading pages).

authors() and people() both describe people, in two stored shapes: authors() keeps the shape of the @lupinum/ginko-docs blog (avatar, a name per language), people() is for a team page (photo, one name, role and bio). A site with a blog and a team page uses posts({ authors: 'people' }): the posts then name their writers in people, and the site needs no authors(). Docs sites keep authors().

Restaurant: @lupinum/ginko-preset-gastro

For restaurants: hours(), hourPlans(), notices(), lunch(), lunchPlans(), menu(), events() and gallery(), with the owner's quick tasks ("Closed today", "Fully booked today", "Dish sold out today", change a price). It also has the 14 EU allergens (allergensEU) and the opening-hours rules hoursOn and todayState. With hourPlans(), give hours() the key of the hours from a date, hours({ plans: 'hourPlans' }): the Studio's line for today then follows the plan that applies on that day.

Start a new site with this preset:

Terminal
npm create ginko@latest my-restaurant -- --preset gastro

The starter shows opening hours and a menu, both read from files. Add --locale de for a German starter.

Add the bricks

ts
// ginko.config.ts
import { defineSite } from '@lupinum/ginko'
import { hours, menu, notices } from '@lupinum/ginko-preset-gastro'

export default defineSite({
  site: 'my-restaurant',
  name: 'My restaurant',
  url: 'https://example.com',
  timeZone: 'Europe/Vienna',
  locales: ['en'],
  currency: 'EUR',
  pages: { home: '/' },
  bricks: {
    hours: hours({ source: 'files' }),
    menu: menu({ allergens: 'none', source: 'files' }),
    notices: notices(),
  },
})

Options

Every factory takes the option source. Without it, the owner edits the brick on the Ginko platform. 'files' reads it from Markdown in content/, so the site runs without the platform.

Labels in the Studio and prefilled texts, such as the lunch placeholder or "We are fully booked today.", come in German and English. The site keeps the texts of its own locales when its main locale is de or en; otherwise it starts without the prefilled texts. A platform lunch() cannot start without them: its days need the placeholder dish, so with another main locale ginko check rejects the new single. Use lunch({ source: 'files' }) there. A code with a region such as de-CH does not count as de.

Some factories have more options: allergens on menu(), lunch() and lunchPlans() ('EU', 'none', or your own list), weekOf on lunch() (required for a platform lunch(): the week the first menu starts; a platform lunch() also needs the main locale de or en, see above), lunchBrick on lunchPlans(), and route on events() (required).

The hours rules count Austrian public holidays. For another country, pass calendar to each rule you call: todayState({ hours, calendar: 'DE' }, today) (or 'SG', or your own function); 'none' turns public holidays off. Instead of calendar you can pass country (from useRuntimeConfig().public.ginko.country). Set country or calendar in ginko.config.ts too (see Bricks), so the Studio and the reminder mail count the same holidays. A day has at most two opening periods (lunch and evening).

eventJsonLd needs timeZone (the site's time zone, from useGinkoSite()): start and end are then written in startDate and endDate with that day's offset, for example 2026-12-03T19:00:00+01:00.

Show the opening hours

ts
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 returns open (with the times), closed (with the owner's reason), holiday or restDay. Join the times in your language with spansText(state.times, 'and').

sites/restaurant in the Ginko repository is a complete example. It runs against a fake platform inside the repository, and its page files have German names.