From 00f7df2660adbc29edd57de92d7da63f9ef100c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hu=C3=A1ng=20J=C3=B9nli=C3=A0ng?= Date: Fri, 26 Jun 2026 09:50:32 -0400 Subject: [PATCH 1/4] feat: onUnusedMarkdownDirectives markdown hooks --- .../docusaurus-mdx-loader/src/processor.ts | 11 +++- .../unusedDirectives/__tests__/index.test.ts | 4 +- .../src/remark/unusedDirectives/index.ts | 63 +++++++++++++------ packages/docusaurus-types/src/index.d.ts | 1 + packages/docusaurus-types/src/markdown.d.ts | 20 ++++++ 5 files changed, 76 insertions(+), 23 deletions(-) diff --git a/packages/docusaurus-mdx-loader/src/processor.ts b/packages/docusaurus-mdx-loader/src/processor.ts index f5120150eeaf..0035ab66c799 100644 --- a/packages/docusaurus-mdx-loader/src/processor.ts +++ b/packages/docusaurus-mdx-loader/src/processor.ts @@ -15,7 +15,7 @@ import details from './remark/details'; import head from './remark/head'; import mermaid from './remark/mermaid'; import transformAdmonitions from './remark/admonitions'; -import unusedDirectivesWarning from './remark/unusedDirectives'; +import unusedDirectives from './remark/unusedDirectives'; import codeCompatPlugin from './remark/mdx1Compat/codeCompatPlugin'; import {getFormat} from './format'; import type {WebpackCompilerName} from '@docusaurus/utils'; @@ -25,6 +25,7 @@ import type {AdmonitionOptions} from './remark/admonitions'; import type {PluginOptions as ResolveMarkdownLinksOptions} from './remark/resolveMarkdownLinks'; import type {PluginOptions as TransformLinksOptions} from './remark/transformLinks'; import type {PluginOptions as TransformImageOptions} from './remark/transformImage'; +import type {PluginOptions as UnusedDirectivesOptions} from './remark/unusedDirectives'; import type {ProcessorOptions} from '@mdx-js/mdx'; // TODO as of April 2023, no way to import/re-export this ESM type easily :/ @@ -151,7 +152,13 @@ async function createProcessorFactory() { gfm, options.markdownConfig.mdx1Compat.comments ? comment : null, ...(options.remarkPlugins ?? []), - unusedDirectivesWarning, + [ + unusedDirectives, + { + onUnusedMarkdownDirectives: + options.markdownConfig.hooks.onUnusedMarkdownDirectives, + } satisfies UnusedDirectivesOptions, + ], ].filter((plugin): plugin is MDXPlugin => Boolean(plugin)); // codeCompatPlugin needs to be applied last after user-provided plugins diff --git a/packages/docusaurus-mdx-loader/src/remark/unusedDirectives/__tests__/index.test.ts b/packages/docusaurus-mdx-loader/src/remark/unusedDirectives/__tests__/index.test.ts index e6eb8a8e3c43..ae6692bfc92a 100644 --- a/packages/docusaurus-mdx-loader/src/remark/unusedDirectives/__tests__/index.test.ts +++ b/packages/docusaurus-mdx-loader/src/remark/unusedDirectives/__tests__/index.test.ts @@ -28,7 +28,9 @@ const processFixture = async ( const result = await remark() .use(directives) .use(admonition) - .use(plugin) + .use(plugin, { + onUnusedMarkdownDirectives: 'warn', + }) .use(remark2rehype) .use(stringify) .process(file); diff --git a/packages/docusaurus-mdx-loader/src/remark/unusedDirectives/index.ts b/packages/docusaurus-mdx-loader/src/remark/unusedDirectives/index.ts index 29b458b19ce9..337a53cce590 100644 --- a/packages/docusaurus-mdx-loader/src/remark/unusedDirectives/index.ts +++ b/packages/docusaurus-mdx-loader/src/remark/unusedDirectives/index.ts @@ -7,12 +7,16 @@ import path from 'path'; import process from 'process'; import logger from '@docusaurus/logger'; -import {posixPath} from '@docusaurus/utils'; +import {toMessageRelativeFilePath, posixPath} from '@docusaurus/utils'; import {formatNodePositionExtraMessage, transformNode} from '../utils'; import type {Root} from 'mdast'; import type {Parent} from 'unist'; import type {Transformer, Processor, Plugin} from 'unified'; import type {Directives, TextDirective} from 'mdast-util-directive'; +import type { + MarkdownConfig, + OnUnusedMarkdownDirectivesFunction, +} from '@docusaurus/types'; type DirectiveType = Directives['type']; @@ -64,19 +68,33 @@ ${warningMessages} Your content might render in an unexpected way. Visit ${customSupportUrl} to find out why and how to fix it.`; } -function logUnusedDirectivesWarning({ - directives, - filePath, -}: { - directives: Directives[]; - filePath: string; -}) { - if (directives.length > 0) { - const message = formatUnusedDirectivesMessage({ - directives, - filePath, - }); - logger.warn(message); +export type PluginOptions = { + onUnusedMarkdownDirectives: MarkdownConfig['hooks']['onUnusedMarkdownDirectives']; +}; + +function asFunction( + onUnusedMarkdownDirectives: PluginOptions['onUnusedMarkdownDirectives'], +): OnUnusedMarkdownDirectivesFunction { + if (typeof onUnusedMarkdownDirectives === 'string') { + const extraHelp = + onUnusedMarkdownDirectives === 'throw' + ? logger.interpolate`\nTo ignore this error, use the code=${'siteConfig.markdown.hooks.onUnusedMarkdownDirectives'} option.` + : ''; + return ({sourceFilePath, directives}) => { + const relativePath = toMessageRelativeFilePath(sourceFilePath); + logger.report(onUnusedMarkdownDirectives)`${formatUnusedDirectivesMessage( + { + directives, + filePath: relativePath, + }, + )}${extraHelp}`; + }; + } else { + return (params) => + onUnusedMarkdownDirectives({ + ...params, + sourceFilePath: toMessageRelativeFilePath(params.sourceFilePath), + }); } } @@ -112,9 +130,14 @@ function isUnusedDirective(directive: Directives) { return !directive.data; } -const plugin: Plugin = function plugin( +const plugin: Plugin = function plugin( this: Processor, + options, ): Transformer { + const onUnusedMarkdownDirectives = asFunction( + options.onUnusedMarkdownDirectives, + ); + return async (tree, file) => { const {visit} = await import('unist-util-visit'); @@ -133,14 +156,14 @@ const plugin: Plugin = function plugin( } }); - // We only enable these warnings for the client compiler - // This avoids emitting duplicate warnings in prod mode + // We only process unused directives for the client compiler + // This avoids emitting duplicate errors/warnings in prod mode // Note: the client compiler is used in both dev/prod modes // Also: the client compiler is what gets used when using crossCompilerCache - if (file.data.compilerName === 'client') { - logUnusedDirectivesWarning({ + if (file.data.compilerName === 'client' && unusedDirectives.length > 0) { + onUnusedMarkdownDirectives({ + sourceFilePath: file.path!, directives: unusedDirectives, - filePath: file.path, }); } }; diff --git a/packages/docusaurus-types/src/index.d.ts b/packages/docusaurus-types/src/index.d.ts index 6dfca63d1f70..6cdd2f839bb1 100644 --- a/packages/docusaurus-types/src/index.d.ts +++ b/packages/docusaurus-types/src/index.d.ts @@ -28,6 +28,7 @@ export { ParseFrontMatter, OnBrokenMarkdownLinksFunction, OnBrokenMarkdownImagesFunction, + OnUnusedMarkdownDirectivesFunction, } from './markdown'; export {ReportingSeverity} from './reporting'; diff --git a/packages/docusaurus-types/src/markdown.d.ts b/packages/docusaurus-types/src/markdown.d.ts index e7904f46bcc1..7c2d84cd3fdf 100644 --- a/packages/docusaurus-types/src/markdown.d.ts +++ b/packages/docusaurus-types/src/markdown.d.ts @@ -7,6 +7,7 @@ import type {ProcessorOptions} from '@mdx-js/mdx'; import type {Image, Definition, Link} from 'mdast'; +import type {Directives} from 'mdast-util-directive'; import type {ReportingSeverity} from './reporting'; @@ -86,6 +87,21 @@ export type OnBrokenMarkdownImagesFunction = (params: { node: Image; }) => void | string; +export type OnUnusedMarkdownDirectivesFunction = (params: { + /** + * Path of the source file on which the unused directive was found + * Relative to the site dir. + * Example: "docs/category/myDoc.mdx" + */ + sourceFilePath: string; + + /** + * The Markdown directives that were unused. + * Example: "myDirective" + */ + directives: Directives[]; +}) => void | string; + export type MarkdownHooks = { /** * The behavior of Docusaurus when it detects any broken Markdown link. @@ -97,6 +113,10 @@ export type MarkdownHooks = { onBrokenMarkdownLinks: ReportingSeverity | OnBrokenMarkdownLinksFunction; onBrokenMarkdownImages: ReportingSeverity | OnBrokenMarkdownImagesFunction; + + onUnusedMarkdownDirectives: + | ReportingSeverity + | OnUnusedMarkdownDirectivesFunction; }; export type MarkdownConfig = { From c80686d78824076e576cde06384a2007f926b2e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hu=C3=A1ng=20J=C3=B9nli=C3=A0ng?= Date: Fri, 26 Jun 2026 10:22:38 -0400 Subject: [PATCH 2/4] test: add new tests --- .../__snapshots__/index.test.ts.snap | 54 ++++ .../unusedDirectives/__tests__/index.test.ts | 274 ++++++++++++++++-- 2 files changed, 308 insertions(+), 20 deletions(-) diff --git a/packages/docusaurus-mdx-loader/src/remark/unusedDirectives/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus-mdx-loader/src/remark/unusedDirectives/__tests__/__snapshots__/index.test.ts.snap index 5735ead56a93..9d8cd750bfe0 100644 --- a/packages/docusaurus-mdx-loader/src/remark/unusedDirectives/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/docusaurus-mdx-loader/src/remark/unusedDirectives/__tests__/__snapshots__/index.test.ts.snap @@ -58,6 +58,60 @@ exports[`directives remark plugin - client compiler > default behavior for text " `; +exports[`directives remark plugin - client compiler > onUnusedMarkdownDirectives > function form > if file contains unused container directive > result 1`] = ` +"

