Type to search pages and sections.

Skip to content
Ginko

Docs sites

The docs layer @lupinum/ginko-docs. A whole docs site from Markdown files, or docs inside a site that already exists. Options, start page, blog, changelog, colours.

What the layer gives you

@lupinum/ginko-docs is a Nuxt layer for documentation. These docs are built with it. You write the pages as Markdown files in content/docs/; the layer adds everything around them:

  • a header with search (⌘K), language menu and light/dark switch,
  • the side navigation with sections and tabs, breadcrumbs, a table of contents with scroll spy, and previous/next links,
  • the Markdown components of @lupinum/ginko-components (callouts, tabs, steps, code groups and more),
  • math with KaTeX and mermaid diagrams, loaded only on pages that use them,
  • "Copy page" as Markdown, "Edit this page" and "Report an issue",
  • an optional start page, blog with RSS, and changelog.

Every page also exists as Markdown for agents, and the layer adds an MCP endpoint for the docs. The page SEO and agents has the details.

Start a docs site

The fastest way is the starter:

Terminal
npm create ginko@latest my-docs -- --preset docs

It creates a site with two sample pages. To set it up by hand, install the packages:

Terminal
npm install @lupinum/ginko @lupinum/ginko-content @lupinum/ginko-i18n @lupinum/ginko-docs

Then describe the docs in ginko.config.ts:

ts
// ginko.config.ts
import { defineSite } from '@lupinum/ginko'
import { components, docsCollection } from '@lupinum/ginko-docs/config'

const locales = ['en', 'de'] as const

export default defineSite({
  site: 'my-docs',
  name: 'My docs',
  url: 'https://docs.example.com',
  timeZone: 'Europe/Vienna',
  locales,
  currency: 'EUR',
  pages: { home: { en: '/', de: '/de' } },
  components,
  bricks: { docs: docsCollection({ locales }) },
})

components is the component registry of @lupinum/ginko-components, which the layer brings along. With it, Ginko checks every component tag and prop in your pages. Without it, a misspelled ::calout goes through without an error.

Extend the layer in nuxt.config.ts:

ts
// nuxt.config.ts
import ginko from './ginko.config.ts'

export default defineNuxtConfig({
  compatibilityDate: '2026-10-01',
  extends: ['@lupinum/ginko-docs'],
  modules: ['@lupinum/ginko/nuxt'],
  ginko,
})

The docs pages live at /docs/<slug>, in other languages at /<locale>/docs/<slug>. docsCollection({ locales, base: '/handbook' }) moves them. Without a start page, / opens the first docs page.

The layer finds its content by brick key. Keep these names: docs, home, posts, authors and changelog. Only docs is required.

Only the docs brick may have another name, such as wissen on a German site. Give the key to the brick and to the layer: docsCollection({ locales, brick: 'wissen' }) in ginko.config.ts and ginkoDocs: { brick: 'wissen' } in nuxt.config.ts. Links to its pages then read $wissen.<page>.

Write pages

Each file in content/docs/ is one page. The number in the file name sets the order, as on Content from files. Besides title and description, the frontmatter can set:

FieldEffect
navTitleA short title for the side navigation, the breadcrumbs and the previous/next links, when the title is too long there.
sectionStarts a group in the side navigation with this heading. The pages after it belong to it until the next section.
tabStarts a tab above the side navigation, for example "Guide" and "Reference". Pages follow the tab before them.
parentNests the page under another one: parent: docs/<name>.
iconAn Iconify name such as lucide:rocket, before the title in the navigation. Set it in the main locale only.
badgeA short label after the title, for example "Beta".

If your site needs no tabs, badges and icons, docsCollection({ locales, navExtras: false }) leaves out the fields tab, badge and icon. People who edit the pages in the Studio then see only the text, the section, the parent page and the short title. Set it when the site starts: on a site with released pages it removes fields, which the platform refuses; retire the three fields instead (retired).

The lucide icons are installed. Another Iconify set works after you install it (npm install -D @iconify-json/<set>). Icons are bundled at build time, so the site calls no icon service.

Options

Options go in the site's app.config.ts. All of them are optional:

ts
// app/app.config.ts
export default defineAppConfig({
  docs: {
    title: 'My docs',                                     // header text next to the logo (default: `name` from ginko.config.ts)
    logo: { light: '/logo.svg', dark: '/logo-dark.svg' }, // `wordmark: true` when the image holds the name
    theme: 'default',                                     // colour preset: 'default' (emerald on zinc) or 'green' (green on slate)
    nav: [{ id: 'guide', href: '/guide' }],               // header links after Docs, Blog and Changelog
    social: [{ icon: 'docs:github', href: 'https://github.com/me/docs', label: 'GitHub' }],
    banner: { id: 'v2', href: '/docs/changes' },          // a notice above the header that readers can close
    repository: { url: 'https://github.com/me/docs', branch: 'main' }, // "Edit this page", "Report an issue"
    markdownActions: { copy: true, openIn: ['chatgpt', 'claude'] },     // "Copy page" and the chats of its menu
    footer: { links: [{ id: 'github', href: 'https://github.com/me/docs' }], attribution: true },
  },
})

The texts of nav, footer.links and banner come from the site's message files, so each language has its own. Each id picks a message:

