Type to search pages and sections.

Skip to content
Ginko

Content from files

Bricks with source files read Markdown, YAML, JSON and CSV from content/. File layout, frontmatter, data files, drafts, old addresses, links and errors.

What files are for

A brick with source: 'files' reads its entries from files in the repo: Markdown, YAML, JSON or CSV. Use it for content that developers maintain: docs, a blog, legal pages, a price list. These docs use it themselves.

The site reads file content the same way as platform content: with useGinko and useGinkoEntry. At build time, Ginko reads the files and builds the same bundle as the platform does. Visibility, order, addresses, locales, and fallback work unchanged.

A brick has exactly one source. The platform never sees file bricks. A site whose bricks all come from files never asks the platform and needs no platform address.

Set it up

You need the package @lupinum/ginko-content in the dependencies of the site. If it is missing, the build stops with a message that says what to install.

You describe the brick like any other and set source: 'files'. The text of the file fills a field of type f.markdown:

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

export default defineSite({
  site: 'ginko-docs',
  name: 'Ginko',
  url: 'https://ginko.lupinum.com',
  timeZone: 'Europe/Vienna',
  locales: ['de', 'en'],
  currency: 'EUR',
  pages: { home: { de: '/', en: '/en' } },
  bricks: {
    docs: collection({
      label: 'Docs',
      source: 'files',
      max: 500,
      order: 'manual',
      route: { de: '/doku/[slug]', en: '/en/docs/[slug]' },
      fields: {
        title: f.text('Title'),
        description: f.longtext('Description', { optional: true }),
        slug: f.slug('Address', { from: 'title' }),
        body: f.markdown('Text'),
      },
    }),
  },
})

These rules apply to file bricks:

  • A file brick has at most one Markdown field: the text of the file. It is not allowed in a list there.
  • profile sets what the text may contain: inline, basic, rich (default) or page (also components such as ::callout). Raw HTML stays text.
  • A slug field must be called slug.

File layout

The files live in the folder content/ next to nuxt.config.ts:

text
content/
  docs/                       # collection "docs"
    1.einstieg.md             # main locale. The number is the order.
    1.einstieg.en.md          # translation: same name plus .en
    2.bausteine.md
  rechtliches.md              # single "rechtliches"
  rechtliches.en.md
  • A collection has a folder content/<brick>/. Each file is one entry.
  • A single is one file content/<brick>.md.
  • The file of the main locale has no locale code. A translation has the same name and ends in .<locale>.md, for example .en.md.
  • The number before the name is the order. Files without a number come after them, sorted by name. The number sets the order for order: 'manual'.
  • The id of an entry is the path without number, locale, and extension, for example docs/einstieg.
  • Images and files live in the folder public/.

A translation without a file in the main locale is an error.

Frontmatter

The frontmatter is a YAML block at the start of the file. It fills the fields of the brick in a simple form. The text below the frontmatter fills the Markdown field. The Markdown field itself never appears in the frontmatter.

markdown
---
title: Getting started
description: What Ginko is.
---

## What Ginko is

Ginko connects a Nuxt site with its content.

This is how you write each type:

TypeForm in the frontmatter
text, longtextText. The locale comes from the file name.
number, boolean42 or true
moneyAmount as a number: 12.50
date, time2026-10-12 and 18:30
daterangefrom: 2026-10-12 and to: 2026-10-14, indented under the field name
choicethe value of the option
choices, lista YAML list. A list row is an object with the columns.
imagesrc (path under public/) and alt, indented under the field name
filepath under public/
ref, refsthe entry id <brick>/<name> (file name without number, locale and extension), e.g. authors/jane; refs as a YAML list

These rules apply to translations:

  • A translation file contains only fields that are translatable. A number field in the file *.en.md is an error.
  • For an image, the translation gives only alt, not src.
  • A list in the translation must not have more rows than the list in the main locale. Ginko matches rows by their position.
  • Translations are always current. There is no "outdated". If a translation is missing, the fallback of the brick applies (untranslated).

