Skip to content

Commit 03495e5

Browse files
committed
post(gin): 归档 Gin 框架原稿 + 纯 markdown 渲染分支
新文章 content/posts/gin-framework-notes.md 是原稿存档,正文一字未动。 原文中混有大量裸 C++/PCL 代码片段(<iostream>、pcl::PointCloud<pcl::PointXYZ>、 裸 { } 等),MDX 会把它们当 JSX/JS 表达式解析并报错,导致 build 失败。 为此在 src/lib/markdown.ts 增加 renderPlainMarkdown:基于 unified + remark-parse + remark-gfm + remark-rehype + rehype-stringify 的纯 markdown 管道,跳过 MDX 表达式解析。复用同一组 rehype 插件 (rehypeShiki / rehypeHeadings / rehypeImgAttrs / rehypeExternalLinks), 确保 TOC 锚点、代码高亮、图片懒加载、外链 rel 与 MDX 路径完全一致。 PLAIN_MARKDOWN_SLUGS 是显式白名单,目前只含 gin-framework-notes, src/app/blog/[slug]/page.tsx 据此分支:白名单走 dangerouslySetInnerHTML, 其余仍走 next-mdx-remote。 依赖:unified / remark-parse / remark-rehype 提升为直接依赖 (之前是 next-mdx-remote 的间接依赖)。 build 验证:42/42 静态页生成成功,含 /blog/gin-framework-notes、 /tags/gin、/tags/编程 等页面。
1 parent cef51a5 commit 03495e5

6 files changed

Lines changed: 1196 additions & 7 deletions

File tree

content/posts/gin-framework-notes.md

Lines changed: 1129 additions & 0 deletions
Large diffs are not rendered by default.

package-lock.json

Lines changed: 4 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,11 @@
2424
"rehype-external-links": "^3.0.0",
2525
"rehype-stringify": "^10.0.1",
2626
"remark-gfm": "^4.0.1",
27+
"remark-parse": "^11.0.0",
28+
"remark-rehype": "^11.1.2",
2729
"shiki": "^4.0.2",
28-
"tailwind-merge": "^3.5.0"
30+
"tailwind-merge": "^3.5.0",
31+
"unified": "^11.0.5"
2932
},
3033
"devDependencies": {
3134
"@tailwindcss/postcss": "^4",

public/search-index.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

src/app/blog/[slug]/page.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { notFound } from "next/navigation"
44
import { ArrowLeft, ArrowRight } from "lucide-react"
55

66
import { getPublishedPosts, getPostBySlug, getAdjacentPosts, getRelatedPosts, getSeriesContext, tagSlug } from "@/lib/posts"
7-
import { renderMDX, extractHeadings } from "@/lib/markdown"
7+
import { renderMDX, renderPlainMarkdown, extractHeadings, PLAIN_MARKDOWN_SLUGS } from "@/lib/markdown"
88
import { computeReadingStats } from "@/lib/reading-time"
99
import { formatDate } from "@/lib/utils"
1010
import { TableOfContents } from "@/components/table-of-contents"
@@ -75,8 +75,10 @@ export default async function PostPage({ params }: PageProps) {
7575
const post = getPostBySlug(slug)
7676
if (!post) notFound()
7777

78-
const [content, headings, { prev, next }, related, stats, seriesContext] = [
79-
await renderMDX(post.content, post.title),
78+
const usePlainMarkdown = PLAIN_MARKDOWN_SLUGS.has(post.slug)
79+
const [renderedMdx, plainHtml, headings, { prev, next }, related, stats, seriesContext] = [
80+
usePlainMarkdown ? null : await renderMDX(post.content, post.title),
81+
usePlainMarkdown ? await renderPlainMarkdown(post.content, post.title) : null,
8082
extractHeadings(post.content, post.title),
8183
getAdjacentPosts(post.slug),
8284
getRelatedPosts(post.slug, 3),
@@ -179,7 +181,11 @@ export default async function PostPage({ params }: PageProps) {
179181
<div className="min-w-0">
180182
<TableOfContents headings={headings} placement="mobile" />
181183
<div id="article-body" className="prose">
182-
{content}
184+
{plainHtml !== null ? (
185+
<div dangerouslySetInnerHTML={{ __html: plainHtml }} />
186+
) : (
187+
renderedMdx
188+
)}
183189
</div>
184190
</div>
185191
<aside>

src/lib/markdown.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
// shiki tokenizer cost is paid once at build time.
44

55
import { 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"
610
import { createHighlighter, type Highlighter } from "shiki"
711
import { visit } from "unist-util-visit"
812
import remarkGfm from "remark-gfm"
@@ -12,6 +16,17 @@ import type { ReactElement } from "react"
1216

1317
import { 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+
197245
export interface Heading {
198246
level: 2 | 3 | 4
199247
text: string

0 commit comments

Comments
 (0)