Files
supabase/apps/www/lib/mdx/extractToc.ts
Charis a1611bf449 www: render blog posts on server (#45461)
Render blog posts on server so they are available in initial HTML
response.

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

* **New Features**
  * New collapsible sections for blog content
  * Server-side MDX compilation for blog posts
  * Improved TOC extraction producing both structured JSON and markdown

* **Refactor**
* Blog rendering converted to a server-rendered flow with unified MDX
components
  * Tag handling normalized for related-post matching

* **Bug Fixes**
* Consistent image/self-closing tag normalization and corrected TOC
indentation
  * Errors are now surfaced instead of being swallowed
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-04 10:39:42 -04:00

76 lines
2.1 KiB
TypeScript

import { remark } from 'remark'
import remarkGfm from 'remark-gfm'
type TocItem = { content: string; slug: string; lvl: number }
function createRemarkCollectToc(maxDepth: number) {
return function remarkCollectToc() {
return function transformer(tree: any) {
const items: Array<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)
if (!tree.data) {
tree.data = {}
}
tree.data.__collectedToc = items
}
}
}
export async function extractToc(
rawMdx: string,
depth: number
): Promise<{ content: string; json: Array<TocItem> }> {
let collectedToc: Array<TocItem> = []
await remark()
.use(remarkGfm)
.use(() => {
const plugin = createRemarkCollectToc(depth)
const transformer = (plugin as any)()
return (tree: any) => {
transformer(tree)
if (tree?.data?.__collectedToc) {
collectedToc = tree.data.__collectedToc as Array<TocItem>
}
}
})
.process(rawMdx)
const minLvl = collectedToc.length > 0 ? Math.min(...collectedToc.map((h) => h.lvl)) : 1
const tocMarkdown = collectedToc
.map((h) => `${' '.repeat(Math.max(0, h.lvl - minLvl))}- [${h.content}](#${h.slug})`)
.join('\n')
return { content: tocMarkdown, json: collectedToc }
}