Slug. Without a value in the frontmatter, Ginko takes the file name without the number, for example einstieg for 1.einstieg.md. A translation may set its own slug with slug:. Two entries with the same slug in one locale are an error.

Data files (YAML, JSON, CSV)

A brick without a Markdown field can also read YAML, JSON and CSV files. Use them for structured content without running text, for example a price list or a team.

YAML and JSON. One file is one entry, like a Markdown file. The whole file is what the frontmatter is in Markdown: the same keys, the same simple forms, the same rules for translations. File names follow the same layout: content/menu/1.kaffee.yml, its translation content/menu/1.kaffee.en.yml, a single content/contact.json. In these examples, as on this site, the main locale is de.

yaml
# content/menu/1.kaffee.yml
title: Kaffee
price: 3.5
vegan: true

The same in JSON, as content/menu/2.tee.json:

json
{
  "title": "Tee",
  "price": 2.8,
  "vegan": true
}

CSV. One file holds many entries, one per row. CSV works for collections only. The first row names the columns, for example in content/menu/preise.csv:

text
slug,locale,title,price,vegan
kaffee,,Kaffee,3.5,true
tee,,Tee,2.8,true
kaffee,en,Coffee,,
  • The column slug is required. It is the name of the entry (the id is menu/kaffee) and its slug.
  • The column locale is optional. An empty cell means the main locale. A row with en is the translation of the row with the same slug.
  • The other columns are field names. An empty cell means "no value".
  • Numbers use a point: 3.5, not 3,5. Yes/no values are true or false.
  • Lists, images, choices and daterange do not fit in a cell. Use a YAML or Markdown file for them.
  • The separator is a comma, or a semicolon when the first row has more semicolons (as Excel writes it in German). A cell in double quotes may contain the separator, line breaks and "" for a quote.
  • The rows keep their order. One folder may mix CSV, YAML and Markdown files.

Drafts

draft: true in the frontmatter (or in a YAML or JSON file, or a draft column in CSV) keeps an entry out of nuxt build, nuxt generate and ginko check. In dev mode, the entry is shown, and the site can mark it with entry.data.draft:

markdown
---
title: Spring opening
draft: true
---

Not ready yet.
vue
<span v-if="'draft' in entry.data && entry.data.draft">Draft</span>

A collection that lists its entries newest first by a date (order: { field: 'date', dir: 'desc' }, like posts()) also schedules entries: an entry dated after the day of the build is left out like a draft, and the first build on or after its date publishes it. This holds for nuxt build too, because file content is part of the build: to publish on the day, rebuild the site every day (for example with a scheduled deploy hook). In dev mode, scheduled entries are shown. A list from the oldest date (events) is not affected: its coming dates are the point.

Old addresses (redirectFrom)

When an entry gets a new slug, its old address should still work. redirectFrom in the frontmatter of the main-locale file lists the old paths. Each one answers with a 301 redirect to the entry's address:

markdown
---
title: Getting started
redirectFrom:
  - /docs/start
  - /docs/first-steps
---

A single path is also fine: redirectFrom: /docs/start. Each path starts with /. Redirects that do not belong to one entry go in redirects in ginko.config.ts. On a site whose bricks all come from files, the build also writes the exact paths as route rules, so they work on a static host.

Show Markdown

<GinkoMarkdown> shows the value of an f.markdown field. The Ginko module registers the component in every site.

vue
<script setup lang="ts">
import Callout from './Callout.vue'

const route = useRoute()
const doc = useGinkoEntry('docs', () => route.params.slug)
</script>

<template>
  <article>
    <h1>{{ doc.data.title }}</h1>
    <GinkoMarkdown :value="doc.data.body" :components="{ callout: Callout }" />
  </article>
</template>

The parser runs at build time. There is no parser at runtime. Ginko highlights code in code blocks with Shiki. Code sits on a dark panel; its colours come from the catppuccin-mocha theme. Ginko knows these languages: typescript, javascript, json, vue, bash, markdown, yaml, html, and css. Other languages appear without color.

Components. In Markdown, you write a component as a block with two colons:

markdown
::callout{type="warning"}
An error in a file stops the build.
::

