Skip to content

Commit 1e9f9cf

Browse files
committed
Implement DocImage component to handle <img> tags with ImageZoom and prepend basePath in rehype plugin
1 parent 7f9d227 commit 1e9f9cf

File tree

2 files changed

+55
-12
lines changed

2 files changed

+55
-12
lines changed

mdx-components.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,42 @@ const mdxComponents = {
139139
Tab,
140140
Check,
141141
Cross: X,
142+
// DocImage is used by the rehypeDocImage plugin which renames explicit <img> JSX tags
143+
// to <DocImage> so they go through this components map and get ImageZoom wrapping.
144+
// The src it receives already has basePath prepended by the rehype plugin.
145+
DocImage: (props: any) => {
146+
const src = typeof props.src === 'object' && props.src !== null && 'src' in props.src ? props.src.src : props.src;
147+
148+
if (
149+
!src ||
150+
(typeof src === 'string' && (
151+
src.endsWith('.svg') ||
152+
src.startsWith('data:') ||
153+
src.includes('img.shields.io')
154+
))
155+
) {
156+
return <img {...props} src={src} />;
157+
}
158+
159+
const defaultHeight = 300;
160+
const defaultWidth = 700;
161+
const width = props.width ? (typeof props.width === 'number' ? props.width : parseInt(props.width) || defaultWidth) : defaultWidth;
162+
const height = props.height ? (typeof props.height === 'number' ? props.height : parseInt(props.height) || defaultHeight) : defaultHeight;
163+
164+
console.debug('Rendering DocImage with src:', src, 'width:', width, 'height:', height);
165+
166+
return (
167+
<ImageZoom
168+
src={src}
169+
alt={props.alt || ''}
170+
height={height}
171+
width={width}
172+
// unoptimized
173+
loading="lazy"
174+
className="rounded-lg"
175+
/>
176+
);
177+
},
142178
img: (props: any) => {
143179
// Resolve src if it's an object (static import)
144180
const src = typeof props.src === 'object' && props.src !== null && 'src' in props.src ? props.src.src : props.src;

source.config.ts

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,20 @@ export const docs = defineDocs({
1919
},
2020
});
2121

22-
// Rehype plugin that prepends basePath to local <img src="/..."> paths.
23-
// In MDX, <img> tags are parsed as JSX (mdxJsxFlowElement / mdxJsxTextElement),
24-
// NOT as rehype `element` nodes. Their attributes live in node.attributes[],
25-
// not node.properties — so we must handle both node types.
26-
function rehypePrependBasePath() {
22+
// Rehype plugin for local <img src="/..."> tags in MDX files.
23+
//
24+
// Problem: In MDX, explicit <img> JSX tags are compiled as React.createElement('img', ...)
25+
// and do NOT reliably go through the components map (unlike Markdown ![]() images).
26+
// This means the `img` override in mdx-components.tsx is never called for them.
27+
//
28+
// Solution: Rename those nodes to <DocImage> (uppercase). Uppercase components ARE always
29+
// resolved from the components context, so DocImage in mdx-components.tsx will be called.
30+
// We also prepend basePath here since Next.js <Image> does NOT do it automatically —
31+
// the src must already include basePath before being passed to next/image.
32+
function rehypeDocImage() {
2733
const basePath =
2834
process.env.NEXT_PUBLIC_BASE_PATH ||
2935
(process.env.NODE_ENV === 'production' ? '/plugins-doc-site' : '');
30-
31-
if (!basePath) return (tree: any) => tree;
3236

3337
function walk(node: any) {
3438
// MDX JSX nodes: mdxJsxFlowElement / mdxJsxTextElement
@@ -37,29 +41,32 @@ function rehypePrependBasePath() {
3741
node.name === 'img' &&
3842
Array.isArray(node.attributes)
3943
) {
44+
// Rename to DocImage so the components map entry is used (enabling ImageZoom)
45+
node.name = 'DocImage';
46+
47+
// Prepend basePath to local src paths
4048
for (const attr of node.attributes) {
4149
if (
4250
attr.type === 'mdxJsxAttribute' &&
4351
attr.name === 'src' &&
4452
typeof attr.value === 'string'
4553
) {
4654
const src: string = attr.value;
47-
if (src.startsWith('/') && !src.startsWith('//') && !src.startsWith(basePath)) {
55+
if (basePath && src.startsWith('/') && !src.startsWith('//') && !src.startsWith(basePath)) {
4856
attr.value = basePath + src;
49-
console.debug('MDX img src after basePath:', attr.value);
5057
}
5158
}
5259
}
5360
}
5461

55-
// Standard rehype element nodes (fallback)
62+
// Standard rehype element nodes (fallback for any genuine HTML <img> nodes)
5663
if (
5764
node.type === 'element' &&
5865
node.tagName === 'img' &&
5966
typeof node.properties?.src === 'string'
6067
) {
6168
const src: string = node.properties.src;
62-
if (src.startsWith('/') && !src.startsWith('//') && !src.startsWith(basePath)) {
69+
if (basePath && src.startsWith('/') && !src.startsWith('//') && !src.startsWith(basePath)) {
6370
node.properties.src = basePath + src;
6471
}
6572
}
@@ -78,6 +85,6 @@ export default defineConfig({
7885
external: false, // Disable fetching external image sizes
7986
},
8087
remarkPlugins: [],
81-
rehypePlugins: [rehypePrependBasePath],
88+
rehypePlugins: [rehypeDocImage],
8289
},
8390
});

0 commit comments

Comments
 (0)