Files
supabase/apps/www/lib/mdx/mdxSerialize.ts
T
Ivan VasilovandClaude Haiku 4.5 3a98d32b6e chore: upgrade next-mdx-remote to v6 in apps/docs (#42748)
## I have read the CONTRIBUTING.md file.

YES

## What kind of change does this PR introduce?

Dependency upgrade (next-mdx-remote v4 → v6)

## What is the current behavior?

The docs app uses next-mdx-remote v4.4.1 with MDX v2.

## What is the new behavior?

- Upgraded to next-mdx-remote v6.0.0 (uses MDX v3)
- Updated @mdx-js/loader and @mdx-js/react to v3
- Upgraded remark-gfm to v4 for MDX v3 compatibility
- Removed deprecated `useDynamicImport` option (now default)
- Added `blockJS: false` to preserve JS expressions in MDX content

Build compiles successfully. Testing shows the same pre-existing
prerender error on /guides/troubleshooting as on master (supabaseUrl is
required).

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Chores**
* Upgraded MDX and markdown tooling to major releases (MDX v3,
next-mdx-remote v6, remark-gfm v4).
* Adjusted MDX serialization to disable embedded JS handling and remove
legacy dynamic-import behavior for more consistent rendering of docs,
guides, and code examples.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-02-12 19:01:53 +00:00

134 lines
3.9 KiB
TypeScript

import { serialize } from 'next-mdx-remote/serialize'
import rehypeSlug from 'rehype-slug'
import remarkGfm from 'remark-gfm'
import { type CodeHikeConfig, remarkCodeHike } from '@code-hike/mdx'
import codeHikeTheme from 'config/code-hike.theme.json' with { type: 'json' }
import { preprocessMdxWithCodeTabs } from '~/components/CodeTabs'
// mdx2 needs self-closing tags.
// dragging an image onto a GitHub discussion creates an <img>
// we need to fix this before running them through mdx
// also checks for <br> and <hr>
function addSelfClosingTags(htmlString: string): string {
// Handle cases where htmlString might be undefined, null, or not a string
if (!htmlString || typeof htmlString !== 'string') {
return ''
}
const modifiedHTML = htmlString.replace(/<img[^>]*>|<br[^>]*>|<hr[^>]*>/g, (match) => {
if (match.endsWith('/>')) {
return match
} else {
// Add slash (/) to make it self-closing
return match.slice(0, -1) + ' />'
}
})
return modifiedHTML
}
type TocItem = { content: string; slug: string; lvl: number }
function createRemarkCollectToc(maxDepth: number) {
return function remarkCollectToc() {
return function transformer(tree: any) {
const items: TocItem[] = []
const getText = (node: any): string => {
if (!node) return ''
if (node.type === 'text') return node.value || ''
if (Array.isArray(node.children)) return node.children.map(getText).join('')
return ''
}
const slugify = (input: string) =>
input
.trim()
.toLowerCase()
.replace(/[`~!@#$%^&*()+=|{}\[\]\\:";'<>?,./]+/g, '')
.replace(/\s+/g, '-')
const walk = (node: any) => {
if (node.type === 'heading' && typeof node.depth === 'number') {
if (node.depth <= maxDepth) {
const text = getText(node)
if (text) {
items.push({ content: text, slug: slugify(text), lvl: node.depth })
}
}
}
if (node.children) {
for (const child of node.children) walk(child)
}
}
walk(tree)
// Attach to tree data for retrieval post-serialize
if (!tree.data) {
tree.data = {}
}
tree.data.__collectedToc = items
}
}
}
export async function mdxSerialize(source: string, options?: { tocDepth?: number }) {
const formattedSource = addSelfClosingTags(source)
// Preprocess MDX to handle CodeTabs transformation
const preprocessedSource = await preprocessMdxWithCodeTabs(formattedSource)
const codeHikeOptions: CodeHikeConfig = {
theme: codeHikeTheme,
lineNumbers: true,
showCopyButton: true,
skipLanguages: ['mermaid'],
autoImport: false,
}
const tocDepth = options?.tocDepth ?? 2
let collectedToc: TocItem[] = []
const mdxSource = await serialize(preprocessedSource, {
blockJS: false,
scope: {
chCodeConfig: codeHikeOptions,
},
mdxOptions: {
remarkPlugins: [
[remarkCodeHike, codeHikeOptions],
remarkGfm,
// Collect headings into a simple TOC structure
() => {
const plugin = createRemarkCollectToc(tocDepth)
const transformer = (plugin as any)()
return (tree: any) => {
transformer(tree)
if (tree?.data?.__collectedToc) {
collectedToc = tree.data.__collectedToc as TocItem[]
}
}
},
],
rehypePlugins: [
// @ts-ignore
rehypeSlug, // add IDs to any h1-h6 tag that doesn't have one, using a slug made from its text
],
},
})
// Expose TOC via scope for consumers (keeps function signature stable)
const tocMarkdown = collectedToc
.map((h) => `${' '.repeat(Math.max(0, h.lvl - 1))}- [${h.content}](#${h.slug})`)
.join('\n')
mdxSource.scope = {
...(mdxSource.scope || {}),
toc: {
content: tocMarkdown,
json: collectedToc,
},
}
return mdxSource
}