Skip to content

Commit bcce353

Browse files
committed
feat: make Markdown pluggable
1 parent 9b81609 commit bcce353

41 files changed

Lines changed: 1272 additions & 270 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/seven-emus-knock.md

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,74 @@
11
---
2-
'astro': patch
3-
'@astrojs/markdown-remark': patch
4-
'@astrojs/mdx': patch
2+
'astro': major
3+
'@astrojs/markdown-remark': major
4+
'@astrojs/markdown-satteri': major
5+
'@astrojs/mdx': major
56
---
67

7-
temp changeset for preview release
8+
Adds the new `markdown.processor` config option, which uses [satteri](https://github.com/bruits/satteri) (a Rust-based Markdown/MDX compiler) by default. Both `.md` and `.mdx` files are now rendered through satteri unless you opt back into the legacy remark/rehype pipeline with the new `unified()` factory.
9+
10+
The previous `experimental.nativeMarkdown` flag has been removed; the same functionality is now available without an experimental opt-in:
11+
12+
```js
13+
// astro.config.mjs — default, no changes needed
14+
import { defineConfig } from 'astro/config';
15+
16+
export default defineConfig({});
17+
```
18+
19+
To pass satteri plugins or enable additional parser features, use the new `satteri()` factory:
20+
21+
```js
22+
// astro.config.mjs
23+
import { defineConfig, satteri } from 'astro/config';
24+
25+
export default defineConfig({
26+
markdown: {
27+
processor: satteri({
28+
hastPlugins: [myPlugin],
29+
features: { directive: true, definitionList: true },
30+
}),
31+
},
32+
});
33+
```
34+
35+
To keep using your existing remark/rehype plugins, opt into the `unified()` processor from `@astrojs/markdown-remark`:
36+
37+
```js
38+
// astro.config.mjs
39+
import { defineConfig } from 'astro/config';
40+
import { unified } from '@astrojs/markdown-remark';
41+
import remarkToc from 'remark-toc';
42+
43+
export default defineConfig({
44+
markdown: {
45+
processor: unified({ remarkPlugins: [remarkToc] }),
46+
},
47+
});
48+
```
49+
50+
The top-level `markdown.remarkPlugins`, `markdown.rehypePlugins`, and `markdown.remarkRehype` options are deprecated. They continue to work when `@astrojs/markdown-remark` is installed — Astro automatically wraps them in `unified({...})` and prints a deprecation warning — but you should migrate to the new factory form.
51+
52+
The satteri processor (and its shared plugin factories) have been extracted from `@astrojs/markdown-remark` into a new dedicated `@astrojs/markdown-satteri` package, which Astro now ships with by default — no extra install needed.
53+
54+
`@astrojs/markdown-remark` is no longer a transitive dependency of `astro`. If you want the legacy `unified()` pipeline (either directly via `markdown.processor: unified({...})` or indirectly via the deprecated `markdown.remarkPlugins` / `rehypePlugins` / `remarkRehype` fields), install it explicitly:
55+
56+
```sh
57+
pnpm add @astrojs/markdown-remark
58+
```
59+
60+
`@astrojs/mdx` now picks up the processor from `markdown.processor` automatically, so a single satteri or unified configuration drives both `.md` and `.mdx` files. To run `.mdx` files through a different processor (or the same processor with different options) than your `.md` files, pass the `processor` option to the integration:
61+
62+
```js
63+
// astro.config.mjs
64+
import { defineConfig, satteri } from 'astro/config';
65+
import mdx from '@astrojs/mdx';
66+
import { unified } from '@astrojs/markdown-remark';
67+
68+
export default defineConfig({
69+
markdown: { processor: satteri() },
70+
integrations: [mdx({ processor: unified({ remarkPlugins: [/* ... */] }) })],
71+
});
72+
```
73+
74+
Third-party processors can plug into the same pipeline by exporting a factory whose return value implements the `MarkdownProcessorEntry` interface (exported from `astro/markdown`). Implement `createRenderer` for `.md` support, and optionally `createMdxRenderer` to enable `.mdx` rendering.

packages/astro/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
},
3232
"exports": {
3333
".": "./dist/index.js",
34+
"./markdown": "./dist/markdown/index.js",
3435
"./env": "./env.d.ts",
3536
"./env/runtime": "./dist/env/runtime.js",
3637
"./env/setup": "./dist/env/setup.js",
@@ -137,7 +138,7 @@
137138
"dependencies": {
138139
"@astrojs/compiler-rs": "^0.1.10",
139140
"@astrojs/internal-helpers": "workspace:*",
140-
"@astrojs/markdown-remark": "workspace:*",
141+
"@astrojs/markdown-satteri": "workspace:*",
141142
"@astrojs/telemetry": "workspace:*",
142143
"@capsizecss/unpack": "^4.0.0",
143144
"@clack/prompts": "^1.1.0",
@@ -196,6 +197,7 @@
196197
},
197198
"devDependencies": {
198199
"@astrojs/check": "workspace:*",
200+
"@astrojs/markdown-remark": "workspace:*",
199201
"satteri": "^0.2.7",
200202
"@playwright/test": "1.58.2",
201203
"@types/aria-query": "^5.0.4",
@@ -235,6 +237,7 @@
235237
"publishConfig": {
236238
"exports": {
237239
".": "./dist/index.js",
240+
"./markdown": "./dist/markdown/index.js",
238241
"./env": "./env.d.ts",
239242
"./env/runtime": "./dist/env/runtime.js",
240243
"./env/setup": "./dist/env/setup.js",

packages/astro/src/config/entrypoint.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export { defineConfig, getViteConfig } from './index.js';
1414
export { sessionDrivers } from '../core/session/drivers.js';
1515
export { svgoOptimizer } from '../assets/svg/svgo.js';
1616
export { logHandlers } from '../core/logger/handlers.js';
17+
export { satteri } from '@astrojs/markdown-satteri';
1718

1819
/**
1920
* Return the configuration needed to use the Sharp-based image service

packages/astro/src/content/content-layer.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
import { existsSync, promises as fs } from 'node:fs';
2-
import {
3-
createMarkdownProcessor,
4-
parseFrontmatter,
5-
type MarkdownProcessor,
6-
} from '@astrojs/markdown-remark';
2+
import type { MarkdownProcessor } from '@astrojs/markdown-satteri';
3+
import { parseFrontmatter } from '../markdown/frontmatter.js';
74
import PQueue from 'p-queue';
85
import type { FSWatcher } from 'vite';
96
import xxhash from 'xxhash-wasm';
@@ -154,7 +151,16 @@ export class ContentLayer {
154151
content: string,
155152
options?: RenderMarkdownOptions,
156153
): Promise<RenderedContent> {
157-
this.#markdownProcessor ??= await createMarkdownProcessor(this.#settings.config.markdown);
154+
if (!this.#markdownProcessor) {
155+
const { markdown, image } = this.#settings.config;
156+
this.#markdownProcessor = await markdown.processor.createRenderer({
157+
image,
158+
syntaxHighlight: markdown.syntaxHighlight,
159+
shikiConfig: markdown.shikiConfig,
160+
gfm: markdown.gfm,
161+
smartypants: markdown.smartypants,
162+
});
163+
}
158164
const { frontmatter, content: body } = parseFrontmatter(content);
159165
const { code, metadata } = await this.#markdownProcessor.render(body, {
160166
frontmatter,

packages/astro/src/content/data-store.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { MarkdownHeading } from '@astrojs/markdown-remark';
1+
import type { MarkdownHeading } from '@astrojs/markdown-satteri';
22
import * as devalue from 'devalue';
33

44
export interface RenderedContent {

packages/astro/src/content/runtime.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { MarkdownHeading } from '@astrojs/markdown-remark';
1+
import type { MarkdownHeading } from '@astrojs/markdown-satteri';
22
import { escape } from 'html-escaper';
33
import { Traverse } from 'neotraverse/modern';
44
import * as z from 'zod/v4';

packages/astro/src/content/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import fsMod from 'node:fs';
22
import path from 'node:path';
33
import { fileURLToPath, pathToFileURL } from 'node:url';
4-
import { parseFrontmatter } from '@astrojs/markdown-remark';
4+
import { parseFrontmatter } from '../markdown/frontmatter.js';
55
import { slug as githubSlug } from 'github-slugger';
66
import colors from 'piccolore';
77
import type { RunnableDevEnvironment, Rolldown } from 'vite';

packages/astro/src/core/config/schemas/base.ts

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1+
import type { ShikiConfig, Smartypants as _Smartypants } from '@astrojs/markdown-satteri';
2+
import { satteri, satteriMarkdownDefaults, syntaxHighlightDefaults } from '@astrojs/markdown-satteri';
3+
import type { MarkdownProcessorEntry } from '../../../markdown/index.js';
14
import type {
25
RehypePlugin as _RehypePlugin,
36
RemarkPlugin as _RemarkPlugin,
47
RemarkRehype as _RemarkRehype,
5-
Smartypants as _Smartypants,
6-
ShikiConfig,
7-
} from '@astrojs/markdown-remark';
8-
import { markdownConfigDefaults, syntaxHighlightDefaults } from '@astrojs/markdown-remark';
8+
} from '../../../types/public/config.js';
99
import type { OutgoingHttpHeaders } from 'node:http';
1010
import { type BuiltinTheme, bundledThemes } from 'shiki';
1111
import * as z from 'zod/v4';
@@ -88,7 +88,12 @@ export const ASTRO_CONFIG_DEFAULTS = {
8888
allowedHosts: [],
8989
},
9090
integrations: [],
91-
markdown: markdownConfigDefaults,
91+
markdown: {
92+
...satteriMarkdownDefaults,
93+
remarkPlugins: [] as RemarkPlugin[],
94+
rehypePlugins: [] as RehypePlugin[],
95+
remarkRehype: {} as RemarkRehype,
96+
},
9297
vite: {},
9398
legacy: {
9499
collectionsBackwardsCompat: false,
@@ -111,7 +116,6 @@ export const ASTRO_CONFIG_DEFAULTS = {
111116
clientPrerender: false,
112117
contentIntellisense: false,
113118
chromeDevtoolsWorkspace: false,
114-
nativeMarkdown: false,
115119
queuedRendering: {
116120
enabled: false,
117121
},
@@ -426,6 +430,21 @@ export const AstroConfigSchema = z.object({
426430
return val;
427431
})
428432
.prefault(ASTRO_CONFIG_DEFAULTS.markdown.smartypants),
433+
processor: z
434+
.custom<MarkdownProcessorEntry>(
435+
(value): value is MarkdownProcessorEntry =>
436+
typeof value === 'object' &&
437+
value !== null &&
438+
'name' in value &&
439+
typeof value.name === 'string' &&
440+
'createRenderer' in value &&
441+
typeof value.createRenderer === 'function',
442+
{
443+
error:
444+
'markdown.processor must be the return value of a markdown processor factory (e.g. satteri() or unified())',
445+
},
446+
)
447+
.default(() => satteri()),
429448
})
430449
.prefault({}),
431450
vite: z
@@ -551,23 +570,6 @@ export const AstroConfigSchema = z.object({
551570
svgOptimizer: SvgOptimizerSchema.optional(),
552571
cache: CacheSchema.optional(),
553572
routeRules: RouteRulesSchema.optional(),
554-
nativeMarkdown: z
555-
.union([
556-
z.boolean(),
557-
z.object({
558-
mdastPlugins: z
559-
.array(z.custom<import('satteri').MdastPluginDefinition>())
560-
.optional()
561-
.default([]),
562-
hastPlugins: z
563-
.array(z.custom<import('satteri').HastPluginDefinition>())
564-
.optional()
565-
.default([]),
566-
features: z.custom<import('satteri').Features>().optional(),
567-
}),
568-
])
569-
.optional()
570-
.default(ASTRO_CONFIG_DEFAULTS.experimental.nativeMarkdown),
571573
queuedRendering: z
572574
.object({
573575
enabled: z.boolean().optional().prefault(false),

packages/astro/src/core/config/validate.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ export async function validateConfig(
1010
): Promise<AstroConfig> {
1111
const AstroConfigRelativeSchema = createRelativeSchema(cmd, root);
1212

13+
await coerceLegacyMarkdownPlugins(userConfig);
14+
1315
// First-Pass Validation
1416
return await validateConfigRefined(
1517
await AstroConfigRelativeSchema.parseAsync(userConfig, {
@@ -26,6 +28,46 @@ export async function validateConfig(
2628
);
2729
}
2830

31+
/**
32+
* Wraps legacy `markdown.{remark,rehype}Plugins` / `remarkRehype` into a
33+
* `unified({...})` processor (with a warning). Mutates `userConfig` in place.
34+
*/
35+
async function coerceLegacyMarkdownPlugins(userConfig: any): Promise<void> {
36+
const md = userConfig?.markdown;
37+
if (!md || typeof md !== 'object' || Array.isArray(md)) return;
38+
if (md.processor) return;
39+
40+
const hasRemarkPlugins = Array.isArray(md.remarkPlugins) && md.remarkPlugins.length > 0;
41+
const hasRehypePlugins = Array.isArray(md.rehypePlugins) && md.rehypePlugins.length > 0;
42+
const hasRemarkRehype =
43+
md.remarkRehype &&
44+
typeof md.remarkRehype === 'object' &&
45+
!Array.isArray(md.remarkRehype) &&
46+
Object.keys(md.remarkRehype).length > 0;
47+
if (!hasRemarkPlugins && !hasRehypePlugins && !hasRemarkRehype) return;
48+
49+
let unified: typeof import('@astrojs/markdown-remark').unified;
50+
try {
51+
({ unified } = await import('@astrojs/markdown-remark'));
52+
} catch {
53+
throw new Error(
54+
'`markdown.remarkPlugins`, `markdown.rehypePlugins`, and `markdown.remarkRehype` require `@astrojs/markdown-remark`. Install it with:\n pnpm add @astrojs/markdown-remark\n\nThen migrate to the new processor API:\n import { unified } from \'@astrojs/markdown-remark\';\n markdown: { processor: unified({ remarkPlugins: [...] }) }',
55+
);
56+
}
57+
58+
console.warn(
59+
'[astro] `markdown.remarkPlugins`, `markdown.rehypePlugins`, and `markdown.remarkRehype` are deprecated. Use `markdown.processor: unified({...})` from `@astrojs/markdown-remark` instead.',
60+
);
61+
md.processor = unified({
62+
remarkPlugins: md.remarkPlugins ?? [],
63+
rehypePlugins: md.rehypePlugins ?? [],
64+
remarkRehype: md.remarkRehype ?? {},
65+
});
66+
delete md.remarkPlugins;
67+
delete md.rehypePlugins;
68+
delete md.remarkRehype;
69+
}
70+
2971
/**
3072
* Used twice:
3173
* - To validate the user config

packages/astro/src/jsx/rehype.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { RehypePlugin } from '@astrojs/markdown-remark';
2-
import type { RootContent } from 'hast';
1+
import type { RootContent, Root as HastRoot } from 'hast';
2+
import type { Plugin } from 'unified';
33
import type {} from 'mdast-util-mdx';
44
import type {
55
MdxJsxAttribute,
@@ -16,7 +16,7 @@ import type { PluginMetadata } from '../vite-plugin-astro/types.js';
1616

1717
const ClientOnlyPlaceholder = 'astro-client-only';
1818

19-
export const rehypeAnalyzeAstroMetadata: RehypePlugin = () => {
19+
export const rehypeAnalyzeAstroMetadata: Plugin<[], HastRoot> = () => {
2020
return (tree, file) => {
2121
// Initial metadata for this MDX file, it will be mutated as we traverse the tree
2222
const metadata = createDefaultAstroMetadata();

0 commit comments

Comments
 (0)