json
{ "docs": { "banner": { "v2": "Version 2 is out." }, "nav": { "guide": "Guide" }, "footer": { "github": "GitHub" } } }
  • Put the file at i18n/en.json (and i18n/de.json and so on). The page Languages explains message files.
  • Internal paths in nav, footer.links and banner.href follow the reader's language: /guide becomes /de/guide. Absolute URLs stay as written.
  • A reader who closes the banner does not see it again. A new id shows it to everyone.
  • Built-in social icons: docs:github, docs:discord, docs:x, docs:bluesky, docs:mastodon, docs:linkedin, docs:youtube. Any other Iconify name works too.
  • repository adds "Edit this page" to every page from a file, and "Report an issue" for GitHub. For other hosts, set the templates editUrl ({url}, {branch}, {source}) and issueUrl ({url}, {title}, {page}).
  • Pages stored on the platform show no "Edit this page" by default, so visitors see no link to the Studio. studioEditLink: true adds it, linking to the page's form in the Studio. Set it for docs that only editors read.

Start page

docsHome() adds a start page: one file content/home.md, and home.<locale>.md per translation. The frontmatter fills the top of the page and the feature grid. The text below the frontmatter follows them and can use every component.

ts
// ginko.config.ts
import { docsCollection, docsHome } from '@lupinum/ginko-docs/config'

// …
  bricks: { home: docsHome(), docs: docsCollection({ locales }) },
md
---
title: Docs for my library.
description: One sentence on what it does.
install: npm install my-library
links:
  - { label: Get started, href: /docs/introduction }
  - { label: GitHub, href: https://github.com/me/lib }
features:
  - { icon: lucide:zap, title: Fast, description: Why it is fast., href: /docs/speed }
---

install shows a command with a copy button. links holds up to two buttons; the first one is the main one. A site's own app/pages/index.vue wins over the start page.

Blog and changelog

Both are optional. Add the bricks and their pages to ginko.config.ts; the layer then adds the pages and the header links:

ts
// ginko.config.ts
import { blogCollections, blogPages, changelogCollection, changelogPages, docsCollection } from '@lupinum/ginko-docs/config'

export default defineSite({
  // …
  pages: { home: { en: '/', de: '/de' }, blog: blogPages({ locales }), changelog: changelogPages({ locales }) },
  bricks: { docs: docsCollection({ locales }), ...blogCollections({ locales }), changelog: changelogCollection() },
})
  • Posts are files in content/posts/, for example content/posts/1.hello.md. The frontmatter has title, description, date (YYYY-MM-DD), authors, image, tags and slug. A post lives at /blog/<slug>; the list at /blog shows the newest first.
  • Authors are YAML files in content/authors/, for example content/authors/jane.yml with name, role, avatar and url. A post names them by id: authors: [authors/jane]. Authors have no page of their own.
  • RSS: /blog/rss.xml, and /<locale>/blog/rss.xml for other languages. The feed holds title and description, not the full text.
  • Changelog: one file per release in content/changelog/, with version and date in the frontmatter. Write the file name without dots (v1-2-0.md), because a dot reads as a language. The page /changelog shows all releases, newest first, each with an anchor such as #v1-2-0.

blogPages and changelogPages put the list pages into pages, so the sitemap and the static build know them. If they are missing, the build warns.

Colours and fonts

Colours, radius, fonts and sizes are CSS tokens, and every value your site sets wins over the defaults. The tokens, the two presets, light and dark, and the override points are on Theming and overrides. All public exports of the layer are on Docs layer exports.

Inside an existing site

The layer can also add docs to a site that has its own design, pages and layout, for example a company site with a handbook under /handbook. Extend the layer as above and add the docs brick next to the site's own bricks:

ts
// ginko.config.ts of the existing site
import { components, docsCollection } from '@lupinum/ginko-docs/config'

// …
  components: { ...components /* , the site's own components: the same name wins */ },
  bricks: { docs: docsCollection({ locales, base: '/handbook' }) /* , the site's own bricks */ },
  • Only the docs pages are added. The layer registers the docs at their base, /blog and /changelog only when those bricks exist, and / only when the site has no start page of its own. A page the site defines itself always wins.
  • The site's frame stays. With its own layouts/default.vue, the site keeps its header and footer, and the docs (navigation, page, table of contents) sit inside. On phones the navigation opens from a bar above the page.
  • Nothing leaks. Every rule of the layer is under a docs- class. The search, the language menu and the light/dark switch are components that the site can put into its own header: <DocsSearch />, <DocsLanguage />, <DocsThemeToggle />. These three are the public components (<DocsThemeToggle segmented /> shows the three-way Auto/Light/Dark switch). All other Docs* components and the useDocs* composables are internal and can change.
  • Title: pages get the title Page · Site name from Ginko (seo.titleTemplate in defineSite). A titleTemplate in app.head or from nuxt-seo-utils does not apply to them, so the name never appears twice.
  • Styling: map the site's design tokens to the token names on Theming and overrides. Two layout tokens line the docs up with the site: --docs-max (the width of the site's content container) and --docs-top (the height of a sticky header, or a small gap when the header scrolls away).
css
:root {
  --primary: var(--color-brand);
  --radius: 0.75rem;
  --docs-max: 82.5rem;
  --docs-top: 4.5rem;
}