Skip to content

Content

Markdown authoring

The prose dialect that markdown() understands: containers, tab groups, fence meta, and diff markers. Zero imports in your content. The compiler injects what a page uses.

On this page

markdown() compiles .svx / .md with an opinionated dialect on top of CommonMark: admonition containers, markdown-native tabs, a fence pipeline with pluggable stages. Authors write zero imports. When a page uses a tab group, the compiler injects the components into the module scope of that page. All content on this page is rendered by the dialect that it documents.

When to use

This page is for people who write content. To wire markdown() into your app, see Content collections. For the code() macro, which renders snippets from components, see code.

Containers

VitePress-compatible admonitions: tip, warning, danger, info, note, caution, important, and the collapsible details. Each takes an optional title on the opening line:

::: warning Heads up
Props must be serializable.
:::

Heads up

Props must be serializable. (This box is the example above, rendered.)

Blank lines inside are optional. VitePress-style tight blocks operate. A literal ::: inside a fenced code block stays untouched. details renders as a native <details> disclosure:

What it expands into

An .og-admonition wrapper that the theme styles. Markdown continues to render inside: code, links, lists, everything.

Tab groups

There are two authoring syntaxes. Both are rewritten to <TabGroup> / <Tab>, with the components auto-injected.

::: code-group: one fence per tab, with the label in [brackets] after the language:

::: code-group

```bash [npm]
npm i ogygia
```

```bash [pnpm]
pnpm add ogygia
```

:::
npm i ogygia
pnpm add ogygia

::: tabs: any markdown per tab, one tab per == Label line:

::: tabs
== On the server
Query the brain in a server load.
== In a worker
Ship the static index; the worker filters by scope.
:::

Groups with the same labels sync: pick pnpm one time, and each install block follows. The choice persists across pages (localStorage). Try it on the group above.

Fence meta

The infostring after the language flows through the fence pipeline (line highlighting, filenames, twoslash). It is the same for a markdown fence and a code() call:

  • ```ts title=src/lib/site.server.ts: a filename in the code-block header, to the left of the copy / permalink actions. This is the common case, and it fills the header so that the header is never empty. When a fence gives no filename, the header falls back to the language name. title= reads to the end of the token. Thus paths with /, ., and +page-style route files operate as-is.
  • ```ts {2-4}: highlight lines 2–4, with transformerMetaHighlight from @shikijs/transformers wired.
  • ```ts twoslash: type-aware hovers, with the twoslash transformer wired.
  • /// file: app.ts on the first line: the same filename, but as a magic comment that is stripped from the rendered code (through the slash_meta() parser). Prefer title=, unless you port content that already uses the comment form.

Diff markers

There are two dialects for “change this code” prose, shipped as fence-pipeline transformers. Line-level: a +++ / --- prefix marks the full line, and the marker never reaches the reader:

```js
--- export let count;
+++ let { count } = $props();
```
export let count;
let { count } = $props();

Inline: +++added+++ / ---removed--- wraps a span inside a line. This is the dialect of svelte.dev, and it composes with twoslash:

```js
let +++doubled = $derived(count * 2)+++;
```
let doubled = $derived(count * 2);

Wire them one time in your app config. They are inert on fences that carry no marker:

import { diff_markers, inline_markers } from 'ogygia/content/markdown';

ogygia({ content: { markdown: { code: { transformers: [diff_markers(), inline_markers()] } } } })

Where each dialect is off

Line markers skip languages where a leading --- is real syntax (yaml, md, diff). Inline markers never treat a --- alone on its line as a delimiter. Thus frontmatter shown inside a fence renders untouched. For those cases, use // [!code ++] comments from @shikijs/transformers.

Extending the pipeline

Each stage above is a value that you pass, not a flag that you set. transformers speak the ecosystem contract of Shiki (@shikijs/transformers, twoslash, the diff markers, your own). meta parsers extend the infostring. variants turn one authored fence into switchable versions (a JS↔TS converter, package-manager tabs). The defaults are on for the prose features (containers, tabs, heading ids + anchors, code ids). Each is one key to set off. The full option list lives in the API reference.

Do / don’t

  • Do author with the dialect. Tabs and admonitions carry structure that plain prose paragraphs lose.
  • Do keep install commands in a code-group. Readers pick their package manager one time. Each block on the page follows.
  • Don’t hand-import TabGroup in a content page. The compiler injects it exactly when the syntax appears. An import of your own only duplicates it.
  • Don’t fake diffs with diff fences when you mean “edit this code”. The diff language loses real syntax highlighting. The markers keep the Shiki colors and strip themselves.