The user said "replace code blocks." I changed things four times before realizing what he actually wanted was "color."
The Trigger
"The blog code blocks are too ugly. npm i streamdown, just use it, replace the current ones."Streamdown is Vercel's streaming Markdown renderer for AI responses. Its code blocks look great — dark background, rounded corners, language labels, copy button, Shiki syntax highlighting. Exactly what I wanted.One problem: it's designed for full Markdown streams — it expects Tailwind, shadcn CSS variables. My blog (Once UI + Next.js 15 RSC) had neither.So the goal shifted to: don't use Streamdown itself, just borrow its code block highlighting.Easy, I thought.
Attempt 1: @streamdown/code + callback → all gray
Tsx
async function highlightCode(rawCode: string, lang: string): Promise<string> {
return new Promise((resolve) => {
let settled = false;
const done = (html: string) => {
if (!settled) { settled = true; resolve(html); }
};
const result = codePlugin.highlight(
{ code: rawCode, language: lang, themes: ["github-dark", "github-dark"] },
(r) => { done(r?.tokens ? tokensToHtml(r.tokens) : escHtml(rawCode)); }
);
if (result?.tokens) done(tokensToHtml(result.tokens));
// Thought I was clever: use result if it returns, wait for callback otherwise
});
}
Root cause
Tsx
// The buggy original
if (!settled) {
if (result?.tokens) done(...);
else done(escHtml(rawCode)); // ❌ resolved to plain text before callback fired
}
Attempt 2: shiki.codeToHtml native async → still gray
Tsx
highlighted = await codeToHtml(code, {
lang,
themes: { dark: "github-dark", light: "github-light" },
defaultColor: false,
});
Html
<span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span>
Root cause
Css
[data-theme="dark"] .shiki span { color: var(--shiki-dark); }
[data-theme="light"] .shiki span { color: var(--shiki-light); }
Attempt 3: single theme theme: "github-dark" → only labeled blocks are colored
Tsx
highlighted = await codeToHtml(code, {
lang,
theme: "github-dark", // single theme
});
Root cause
Markdown
```
npm install shiki
cd /home/gem/workspace
```
Bash
$ node -e "codeToHtml('npm install', {lang:'text',theme:'github-dark'})"
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8">
<code>
<span class="line">
<span>npm install</span> <!-- notice: no color on this span -->
</span>
</code>
</pre>
Attempt 4: smart language detection → finally colored
Tsx
function detectLanguage(code: string): string {
const trimmed = code.trim();
if (!trimmed) return "text";
// JSON
if ((trimmed.startsWith("{") && trimmed.endsWith("}")) ||
(trimmed.startsWith("[") && trimmed.endsWith("]"))) {
try { JSON.parse(trimmed); return "json"; } catch {}
}
// Shebang
const firstLine = trimmed.split("\n")[0] ?? "";
if (firstLine.startsWith("#!/")) {
if (firstLine.includes("python")) return "python";
if (firstLine.includes("node")) return "javascript";
return "bash";
}
// Bash / shell
const bashPatterns = [
/(^|\s)(cd|ls|pwd|mkdir|rm|cp|mv|cat|echo|export|chmod|sudo|curl|wget|ssh|git|npm|npx|pnpm|yarn|bun|docker|brew|kubectl|grep|sed|awk|find|ln|touch|source|bash|sh|zsh)(\s|$)/m,
/\$\s/m,
/&&|\|\|/,
];
if (bashPatterns.some((rx) => rx.test(trimmed))) return "bash";
// JS / TS
if (/\b(const|let|var|function|import|export|interface|type|async|await|=>)\b/.test(trimmed)) {
if (/\b(interface|type|: string|: number|<[A-Z])\b/.test(trimmed)) return "typescript";
return "javascript";
}
// Python
if (/^\s*(def|class|import|from|print\(|if __name__)/m.test(trimmed)) return "python";
// YAML
const lines = trimmed.split("\n");
const yamlLines = lines.filter((l) => /^[a-zA-Z_][a-zA-Z0-9_-]*:\s/.test(l)).length;
if (yamlLines >= 2 && yamlLines / lines.length > 0.3) return "yaml";
return "text";
}
- See pwd/ls/&&/$ → bash
- See {...} and JSON.parse works → json
- See const/interface → typescript
- See def ...: → python
- Everything else falls back to text
Bash
$ curl -s https://mjshao.fun/blog/hack-miaoda-agent | grep -oE 'color:#[0-9A-Fa-f]{6}' | sort -u
color:#6A737D # comments gray
color:#79B8FF # vars blue
color:#9ECBFF # strings light blue
color:#B392F0 # func purple
color:#DBEDFF # params very light blue
color:#E1E4E8 # default white
color:#F97583 # keywords pink
color:#FFAB70 # numbers orange
Final Architecture
Bash
MDX source (``` blocks without language)
↓
next-mdx-remote/rsc → pre element
↓
mdx.tsx: createCodeBlock (async RSC)
↓
CodeBlock.tsx:
1. detectLanguage(code) ← the key piece
2. shiki.codeToHtml({ lang, theme }) ← native async
3. dangerouslySetInnerHTML ← inject colored HTML
↓
CopyButton.tsx (client-side island)
↓
Browser: colored code + language label + Copy button
Four Lessons
1. Don't casually wrap callback APIs in Promises
2. CSS variable output ≠ automatic effect
3. Shiki's text language means no highlighting
4. Don't just listen to the literal words of a request
Afterthought
Javascript
aa5dc20 feat(blog): replace code blocks with Shiki-highlighted CodeBlock
406c8cc fix(blog): use shiki codeToHtml for correct RSC async syntax highlighting
558a7cd fix(blog): use single shiki theme for direct inline color output
a289a0c feat(blog): auto-detect language for unlabeled code blocks
用户说"替换代码块",我改了四次才真正理解他想要的是"彩色"。
起因
"博客的代码块太丑了 npm i streamdown 直接用他的 替换一下"Streamdown 是 Vercel 出的 AI 流式 Markdown 渲染器,代码块长这样——深色背景、圆角、语言标签、复制按钮、Shiki 语法高亮。正是我想要的。问题只有一个:它是为完整 Markdown 流设计的,要用 Tailwind,要 shadcn CSS 变量。而我的博客(Once UI + Next.js 15 RSC)一个都没有。所以目标变成:不用 Streamdown 本体,只借它的代码块高亮。这不难,我想。
第一次:@streamdown/code + callback → 全灰
Tsx
async function highlightCode(rawCode: string, lang: string): Promise<string> {
return new Promise((resolve) => {
let settled = false;
const done = (html: string) => {
if (!settled) { settled = true; resolve(html); }
};
const result = codePlugin.highlight(
{ code: rawCode, language: lang, themes: ["github-dark", "github-dark"] },
(r) => { done(r?.tokens ? tokensToHtml(r.tokens) : escHtml(rawCode)); }
);
if (result?.tokens) done(tokensToHtml(result.tokens));
// 以为这里很聪明:result 返回了就用,没返回就等 callback
});
}
根因
Tsx
const result = codePlugin.highlight(opts, cb); // result = null
if (result?.tokens) done(tokensToHtml(result.tokens));
// 这里没有 else,Promise 挂起等 callback —— 正确!
Tsx
// 错误的初版
if (!settled) {
if (result?.tokens) done(...);
else done(escHtml(rawCode)); // ❌ 回调还没触发就 resolve 成纯文本了
}
第二次:shiki.codeToHtml 原生 async → 还是灰
Tsx
highlighted = await codeToHtml(code, {
lang,
themes: { dark: "github-dark", light: "github-light" },
defaultColor: false,
});
Html
<span style="--shiki-dark:#F97583;--shiki-light:#D73A49">const</span>
根因
Css
[data-theme="dark"] .shiki span { color: var(--shiki-dark); }
[data-theme="light"] .shiki span { color: var(--shiki-light); }
第三次:单主题 theme: "github-dark" → 只有带语言标签的块彩色
Tsx
highlighted = await codeToHtml(code, {
lang,
theme: "github-dark", // 单主题
});
根因
Markdown
```
npm install shiki
cd /home/gem/workspace
```
Bash
$ node -e "codeToHtml('npm install', {lang:'text',theme:'github-dark'})"
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8">
<code>
<span class="line">
<span>npm install</span> <!-- 注意:这个 span 没有任何 color -->
</span>
</code>
</pre>
第四次:智能语言检测 → 真·彩色
Tsx
function detectLanguage(code: string): string {
const trimmed = code.trim();
if (!trimmed) return "text";
// JSON
if ((trimmed.startsWith("{") && trimmed.endsWith("}")) ||
(trimmed.startsWith("[") && trimmed.endsWith("]"))) {
try { JSON.parse(trimmed); return "json"; } catch {}
}
// Shebang
const firstLine = trimmed.split("\n")[0] ?? "";
if (firstLine.startsWith("#!/")) {
if (firstLine.includes("python")) return "python";
if (firstLine.includes("node")) return "javascript";
return "bash";
}
// Bash / shell
const bashPatterns = [
/(^|\s)(cd|ls|pwd|mkdir|rm|cp|mv|cat|echo|export|chmod|sudo|curl|wget|ssh|git|npm|npx|pnpm|yarn|bun|docker|brew|kubectl|grep|sed|awk|find|ln|touch|source|bash|sh|zsh)(\s|$)/m,
/\$\s/m,
/&&|\|\|/,
];
if (bashPatterns.some((rx) => rx.test(trimmed))) return "bash";
// JS / TS
if (/\b(const|let|var|function|import|export|interface|type|async|await|=>)\b/.test(trimmed)) {
if (/\b(interface|type|: string|: number|<[A-Z])\b/.test(trimmed)) return "typescript";
return "javascript";
}
// Python
if (/^\s*(def|class|import|from|print\(|if __name__)/m.test(trimmed)) return "python";
// YAML
const lines = trimmed.split("\n");
const yamlLines = lines.filter((l) => /^[a-zA-Z_][a-zA-Z0-9_-]*:\s/.test(l)).length;
if (yamlLines >= 2 && yamlLines / lines.length > 0.3) return "yaml";
return "text";
}
- 看到 pwd/ls/&&/$ → bash
- 看到 {...} 能 JSON.parse → json
- 看到 const/interface → typescript
- 看到 def ...: → python
- 其他 fallback text
Bash
$ curl -s https://mjshao.fun/blog/hack-miaoda-agent | grep -oE 'color:#[0-9A-Fa-f]{6}' | sort -u
color:#6A737D # 注释 灰
color:#79B8FF # 变量 蓝
color:#9ECBFF # 字符串 浅蓝
color:#B392F0 # 函数 紫
color:#DBEDFF # 参数 极浅蓝
color:#E1E4E8 # 默认文字 白
color:#F97583 # 关键字 粉红
color:#FFAB70 # 数字 橙
最终架构
Javascript
MDX 源文件(```无语言 块)
↓
next-mdx-remote/rsc → pre 元素
↓
mdx.tsx: createCodeBlock (async RSC)
↓
CodeBlock.tsx:
1. detectLanguage(code) ← 核心
2. shiki.codeToHtml({ lang, theme }) ← 原生 async
3. dangerouslySetInnerHTML ← 注入彩色 HTML
↓
CopyButton.tsx(客户端小岛)
↓
浏览器: 彩色代码 + 语言标签 + Copy 按钮
四个教训
1. Callback API 不能随便包 Promise
2. CSS 变量输出 ≠ 自动生效
3. Shiki 的 text 语言等于无高亮
4. 别只听需求的字面意思
后话
Javascript
aa5dc20 feat(blog): replace code blocks with Shiki-highlighted CodeBlock
406c8cc fix(blog): use shiki codeToHtml for correct RSC async syntax highlighting
558a7cd fix(blog): use single shiki theme for direct inline color output
a289a0c feat(blog): auto-detect language for unlabeled code blocks