Take care of snowstorms...

+
+

:::NotAContainerDirective with a phrase after

+

:::

+

Phrase before :::NotAContainerDirective

+

:::

" +`; + +exports[`directives remark plugin - client compiler > onUnusedMarkdownDirectives > function form > if file contains unused leaf directive > result 1`] = ` +"
+

Leaf directive in a phrase ::NotALeafDirective

+

::NotALeafDirective with a phrase after

" +`; + +exports[`directives remark plugin - client compiler > onUnusedMarkdownDirectives > function form > if file contains unused text directive > result 1`] = ` +"

Simple: textDirective1

+
Simple: textDirectiveCode
+
+

Simple:textDirective2

+

Simple

+

Simple

+

Simple:textDirective5

+
Simple:textDirectiveCode
+
" +`; + +exports[`directives remark plugin - client compiler > onUnusedMarkdownDirectives > ignore > if file contains unused container directive > result 1`] = ` +"

Take care of snowstorms...

+

unused directive content

+

:::NotAContainerDirective with a phrase after

+

:::

+

Phrase before :::NotAContainerDirective

+

:::

" +`; + +exports[`directives remark plugin - client compiler > onUnusedMarkdownDirectives > ignore > if file contains unused leaf directive > result 1`] = ` +"
+

Leaf directive in a phrase ::NotALeafDirective

