Files
supabase/apps/docs/lib/docs.ts
Ivan Vasilov 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

145 lines
4.6 KiB
TypeScript

import matter from 'gray-matter'
import { serialize } from 'next-mdx-remote/serialize'
import { existsSync } from 'node:fs'
import { readdir, readFile } from 'node:fs/promises'
import { basename, extname, join, sep } from 'node:path'
import rehypeKatex from 'rehype-katex'
import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math'
import { type SerializeOptions } from '~/types/next-mdx-remote-serialize'
// MUST be process.cwd() here, not import.meta.url, or files that are added
// with outputFileTracingIncludes (not auto-traced) will not be found at
// runtime.
export const DOCS_DIRECTORY = process.cwd()
export const CONTENT_DIRECTORY = join(DOCS_DIRECTORY, 'content')
export const EXAMPLES_DIRECTORY = join(DOCS_DIRECTORY, 'examples')
export const GUIDES_DIRECTORY = join(CONTENT_DIRECTORY, 'guides')
export const PARTIALS_DIRECTORY = join(CONTENT_DIRECTORY, '_partials')
export const REF_DOCS_DIRECTORY = join(DOCS_DIRECTORY, 'docs/ref')
export const SPEC_DIRECTORY = join(DOCS_DIRECTORY, 'spec')
export type GuideFrontmatter = {
title: string
subtitle?: string
description?: string
canonical?: string
hideToc?: boolean
/** @deprecated */
hide_table_of_contents?: boolean
tocVideo?: string
}
/**
* Validate the frontmatter for guide MDX files.
*
* @throws Throws if frontmatter is invalid.
*/
export function isValidGuideFrontmatter(obj: object): obj is GuideFrontmatter {
if (!('title' in obj) || typeof obj.title !== 'string') {
throw Error(
// @ts-expect-error - Getting undefined for unknown property is desired here.
`Invalid guide frontmatter: Title must exist and be a string. Received: ${obj.title}`
)
}
if ('subtitle' in obj && typeof obj.subtitle !== 'string') {
throw Error(`Invalid guide frontmatter: Subtitle must be a sring. Received: ${obj.subtitle}`)
}
if ('description' in obj && typeof obj.description !== 'string') {
throw Error(
`Invalid guide frontmatter: Description must be a string. Received: ${obj.description}`
)
}
if ('canonical' in obj && typeof obj.canonical !== 'string') {
throw Error(`Invalid guide frontmatter: Canonical must be a string. Received: ${obj.canonical}`)
}
if ('hideToc' in obj && typeof obj.hideToc !== 'boolean') {
throw Error(`Invalid guide frontmatter: hideToc must be a boolean. Received: ${obj.hideToc}`)
}
if ('hide_table_of_contents' in obj && typeof obj.hide_table_of_contents !== 'boolean') {
throw Error(
`Invalid guide frontmatter: hide_table_of_contents must be a boolean. Received ${obj.hide_table_of_contents}`
)
}
if ('tocVideo' in obj && typeof obj.tocVideo !== 'string') {
throw Error(`Invalid guide frontmatter: tocVideo must be a string. Received ${obj.tocVideo}`)
}
return true
}
export async function getGuidesStaticPaths(section: string) {
const directory = join(GUIDES_DIRECTORY, section)
const files = (await readdir(directory, { recursive: true }))
.filter((file) => extname(file) === '.mdx' && !basename(file).startsWith('_'))
.map((file) => ({
params: {
slug: file.replace(/\.mdx$/, '').split(sep),
},
}))
// Index page isn't included in the directory
const indexFile = join(GUIDES_DIRECTORY, `${section}.mdx`)
if (existsSync(indexFile)) {
files.push({ params: { slug: [] } })
}
return {
paths: files,
fallback: false,
}
}
export async function getGuidesStaticProps(
section: string,
{ params }: { params?: { slug?: string | Array<string> } }
) {
let relPath: string
switch (typeof params?.slug) {
case 'string':
relPath = section + sep + params.slug
break
case 'object': // actually an array
relPath = section + sep + params.slug.join(sep)
break
case 'undefined':
relPath = section
}
const fullPath = join(GUIDES_DIRECTORY, relPath + '.mdx')
/**
* SAFETY CHECK:
* Prevent accessing anything outside of GUIDES_DIRECTORY
*/
if (!fullPath.startsWith(GUIDES_DIRECTORY)) {
throw Error('Accessing forbidden route. Content must be within the GUIDES_DIRECTORY.')
}
const mdx = await readFile(fullPath, 'utf-8')
const editLink = `supabase/supabase/blob/master/apps/docs/content/guides/${relPath}.mdx`
const { data: frontmatter, content } = matter(mdx)
if (!isValidGuideFrontmatter(frontmatter)) {
// Will have thrown
return
}
const mdxOptions: SerializeOptions = {
blockJS: false,
mdxOptions: {
remarkPlugins: [[remarkMath, { singleDollarTextMath: false }], remarkGfm],
rehypePlugins: [rehypeKatex as any],
},
}
const mdxSource = await serialize(content, mdxOptions)
return {
props: {
frontmatter,
mdxSource,
editLink,
},
}
}