diff --git a/__tests__/unit/node/markdown/markdown.test.ts b/__tests__/unit/node/markdown/markdown.test.ts
index 5db12abd2..6475773f5 100644
--- a/__tests__/unit/node/markdown/markdown.test.ts
+++ b/__tests__/unit/node/markdown/markdown.test.ts
@@ -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(
+ '
Hello world
\n'
+ )
+ expect(await renderMd('Hello **world**', { inline: true })).toBe(
+ 'Hello world'
+ )
+ })
+ })
})
diff --git a/docs/config.ts b/docs/config.ts
index aa7519830..86a8df7e9 100644
--- a/docs/config.ts
+++ b/docs/config.ts
@@ -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-',
diff --git a/docs/en/guide/data-loading.md b/docs/en/guide/data-loading.md
index 1c11e4698..407887f6d 100644
--- a/docs/en/guide/data-loading.md
+++ b/docs/en/guide/data-loading.md
@@ -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
+
+
+
+```
+
## `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:
diff --git a/docs/en/reference/node-api.md b/docs/en/reference/node-api.md
new file mode 100644
index 000000000..5d19ef6c9
--- /dev/null
+++ b/docs/en/reference/node-api.md
@@ -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`
+
+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`
+
+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`
+
+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`
+
+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**')
+// 'Hello world
\n'
+
+const inline = await renderMd('Hello **world**', { inline: true })
+// 'Hello world'
+```
+
+```ts
+interface RenderMdOptions extends Partial {
+ // 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.
diff --git a/docs/en/reference/site-config.md b/docs/en/reference/site-config.md
index d79eb6378..0d3563ccf 100644
--- a/docs/en/reference/site-config.md
+++ b/docs/en/reference/site-config.md
@@ -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.
+---
+
+
+```
diff --git a/src/node/cli.ts b/src/node/cli.ts
index 76da98c4e..59f901e3d 100644
--- a/src/node/cli.ts
+++ b/src/node/cli.ts
@@ -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'
diff --git a/src/node/index.ts b/src/node/index.ts
index a86f49b20..80a5b9160 100644
--- a/src/node/index.ts
+++ b/src/node/index.ts
@@ -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'
diff --git a/src/node/markdown/markdown.ts b/src/node/markdown/markdown.ts
index a5f2e8ae1..5b18d1a8b 100644
--- a/src/node/markdown/markdown.ts
+++ b/src/node/markdown/markdown.ts
@@ -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..markdown` in the site config - pass
- * directly only when using `createMarkdownRenderer` standalone.
+ * automatically from `locales..markdown` in the site config.
*/
locales?: Record
@@ -358,9 +358,33 @@ export function disposeMdItInstance() {
}
}
+export interface RenderMdOptions extends Partial {
+ /**
+ * 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 {
+ 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 = {},