Skip to content

Commit 50e66eb

Browse files
committed
feat: migrate markdown rendering to RSC with rehype-react
- Use rehype-react to render markdown directly to JSX on server - Stream RSC content to client via renderServerComponent() - Remove markdown processor from client bundle - Fix code block visibility (CSS was hiding dual-theme Shiki blocks) - Fix hydration errors (nested button in DropdownTrigger) - Add 'use client' directives to interactive components - Move MarkdownHeading type to separate types.ts file - Simplify MarkdownContent to only accept RSC content
1 parent 2ced10d commit 50e66eb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1978
-824
lines changed

package.json

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,23 +45,25 @@
4545
"@sentry/node": "^10.33.0",
4646
"@sentry/tanstackstart-react": "^10.32.1",
4747
"@sentry/vite-plugin": "^4.6.1",
48+
"@shikijs/rehype": "^3.22.0",
4849
"@stackblitz/sdk": "^1.11.0",
4950
"@tailwindcss/typography": "^0.5.13",
5051
"@tailwindcss/vite": "^4.1.11",
5152
"@tanstack/create": "^0.49.1",
5253
"@tanstack/pacer": "^0.16.4",
5354
"@tanstack/react-pacer": "^0.17.4",
5455
"@tanstack/react-query": "^5.90.12",
55-
"@tanstack/react-router": "1.157.16",
56-
"@tanstack/react-router-devtools": "1.157.16",
57-
"@tanstack/react-router-ssr-query": "1.157.16",
58-
"@tanstack/react-start": "1.157.16",
56+
"@tanstack/react-router": "file:../start-rsc/packages/react-router",
57+
"@tanstack/react-router-devtools": "file:../start-rsc/packages/react-router-devtools",
58+
"@tanstack/react-router-ssr-query": "file:../start-rsc/packages/react-router-ssr-query",
59+
"@tanstack/react-start": "file:../start-rsc/packages/react-start",
5960
"@tanstack/react-table": "^8.21.3",
6061
"@types/d3": "^7.4.3",
6162
"@uploadthing/react": "^7.3.3",
6263
"@visx/hierarchy": "^2.10.0",
6364
"@visx/responsive": "^2.10.0",
6465
"@vitejs/plugin-react": "^4.3.3",
66+
"@vitejs/plugin-rsc": "^0.5.15",
6567
"@webcontainer/api": "^1.6.1",
6668
"@xstate/react": "^6.0.0",
6769
"algoliasearch": "^5.23.4",
@@ -74,6 +76,7 @@
7476
"eslint-plugin-jsx-a11y": "^6.10.2",
7577
"gray-matter": "^4.0.3",
7678
"hast-util-is-element": "^3.0.0",
79+
"hast-util-to-html": "^9.0.5",
7780
"hast-util-to-string": "^3.0.1",
7881
"hono": "^4.11.3",
7982
"html-react-parser": "^5.1.10",
@@ -93,14 +96,15 @@
9396
"rehype-callouts": "^2.1.2",
9497
"rehype-parse": "^9.0.1",
9598
"rehype-raw": "^7.0.0",
99+
"rehype-react": "^8.0.0",
96100
"rehype-slug": "^6.0.0",
97101
"rehype-stringify": "^10.0.1",
98102
"remark-gfm": "^4.0.1",
99103
"remark-parse": "^11.0.0",
100104
"remark-rehype": "^11.1.2",
101105
"remove-markdown": "^0.5.0",
102106
"resend": "^6.6.0",
103-
"shiki": "^1.4.0",
107+
"shiki": "^3.22.0",
104108
"tailwind-merge": "^1.14.0",
105109
"three": "^0.182.0",
106110
"troika-three-text": "^0.52.4",
@@ -119,7 +123,7 @@
119123
"@content-collections/vite": "^0.2.4",
120124
"@eslint/js": "^9.39.1",
121125
"@playwright/test": "^1.57.0",
122-
"@shikijs/transformers": "^1.10.3",
126+
"@shikijs/transformers": "^3.22.0",
123127
"@types/express": "^5.0.3",
124128
"@types/hast": "^3.0.4",
125129
"@types/node": "^24.3.0",
@@ -158,7 +162,28 @@
158162
"jws": ">=3.2.3",
159163
"qs": ">=6.14.1",
160164
"js-yaml": "^3.14.2",
161-
"brace-expansion": ">=1.1.12"
165+
"brace-expansion": ">=1.1.12",
166+
"@tanstack/history": "file:../start-rsc/packages/history",
167+
"@tanstack/router-core": "file:../start-rsc/packages/router-core",
168+
"@tanstack/react-router": "file:../start-rsc/packages/react-router",
169+
"@tanstack/react-router-devtools": "file:../start-rsc/packages/react-router-devtools",
170+
"@tanstack/router-devtools-core": "file:../start-rsc/packages/router-devtools-core",
171+
"@tanstack/router-ssr-query-core": "file:../start-rsc/packages/router-ssr-query-core",
172+
"@tanstack/react-router-ssr-query": "file:../start-rsc/packages/react-router-ssr-query",
173+
"@tanstack/react-start": "file:../start-rsc/packages/react-start",
174+
"@tanstack/react-start-client": "file:../start-rsc/packages/react-start-client",
175+
"@tanstack/react-start-server": "file:../start-rsc/packages/react-start-server",
176+
"@tanstack/react-start-rsc": "file:../start-rsc/packages/react-start-rsc",
177+
"@tanstack/start-plugin-core": "file:../start-rsc/packages/start-plugin-core",
178+
"@tanstack/start-client-core": "file:../start-rsc/packages/start-client-core",
179+
"@tanstack/start-server-core": "file:../start-rsc/packages/start-server-core",
180+
"@tanstack/start-storage-context": "file:../start-rsc/packages/start-storage-context",
181+
"@tanstack/start-fn-stubs": "file:../start-rsc/packages/start-fn-stubs",
182+
"@tanstack/router-utils": "file:../start-rsc/packages/router-utils",
183+
"@tanstack/router-generator": "file:../start-rsc/packages/router-generator",
184+
"@tanstack/router-plugin": "file:../start-rsc/packages/router-plugin",
185+
"@tanstack/virtual-file-routes": "file:../start-rsc/packages/virtual-file-routes",
186+
"@tanstack/valibot-adapter": "file:../start-rsc/packages/valibot-adapter"
162187
}
163188
}
164189
}