+

::NotALeafDirective with a phrase after

" +`; + +exports[`directives remark plugin - client compiler > onUnusedMarkdownDirectives > ignore > if file contains unused text directive > result 1`] = ` +"

Simple: textDirective1

+
Simple: textDirectiveCode
+
+

Simple:textDirective2

+

Simple

label

+

Simple

+

Simple:textDirective5

+
Simple:textDirectiveCode
+
" +`; + exports[`directives remark plugin - server compiler > default behavior for container directives > result 1`] = ` "

Take care of snowstorms...

unused directive content

diff --git a/packages/docusaurus-mdx-loader/src/remark/unusedDirectives/__tests__/index.test.ts b/packages/docusaurus-mdx-loader/src/remark/unusedDirectives/__tests__/index.test.ts index ae6692bfc92a..b67f8356b78c 100644 --- a/packages/docusaurus-mdx-loader/src/remark/unusedDirectives/__tests__/index.test.ts +++ b/packages/docusaurus-mdx-loader/src/remark/unusedDirectives/__tests__/index.test.ts @@ -10,40 +10,47 @@ import path from 'path'; import remark2rehype from 'remark-rehype'; import stringify from 'rehype-stringify'; import vfile from 'to-vfile'; -import plugin from '../index'; +import plugin, {type PluginOptions} from '../index'; import admonition from '../../admonitions'; import type {WebpackCompilerName} from '@docusaurus/utils'; -const processFixture = async ( - name: string, - {compilerName}: {compilerName: WebpackCompilerName}, -) => { +const getProcessor = async (options?: Partial) => { const {remark} = await import('remark'); const {default: directives} = await import('remark-directive'); - const filePath = path.join(__dirname, '__fixtures__', `${name}.md`); - const file = await vfile.read(filePath); - file.data.compilerName = compilerName; - - const result = await remark() + return remark() .use(directives) .use(admonition) .use(plugin, { onUnusedMarkdownDirectives: 'warn', + ...options, }) .use(remark2rehype) - .use(stringify) - .process(file); + .use(stringify); +}; + +const processFixture = async ( + name: string, + {compilerName}: {compilerName: WebpackCompilerName}, + options?: Partial, +) => { + const processor = await getProcessor(options); + + const filePath = path.join(__dirname, '__fixtures__', `${name}.md`); + const file = await vfile.read(filePath); + file.data.compilerName = compilerName; + + const result = await processor.process(file); return result.value; }; describe('directives remark plugin - client compiler', () => { - const options = {compilerName: 'client'} as const; + const fileData = {compilerName: 'client'} as const; it('default behavior for container directives', async () => { using warn = vi.spyOn(console, 'warn'); - const result = await processFixture('containerDirectives', options); + const result = await processFixture('containerDirectives', fileData); expect(result).toMatchSnapshot('result'); expect(warn).toHaveBeenCalledTimes(1); expect(warn.mock.calls).toMatchSnapshot('console'); @@ -51,7 +58,7 @@ describe('directives remark plugin - client compiler', () => { it('default behavior for leaf directives', async () => { using warn = vi.spyOn(console, 'warn'); - const result = await processFixture('leafDirectives', options); + const result = await processFixture('leafDirectives', fileData); expect(result).toMatchSnapshot('result'); expect(warn).toHaveBeenCalledTimes(1); expect(warn.mock.calls).toMatchSnapshot('console'); @@ -59,33 +66,260 @@ describe('directives remark plugin - client compiler', () => { it('default behavior for text directives', async () => { using warn = vi.spyOn(console, 'warn'); - const result = await processFixture('textDirectives', options); + const result = await processFixture('textDirectives', fileData); expect(result).toMatchSnapshot('result'); expect(warn).toHaveBeenCalledTimes(1); expect(warn.mock.calls).toMatchSnapshot('console'); }); + + describe('onUnusedMarkdownDirectives', () => { + describe('throws', () => { + const options = {onUnusedMarkdownDirectives: 'throw'} as const; + it('if file contains unused container directive', async () => { + await expect(processFixture('containerDirectives', fileData, options)) + .rejects.toThrowErrorMatchingInlineSnapshot(` + [Error: Docusaurus found 1 unused Markdown directives in file "packages/docusaurus-mdx-loader/src/remark/unusedDirectives/__tests__/__fixtures__/containerDirectives.md" + - :::unusedDirective (7:1) + Your content might render in an unexpected way. Visit https://github.com/facebook/docusaurus/pull/9394 to find out why and how to fix it. + To ignore this error, use the \`siteConfig.markdown.hooks.onUnusedMarkdownDirectives\` option.] + `); + }); + it('if file contains unused leaf directive', async () => { + await expect(processFixture('leafDirectives', fileData, options)) + .rejects.toThrowErrorMatchingInlineSnapshot(` + [Error: Docusaurus found 1 unused Markdown directives in file "packages/docusaurus-mdx-loader/src/remark/unusedDirectives/__tests__/__fixtures__/leafDirectives.md" + - ::unusedLeafDirective (1:1) + Your content might render in an unexpected way. Visit https://github.com/facebook/docusaurus/pull/9394 to find out why and how to fix it. + To ignore this error, use the \`siteConfig.markdown.hooks.onUnusedMarkdownDirectives\` option.] + `); + }); + it('if file contains unused text directive', async () => { + await expect(processFixture('textDirectives', fileData, options)) + .rejects.toThrowErrorMatchingInlineSnapshot(` + [Error: Docusaurus found 2 unused Markdown directives in file "packages/docusaurus-mdx-loader/src/remark/unusedDirectives/__tests__/__fixtures__/textDirectives.md" + - :textDirective3 (9:7) + - :textDirective4 (11:7) + Your content might render in an unexpected way. Visit https://github.com/facebook/docusaurus/pull/9394 to find out why and how to fix it. + To ignore this error, use the \`siteConfig.markdown.hooks.onUnusedMarkdownDirectives\` option.] + `); + }); + }); + + describe('function form', () => { + const options: PluginOptions = { + onUnusedMarkdownDirectives: (params) => { + console.log('onUnusedMarkdownDirectives called with', params); + // We can alter the AST Node + params.directives.forEach((directive) => { + directive.name = `fixed-${directive.name}`; + directive.children = []; + }); + }, + }; + it('if file contains unused container directive', async () => { + using log = vi.spyOn(console, 'log'); + const result = await processFixture( + 'containerDirectives', + fileData, + options, + ); + expect(result).toMatchSnapshot('result'); + expect(log).toHaveBeenCalledTimes(1); + expect(log.mock.calls).toMatchInlineSnapshot(` + [ + [ + "onUnusedMarkdownDirectives called with", + { + "directives": [ + { + "attributes": {}, + "children": [], + "name": "fixed-unusedDirective", + "position": { + "end": { + "column": 4, + "line": 11, + "offset": 93, + }, + "start": { + "column": 1, + "line": 7, + "offset": 44, + }, + }, + "type": "containerDirective", + }, + ], + "sourceFilePath": "packages/docusaurus-mdx-loader/src/remark/unusedDirectives/__tests__/__fixtures__/containerDirectives.md", + }, + ], + ] + `); + }); + it('if file contains unused leaf directive', async () => { + using log = vi.spyOn(console, 'log'); + const result = await processFixture( + 'leafDirectives', + fileData, + options, + ); + expect(result).toMatchSnapshot('result'); + expect(log).toHaveBeenCalledTimes(1); + expect(log.mock.calls).toMatchInlineSnapshot(` + [ + [ + "onUnusedMarkdownDirectives called with", + { + "directives": [ + { + "attributes": {}, + "children": [], + "name": "fixed-unusedLeafDirective", + "position": { + "end": { + "column": 22, + "line": 1, + "offset": 21, + }, + "start": { + "column": 1, + "line": 1, + "offset": 0, + }, + }, + "type": "leafDirective", + }, + ], + "sourceFilePath": "packages/docusaurus-mdx-loader/src/remark/unusedDirectives/__tests__/__fixtures__/leafDirectives.md", + }, + ], + ] + `); + }); + it('if file contains unused text directive', async () => { + using log = vi.spyOn(console, 'log'); + const result = await processFixture( + 'textDirectives', + fileData, + options, + ); + expect(result).toMatchSnapshot('result'); + expect(log).toHaveBeenCalledTimes(1); + expect(log.mock.calls).toMatchInlineSnapshot(` + [ + [ + "onUnusedMarkdownDirectives called with", + { + "directives": [ + { + "attributes": {}, + "children": [], + "name": "fixed-textDirective3", + "position": { + "end": { + "column": 29, + "line": 9, + "offset": 112, + }, + "start": { + "column": 7, + "line": 9, + "offset": 90, + }, + }, + "type": "textDirective", + }, + { + "attributes": { + "age": "42", + }, + "children": [], + "name": "fixed-textDirective4", + "position": { + "end": { + "column": 30, + "line": 11, + "offset": 143, + }, + "start": { + "column": 7, + "line": 11, + "offset": 120, + }, + }, + "type": "textDirective", + }, + ], + "sourceFilePath": "packages/docusaurus-mdx-loader/src/remark/unusedDirectives/__tests__/__fixtures__/textDirectives.md", + }, + ], + ] + `); + }); + }); + + describe('ignore', () => { + const options = {onUnusedMarkdownDirectives: 'ignore'} as const; + it('if file contains unused container directive', async () => { + using warn = vi.spyOn(console, 'warn'); + using log = vi.spyOn(console, 'log'); + const result = await processFixture( + 'containerDirectives', + fileData, + options, + ); + expect(result).toMatchSnapshot('result'); + expect(log).toHaveBeenCalledTimes(0); + expect(warn).toHaveBeenCalledTimes(0); + }); + it('if file contains unused leaf directive', async () => { + using warn = vi.spyOn(console, 'warn'); + using log = vi.spyOn(console, 'log'); + const result = await processFixture( + 'leafDirectives', + fileData, + options, + ); + expect(result).toMatchSnapshot('result'); + expect(log).toHaveBeenCalledTimes(0); + expect(warn).toHaveBeenCalledTimes(0); + }); + it('if file contains unused text directive', async () => { + using warn = vi.spyOn(console, 'warn'); + using log = vi.spyOn(console, 'log'); + const result = await processFixture( + 'textDirectives', + fileData, + options, + ); + expect(result).toMatchSnapshot('result'); + expect(log).toHaveBeenCalledTimes(0); + expect(warn).toHaveBeenCalledTimes(0); + }); + }); + }); }); describe('directives remark plugin - server compiler', () => { - const options = {compilerName: 'server'} as const; + const fileData = {compilerName: 'server'} as const; it('default behavior for container directives', async () => { using warn = vi.spyOn(console, 'warn'); - const result = await processFixture('containerDirectives', options); + const result = await processFixture('containerDirectives', fileData); expect(result).toMatchSnapshot('result'); expect(warn).toHaveBeenCalledTimes(0); }); it('default behavior for leaf directives', async () => { using warn = vi.spyOn(console, 'warn'); - const result = await processFixture('leafDirectives', options); + const result = await processFixture('leafDirectives', fileData); expect(result).toMatchSnapshot('result'); expect(warn).toHaveBeenCalledTimes(0); }); it('default behavior for text directives', async () => { using warn = vi.spyOn(console, 'warn'); - const result = await processFixture('textDirectives', options); + const result = await processFixture('textDirectives', fileData); expect(result).toMatchSnapshot('result'); expect(warn).toHaveBeenCalledTimes(0); }); From 6e6edc1e0677c52af7b6bcf6273deecb1e047e89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hu=C3=A1ng=20J=C3=B9nli=C3=A0ng?= Date: Fri, 26 Jun 2026 10:33:25 -0400 Subject: [PATCH 3/4] feat: pass onUnusedMarkdownDirectives from config --- .../__snapshots__/config.test.ts.snap | 10 +++++ .../__tests__/__snapshots__/site.test.ts.snap | 12 ++++++ .../server/__tests__/configValidation.test.ts | 43 +++++++++++++++++++ .../docusaurus/src/server/configValidation.ts | 7 +++ 4 files changed, 72 insertions(+) diff --git a/packages/docusaurus/src/server/__tests__/__snapshots__/config.test.ts.snap b/packages/docusaurus/src/server/__tests__/__snapshots__/config.test.ts.snap index 229eb8ecc9d4..91cd600f31af 100644 --- a/packages/docusaurus/src/server/__tests__/__snapshots__/config.test.ts.snap +++ b/packages/docusaurus/src/server/__tests__/__snapshots__/config.test.ts.snap @@ -50,6 +50,7 @@ exports[`loadSiteConfig > website with .cjs siteConfig 1`] = ` "hooks": { "onBrokenMarkdownImages": "throw", "onBrokenMarkdownLinks": "warn", + "onUnusedMarkdownDirectives": "warn", }, "mdx1Compat": { "admonitions": true, @@ -137,6 +138,7 @@ exports[`loadSiteConfig > website with ts + js config 1`] = ` "hooks": { "onBrokenMarkdownImages": "throw", "onBrokenMarkdownLinks": "warn", + "onUnusedMarkdownDirectives": "warn", }, "mdx1Compat": { "admonitions": true, @@ -224,6 +226,7 @@ exports[`loadSiteConfig > website with valid JS CJS config 1`] = ` "hooks": { "onBrokenMarkdownImages": "throw", "onBrokenMarkdownLinks": "warn", + "onUnusedMarkdownDirectives": "warn", }, "mdx1Compat": { "admonitions": true, @@ -311,6 +314,7 @@ exports[`loadSiteConfig > website with valid JS ESM config 1`] = ` "hooks": { "onBrokenMarkdownImages": "throw", "onBrokenMarkdownLinks": "warn", + "onUnusedMarkdownDirectives": "warn", }, "mdx1Compat": { "admonitions": true, @@ -398,6 +402,7 @@ exports[`loadSiteConfig > website with valid TypeScript CJS config 1`] = ` "hooks": { "onBrokenMarkdownImages": "throw", "onBrokenMarkdownLinks": "warn", + "onUnusedMarkdownDirectives": "warn", }, "mdx1Compat": { "admonitions": true, @@ -485,6 +490,7 @@ exports[`loadSiteConfig > website with valid TypeScript ESM config 1`] = ` "hooks": { "onBrokenMarkdownImages": "throw", "onBrokenMarkdownLinks": "warn", + "onUnusedMarkdownDirectives": "warn", }, "mdx1Compat": { "admonitions": true, @@ -572,6 +578,7 @@ exports[`loadSiteConfig > website with valid async config 1`] = ` "hooks": { "onBrokenMarkdownImages": "throw", "onBrokenMarkdownLinks": "warn", + "onUnusedMarkdownDirectives": "warn", }, "mdx1Compat": { "admonitions": true, @@ -661,6 +668,7 @@ exports[`loadSiteConfig > website with valid async config creator function 1`] = "hooks": { "onBrokenMarkdownImages": "throw", "onBrokenMarkdownLinks": "warn", + "onUnusedMarkdownDirectives": "warn", }, "mdx1Compat": { "admonitions": true, @@ -750,6 +758,7 @@ exports[`loadSiteConfig > website with valid config creator function 1`] = ` "hooks": { "onBrokenMarkdownImages": "throw", "onBrokenMarkdownLinks": "warn", + "onUnusedMarkdownDirectives": "warn", }, "mdx1Compat": { "admonitions": true, @@ -842,6 +851,7 @@ exports[`loadSiteConfig > website with valid siteConfig 1`] = ` "hooks": { "onBrokenMarkdownImages": "throw", "onBrokenMarkdownLinks": "warn", + "onUnusedMarkdownDirectives": "warn", }, "mdx1Compat": { "admonitions": true, diff --git a/packages/docusaurus/src/server/__tests__/__snapshots__/site.test.ts.snap b/packages/docusaurus/src/server/__tests__/__snapshots__/site.test.ts.snap index 0cda8a64877a..f2859430fdc3 100644 --- a/packages/docusaurus/src/server/__tests__/__snapshots__/site.test.ts.snap +++ b/packages/docusaurus/src/server/__tests__/__snapshots__/site.test.ts.snap @@ -138,6 +138,7 @@ exports[`loadSite > custom-i18n-site > loads site 1`] = ` "hooks": { "onBrokenMarkdownImages": "throw", "onBrokenMarkdownLinks": "warn", + "onUnusedMarkdownDirectives": "warn", }, "mdx1Compat": { "admonitions": true, @@ -305,6 +306,7 @@ exports[`loadSite > simple-site-with-baseUrl > loads site - custom config 1`] = "hooks": { "onBrokenMarkdownImages": "throw", "onBrokenMarkdownLinks": "warn", + "onUnusedMarkdownDirectives": "warn", }, "mdx1Compat": { "admonitions": true, @@ -472,6 +474,7 @@ exports[`loadSite > simple-site-with-baseUrl > loads site - custom outDir 1`] = "hooks": { "onBrokenMarkdownImages": "throw", "onBrokenMarkdownLinks": "warn", + "onUnusedMarkdownDirectives": "warn", }, "mdx1Compat": { "admonitions": true, @@ -639,6 +642,7 @@ exports[`loadSite > simple-site-with-baseUrl > loads site 1`] = ` "hooks": { "onBrokenMarkdownImages": "throw", "onBrokenMarkdownLinks": "warn", + "onUnusedMarkdownDirectives": "warn", }, "mdx1Compat": { "admonitions": true, @@ -872,6 +876,7 @@ exports[`loadSite > simple-site-with-baseUrl-i18n > loads site - locale fr + cu "hooks": { "onBrokenMarkdownImages": "throw", "onBrokenMarkdownLinks": "warn", + "onUnusedMarkdownDirectives": "warn", }, "mdx1Compat": { "admonitions": true, @@ -1105,6 +1110,7 @@ exports[`loadSite > simple-site-with-baseUrl-i18n > loads site - custom outDir 1 "hooks": { "onBrokenMarkdownImages": "throw", "onBrokenMarkdownLinks": "warn", + "onUnusedMarkdownDirectives": "warn", }, "mdx1Compat": { "admonitions": true, @@ -1338,6 +1344,7 @@ exports[`loadSite > simple-site-with-baseUrl-i18n > loads site - locale de 1`] = "hooks": { "onBrokenMarkdownImages": "throw", "onBrokenMarkdownLinks": "warn", + "onUnusedMarkdownDirectives": "warn", }, "mdx1Compat": { "admonitions": true, @@ -1571,6 +1578,7 @@ exports[`loadSite > simple-site-with-baseUrl-i18n > loads site - locale en 1`] = "hooks": { "onBrokenMarkdownImages": "throw", "onBrokenMarkdownLinks": "warn", + "onUnusedMarkdownDirectives": "warn", }, "mdx1Compat": { "admonitions": true, @@ -1804,6 +1812,7 @@ exports[`loadSite > simple-site-with-baseUrl-i18n > loads site - locale es 1`] = "hooks": { "onBrokenMarkdownImages": "throw", "onBrokenMarkdownLinks": "warn", + "onUnusedMarkdownDirectives": "warn", }, "mdx1Compat": { "admonitions": true, @@ -2037,6 +2046,7 @@ exports[`loadSite > simple-site-with-baseUrl-i18n > loads site - locale fr 1`] = "hooks": { "onBrokenMarkdownImages": "throw", "onBrokenMarkdownLinks": "warn", + "onUnusedMarkdownDirectives": "warn", }, "mdx1Compat": { "admonitions": true, @@ -2270,6 +2280,7 @@ exports[`loadSite > simple-site-with-baseUrl-i18n > loads site - locale it 1`] = "hooks": { "onBrokenMarkdownImages": "throw", "onBrokenMarkdownLinks": "warn", + "onUnusedMarkdownDirectives": "warn", }, "mdx1Compat": { "admonitions": true, @@ -2503,6 +2514,7 @@ exports[`loadSite > simple-site-with-baseUrl-i18n > loads site 1`] = ` "hooks": { "onBrokenMarkdownImages": "throw", "onBrokenMarkdownLinks": "warn", + "onUnusedMarkdownDirectives": "warn", }, "mdx1Compat": { "admonitions": true, diff --git a/packages/docusaurus/src/server/__tests__/configValidation.test.ts b/packages/docusaurus/src/server/__tests__/configValidation.test.ts index 63dce02913f5..8f4bacaad688 100644 --- a/packages/docusaurus/src/server/__tests__/configValidation.test.ts +++ b/packages/docusaurus/src/server/__tests__/configValidation.test.ts @@ -130,6 +130,7 @@ describe('normalizeConfig', () => { hooks: { onBrokenMarkdownLinks: 'log', onBrokenMarkdownImages: 'log', + onUnusedMarkdownDirectives: 'log', }, }, }; @@ -546,6 +547,7 @@ describe('markdown', () => { hooks: { onBrokenMarkdownLinks: 'log', onBrokenMarkdownImages: 'warn', + onUnusedMarkdownDirectives: 'warn', }, }; expect(normalizeMarkdown(markdown)).toEqual(markdown); @@ -818,6 +820,47 @@ describe('markdown', () => { `); }); }); + + describe('onUnusedMarkdownDirectives', () => { + function normalizeValue( + onUnusedMarkdownDirectives?: MarkdownHooks['onUnusedMarkdownDirectives'], + ) { + return normalizeHooks({ + onUnusedMarkdownDirectives, + }).onUnusedMarkdownDirectives; + } + + it('accepts undefined', () => { + expect(normalizeValue(undefined)).toBe('warn'); + }); + + it('accepts severity level', () => { + expect(normalizeValue('log')).toBe('log'); + }); + + it('rejects number', () => { + expect(() => + normalizeValue( + // @ts-expect-error: bad value + 42, + ), + ).toThrowErrorMatchingInlineSnapshot(` + [Error: "markdown.hooks.onUnusedMarkdownDirectives" does not match any of the allowed types + ] + `); + }); + + it('accepts function', () => { + expect(normalizeValue(() => {})).toBeInstanceOf(Function); + }); + + it('rejects null', () => { + expect(() => normalizeValue(null)).toThrowErrorMatchingInlineSnapshot(` + [Error: "markdown.hooks.onUnusedMarkdownDirectives" does not match any of the allowed types + ] + `); + }); + }); }); }); diff --git a/packages/docusaurus/src/server/configValidation.ts b/packages/docusaurus/src/server/configValidation.ts index 462e5b2274d6..a108818d3b33 100644 --- a/packages/docusaurus/src/server/configValidation.ts +++ b/packages/docusaurus/src/server/configValidation.ts @@ -123,6 +123,7 @@ export const DEFAULT_FUTURE_CONFIG: FutureConfig = { export const DEFAULT_MARKDOWN_HOOKS: MarkdownHooks = { onBrokenMarkdownLinks: 'warn', onBrokenMarkdownImages: 'throw', + onUnusedMarkdownDirectives: 'warn', }; export const DEFAULT_MARKDOWN_MDX1COMPAT: MDX1CompatOptions = { @@ -548,6 +549,12 @@ export const ConfigSchema = Joi.object({ Joi.function(), ) .default(DEFAULT_CONFIG.markdown.hooks.onBrokenMarkdownImages), + onUnusedMarkdownDirectives: Joi.alternatives() + .try( + Joi.string().equal('ignore', 'log', 'warn', 'throw'), + Joi.function(), + ) + .default(DEFAULT_CONFIG.markdown.hooks.onUnusedMarkdownDirectives), }).default(DEFAULT_CONFIG.markdown.hooks), }).default({ ...DEFAULT_CONFIG.markdown, From f9a5ce99168a8d849ac074eca105832f6c8ecc1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hu=C3=A1ng=20J=C3=B9nli=C3=A0ng?= Date: Fri, 26 Jun 2026 10:33:47 -0400 Subject: [PATCH 4/4] docs: update API docs --- website/docs/api/docusaurus.config.js.mdx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/website/docs/api/docusaurus.config.js.mdx b/website/docs/api/docusaurus.config.js.mdx index 304235fa2f3e..f0400b72009c 100644 --- a/website/docs/api/docusaurus.config.js.mdx +++ b/website/docs/api/docusaurus.config.js.mdx @@ -661,7 +661,7 @@ type MarkdownAnchorsConfig = { type OnBrokenMarkdownLinksFunction = (params: { sourceFilePath: string; // MD/MDX source file relative to cwd url: string; // Link url - node: Link | Definition; // mdast Node + node: Link | Definition; // mdast node }) => void | string; type OnBrokenMarkdownImagesFunction = (params: { @@ -670,11 +670,19 @@ type OnBrokenMarkdownImagesFunction = (params: { node: Image; // mdast node }) => void | string; +type OnUnusedMarkdownDirectivesFunction = (params: { + sourceFilePath: string; // MD/MDX source file relative to cwd + directives: Directives[]; // mdast nodes +}) => void | string; + type ReportingSeverity = 'ignore' | 'log' | 'warn' | 'throw'; type MarkdownHooks = { onBrokenMarkdownLinks: ReportingSeverity | OnBrokenMarkdownLinksFunction; onBrokenMarkdownImages: ReportingSeverity | OnBrokenMarkdownImagesFunction; + onUnusedMarkdownDirectives: + | ReportingSeverity + | onUnusedMarkdownDirectivesFunction; }; type MarkdownConfig = { @@ -718,6 +726,7 @@ export default { hooks: { onBrokenMarkdownLinks: 'warn', onBrokenMarkdownImages: 'throw', + onUnusedMarkdownDirectives: 'warn', }, }, };