feat(markdown)!: replace createMarkdownRenderer with renderMd

`renderMd()` renders markdown with the site's markdown-it instance and is
available once VitePress is running - in build hooks, `transformPageData`
and data loaders. It replaces the experimental `createMarkdownRenderer`,
whose arguments were ignored after the first call anyway since the
instance is a singleton. The Node API gets a reference page of its own.

closes #2410

BREAKING CHANGE: `createMarkdownRenderer`, `disposeMdItInstance` and
`mergeMarkdownLocales` are no longer exported. Use `renderMd()` to render
markdown with the site's configuration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat/render-md
Divyansh Singh 1 month ago
parent 2e9d665c6f
commit b583e6ae00

@ -4,6 +4,7 @@ import { MarkdownItAsync } from 'markdown-it-async'
import {
createMarkdownRenderer,
disposeMdItInstance,
renderMd,
type MarkdownOptions
} from 'node/markdown/markdown'
@ -203,4 +204,24 @@ describe('node/markdown/markdown', () => {
)
}
})
describe('renderMd', () => {
test('rejects until the renderer exists', async () => {
disposeMdItInstance()
await expect(renderMd('hi')).rejects.toThrow(
'only available while VitePress is running'
)
})
test('renders with the shared instance', async () => {
disposeMdItInstance()
await createMarkdownRenderer('.', { highlight: (code) => code })
expect(await renderMd('Hello **world**')).toBe(
'<p>Hello <strong>world</strong></p>\n'
)
expect(await renderMd('Hello **world**', { inline: true })).toBe(
'Hello <strong>world</strong>'
)
})
})
})

