Skip to content

Content

Markdown authoring

The prose dialect that markdown() understands, covering containers, tab groups, fence meta and diff markers. Content files need no imports, because the compiler injects whatever a page uses.

On this page

markdown() compiles .svx / .md files with an opinionated dialect on top of CommonMark: admonition containers, markdown-native tabs, and a fence pipeline with pluggable stages. Authors write no imports; when a page uses a tab group, for example, the compiler injects the components into that page’s module scope. Everything on this page is rendered by the dialect it documents.

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

Containers

The dialect supports VitePress-compatible admonitions (tip, warning, danger, info, note, caution and important) as well as 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, so VitePress-style tight blocks work, and a literal ::: inside a fenced code block is left alone. details renders as a native <details> disclosure:

What it expands into

An .og-admonition wrapper that the theme styles. Markdown keeps rendering inside it, including code, links and lists.

Tab groups

There are two syntaxes for tabs. Both are rewritten to <TabGroup> / <Tab>, and the components are injected for you.

::: code-group takes 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 takes any markdown per tab, with 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 stay in sync: pick pnpm once and every install block follows, and the choice persists across pages (in localStorage). You can try it on the group above.

Fence meta

The infostring after the language goes through the fence pipeline (line highlighting, filenames, twoslash), and it works the same way for a markdown fence and a code() call:

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

Diff markers

Two dialects for “change this code” prose ship as fence-pipeline transformers. The line-level dialect marks a whole line with a +++ / --- prefix, and the marker never reaches the reader:

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

The inline dialect wraps a span inside a line in +++added+++ / ---removed---. It is the dialect svelte.dev uses, and it works together with twoslash:

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

Wire them once in your app config. They have no effect on fences that carry no marker:

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

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

Where each dialect is off

Line markers skip languages in which a leading --- is real syntax (yaml, md, diff). Inline markers never treat a --- alone on its line as a delimiter, so 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 you pass rather than a flag you set. transformers follow Shiki’s ecosystem contract (@shikijs/transformers, twoslash, the diff markers, or your own), meta parsers extend the infostring, and variants turn one authored fence into switchable versions (such as a JS↔TS converter or package-manager tabs). The prose features (containers, tabs, heading ids and anchors, code ids) are on by default, and each can be turned off with one key. The full option list is in the API reference.

Do / don’t

  • Do write with the dialect, because tabs and admonitions carry structure that plain paragraphs lose.
  • Do keep install commands in a code-group, so readers pick their package manager once and every block on the page follows.
  • Don’t import TabGroup by hand in a content page. The compiler injects it exactly when the syntax appears, and your own import only duplicates it.
  • Don’t fake diffs with diff fences when you mean “edit this code”. The diff language loses real syntax highlighting, while the markers keep the Shiki colours and strip themselves out.