33// shiki tokenizer cost is paid once at build time.
44
55import { compileMDX } from "next-mdx-remote/rsc"
6+ import { unified } from "unified"
7+ import remarkParse from "remark-parse"
8+ import remarkRehype from "remark-rehype"
9+ import rehypeStringify from "rehype-stringify"
610import { createHighlighter , type Highlighter } from "shiki"
711import { visit } from "unist-util-visit"
812import remarkGfm from "remark-gfm"
@@ -12,6 +16,17 @@ import type { ReactElement } from "react"
1216
1317import { mdxComponents } from "@/components/mdx"
1418
19+ /**
20+ * Slugs that should bypass the MDX pipeline and be rendered as plain markdown.
21+ * Use this only when the post body legitimately contains literal `<` / `{`
22+ * sequences (e.g. archived raw C++/C source, math-y prose) that MDX would
23+ * otherwise try to parse as JSX/expressions. The visual output and TOC
24+ * behaviour are otherwise identical to renderMDX().
25+ */
26+ export const PLAIN_MARKDOWN_SLUGS : ReadonlySet < string > = new Set ( [
27+ "gin-framework-notes" ,
28+ ] )
29+
1530// Languages registered with Shiki at build time. Audit of content/posts
1631// shows we only currently fence bash / java / yaml, but registering the
1732// common-author set below keeps new posts working without touching this
@@ -194,6 +209,39 @@ export async function renderMDX(source: string, postTitle?: string): Promise<Rea
194209 return content
195210}
196211
212+ /**
213+ * Plain-markdown rendering path. Used for posts whose body contains literal
214+ * `<` / `{` sequences that MDX (correctly) refuses to parse as text — see
215+ * PLAIN_MARKDOWN_SLUGS above. Output is a sanitised-by-construction HTML
216+ * string that the page renders via `dangerouslySetInnerHTML`. Keep the rehype
217+ * plugin set in lockstep with renderMDX() so TOC anchors, image hints, code
218+ * highlighting, and external-link safety are identical.
219+ */
220+ export async function renderPlainMarkdown ( source : string , postTitle ?: string ) : Promise < string > {
221+ const highlighter = await getHighlighter ( )
222+
223+ let body = source
224+ if ( postTitle ) {
225+ body = body . replace ( / ^ # \s + ( .+ ?) \s * $ / m, ( full , t : string ) => {
226+ const norm = ( s : string ) => s . replace ( / \s + / g, "" ) . trim ( )
227+ return norm ( t ) === norm ( postTitle ) ? "" : full
228+ } )
229+ }
230+
231+ const file = await unified ( )
232+ . use ( remarkParse )
233+ . use ( remarkGfm )
234+ . use ( remarkRehype , { allowDangerousHtml : false } )
235+ . use ( rehypeHeadings )
236+ . use ( rehypeImgAttrs )
237+ . use ( rehypeShiki , highlighter )
238+ . use ( rehypeExternalLinks , { target : "_blank" , rel : [ "noopener" , "noreferrer" ] } )
239+ . use ( rehypeStringify , { allowDangerousHtml : false } )
240+ . process ( body )
241+
242+ return String ( file )
243+ }
244+
197245export interface Heading {
198246 level : 2 | 3 | 4
199247 text : string
0 commit comments