@ -114,6 +114,7 @@ function sidebarReference(): DefaultTheme.SidebarItem[] {
{ text: 'Frontmatter Config', link: 'frontmatter-config' },
{ text: 'Runtime API', link: 'runtime-api' },
{ text: 'CLI', link: 'cli' },
{ text: 'Node API', link: 'node-api' },
{
text: 'Default Theme',
base: '/reference/default-theme-',

@ -83,6 +83,32 @@ export default {
}
```
## Rendering Markdown
Loaders run in Node.js, so Markdown they produce - fetched from a CMS, generated by a script or read from a file - can be rendered with the same parser and plugins VitePress uses for pages via [`renderMd`](../reference/node-api#rendermd), and displayed with `v-html`:
```js
// posts.data.js
import { renderMd } from 'vitepress'
export default {
async load() {
const posts = await (await fetch('https://cms.example.com/posts')).json()
return Promise.all(
posts.map(async (post) => ({ ...post, html: await renderMd(post.body) }))
)
}
}
```
```md
<script setup>
import { data as posts } from './posts.data.js'
</script>
<article v-for="post in posts" v-html="post.html" />
```
## `createContentLoader`
When building a content focused site, we often need to create an "archive" or "index" page: a page where we list all available entries in our content collection, for example blog posts or API pages. We **can** implement this directly with the data loader API, but since this is such a common use case, VitePress also provides a `createContentLoader` helper to simplify this:

@ -0,0 +1,75 @@
---
description: Reference of the VitePress Node API for building, serving and rendering Markdown programmatically.
---
# Node API
Besides the [CLI](./cli), VitePress can be driven from Node.js. The functions below are exported from `vitepress` for build scripts, tests and the Node-side extension points - [build hooks](./site-config#build-hooks) and [data loaders](../guide/data-loading). They are not available in the browser; see the [Runtime API](./runtime-api) for that.
Other Node exports are documented with the feature they belong to: [`defineConfig`](./site-config#config-intellisense), [`createContentLoader` and `defineLoader`](../guide/data-loading), [`defineRoutes`](../guide/routing#dynamic-routes), [`loadEnv`](../guide/cms) and [`postcssIsolateStyles`](../guide/markdown#raw).
## `build`
- Type: `(root?: string, options?: BuildOptions & { base?: string }) => Promise<void>`
Builds the site, like [`vitepress build`](./cli#vitepress-build). `root` defaults to the current working directory. `options` accepts Vite's [build options](https://vite.dev/config/build-options) (`outDir` is resolved against the current working directory) plus a `base` override.
```ts
import { build } from 'vitepress'
await build('docs', { outDir: 'dist' })
```
## `createServer`
- Type: `(root?: string, options?: ServerOptions & { base?: string }) => Promise<ViteDevServer>`
Creates the dev server behind [`vitepress dev`](./cli#vitepress-dev) without starting it. `options` accepts Vite's [server options](https://vite.dev/config/server-options) plus a `base` override.
```ts
import { createServer } from 'vitepress'
const server = await createServer('docs', { port: 5173 })
await server.listen()
// ...
await server.close()
```
## `serve`
- Type: `(options?: { root?: string; base?: string; port?: number }) => Promise<Polka>`
Serves a built site, like [`vitepress preview`](./cli#vitepress-preview). It resolves once the server is listening (on port `4173` by default) with the underlying [Polka](https://github.com/lukeed/polka) app.
```ts
import { serve } from 'vitepress'
const app = await serve({ root: 'docs', port: 4173 })
// ...
app.server.close()
```
## `renderMd`
- Type: `(src: string, options?: RenderMdOptions) => Promise<string>`
Renders Markdown to HTML with the `markdown-it` instance VitePress uses for pages, so every [Markdown extension](../guide/markdown) and [`markdown`](./site-config#markdown) config option applies. It is available once VitePress is running - in [build hooks](./site-config#build-hooks), [`transformPageData`](./site-config#transformpagedata) and [data loaders](../guide/data-loading#rendering-markdown) - and rejects when called earlier, for example while the config file is evaluated.
```ts
import { renderMd } from 'vitepress'
const html = await renderMd('Hello **world**')
// '<p>Hello <strong>world</strong></p>\n'
const inline = await renderMd('Hello **world**', { inline: true })
// 'Hello <strong>world</strong>'
```
```ts
interface RenderMdOptions extends Partial<MarkdownEnv> {
// render without the wrapping paragraph, like markdown-it's `renderInline`
inline?: boolean
}
```
The other options are the `MarkdownEnv` fields the renderer reads, mainly `path` - the absolute path of the file the Markdown belongs to, used to resolve relative [includes](../guide/markdown#markdown-file-inclusion) and [snippets](../guide/markdown#import-code-snippets) - and `cleanUrls`, which defaults to the [site config](./site-config#cleanurls). The result is plain HTML for `v-html`: Vue components and expressions in it are not processed.

@ -784,3 +784,27 @@ export default {
}
}
```
#### Example: Rendering Markdown in frontmatter
Frontmatter values are plain strings. To write Markdown in them, render the value with [`renderMd`](./node-api#rendermd) and display the result with `v-html`:
```ts
import { renderMd } from 'vitepress'
export default {
async transformPageData(pageData) {
const { intro } = pageData.frontmatter
if (intro) pageData.frontmatter.introHtml = await renderMd(intro)
}
}
```
```md
---
intro: |
Welcome to **our** docs.
---
<div v-html="$frontmatter.introHtml" />
```

@ -1,14 +1,9 @@
import minimist from 'minimist'
import c from 'picocolors'
import { createLogger } from 'vite'
import {
build,
createServer,
disposeMdItInstance,
resolveConfig,
serve
} from '.'
import { build, createServer, resolveConfig, serve } from '.'
import { init } from './init/init'
import { disposeMdItInstance } from './markdown/markdown'
import { clearCache } from './markdownToVue'
import { bindShortcuts } from './shortcuts'
import { logVersion } from './utils/logVersion'

@ -4,7 +4,13 @@ export * from './config'
export * from './contentLoader'
export type { DefaultTheme } from './defaultTheme'
export * from './init/init'
export * from './markdown/markdown'
export {
renderMd,
type MarkdownOptions,
type MarkdownRenderer,
type RenderMdOptions,
type ThemeOptions
} from './markdown/markdown'
export {
defineRoutes,
type ResolvedRouteConfig,
@ -20,6 +26,7 @@ export * from './utils/getGitTimestamp'
export type {
HeadConfig,
Header,
MarkdownEnv,
MarkdownLocaleOptions,
SiteData
} from '../../types/shared'

@ -42,6 +42,7 @@ import type {
Awaitable,
CodeCopyButtonOptions,
LocaleConfig,
MarkdownEnv,
MarkdownLocaleOptions
} from '../shared'
import {
@ -109,8 +110,7 @@ export interface MarkdownOptions extends MarkdownItAsyncOptions {
/**
* Per-locale overrides for build-time markdown strings (container titles
* and the code copy button title), keyed by locale index. Populated
* automatically from `locales.<index>.markdown` in the site config - pass
* directly only when using `createMarkdownRenderer` standalone.
* automatically from `locales.<index>.markdown` in the site config.
*/
locales?: Record<string, MarkdownLocaleOptions>
@ -358,9 +358,33 @@ export function disposeMdItInstance() {
}
}
export interface RenderMdOptions extends Partial<MarkdownEnv> {
/**
* Render without the wrapping paragraph, like markdown-it's `renderInline`.
*/
inline?: boolean
}
/**
* @experimental
* Renders markdown with the site's markdown-it instance. Available once
* VitePress is running - in build hooks, `transformPageData`, data loaders
* and content loaders - and rejects otherwise.
*/
export async function renderMd(
src: string,
{ inline, ...env }: RenderMdOptions = {}
): Promise<string> {
if (!md) {
throw new Error(
'renderMd() is only available while VitePress is running, e.g. inside build hooks or data loaders.'
)
}
env.cleanUrls ??= (global as any).VITEPRESS_CONFIG?.cleanUrls ?? false
return inline ? md.renderInline(src, env) : md.renderAsync(src, env)
}
// shared by everything that renders markdown in a run (pages, the search
// index, content loaders and `renderMd()`), so the first caller's options win
export async function createMarkdownRenderer(
srcDir: string,
options: MarkdownOptions = {},

Loading…
Cancel
Save