pnpm-lock.yaml

Lines changed: 566 additions & 152 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/Breadcrumbs.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Link } from '@tanstack/react-router'
22
import { ChevronDown } from 'lucide-react'
33
import { twMerge } from 'tailwind-merge'
4-
import type { MarkdownHeading } from '~/utils/markdown/processor'
4+
import type { MarkdownHeading } from '~/utils/markdown/types'
55
import {
66
Dropdown,
77
DropdownTrigger,
@@ -43,7 +43,7 @@ export function Breadcrumbs({
4343
)}
4444
{showTocToggle && (
4545
<Dropdown>
46-
<DropdownTrigger asChild={false}>
46+
<DropdownTrigger>
4747
<button
4848
className={twMerge(
4949
hiddenClass,

src/components/CodeExampleCard.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { useState } from 'react'
22
import { Card } from '~/components/Card'
3-
import { CodeBlock } from '~/components/markdown'
3+
import { HighlightedCodeBlock } from '~/components/markdown'
44
import { FrameworkIconTabs } from '~/components/FrameworkIconTabs'
55
import type { Framework } from '~/libraries'
66

77
interface CodeExampleCardProps {
88
title?: string
99
frameworks: Framework[]
10-
codeByFramework: Partial<Record<Framework, { lang: string; code: string }>>
10+
/** Pre-highlighted HTML by framework (from server-side Shiki) */
11+
codeByFramework: Partial<Record<Framework, { lang: string; html: string }>>
1112
}
1213

1314
export function CodeExampleCard({
@@ -32,9 +33,12 @@ export function CodeExampleCard({
3233
value={framework}
3334
onChange={setFramework}
3435
/>
35-
<CodeBlock className="mt-0 border-0" showTypeCopyButton={false}>
36-
<code className={`language-${selected.lang}`}>{selected.code}</code>
37-
</CodeBlock>
36+
<HighlightedCodeBlock
37+
html={selected.html}
38+
lang={selected.lang}
39+
showCopyButton={false}
40+
className="mt-0 border-0"
41+
/>
3842
</div>
3943
</Card>
4044
</div>

src/components/CodeExplorer.tsx

Lines changed: 36 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,47 @@
11
import React from 'react'
2-
import { CodeBlock } from '~/components/markdown'
2+
import { HighlightedCodeBlock } from '~/components/markdown'
33
import { FileExplorer } from './FileExplorer'
44
import { InteractiveSandbox } from './InteractiveSandbox'
55
import { CodeExplorerTopBar } from './CodeExplorerTopBar'
66
import type { GitHubFileNode } from '~/utils/documents.server'
77
import type { Library } from '~/libraries'
88
import { twMerge } from 'tailwind-merge'
99

10-
function overrideExtension(ext: string | undefined) {
11-
if (!ext) return 'txt'
10+
function getLanguageFromPath(path: string): string {
11+
const ext = path.split('.').pop() || ''
1212

13-
// Override some extensions
14-
if (['cts', 'mts'].includes(ext)) return 'ts'
15-
if (['cjs', 'mjs'].includes(ext)) return 'js'
16-
if (['prettierrc', 'babelrc', 'webmanifest'].includes(ext)) return 'json'
17-
if (['env', 'example'].includes(ext)) return 'sh'
18-
if (
19-
[
20-
'gitignore',
21-
'prettierignore',
22-
'log',
23-
'gitattributes',
24-
'editorconfig',
25-
'lock',
26-
'opts',
27-
'Dockerfile',
28-
'dockerignore',
29-
'npmrc',
30-
'nvmrc',
31-
].includes(ext)
32-
)
33-
return 'txt'
13+
const langMap: Record<string, string> = {
14+
ts: 'typescript',
15+
tsx: 'tsx',
16+
js: 'javascript',
17+
jsx: 'jsx',
18+
mts: 'typescript',
19+
cts: 'typescript',
20+
mjs: 'javascript',
21+
cjs: 'javascript',
22+
json: 'json',
23+
md: 'markdown',
24+
html: 'html',
25+
css: 'css',
26+
scss: 'scss',
27+
yaml: 'yaml',
28+
yml: 'yaml',
29+
toml: 'toml',
30+
sh: 'bash',
31+
bash: 'bash',
32+
sql: 'sql',
33+
vue: 'vue',
34+
svelte: 'svelte',
35+
}
3436

35-
return ext
37+
return langMap[ext] || 'text'
3638
}
3739

3840
interface CodeExplorerProps {
3941
activeTab: 'code' | 'sandbox'
4042
codeSandboxUrl: string
41-
currentCode: string
43+
/** Pre-highlighted HTML from server-side Shiki */
44+
currentCodeHtml: string
4245
currentPath: string
4346
examplePath: string
4447
githubContents: GitHubFileNode[] | undefined
@@ -52,7 +55,7 @@ interface CodeExplorerProps {
5255
export function CodeExplorer({
5356
activeTab,
5457
codeSandboxUrl,
55-
currentCode,
58+
currentCodeHtml,
5659
currentPath,
5760
examplePath,
5861
githubContents,
@@ -85,6 +88,8 @@ export function CodeExplorer({
8588
return () => window.removeEventListener('closeSidebar', handleCloseSidebar)
8689
}, [])
8790

91+
const lang = getLanguageFromPath(currentPath)
92+
8893
return (
8994
<div
9095
className={`flex flex-col min-h-[60dvh] sm:min-h-[80dvh] border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden ${
@@ -117,21 +122,15 @@ export function CodeExplorer({
117122
setCurrentPath={setCurrentPath}
118123
/>
119124
<div className="flex-1 overflow-auto relative">
120-
<CodeBlock
125+
<HighlightedCodeBlock
126+
html={currentCodeHtml}
127+
lang={lang}
121128
isEmbedded
122129
className={twMerge(
123130
'h-full border-0',
124131
isFullScreen ? 'max-h-[90dvh]' : 'max-h-[80dvh]',
125132
)}
126-
>
127-
<code
128-
className={`language-${overrideExtension(
129-
currentPath.split('.').pop(),
130-
)}`}
131-
>
132-
{currentCode}
133-
</code>
134-
</CodeBlock>
133+
/>
135134
</div>
136135
</div>
137136
<InteractiveSandbox

src/components/Doc.tsx

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,17 @@ import { useWidthToggle, DocNavigation } from '~/components/DocsLayout'
55
import { AdGate } from '~/contexts/AdsContext'
66
import { GamHeader } from './Gam'
77
import { Toc } from './Toc'
8-
import { renderMarkdown } from '~/utils/markdown'
98
import { DocBreadcrumb } from './DocBreadcrumb'
109
import { MarkdownContent } from '~/components/markdown'
1110
import type { ConfigSchema } from '~/utils/config'
1211
import { useLocalCurrentFramework } from './FrameworkSelect'
1312
import { useParams } from '@tanstack/react-router'
13+
import type { MarkdownHeading } from '~/utils/markdown/types'
1414

1515
type DocProps = {
1616
title: string
17-
content: string
17+
contentRsc: React.ReactNode
18+
headings: MarkdownHeading[]
1819
repo: string
1920
branch: string
2021
filePath: string
@@ -36,7 +37,8 @@ type DocProps = {
3637

3738
export function Doc({
3839
title,
39-
content,
40+
contentRsc,
41+
headings,
4042
repo,
4143
branch,
4244
filePath,
@@ -51,12 +53,6 @@ export function Doc({
5153
footer,
5254
framework: frameworkProp,
5355
}: DocProps) {
54-
// Extract headings synchronously during render to avoid hydration mismatch
55-
const { headings, markup } = React.useMemo(
56-
() => renderMarkdown(content),
57-
[content],
58-
)
59-
6056
// Get current framework from prop, URL params, or local storage
6157
const { framework: paramsFramework } = useParams({ strict: false })
6258
const localCurrentFramework = useLocalCurrentFramework()
@@ -159,7 +155,7 @@ export function Doc({
159155
repo={repo}
160156
branch={branch}
161157
filePath={filePath}
162-
htmlMarkup={markup}
158+
contentRsc={contentRsc}
163159
containerRef={markdownContainerRef}
164160
libraryId={libraryId}
165161
libraryVersion={libraryVersion}

src/components/DocBreadcrumb.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useParams } from '@tanstack/react-router'
22
import { Breadcrumbs } from './Breadcrumbs'
33
import type { ConfigSchema } from '~/utils/config'
4-
import type { MarkdownHeading } from '~/utils/markdown/processor'
4+
import type { MarkdownHeading } from '~/utils/markdown/types'
55

66
function findSectionForDoc(
77
config: ConfigSchema,

src/components/FeedEntry.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { format, formatDistanceToNow } from '~/utils/dates'
2-
import { Markdown } from '~/components/markdown'
2+
// TODO: Fix feed to use server-rendered markdown
3+
// import { Markdown } from '~/components/markdown'
34
import { libraries } from '~/libraries'
45
import { partners } from '~/utils/partners'
56
import { twMerge } from 'tailwind-merge'
@@ -415,7 +416,8 @@ export function FeedEntry({
415416

416417
{/* Content */}
417418
<div className="text-xs text-gray-900 dark:text-gray-100 leading-snug mb-3">
418-
<Markdown rawContent={entry.content} />
419+
{/* TODO: Fix feed to use server-rendered markdown */}
420+
<div>{entry.content}</div>
419421
</div>
420422

421423
{/* External Link */}

src/components/FeedEntryTimeline.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as React from 'react'
22
import { format, formatDistanceToNow } from '~/utils/dates'
3-
import { Markdown } from '~/components/markdown'
3+
// TODO: Fix feed to use server-rendered markdown
4+
// import { Markdown } from '~/components/markdown'
45
import { Card } from '~/components/Card'
56
import { libraries } from '~/libraries'
67
import { partners } from '~/utils/partners'
@@ -333,7 +334,8 @@ export function FeedEntryTimeline({
333334
!expanded && 'line-clamp-6',
334335
)}
335336
>
336-
<Markdown rawContent={entry.content} />
337+
{/* TODO: Fix feed to use server-rendered markdown */}
338+
<div>{entry.content}</div>
337339
</div>
338340

339341
{/* Show more/less button */}

src/components/SearchModal.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -566,7 +566,7 @@ function LibraryRefinement() {
566566

567567
return (
568568
<Dropdown>
569-
<DropdownTrigger asChild={false}>
569+
<DropdownTrigger>
570570
<button className="flex items-center gap-1 text-sm focus:outline-none cursor-pointer font-bold">
571571
{currentLibrary ? (
572572
<span className="uppercase font-black [letter-spacing:-.05em]">
@@ -638,7 +638,7 @@ function FrameworkRefinement() {
638638

639639
return (
640640
<Dropdown>
641-
<DropdownTrigger asChild={false}>
641+
<DropdownTrigger>
642642
<button className="flex items-center gap-1 text-sm font-bold focus:outline-none cursor-pointer">
643643
{currentFramework && (
644644
<img

0 commit comments

Comments
 (0)