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
mermaiddiagrams, 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:
npm create ginko@latest my-docs -- --preset docsIt creates a site with two sample pages. To set it up by hand, install the packages:
npm install @lupinum/ginko @lupinum/ginko-content @lupinum/ginko-i18n @lupinum/ginko-docsThen describe the docs in ginko.config.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:
// 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:
| Field | Effect |
|---|---|
navTitle | A short title for the side navigation, the breadcrumbs and the previous/next links, when the title is too long there. |
section | Starts a group in the side navigation with this heading. The pages after it belong to it until the next section. |
tab | Starts a tab above the side navigation, for example "Guide" and "Reference". Pages follow the tab before them. |
parent | Nests the page under another one: parent: docs/<name>. |
icon | An Iconify name such as lucide:rocket, before the title in the navigation. Set it in the main locale only. |
badge | A 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:
// 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:
{ "docs": { "banner": { "v2": "Version 2 is out." }, "nav": { "guide": "Guide" }, "footer": { "github": "GitHub" } } }- Put the file at
i18n/en.json(andi18n/de.jsonand so on). The page Languages explains message files. - Internal paths in
nav,footer.linksandbanner.hreffollow the reader's language:/guidebecomes/de/guide. Absolute URLs stay as written. - A reader who closes the banner does not see it again. A new
idshows it to everyone. - Built-in
socialicons:docs:github,docs:discord,docs:x,docs:bluesky,docs:mastodon,docs:linkedin,docs:youtube. Any other Iconify name works too. repositoryadds "Edit this page" to every page from a file, and "Report an issue" for GitHub. For other hosts, set the templateseditUrl({url},{branch},{source}) andissueUrl({url},{title},{page}).- Pages stored on the platform show no "Edit this page" by default, so visitors see no link to the Studio.
studioEditLink: trueadds 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.
// ginko.config.ts
import { docsCollection, docsHome } from '@lupinum/ginko-docs/config'
// …
bricks: { home: docsHome(), docs: docsCollection({ locales }) },---
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:
// 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 examplecontent/posts/1.hello.md. The frontmatter hastitle,description,date(YYYY-MM-DD),authors,image,tagsandslug. A post lives at/blog/<slug>; the list at/blogshows the newest first. - Authors are YAML files in
content/authors/, for examplecontent/authors/jane.ymlwithname,role,avatarandurl. A post names them by id:authors: [authors/jane]. Authors have no page of their own. - RSS:
/blog/rss.xml, and/<locale>/blog/rss.xmlfor other languages. The feed holds title and description, not the full text. - Changelog: one file per release in
content/changelog/, withversionanddatein the frontmatter. Write the file name without dots (v1-2-0.md), because a dot reads as a language. The page/changelogshows 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:
// 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,/blogand/changelogonly 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 otherDocs*components and theuseDocs*composables are internal and can change. - Title: pages get the title
Page · Site namefrom Ginko (seo.titleTemplateindefineSite). AtitleTemplateinapp.heador 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).
:root {
--primary: var(--color-brand);
--radius: 0.75rem;
--docs-max: 82.5rem;
--docs-top: 4.5rem;
}