@diplodoc/mdx-extension

MDX extension for Diplodoc's markdown transformer that allows embedding MDX/JSX components within markdown content.
Installation
npm install @diplodoc/mdx-extension
yarn add @diplodoc/mdx-extension
Features
- Seamlessly integrate JSX/MDX components within markdown content
- Support for both client-side (CSR) and server-side (SSR) rendering
- Context support with tracking of context changes
- Multiple syntax options:
- Explicit
<MDX>...</MDX> tags
- Short form JSX fragments
<>...</>
- Direct React component usage
<Component />
- Built-in security with MDX input validation
- Portal support for advanced component mounting with
withPortal
- Asynchronous component loading support via
idMdxComponentLoader
Usage
Basic Setup
First, add the mdxPlugin() to your Diplodoc transform plugins:
import transform from '@diplodoc/transform';
import DefaultPlugins from '@diplodoc/transform/lib/plugins';
import {mdxPlugin} from '@diplodoc/mdx-extension';
const result = transform(markdownContent, {
plugins: [...DefaultPlugins, mdxPlugin()],
});
Enabling MDX Input Validation
To enable security validation of MDX input and prevent execution of potentially unsafe code:
import {mdxPlugin, validateMdx} from '@diplodoc/mdx-extension';
const result = transform(markdownContent, {
plugins: [
...DefaultPlugins,
mdxPlugin({
compileOptions: {
recmaPlugins: [validateMdx],
},
}),
],
});
This will:
- Validate all user-provided MDX/JSX content
- Prevent execution of unsafe code on the server
- Throw validation errors for suspicious patterns
Client-side Rendering (CSR)
import React, {Fragment, useMemo, useRef} from 'react';
import transform from '@diplodoc/transform';
import DefaultPlugins from '@diplodoc/transform/lib/plugins';
import {mdxPlugin, useMdx, isWithMdxArtifacts, validateMdx} from '@diplodoc/mdx-extension';
const Components = {
CustomComponent: (props) => <div {...props}>Custom</div>,
};
const CONTENT = `
# Markdown Content
<CustomComponent style={{color: 'red'}} />
<MDX>
<div>This will be rendered as MDX</div>
</MDX>
`;
function App() {
const ref = useRef(null);
const {html, mdxArtifacts} = useMemo(() => {
const {result} = transform(CONTENT, {
plugins: [
...DefaultPlugins,
mdxPlugin({
compileOptions: {
recmaPlugins: [validateMdx],
},
}),
],
});
isWithMdxArtifacts(result);
return result;
}, []);
const portals = useMdx({
refCtr: ref,
html,
components: Components,
mdxArtifacts,
});
return (
<Fragment>
<div ref={ref}></div>
{portals}
</Fragment>
);
}
Server-side Rendering (SSR)
import React from 'react';
import transform from '@diplodoc/transform';
import DefaultPlugins from '@diplodoc/transform/lib/plugins';
import {mdxPlugin, useMdxSsr, getSsrRenderer, validateMdx} from '@diplodoc/mdx-extension';
const Components = {
ServerComponent: (props) => <strong {...props}>Server Rendered</strong>,
};
const CONTENT = `
# Server Rendered Content
<ServerComponent />
`;
export async function getServerSideProps() {
const render = await getSsrRenderer({
components: Components,
compileOptions: {
recmaPlugins: [validateMdx],
},
});
const {result} = transform(CONTENT, {
plugins: [...DefaultPlugins, mdxPlugin({render})],
});
isWithMdxArtifacts(result);
const {html, mdxArtifacts} = result;
return {props: {html, mdxArtifacts}};
}
function ServerPage({html, mdxArtifacts}) {
const ref = useRef(null);
const portals = useMdxSsr({
refCtr: ref,
components: Components,
mdxArtifacts,
html,
});
const innerHtml = useMemo(() => {
return {__html: html};
}, [html]);
return (
<Fragment>
<div ref={ref} dangerouslySetInnerHTML={innerHtml}></div>
{portals}
</Fragment>
);
}
Collect Plugin
The collect plugin provides functionality to process and transform MDX content while collecting artifacts. It comes in both synchronous and asynchronous versions.
Synchronous Collect Plugin
import {getMdxCollectPlugin} from '@diplodoc/mdx-extension';
const plugin = getMdxCollectPlugin({
tagNames: ['CustomComponent'],
pureComponents: PURE_COMPONENTS,
compileOptions: {
},
});
const transformedContent = plugin(originalContent);
Asynchronous Collect Plugin
import {getAsyncMdxCollectPlugin} from '@diplodoc/mdx-extension';
const asyncPlugin = getAsyncMdxCollectPlugin({
tagNames: ['AsyncComponent'],
pureComponents: PURE_COMPONENTS,
compileOptions: {
},
});
const transformedContent = await asyncPlugin(originalContent);
API Reference
mdxPlugin(options?: { render?: MDXRenderer })
The main plugin function that enables MDX processing.
Options:
render: Optional renderer function, for SSR use getSsrRenderer
tagNames?: string[] - Optional array of tag names to filter which components will be processed
useMdx(options: UseMdxProps): React.Fragment
React hook for client-side MDX processing.
Options:
refCtr: Ref to the container element
html: HTML string from Diplodoc transform
components: Object of React components to use
mdxArtifacts: MDX artifacts from transform
pureComponents?: Optional object of components that shouldn't hydrate (MDXComponents)
contextList?: Array of React contexts to provide to MDX components
useMdxSsr(options: UseMdxSsrProps): React.Fragment
React hook for SSR-processed MDX content.
Options:
refCtr: Ref to the container element
html: HTML string from Diplodoc transform
components: Object of React components to use
mdxArtifacts: MDX artifacts from transform
pureComponents?: Optional object of components that shouldn't hydrate (MDXComponents)
contextList?: Array of React contexts to provide to MDX components
getRenderer(options: GetRenderProps)
Creates an renderer function for client-side processing.
Options:
getSsrRenderer(options: GetSsrRendererProps)
Creates an SSR renderer function for server-side processing.
Options:
components: Object of React components to use
pureComponents?: Optional object of components that shouldn't hydrate (MDXComponents)
compileOptions?: MDX compilation options (see MDX documentation)
contextList?: Array of React contexts to provide to MDX components. Use { ctx, initValue } format to pass initial values for SSR
getAsyncSsrRenderer(options: GetAsyncSsrRendererProps)
Creates an asynchronous SSR renderer that supports withInitialProps.
Options:
components: Object of React components to use
pureComponents?: Optional object of components that shouldn't hydrate (MDXComponents)
compileOptions?: MDX compilation options (see MDX documentation)
contextList?: Array of React contexts to provide to MDX components. Use { ctx, initValue } format to pass initial values for SSR
getMdxCollectPlugin(options: Options)
Creates a synchronous collect plugin for processing MDX content.
Options:
tagNames?: string[] - Optional array of tag names to filter processing
pureComponents?: Components that should skip client-side hydration
compileOptions?: MDX compilation options
getAsyncMdxCollectPlugin(options: AsyncOptions)
Creates an asynchronous collect plugin that supports components with initial props.
Options:
tagNames?: string[] - Optional array of tag names to filter processing
pureComponents?: Components that should skip client-side hydration
compileOptions?: MDX compilation options
State Management Contexts
MdxStateCtx: Context<MdxStateCtxValue>
Provides access to the current MDX state:
const state = useContext(MdxStateCtx);
MdxSetStateCtx: Context<MdxSetStateCtxValue>
Provides state setter function (only available during SSR):
const setState = useContext(MdxSetStateCtx);
setState?.({key: value});
Component Enhancers
withInitialProps: WithInitialProps
Wraps a component to enable initial props fetching during SSR.
Parameters:
component: React component to wrap
getInitProps: Function that receives props and MDX state, returns props (sync or async)
withPortal: WithPortalProps
Wraps a component to render it through React.createPortal, allowing for more flexible mounting.
Parameters:
component: React component to wrap
fallback: Optional fallback component to show before portal is mounted
Usage:
export const COMPONENTS = {
Tabs: withPortal(TabsLocal, () => <Skeleton />),
};
When using withPortal, the component will:
- Render the fallback component (if provided) initially
- Create a portal to mount the actual component when ready
- Clean up the portal when unmounted
Syntax Examples
Explicit MDX tags
<MDX>
<MyComponent prop="value" />
</MDX>
JSX Fragments
<>
<div>Fragment content</div>
</>
Direct Component Usage
<Button onClick={() => console.log('click')}>
Click me
</Button>
Advanced Features
State Management in SSR
The library provides two context providers for managing state during Server-Side Rendering (SSR):
-
MdxSetStateCtx - A context that provides a function to update the MDX state. This function is only available during SSR (null on client-side). If you set a component's state using this context, it will be:
- Serialized into the
data-mdx-state attribute during SSR
- Available in
MdxStateCtx when the component renders
-
MdxStateCtx - A context that provides access to the current MDX state value
Asynchronous SSR with Initial Props
Example usage:
const getInitialProps: MDXGetInitialProps<CounterProps> = (props, mdxState) => {
mdxState.initialValue = 10;
return props;
};
export const SSR_COMPONENTS = {
...COMPONENTS,
Counter: withInitialProps(Counter, getInitialProps),
};
Pure Components
The library supports pure components that:
- Are only rendered once during SSR
- Skip hydration on the client side
- Can be specified via the
pureComponents option in:
useMdx
useMdxSsr
getSsrRenderer
getAsyncSsrRenderer
Example:
export const PURE_COMPONENTS = {
KatexFormula,
Label,
CompatTable,
Alert,
};
Asynchronous Component Loading
The useMdx and useMdxSsr hooks now support an optional idMdxComponentLoader parameter that enables asynchronous loading of MDX components:
interface PageProps {
html: string;
mdxArtifacts?: MdxArtifacts;
withLoader?: boolean;
}
const Page: FC<PageProps> = ({html, mdxArtifacts}) => {
const [isSuccess, setSuccess] = React.useState(false);
const [data, setData] = React.useState<IdMdxComponentLoader['data']>(undefined);
useMdxSsr({
idMdxComponentLoader: {isSuccess, data},
});
useEffect(() => {
(async () => {
const idMdxComponent: Record<string, React.ComponentType<MDXProps>> = {};
for (const [artifactId, code] of Object.entries(mdxArtifacts?.idMdx ?? {})) {
const fn = await asyncExecuteCode(code);
idMdxComponent[artifactId] = fn(runtime).default;
}
setData(idMdxComponent);
setSuccess(true);
})();
}, [mdxArtifacts]);
};
Compilation Options
All renderer functions (getSsrRenderer, getAsyncSsrRenderer, getRenderer) now accept optional MDX compilation options:
const renderer = await getAsyncSsrRenderer({
components: SSR_COMPONENTS,
pureComponents: PURE_COMPONENTS,
compileOptions: {
},
});
This allows for fine-grained control over the MDX compilation process while maintaining the library's core functionality.
License
MIT