# Svelte DocSmith
> A documentation framework for Svelte. Interactive examples in one real, stateful app, markdown as routes, and a sidebar that builds itself.
# Introduction
## What is Svelte DocSmith?
Svelte DocSmith is a framework for building documentation sites with Svelte 5
and SvelteKit. Your interactive examples live inside one real, stateful app.
They are not sandboxed as isolated islands, and not screenshots of a component
that used to work.
You write markdown; DocSmith gives you the pipeline that turns it into styled,
navigable, syntax-highlighted pages, and lets you drop live Svelte components
straight into the prose.
## Why another docs tool?
Most Svelte docs are written by hand or bolted onto a general-purpose static
site generator. Either way you end up maintaining a navigation tree, wiring up a
markdown pipeline, and rebuilding the same layout every project. DocSmith gives
you the pipeline, the layout, and the navigation out of the box, so you can
focus on the content.
And because a page is a real SvelteKit route, an example in your docs is the
same component your users import: running, stateful, and impossible to let rot.
## Highlights
- **Markdown as routes.** `.md` files compile to real Svelte components via
mdsvex. No loader, no catch-all route.
- **Live examples.** Drop a component into a page; it runs, and its source is
shown from the same file, so the two can never drift.
- **Syntax highlighting.** Shiki runs at build time on the HAST tree, with a
generous language set and dual light/dark themes.
- **Nav derives itself.** The sidebar is built from each page's frontmatter,
never hand-written.
- **The whole chrome.** Header, collapsible sidebar, mobile nav, in-page table
of contents, breadcrumbs, and prev/next links, all included.
- **Yours to theme.** One CSS import ships the Tailwind and shadcn token system;
override any token to make it your own.
## Where to next
{#snippet icon()}{/snippet}
Add DocSmith to a SvelteKit project and wire up the one-line CSS contract.
{#snippet icon()}{/snippet}
Register the pipeline and render your first page in four steps.
# Installation
## Start a new project
The fastest way to begin is the scaffolder. It creates a ready-to-run SvelteKit
project already wired with DocSmith: the markdown pipeline, the Vite plugin, the
style contract, a `DocsShell` layout, a 404 page, and a couple of sample pages.
```bash
pnpm create svelte-docsmith my-docs
```
```bash
npm create svelte-docsmith@latest my-docs
```
```bash
yarn create svelte-docsmith my-docs
```
```bash
bun create svelte-docsmith my-docs
```
Then install dependencies and start the dev server:
```bash
cd my-docs
npm install
npm run dev
```
That is the whole setup. Skip ahead to [Writing pages](/docs/writing-pages) to
start authoring. The rest of this page covers adding DocSmith to a project you
already have.
## Add to an existing project
Svelte DocSmith is a SvelteKit library. Install it with your package manager of
choice:
```bash
pnpm add -D svelte-docsmith
```
```bash
npm install -D svelte-docsmith
```
```bash
yarn add -D svelte-docsmith
```
```bash
bun add -D svelte-docsmith
```
DocSmith expects **Svelte 5**, **SvelteKit 2**, and **Tailwind CSS v4** as peer
dependencies, the same stack this documentation site runs on. A fresh
`npx sv create` app with Tailwind selected already has them.
## The CSS contract
Components are styled with Tailwind and shadcn design tokens. The whole contract
is one import in your app's stylesheet:
```css
@import 'tailwindcss';
@import 'svelte-docsmith/theme.css';
```
`theme.css` makes Tailwind scan the package (so the utility classes its
components use are generated), defines the shadcn theme tokens (`--background`,
`--primary`, `--radius`, and the rest) for `:root` and `.dark`, and pulls in the
typography and animation plugins.
That single import is also the whole customization surface: redefine any token
after it to rebrand the entire system. See [Theming](/docs/theming) for the full
token list and how dark mode is wired.
## Next
With the package installed, continue to the [Quick Start](/docs/quick-start) to
wire up the pipeline and render your first page.
# Quick Start
Four steps take you from an installed package to a live docs page in the
sidebar. Each one edits a single file.
`npm create svelte-docsmith` does every step on this page for you. This walk
through is for adding DocSmith to an existing app, or for understanding the
pieces. See [Installation](/docs/installation) for the scaffolder.
In `svelte.config.js`, add `.md` to your extensions and call `docsmith()`. It
bundles mdsvex, Shiki highlighting with a generous language set, heading
anchors, and the DocSmith page layout:
```js
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
import { docsmith } from 'svelte-docsmith/preprocess';
export default {
extensions: ['.svelte', '.md'],
preprocess: [vitePreprocess(), docsmith()],
kit: { adapter: adapter() }
};
```
In `vite.config.ts`, add `docsmith()`. It scans your pages' frontmatter into the
`svelte-docsmith/content` module and powers live examples:
```ts
import { sveltekit } from '@sveltejs/kit/vite';
import tailwindcss from '@tailwindcss/vite';
import { docsmith } from 'svelte-docsmith/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [docsmith(), tailwindcss(), sveltekit()]
});
```
In `src/routes/docs/+layout.svelte`, render `DocsShell`. It builds the sidebar
from the generated content index, so there is no nav array to maintain:
```svelte
{@render children()}
```
Create `src/routes/docs/getting-started/+page.md`. The frontmatter drives the
sidebar; everything below it is your content:
````md
---
title: Getting Started
description: Your first steps.
section: Guides
order: 1
---
## Hello
This is a real SvelteKit route. Code blocks are highlighted by Shiki, and you
can emphasise a line with the notation transformer:
```ts
const docs = loadDocs(); // [!code highlight]
```
````
## What you end up with
Those four files sit exactly here:
Drop a markdown file under `src/routes/docs/` and it appears in the sidebar,
styled, highlighted, with breadcrumbs and a table of contents. To embed a
running component, see [Writing pages](/docs/writing-pages), which covers
frontmatter, live examples, and code highlighting in full.
# Configuration
Everything you wire up once: what the package exports, the config object you pass
to the shell, and the two page-level components that own the site chrome. Props
for the authoring components (`Callout`, `Tabs`, `Badge`, and the rest) live on
their own pages in the [Components](/docs/components/callout) section.
## Package exports
- **`svelte-docsmith`**: every component, plus `defineConfig`, `createSearchEngine`, `generateSitemap`, and the types. Components are documented in [Components](/docs/components/callout); the rest is below.
- **`svelte-docsmith/preprocess`**: the mdsvex + Shiki preprocessor for `svelte.config.js`.
- **`svelte-docsmith/vite`**: the Vite plugin (content index, search index, and the `?source` transform).
- **`svelte-docsmith/content`**: the generated sidebar index, exported as `docs`.
- **`svelte-docsmith/search`**: the generated full-text search index, exported as `docs` (lazy-load it; see [Search](/docs/search)).
- **`svelte-docsmith/theme.css`**: the base style contract.
- **`svelte-docsmith/themes/*.css`**: the pre-installed theme presets (see [Theming](/docs/theming)).
## defineConfig
Validates a `DocsmithConfig` and returns it unchanged, throwing a clear error on
an invalid or dynamically-built config instead of rendering a blank header.
```ts
const config = defineConfig({
title: 'My Library',
description: 'A short tagline, used as the default meta description.',
url: 'https://my-library.dev',
github: 'https://github.com/you/my-library',
version: '1.0.0'
});
```
Site title, shown in the header/sidebar and as the <title> suffix.
Default meta description, used for pages without their own. See SEO.
Canonical site origin. Enables <link rel="canonical"> and absolute Open Graph URLs.
Default social-share image (absolute, or a path resolved against url).
Base URL for the per-page “Edit this page” link, e.g.
https://github.com/you/repo/edit/main/apps/docs. Each page's source
path is appended. (“Last updated” is added from git automatically.)
GitHub URL; renders a link in the header when set.
Version string shown in the header.
Logo image src; falls back to the built-in book mark.
Top-level header navigation links.
A thin bar above the header. text is required; add a
tag for a leading pill (e.g. "New") and an
href to link it. It's dismissible by default and stays dismissed
until you change id (or the text), so bump id to
re-show a new announcement.
Footer copyright line, titled link columns, and the “Powered by” toggle.
This site runs one: the thin bar above the header is `config.announcement`. Its
`id` tracks the library version, so it returns after each release and stays out
of the way in between. Dismiss it and it holds until the next version.
## DocsShell
The full documentation shell: header, sidebar, content area, and table of
contents.
```svelte
{@render children()}
```
The site config (see defineConfig above).
The content index; the sidebar nav is derived from it.
The rendered page.
Enable the ⌘K search palette by lazily providing the generated index, e.g.
{'() => import(\'svelte-docsmith/search\').then((m) => m.docs)'}.
See Search.
Override the head tags for this page (title, meta description). Doc pages get
these from frontmatter automatically. See SEO.
Custom logo mark for the header and mobile menu.
Extra header controls, before the theme toggle.
Content rendered below the page column.
Render the decorative grid-and-glow page background.
Show the "Copy page" split button on doc pages (copy as Markdown, view the
raw .md, or open in ChatGPT / Claude). Needs the .md
endpoint. See SEO.
Show the estimated reading time on doc pages (computed at build time from
the page's word count). Set false to hide it.
void)"}>
Show the "Was this page helpful?" widget at the foot of doc pages. Pass
true for the UI alone, or a callback to record votes (wire it to
your analytics). Omit to hide it.
docs is the three-column shell; page is full-bleed
content with the same header and footer but no sidebar or TOC.
`ThemeProvider` and `ThemeToggle` handle light and dark with no consumer wiring.
`DocsShell` mounts the provider internally, so you never touch `mode-watcher`
yourself. Use `ThemeProvider` directly to wrap a page you build outside
`DocsShell`.
## ErrorPage
A styled 404 / error screen that keeps the site chrome (header, search, footer,
theme). Drop it into a SvelteKit `+error.svelte`:
```svelte
```
Same config as DocsShell, so the chrome matches.
Content index, so the header/footer nav still work.
HTTP status. Defaults to the current page's status.
Heading. Defaults to “Page not found” for 404, else “Something went wrong”.
Body line. Defaults to the error message, then a status-appropriate default.
Where the primary action links.
Label of the primary action.
Enable the ⌘K palette on the error page (same loader as DocsShell).
## Types
- **`DocsmithConfig`**: the config object above.
- **`DocsContentItem`**: a content-index entry with `title`, `path`, and optional
`section`, `order`, `description`, `toc`.
- **`SearchDoc`** / **`SearchResult`** / **`SearchEngine`**: the search index
entry and the shape returned by [`createSearchEngine`](/docs/search).
- **`CalloutVariant`** / **`BadgeVariant`**: the intent unions for `Callout` and `Badge`.
The vendored shadcn primitives and internal helpers (the TOC engine, the
clipboard utility, the markdown renderer map) are **not** part of the public API
and may change between releases.
# How it works
## Markdown as routes
DocSmith leans on mdsvex, which compiles markdown into real Svelte components. A
file becomes a page by its position on disk, with no loader and no catch-all
route:
The file above serves `/docs/guides/routing`. Because it is a normal SvelteKit
route, you can drop interactive Svelte components straight into the markdown and
they run as part of the same app.
## One name, two plugins
DocSmith ships two things both called `docsmith()`, imported from two entry
points. They do different jobs:
- `svelte-docsmith/preprocess` is a **Svelte preprocessor**, added in
`svelte.config.js`. It runs at compile time and turns each `.md` file into a
styled, highlighted page (mdsvex, Shiki, heading anchors, the page layout).
- `svelte-docsmith/vite` is a **Vite plugin**, added in `vite.config.ts`. It
runs at build time and generates the content index (below) plus the `?source`
transform that powers live examples.
The preprocessor and the Vite plugin are not interchangeable. The preprocessor
renders your pages; the Vite plugin builds the sidebar and live-example source.
Register one without the other and either your pages or your navigation goes
missing.
## Nav is derived, never written
There is no navigation array to maintain. The `docsmith()` Vite plugin reads
each page's frontmatter into the `svelte-docsmith/content` module, and
`DocsShell` groups the entries into the sidebar:
```ts
{
title: 'How it works',
description: 'The content model...',
section: 'Core Concepts', // sidebar group
order: 4 // sort key
}
```
`section` names the group; `order` sorts entries within it; groups are ordered
by the smallest `order` they contain. Add a page, and it slots into the sidebar
in the right place automatically.
## Two tables of contents, two jobs
DocSmith keeps two structures, and they never overlap:
- The **content index** owns build-time structure. The `docsmith()` Vite plugin
scans frontmatter into the sidebar navigation.
- The runtime **TOC engine** owns in-page scroll tracking. It scans the rendered
headings and highlights the section you are reading.
## The highlighting pipeline
Code blocks are highlighted by [Shiki](https://shiki.style) inside the
`docsmith()` preprocessor, with a generous default language set. Unknown
languages fall back to plain text instead of failing the build:
```python
def greet(name: str) -> str:
return f"Hello, {name}"
```
Highlighting is dual-theme: the same markup carries light and dark colors and
flips with the page theme, so your code reads correctly either way.
# Writing pages
## A page is a file
Every page is a `+page.md` file under `src/routes/docs/`. The directory name is
the URL, so this file serves `/docs/guides/routing`:
Create the file, and the page exists.
## Frontmatter
The frontmatter block at the top of each page drives the sidebar. Four fields:
| Field | Required | Purpose |
| ------------- | -------- | -------------------------------------------------------------------------- |
| `title` | yes | The sidebar label and page heading. |
| `description` | no | One-line summary, shown under the title. |
| `section` | no | Sidebar group, a string or a nested path. Omitted pages fall under "Docs". |
| `order` | no | Sort key within the group. |
```md
---
title: Routing
description: How pages map to URLs.
section: Guides
order: 1
---
```
`section` names the group and `order` sorts within it; groups themselves are
ordered by the smallest `order` they contain.
### Nested sections
Give `section` an array to nest a page inside a collapsible subsection. Each
entry is one level of the group path:
```md
---
title: Middleware
section: [Guides, Advanced]
order: 2
---
```
This puts "Middleware" under a collapsible **Advanced** group inside **Guides**.
Nesting can go as deep as you like, `order` still sorts each level, and a
subsection inherits the smallest `order` of its pages. The branch holding the
current page is expanded on load; the rest start collapsed.
A page with no `title` is skipped by the sidebar. If a page isn't showing up,
check its frontmatter before anything else: a stray indent or a typo'd key is
the usual culprit.
## Headings
Don't write an `#` (h1) in the body. The `title` from frontmatter is the page
heading, so start your content at `##`. Every heading gets an anchor id
automatically, and the in-page table of contents is built from `##` and `###`
headings as the page renders.
## Code blocks
Fenced code blocks are highlighted by Shiki at build time; tag the fence with a
language. To emphasise a line, append the comment `// [!code highlight]` to it
(a real comment in that language). Shiki strips the comment and highlights the
line, like the second line below:
```ts
const docs = loadDocs();
const current = docs.find((d) => d.active); // [!code highlight]
```
Unknown languages fall back to plain text rather than failing the build, so a
stray ` ```mermaid ` won't break your site.
Line highlighting is just the start. See [Code blocks](/docs/code-blocks) for
diffs, focus, error and warning lines, and word highlighting.
## Live examples
To show a real, running component next to its source, put the component in
`src/lib/examples/`. Import `LiveExample`, then import your component twice: once
as the component, and once with the `?source` query for its build-time
highlighted source. Pass both to `LiveExample`:
```md
```
Both come from the same file, so the demo you render and the code you show can
never drift. See [Live Examples](/docs/live-examples) for a running one.
## Tabbed content
For alternatives such as package managers or framework variants, group blocks
with `Tabs` and `TabItem`. Pass the tab labels as `items`; each `TabItem`'s
`value` matches one label:
````md
```bash
npm i -D svelte-docsmith
```
```bash
pnpm add -D svelte-docsmith
```
````
See the [Components](/docs/components/callout) section for the full set you can
drop into a page: callouts, steps, cards, accordions, file trees, badges, and
more.
# Code blocks
Every fenced code block is highlighted by Shiki at build time. On top of that,
you annotate lines and words with comment markers written right in the code. The
marker comment is stripped from the rendered output, so what readers see stays
clean.
Each section below shows the markdown you write, then how it renders.
## Highlight a line
Append `// [!code highlight]` to a line (a real comment in the fence's language)
to give it a highlighted background.
You write:
```text
const config = defineConfig({
title: 'My Library' // [!code highlight]
});
```
Which renders as:
```ts
const config = defineConfig({
title: 'My Library' // [!code highlight]
});
```
## Additions and deletions
Mark a line with `// [!code ++]` for an addition or `// [!code --]` for a
deletion. They render with a colored background and a `+` / `-` gutter marker.
You write:
```text
export default {
preprocess: [vitePreprocess()], // [!code --]
preprocess: [vitePreprocess(), docsmith()], // [!code ++]
};
```
Which renders as:
```ts
export default {
preprocess: [vitePreprocess()], // [!code --]
preprocess: [vitePreprocess(), docsmith()] // [!code ++]
};
```
## Focus
`// [!code focus]` dims the other lines so the eye lands on what matters. Hover
the rendered block to bring the rest back.
You write:
```text
function setup() {
const app = createApp();
app.use(docsmith()); // [!code focus]
return app;
}
```
Which renders as:
```ts
function setup() {
const app = createApp();
app.use(docsmith()); // [!code focus]
return app;
}
```
## Errors and warnings
`// [!code error]` and `// [!code warning]` tint a line red or amber, for
call-outs like a deprecated call or a footgun.
You write:
```text
const ok = readFile('./page.md');
const bad = readFile(); // [!code error]
const risky = readFileSync('./page.md'); // [!code warning]
```
Which renders as:
```ts
const ok = readFile('./page.md');
const bad = readFile(); // [!code error]
const risky = readFileSync('./page.md'); // [!code warning]
```
## Highlight a word
`// [!code word:name]` highlights every occurrence of `name` on the next line.
Add a count like `word:name:2` to limit how many.
You write:
```text
const name = frontmatter.title; // [!code word:name]
```
Which renders as:
```ts
const name = frontmatter.title; // [!code word:name]
```
Write the marker inside a comment your language understands: `//` for
JS/TS/Svelte, `#` for bash or YAML, `` for HTML. Shiki strips the comment
along with the marker, so it never ships to the reader. (To show a marker
literally instead of applying it, as the "You write" blocks above do, put it in a
plain `text` block.)
# Live Examples
## A real, running component
The button below is a real Svelte component running as part of this app. It is
not a screenshot, and not a sandboxed iframe. Click it, then open the source:
## Single source of truth
The rendered component and the source panel above both come from **one file**,
`counter.svelte`. It is imported twice: once as a component (rendered) and once
as `?source` (highlighted at build time by the `docsmith()` plugin from
`svelte-docsmith/vite`), so the demo and its code can never drift.
The example in your docs is the same component your users import. When the
component changes, the rendered demo and its shown source both change with it,
so it can never decay into a screenshot of something that used to work.
See [Writing pages](/docs/writing-pages) for the import pattern to copy.
## API reference
The live, rendered component.
Pre-highlighted source HTML. Pass a ?source import of the same file.
# Theming
DocSmith ships its entire look as shadcn-style design tokens behind a single
stylesheet. You restyle the whole system by redefining tokens, not by touching
components, so a rebrand is a handful of CSS variables instead of a fork.
## One import, one contract
The style contract is one import on top of Tailwind:
```css
@import 'tailwindcss';
@import 'svelte-docsmith/theme.css';
```
`theme.css` does three things: it makes Tailwind scan the package so the
components' utility classes are generated, it registers the shadcn token set for
`:root` and `.dark`, and it pulls in the typography and animation plugins. Every
component reads those tokens, so the tokens are the only surface you style.
On its own, `theme.css` gives you the default theme, **Darkmatter**: a
near-monochrome shell with a warm orange primary. You import nothing else to get
it.
## The presets
Eleven presets ship in the box. Pick one below to preview it, and toggle the
site's dark mode to see both sides:
A preset is a stylesheet that redefines the color tokens, and for some the corner
radius, and nothing else. Import it after `theme.css` and it wins:
```css
@import 'tailwindcss';
@import 'svelte-docsmith/theme.css';
@import 'svelte-docsmith/themes/amethyst.css';
```
Darkmatter is already baked into `theme.css`, so you only import
`themes/darkmatter.css` to return to it after trying another preset.
Available: `darkmatter` (default), `tangerine`, `amethyst`, `graphite`,
`evergreen`, `rose`, `ocean`, `nord`, `claude`, `bubblegum`, and `mono`. Each
covers light and dark. Want your own brand color instead? Skip the preset and
override the tokens directly.
## Overriding tokens
Redefine any token after the import and it wins. Tokens are OKLCH, so change the
primary and every button, link, and accent follows:
```css
@import 'tailwindcss';
@import 'svelte-docsmith/theme.css';
:root {
--primary: oklch(0.55 0.2 265); /* your brand color */
--radius: 0.5rem; /* tighter corners */
}
```
If an override is not taking effect, import order is almost always the cause.
Your redefinition has to come after the `theme.css` import, or the package's own
value wins. The same rule applies to preset stylesheets.
## The token set
The tokens are standard shadcn. The ones you reach for most:
| Token | Controls |
| ------------------------------------ | ------------------------------------- |
| `--background` / `--foreground` | Page surface and body text |
| `--primary` / `--primary-foreground` | Brand color and text on it |
| `--muted` / `--muted-foreground` | Quiet fills and secondary text |
| `--accent` / `--accent-foreground` | Hover and highlight surfaces |
| `--border` / `--input` / `--ring` | Hairlines, field borders, focus rings |
| `--card` / `--popover` | Raised surfaces |
| `--sidebar*` | The docs sidebar, tokened separately |
| `--radius` | Corner rounding across the system |
## Dark mode
Dark mode is class-based: every token has a `.dark` variant, and DocSmith styles
respond to a `dark` class on the `` element. The docs site toggles it with
[`mode-watcher`](https://github.com/svecosystem/mode-watcher). Drop its
`` in your root layout and the theme toggle and system preference
work out of the box.
An override only covers the theme whose selector you write it under, so set a
token for both modes to change both:
```css
:root {
--primary: oklch(0.55 0.2 265);
}
.dark {
--primary: oklch(0.7 0.16 265); /* lighter, for the dark surface */
}
```
## Fonts
Three families are set as tokens: `--font-sans`, `--font-serif`, and
`--font-mono`. Point them at your own faces and load the fonts however you
normally would (a `` in `app.html`, `@fontsource`, or your host):
```css
:root {
--font-sans: 'Geist', sans-serif;
--font-mono: 'Geist Mono', monospace;
}
```
# Search
DocSmith builds a full-text search index from your pages at build time and ships
a ⌘K / Ctrl-K command palette that searches it. There is nothing to host and no
service to configure.
## Enable it
Pass a `search` loader to `DocsShell`. It hands back the generated index, which
DocSmith lazily fetches the first time the palette opens, so it never weighs down
your initial load:
```svelte
import('svelte-docsmith/search').then((m) => m.docs)}
>
{@render children()}
```
That is the whole setup. A search button appears in the header, ⌘K (Ctrl-K on
Windows and Linux) opens the palette from anywhere, and results link straight to
the matching page. Omit the prop to leave search off.
Passing a function that dynamically imports `svelte-docsmith/search` lets your
bundler split the index into its own chunk. The index is fetched only when a
reader first opens search, not on every page view.
## What gets indexed
Each page contributes its `title`, its `h2`/`h3` headings, its frontmatter
`description`, and its body text, reduced to plain prose, with code blocks,
component markup, and markdown punctuation stripped out. Title and heading
matches rank above body matches.
## A custom search UI
The palette is the default, but the engine is exported if you want to build your
own input. `createSearchEngine` takes the generated index and returns a
`search(query, limit?)` that yields ranked results with a context snippet:
```ts
import { createSearchEngine } from 'svelte-docsmith';
import { docs } from 'svelte-docsmith/search';
const engine = createSearchEngine(docs);
for (const hit of engine.search('theming')) {
console.log(hit.title, hit.path, hit.snippet);
}
```
# SEO
`DocsShell` writes the head tags for every page (`
`, meta description,
canonical URL, and Open Graph / Twitter Card tags) with no per-page wiring. Doc
pages get theirs straight from frontmatter.
## Per-page, from frontmatter
A page's `title` becomes `Page · Site Title`, and its `description` becomes the
meta and social description. You already write both to drive the sidebar, so
there is nothing extra to add:
```md
---
title: Installation
description: Add Svelte DocSmith to a SvelteKit project.
section: Getting Started
order: 2
---
```
## Site-wide defaults
Set the defaults once in your `DocsmithConfig`. `url` is the piece that unlocks
absolute links (a canonical `` and absolute `og:url`/image), so search
engines and social scrapers resolve them correctly:
```ts
export const siteConfig = defineConfig({
title: 'My Library',
description: 'A short tagline, used when a page has no description.',
url: 'https://my-library.dev',
ogImage: '/og.png' // absolute, or resolved against `url`
});
```
| Field | Used for |
| ------------- | -------------------------------------------------------------------- |
| `description` | Default meta description for pages without their own |
| `url` | Canonical origin; enables `` and absolute URLs |
| `ogImage` | Default social-share image |
Without `url`, DocSmith can't build an absolute address, so it omits the
canonical and `og:url` tags rather than emit a wrong one. Set `url` to your
deployed origin to turn them on.
## Non-doc pages
Pages that aren't markdown (a landing page, a custom route) have no
frontmatter, so pass the `seo` prop to set or override the head:
```svelte
{@render children()}
```
## Sitemap
`generateSitemap` builds a `sitemap.xml` from your content index. Add a
`src/routes/sitemap.xml/+server.ts`:
```ts
import { docs } from 'svelte-docsmith/content';
import { generateSitemap } from 'svelte-docsmith';
import { siteConfig } from '$lib/site-config';
export const prerender = true;
export function GET() {
const body = generateSitemap(siteConfig.url ?? '', [
{ path: '/' },
...docs.map((d) => ({ path: d.path, lastmod: d.lastUpdated }))
]);
return new Response(body, { headers: { 'content-type': 'application/xml' } });
}
```
Each entry gets a `` from the page's last git commit. Then point
crawlers at it from `static/robots.txt`:
```txt
User-agent: *
Allow: /
Sitemap: https://your-docs.dev/sitemap.xml
```
## llms.txt
The [llms.txt](https://llmstxt.org) standard gives AI tools a clean, plain-text
view of your docs. Svelte DocSmith generates the data at build time in the
`svelte-docsmith/llms` module, and two helpers turn it into the two files the
standard defines: `llms.txt` (a curated index of links) and `llms-full.txt`
(the full text of every page).
Add `src/routes/llms.txt/+server.ts`:
```ts
import { docs } from 'svelte-docsmith/llms';
import { generateLlmsTxt } from 'svelte-docsmith';
import { siteConfig } from '$lib/site-config';
export const prerender = true;
export function GET() {
const body = generateLlmsTxt(
{ title: siteConfig.title, description: siteConfig.description, origin: siteConfig.url },
docs
);
return new Response(body, { headers: { 'content-type': 'text/plain; charset=utf-8' } });
}
```
And `src/routes/llms-full.txt/+server.ts`, identical but for `generateLlmsFullTxt`:
```ts
import { docs } from 'svelte-docsmith/llms';
import { generateLlmsFullTxt } from 'svelte-docsmith';
import { siteConfig } from '$lib/site-config';
export const prerender = true;
export function GET() {
const body = generateLlmsFullTxt(
{ title: siteConfig.title, description: siteConfig.description, origin: siteConfig.url },
docs
);
return new Response(body, { headers: { 'content-type': 'text/plain; charset=utf-8' } });
}
```
Both follow your sidebar reading order, grouping pages by `section` and sorting
by `order`. Each page's title becomes an `h1`, and its `description` frontmatter
annotates the link in the index.
You are reading these docs through this exact pipeline. Open
[/llms.txt](/llms.txt) and [/llms-full.txt](/llms-full.txt) to see the output.
## Copy page
The same per-page markdown powers a "Copy page" button on every doc page. Turn
it on with the `copyPage` prop on `DocsShell`:
```svelte
{@render children()}
```
The split button copies the page as Markdown, and its dropdown links to the raw
`.md`, or opens the page in ChatGPT or Claude. It expects each page to be
available at `.md`, so add one catch-all endpoint,
`src/routes/[...slug].md/+server.ts`:
```ts
import { docs } from 'svelte-docsmith/llms';
import { error } from '@sveltejs/kit';
import type { EntryGenerator, RequestHandler } from './$types';
export const prerender = true;
export const entries: EntryGenerator = () =>
docs.map((doc) => ({ slug: doc.path.replace(/^\//, '') }));
export const GET: RequestHandler = ({ params }) => {
const doc = docs.find((d) => d.path === `/${params.slug}`);
if (!doc) error(404, 'Not found');
return new Response(doc.content, {
headers: { 'content-type': 'text/markdown; charset=utf-8' }
});
};
```
Try it: the "Copy page" button at the top of this page, or open
[this page as Markdown](/docs/seo.md).
# Callout
Highlight something the reader shouldn't miss. Four intents, each with its own
icon and color. The body stays on the page's normal text color so it's always
legible.
This is a **note**, neutral informational context.
This is a **tip**, a helpful shortcut or best practice.
This is a **warning**, so proceed carefully.
This is a **danger** callout: something here can break or lose data.
## Usage
Import it and pick a `variant`. The default is `note`; pass `title` to override
the heading. Leave **blank lines** around the content so mdsvex parses the
markdown inside (bold, links, code). Without them it renders as literal text.
```svelte
You can override the heading with the `title` prop, and use **markdown**
inside, including `code` and [links](/docs/theming).
```
## API reference
Visual intent.
Heading above the body.
# Steps
A numbered walkthrough (a connecting line with numbered badges) for setup flows
where the order matters. Each step is a ``; the numbers are automatic.
Add `svelte-docsmith` to your SvelteKit project.
Add `docsmith()` in `svelte.config.js` and `vite.config.ts`.
Drop a `+page.md` under `src/routes/docs/` and it appears in the sidebar.
## Usage
`Steps` and `Step` are plain components. They work in a markdown page **and** in
any `.svelte` file, with no preprocessor required. Give each `Step` an optional
`title`; leave blank lines around markdown content so it's parsed.
```svelte
Do this.Do that.
```
### Markdown shortcut
Inside a markdown page only, you can skip the `` tags and use a plain
ordered list, and mdsvex turns it into steps:
```md
1. Do this.
2. Do that.
```
## API reference
### Steps
Wraps its `` children; no other props.
### Step
Optional heading for the step.
# Card & CardGrid
A `Card` groups a title and description, optionally as a link. Give it an `href`
and it becomes clickable, with a hover state and a trailing arrow. `CardGrid`
lays cards out in a responsive grid that reflows without breakpoints: as many
columns as fit, down to one on narrow screens.
What Svelte DocSmith is and who it's for.
Wire up the pipeline and render your first page.
Override tokens or pick a pre-installed theme.
## Usage
```svelte
Wire up the pipeline and render your first page.
Opens in a new tab.
```
## API reference
### Card
Card heading.
Makes the card a link.
Open the link in a new tab.
Optional leading icon.
### CardGrid
The Cards to lay out.
# Tabs
Group alternatives such as package managers, framework variants, or OS-specific
commands so the reader sees one at a time. Give each `TabItem` a `label`; `Tabs`
builds the tab row from them, so there is nothing to keep in sync.
```bash
npm i -D svelte-docsmith
```
```bash
pnpm add -D svelte-docsmith
```
```bash
yarn add -D svelte-docsmith
```
## Usage
Leave blank lines around the content inside each `TabItem` so the markdown (code
fences, prose) is parsed. The first tab is selected by default; pass `value` on
`Tabs` to start on a different one.
````svelte
```bash
npm i -D svelte-docsmith
```
```bash
pnpm add -D svelte-docsmith
```
````
## Synced tabs
Give related `Tabs` the same `syncKey` and they share one selection: pick `pnpm`
in any block and every block with that key switches to `pnpm`, across the page
and the rest of the site. The choice is remembered across reloads. Use it for
package managers, runtimes, or any choice a reader makes once and keeps.
Try it. These two blocks share `syncKey="demo-pm"`; changing one moves the other:
```bash
npm i -D svelte-docsmith
```
```bash
pnpm add -D svelte-docsmith
```
```bash
yarn add -D svelte-docsmith
```
```bash
npx sv add svelte-docsmith
```
```bash
pnpm dlx sv add svelte-docsmith
```
```bash
yarn dlx sv add svelte-docsmith
```
## API reference
### Tabs
Label of the tab selected by default.
Sync group. Blocks with the same key share their selection and remember it
across reloads.
### TabItem
The tab's trigger text.
Underlying value, only needed to disambiguate duplicate labels.
# Accordion
Fold FAQs, optional details, or long asides behind a heading the reader expands
on demand, one panel at a time by default, so the page stays scannable.
A documentation framework for Svelte 5. Your markdown files _are_ SvelteKit
routes, so examples run as part of one real app.
No. Nav is derived from each page's frontmatter, never hand-written.
Yes. Pass `multiple` to the `Accordion` and every panel toggles independently.
## Usage
Each `AccordionItem` takes a `title` (the always-visible summary) and claims
its own value automatically, so you never wire up ids. Leave **blank lines**
around the panel content so mdsvex parses the markdown inside.
```svelte
Answer with full **markdown**, including `code` and [links](/docs/theming).
Another answer.
```
Pass `multiple` to let panels stay open independently:
```svelte
```
## API reference
### Accordion
Allow several panels open at once.
### AccordionItem
The heading that toggles the panel.
# Badge
Label a status, version, or category inline. Six intents cover the usual docs
needs, neutral through danger, each driven by your theme tokens so they recolor
with the rest of the site.
## Usage
Import it and pick a `variant`. The default is `default`; pass `href` to turn the
badge into a link.
```svelte
StableBetaNew
```
## API reference
Visual intent.
Renders the badge as a link.
For a linked badge, open in a new tab with rel="noopener noreferrer".
# Kbd
Show a key or shortcut the way a keyboard does: a small, raised cap that reads as
_press this_, not as code.
Save with ⌘S, or open the palette with
CtrlK.
## Usage
Wrap each key in its own `Kbd`; combine several for a chord.
```svelte
Press CtrlK to search.
```
## API reference
The key label to show.
# File Tree
Sketch a folder layout so the reader sees where a file goes. Indentation and
folder icons carry the structure, no ASCII art to keep aligned by hand.
## Usage
Nest `FileTreeItem`s to nest directories; an item with children is a folder
automatically. Pass `folder` to style an empty directory, and `highlight` to
call out the entry a guide is about to touch.
```svelte
```
## API reference
### FileTree
Wraps its `` children; no other props.
### FileTreeItem
File or directory name.
Force folder styling for an empty directory.
Emphasize the entry.
# Props Table
Document a component's props the same way on every page: name, type, and
description in one styled table, so the API reference reads consistently instead
of drifting per author.
Visual intent of the component.
Turns the element into a link.
Prevents interaction.
## Usage
Wrap `Prop` rows in a `PropsTable`. Give each `Prop` a `name`; add `type`,
`default`, or `required` as needed, and put the description in the body.
```svelte
Visual intent of the component.
Turns the element into a link.
```
## API reference
### PropsTable
Optional caption, e.g. the component name when a page documents several.
### Prop
Property name.
Type signature, shown as code.
Default value, shown beside the type.
Marks the property as required.