Ginko stores the block as an element callout. The site decides with :components which Vue component belongs to it. The package @lupinum/ginko-components brings ready-made components such as callout, kbd and badge. How to add your own is in Add your own component.

Table of contents. In the bundle of a page, a Markdown field holds only a reference. <GinkoMarkdown> fetches the text of the current page. So the page carries only the texts that it shows. The table of contents toc comes with the loaded text. It holds the headings of levels 2 and 3. Each link has id and text:

ts
const tree = await useGinkoMarkdown(() => doc.value.data.body)
const toc = computed(() => tree.value?.toc?.links ?? [])

Images

An image can sit next to the Markdown file and be referenced with a relative path. Ginko reads the size from the file, stores the image under a name from its content (/_ginko/img/<hash>.jpg, cached for a year) and sets width and height, so the page does not shift while loading.

md
![The kitchen in morning light](./kueche.jpg)

The kitchen in morning light

The path must stay inside content/; if the file is missing, the build fails with the file and path. Paths starting with / look in public/ (only the size is added); https:// images stay unchanged. Image fields render with <GinkoImage :image="entry.data.hero" />, with size and loading="lazy" (eager for the first image on the page).

Image sizes. On build, Ginko writes WebP copies 480, 960 and 1600 pixels wide, each only when the image is wider. Markdown images and <GinkoImage> list them in srcset, so phones download a small file. SVG and GIF images stay as they are.

Image format. {aspect="4:3"} after an image shows it in that format: cropped around its focus point (for images from the Studio file library, otherwise around the middle), or whole with fit="contain". In the Studio this is “Format” and “Füllend / Eingepasst” (Fill / Fit) in the image's menu.

md
![The kitchen in morning light](./kueche.jpg){aspect="4:3"}

In Markdown, link to an entry with $<brick>.<name> and to a page of pages with $page.<key>. The name is the entry's id without the brick, for example einstieg for docs/einstieg; its slug works too. Ginko writes the address of the current locale at build time, so the link stays right when a slug or a route changes. An anchor is kept:

markdown
See [what Ginko is]($docs.einstieg#what-ginko-is) and [the start page]($page.home).

On every build and on ginko check, Ginko checks the links in all Markdown fields:

CheckWhat it findsDefault
brokena $… link to an entry or page that does not exist; a /… path that matches no entry, page or file in public/ (on sites whose bricks all come from files)'error'
anchorsa #anchor that is not a heading on the target page'warn'
redirecteda link to an old address that redirects'warn'
externalan http(s):// link whose server answers 404 or 410, whose host does not exist, or that times out twice'off'

'error' stops the build, 'warn' prints the problem, 'off' skips the check. You set the levels with linkCheck in ginko.config.ts:

ts
export default defineSite({
  // …
  linkCheck: { anchors: 'error', external: 'warn' },
})

The external check asks the servers over the network, on nuxt build, nuxt generate and ginko check, never in dev mode. More on it in Platform and CLI.

Errors

An error in a file stops the build. The message starts with [ginko] Errors in content/: and names the file and the field; for a CSV row or an unreadable file, also the line. For example:

text
[ginko] Errors in content/:
- content/docs/1.einstieg.md (colour): Unknown field “colour”.
- content/menu/preise.csv:3 (price): Column “price”: “3,5” is not a number. Use a point as the decimal separator (12.5, not 12,5).

These are examples:

  • a frontmatter key that the brick does not know
  • a locale code in the file name that the site does not have
  • a translation without a file in the main locale
  • two entries with the same slug in one locale
  • an image or file that is missing under public/
  • a value that the rules of the field reject, for example a title that is too long
  • text below the frontmatter although the brick has no Markdown field
  • a YAML, JSON or CSV file that cannot be read, or a CSV cell that does not fit its field
  • in a ```md preview block, Markdown that the field does not allow, for example ::script or a javascript: link
In dev mode, an error does not stop the server. Ginko writes the error to the log ([ginko] Errors in content/ (these files are missing from the site):) and leaves the file out. So check the log when a page is missing.