From 50e66eb4e34294c286b0c93f2ed819c3641793b2 Mon Sep 17 00:00:00 2001
From: Tanner Linsley
Date: Tue, 10 Feb 2026 11:05:54 -0700
Subject: [PATCH 1/5] 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
---
package.json | 39 +-
pnpm-lock.yaml | 718 ++++++++++++++----
src/components/Breadcrumbs.tsx | 4 +-
src/components/CodeExampleCard.tsx | 14 +-
src/components/CodeExplorer.tsx | 73 +-
src/components/Doc.tsx | 16 +-
src/components/DocBreadcrumb.tsx | 2 +-
src/components/FeedEntry.tsx | 6 +-
src/components/FeedEntryTimeline.tsx | 6 +-
src/components/SearchModal.tsx | 4 +-
src/components/SimpleMarkdown.tsx | 33 +-
src/components/ToastProvider.tsx | 2 +
src/components/Toc.tsx | 2 +-
src/components/TocMobile.tsx | 2 +-
src/components/admin/FeedEntryEditor.tsx | 6 +-
src/components/markdown/BlogContent.tsx | 18 +
src/components/markdown/CodeBlock.tsx | 435 ++++++-----
src/components/markdown/DocContent.tsx | 14 +
src/components/markdown/FileTabs.tsx | 2 +
src/components/markdown/FrameworkContent.tsx | 2 +
src/components/markdown/Markdown.tsx | 25 +-
src/components/markdown/MarkdownContent.tsx | 20 +-
.../markdown/MarkdownFrameworkHandler.tsx | 32 +-
.../markdown/MarkdownHeadingContext.tsx | 2 +-
src/components/markdown/MarkdownLink.tsx | 2 +
.../markdown/MarkdownTabsHandler.tsx | 56 +-
src/components/markdown/MdComponents.tsx | 184 +++++
.../markdown/PackageManagerTabs.tsx | 10 +-
src/components/markdown/Tabs.tsx | 2 +
src/components/markdown/index.ts | 3 +-
src/routeTree.gen.ts | 78 +-
src/routes/$libraryId/$version.docs.$.tsx | 5 +-
.../$version.docs.framework.$framework.$.tsx | 5 +-
src/routes/blog.$.tsx | 107 +--
src/routes/blog.index.tsx | 110 +--
src/routes/feed.$id.tsx | 36 +-
src/routes/index.tsx | 42 +-
src/styles/app.css | 39 +-
src/utils/{docs.ts => docs.tsx} | 17 +-
src/utils/headers.server.ts | 22 +
src/utils/markdown/index.ts | 10 +-
src/utils/markdown/processor.ts | 75 --
src/utils/markdown/processor.tsx | 432 +++++++++++
src/utils/markdown/types.ts | 8 +
src/utils/renderBlogContent.tsx | 15 +
vite.config.ts | 67 +-
46 files changed, 1978 insertions(+), 824 deletions(-)
create mode 100644 src/components/markdown/BlogContent.tsx
create mode 100644 src/components/markdown/DocContent.tsx
create mode 100644 src/components/markdown/MdComponents.tsx
rename src/utils/{docs.ts => docs.tsx} (81%)
create mode 100644 src/utils/headers.server.ts
delete mode 100644 src/utils/markdown/processor.ts
create mode 100644 src/utils/markdown/processor.tsx
create mode 100644 src/utils/markdown/types.ts
create mode 100644 src/utils/renderBlogContent.tsx
diff --git a/package.json b/package.json
index 8fb81cd1e..8262952a0 100644
--- a/package.json
+++ b/package.json
@@ -45,6 +45,7 @@
"@sentry/node": "^10.33.0",
"@sentry/tanstackstart-react": "^10.32.1",
"@sentry/vite-plugin": "^4.6.1",
+ "@shikijs/rehype": "^3.22.0",
"@stackblitz/sdk": "^1.11.0",
"@tailwindcss/typography": "^0.5.13",
"@tailwindcss/vite": "^4.1.11",
@@ -52,16 +53,17 @@
"@tanstack/pacer": "^0.16.4",
"@tanstack/react-pacer": "^0.17.4",
"@tanstack/react-query": "^5.90.12",
- "@tanstack/react-router": "1.157.16",
- "@tanstack/react-router-devtools": "1.157.16",
- "@tanstack/react-router-ssr-query": "1.157.16",
- "@tanstack/react-start": "1.157.16",
+ "@tanstack/react-router": "file:../start-rsc/packages/react-router",
+ "@tanstack/react-router-devtools": "file:../start-rsc/packages/react-router-devtools",
+ "@tanstack/react-router-ssr-query": "file:../start-rsc/packages/react-router-ssr-query",
+ "@tanstack/react-start": "file:../start-rsc/packages/react-start",
"@tanstack/react-table": "^8.21.3",
"@types/d3": "^7.4.3",
"@uploadthing/react": "^7.3.3",
"@visx/hierarchy": "^2.10.0",
"@visx/responsive": "^2.10.0",
"@vitejs/plugin-react": "^4.3.3",
+ "@vitejs/plugin-rsc": "^0.5.15",
"@webcontainer/api": "^1.6.1",
"@xstate/react": "^6.0.0",
"algoliasearch": "^5.23.4",
@@ -74,6 +76,7 @@
"eslint-plugin-jsx-a11y": "^6.10.2",
"gray-matter": "^4.0.3",
"hast-util-is-element": "^3.0.0",
+ "hast-util-to-html": "^9.0.5",
"hast-util-to-string": "^3.0.1",
"hono": "^4.11.3",
"html-react-parser": "^5.1.10",
@@ -93,6 +96,7 @@
"rehype-callouts": "^2.1.2",
"rehype-parse": "^9.0.1",
"rehype-raw": "^7.0.0",
+ "rehype-react": "^8.0.0",
"rehype-slug": "^6.0.0",
"rehype-stringify": "^10.0.1",
"remark-gfm": "^4.0.1",
@@ -100,7 +104,7 @@
"remark-rehype": "^11.1.2",
"remove-markdown": "^0.5.0",
"resend": "^6.6.0",
- "shiki": "^1.4.0",
+ "shiki": "^3.22.0",
"tailwind-merge": "^1.14.0",
"three": "^0.182.0",
"troika-three-text": "^0.52.4",
@@ -119,7 +123,7 @@
"@content-collections/vite": "^0.2.4",
"@eslint/js": "^9.39.1",
"@playwright/test": "^1.57.0",
- "@shikijs/transformers": "^1.10.3",
+ "@shikijs/transformers": "^3.22.0",
"@types/express": "^5.0.3",
"@types/hast": "^3.0.4",
"@types/node": "^24.3.0",
@@ -158,7 +162,28 @@
"jws": ">=3.2.3",
"qs": ">=6.14.1",
"js-yaml": "^3.14.2",
- "brace-expansion": ">=1.1.12"
+ "brace-expansion": ">=1.1.12",
+ "@tanstack/history": "file:../start-rsc/packages/history",
+ "@tanstack/router-core": "file:../start-rsc/packages/router-core",
+ "@tanstack/react-router": "file:../start-rsc/packages/react-router",
+ "@tanstack/react-router-devtools": "file:../start-rsc/packages/react-router-devtools",
+ "@tanstack/router-devtools-core": "file:../start-rsc/packages/router-devtools-core",
+ "@tanstack/router-ssr-query-core": "file:../start-rsc/packages/router-ssr-query-core",
+ "@tanstack/react-router-ssr-query": "file:../start-rsc/packages/react-router-ssr-query",
+ "@tanstack/react-start": "file:../start-rsc/packages/react-start",
+ "@tanstack/react-start-client": "file:../start-rsc/packages/react-start-client",
+ "@tanstack/react-start-server": "file:../start-rsc/packages/react-start-server",
+ "@tanstack/react-start-rsc": "file:../start-rsc/packages/react-start-rsc",
+ "@tanstack/start-plugin-core": "file:../start-rsc/packages/start-plugin-core",
+ "@tanstack/start-client-core": "file:../start-rsc/packages/start-client-core",
+ "@tanstack/start-server-core": "file:../start-rsc/packages/start-server-core",
+ "@tanstack/start-storage-context": "file:../start-rsc/packages/start-storage-context",
+ "@tanstack/start-fn-stubs": "file:../start-rsc/packages/start-fn-stubs",
+ "@tanstack/router-utils": "file:../start-rsc/packages/router-utils",
+ "@tanstack/router-generator": "file:../start-rsc/packages/router-generator",
+ "@tanstack/router-plugin": "file:../start-rsc/packages/router-plugin",
+ "@tanstack/virtual-file-routes": "file:../start-rsc/packages/virtual-file-routes",
+ "@tanstack/valibot-adapter": "file:../start-rsc/packages/valibot-adapter"
}
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 114304ceb..c433ac994 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -12,6 +12,27 @@ overrides:
qs: '>=6.14.1'
js-yaml: ^3.14.2
brace-expansion: '>=1.1.12'
+ '@tanstack/history': file:../start-rsc/packages/history
+ '@tanstack/router-core': file:../start-rsc/packages/router-core
+ '@tanstack/react-router': file:../start-rsc/packages/react-router
+ '@tanstack/react-router-devtools': file:../start-rsc/packages/react-router-devtools
+ '@tanstack/router-devtools-core': file:../start-rsc/packages/router-devtools-core
+ '@tanstack/router-ssr-query-core': file:../start-rsc/packages/router-ssr-query-core
+ '@tanstack/react-router-ssr-query': file:../start-rsc/packages/react-router-ssr-query
+ '@tanstack/react-start': file:../start-rsc/packages/react-start
+ '@tanstack/react-start-client': file:../start-rsc/packages/react-start-client
+ '@tanstack/react-start-server': file:../start-rsc/packages/react-start-server
+ '@tanstack/react-start-rsc': file:../start-rsc/packages/react-start-rsc
+ '@tanstack/start-plugin-core': file:../start-rsc/packages/start-plugin-core
+ '@tanstack/start-client-core': file:../start-rsc/packages/start-client-core
+ '@tanstack/start-server-core': file:../start-rsc/packages/start-server-core
+ '@tanstack/start-storage-context': file:../start-rsc/packages/start-storage-context
+ '@tanstack/start-fn-stubs': file:../start-rsc/packages/start-fn-stubs
+ '@tanstack/router-utils': file:../start-rsc/packages/router-utils
+ '@tanstack/router-generator': file:../start-rsc/packages/router-generator
+ '@tanstack/router-plugin': file:../start-rsc/packages/router-plugin
+ '@tanstack/virtual-file-routes': file:../start-rsc/packages/virtual-file-routes
+ '@tanstack/valibot-adapter': file:../start-rsc/packages/valibot-adapter
importers:
@@ -37,7 +58,7 @@ importers:
version: 0.1.0
'@netlify/vite-plugin-tanstack-start':
specifier: ^1.0.2
- version: 1.0.2(@tanstack/react-start@1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)))(babel-plugin-macros@3.1.0)(rollup@4.53.3)(uploadthing@7.7.4(express@5.2.1)(h3@1.15.4)(tailwindcss@4.1.11))(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))
+ version: 1.0.2(@tanstack/react-start@file:../start-rsc/packages/react-start(@vitejs/plugin-rsc@0.5.19(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)))(babel-plugin-macros@3.1.0)(rollup@4.53.3)(uploadthing@7.7.4(express@5.2.1)(h3@1.15.4)(tailwindcss@4.1.11))(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))
'@number-flow/react':
specifier: ^0.4.1
version: 0.4.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -74,6 +95,9 @@ importers:
'@sentry/vite-plugin':
specifier: ^4.6.1
version: 4.6.1
+ '@shikijs/rehype':
+ specifier: ^3.22.0
+ version: 3.22.0
'@stackblitz/sdk':
specifier: ^1.11.0
version: 1.11.0
@@ -96,17 +120,17 @@ importers:
specifier: ^5.90.12
version: 5.90.12(react@19.2.3)
'@tanstack/react-router':
- specifier: 1.157.16
- version: 1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ specifier: file:../start-rsc/packages/react-router
+ version: file:../start-rsc/packages/react-router(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@tanstack/react-router-devtools':
- specifier: 1.157.16
- version: 1.157.16(@tanstack/react-router@1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.157.16)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ specifier: file:../start-rsc/packages/react-router-devtools
+ version: file:../start-rsc/packages/react-router-devtools(@tanstack/react-router@file:../start-rsc/packages/react-router(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@tanstack/react-router-ssr-query':
- specifier: 1.157.16
- version: 1.157.16(@tanstack/query-core@5.90.12)(@tanstack/react-query@5.90.12(react@19.2.3))(@tanstack/react-router@1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.157.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ specifier: file:../start-rsc/packages/react-router-ssr-query
+ version: file:../start-rsc/packages/react-router-ssr-query(@tanstack/query-core@5.90.12)(@tanstack/react-query@5.90.12(react@19.2.3))(@tanstack/react-router@file:../start-rsc/packages/react-router(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@tanstack/react-start':
- specifier: 1.157.16
- version: 1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))
+ specifier: file:../start-rsc/packages/react-start
+ version: file:../start-rsc/packages/react-start(@vitejs/plugin-rsc@0.5.19(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))
'@tanstack/react-table':
specifier: ^8.21.3
version: 8.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -125,6 +149,9 @@ importers:
'@vitejs/plugin-react':
specifier: ^4.3.3
version: 4.3.4(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))
+ '@vitejs/plugin-rsc':
+ specifier: ^0.5.15
+ version: 0.5.19(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))
'@webcontainer/api':
specifier: ^1.6.1
version: 1.6.1
@@ -161,6 +188,9 @@ importers:
hast-util-is-element:
specifier: ^3.0.0
version: 3.0.0
+ hast-util-to-html:
+ specifier: ^9.0.5
+ version: 9.0.5
hast-util-to-string:
specifier: ^3.0.1
version: 3.0.1
@@ -218,6 +248,9 @@ importers:
rehype-raw:
specifier: ^7.0.0
version: 7.0.0
+ rehype-react:
+ specifier: ^8.0.0
+ version: 8.0.0
rehype-slug:
specifier: ^6.0.0
version: 6.0.0
@@ -240,8 +273,8 @@ importers:
specifier: ^6.6.0
version: 6.6.0
shiki:
- specifier: ^1.4.0
- version: 1.10.3
+ specifier: ^3.22.0
+ version: 3.22.0
tailwind-merge:
specifier: ^1.14.0
version: 1.14.0
@@ -292,8 +325,8 @@ importers:
specifier: ^1.57.0
version: 1.57.0
'@shikijs/transformers':
- specifier: ^1.10.3
- version: 1.10.3
+ specifier: ^3.22.0
+ version: 3.22.0
'@types/express':
specifier: ^5.0.3
version: 5.0.6
@@ -1868,6 +1901,7 @@ packages:
'@netlify/vite-plugin-tanstack-start@1.0.2':
resolution: {integrity: sha512-2v21F6K28Wc7HGrGfASzs04o8xrDprHWt323+J7b1ToH7r0eC5IGkM1l3rPQlcom+TVkNvOejVlyEuqfJsr05g==}
+ version: 1.0.2
engines: {node: ^22.12.0}
peerDependencies:
'@tanstack/react-start': '>=1.132.0'
@@ -2730,6 +2764,9 @@ packages:
'@rolldown/pluginutils@1.0.0-beta.40':
resolution: {integrity: sha512-s3GeJKSQOwBlzdUrj4ISjJj5SfSh+aqn0wjOar4Bx95iV1ETI7F6S/5hLcfAxZ9kXDcyrAkxPlqmd1ZITttf+w==}
+ '@rolldown/pluginutils@1.0.0-rc.2':
+ resolution: {integrity: sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==}
+
'@rollup/pluginutils@5.3.0':
resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==}
engines: {node: '>=14.0.0'}
@@ -3006,11 +3043,32 @@ packages:
resolution: {integrity: sha512-Qvys1y3o8/bfL3ikrHnJS9zxdjt0z3POshdBl3967UcflrTqBmnGNkcVk53SlmtJWIfh85fgmrLvGYwZ2YiqNg==}
engines: {node: '>= 14'}
- '@shikijs/core@1.10.3':
- resolution: {integrity: sha512-D45PMaBaeDHxww+EkcDQtDAtzv00Gcsp72ukBtaLSmqRvh0WgGMq3Al0rl1QQBZfuneO75NXMIzEZGFitThWbg==}
+ '@shikijs/core@3.22.0':
+ resolution: {integrity: sha512-iAlTtSDDbJiRpvgL5ugKEATDtHdUVkqgHDm/gbD2ZS9c88mx7G1zSYjjOxp5Qa0eaW0MAQosFRmJSk354PRoQA==}
+
+ '@shikijs/engine-javascript@3.22.0':
+ resolution: {integrity: sha512-jdKhfgW9CRtj3Tor0L7+yPwdG3CgP7W+ZEqSsojrMzCjD1e0IxIbwUMDDpYlVBlC08TACg4puwFGkZfLS+56Tw==}
+
+ '@shikijs/engine-oniguruma@3.22.0':
+ resolution: {integrity: sha512-DyXsOG0vGtNtl7ygvabHd7Mt5EY8gCNqR9Y7Lpbbd/PbJvgWrqaKzH1JW6H6qFkuUa8aCxoiYVv8/YfFljiQxA==}
+
+ '@shikijs/langs@3.22.0':
+ resolution: {integrity: sha512-x/42TfhWmp6H00T6uwVrdTJGKgNdFbrEdhaDwSR5fd5zhQ1Q46bHq9EO61SCEWJR0HY7z2HNDMaBZp8JRmKiIA==}
+
+ '@shikijs/rehype@3.22.0':
+ resolution: {integrity: sha512-69b2VPc6XBy/VmAJlpBU5By+bJSBdE2nvgRCZXav7zujbrjXuT0F60DIrjKuutjPqNufuizE+E8tIZr2Yn8Z+g==}
- '@shikijs/transformers@1.10.3':
- resolution: {integrity: sha512-MNjsyye2WHVdxfZUSr5frS97sLGe6G1T+1P41QjyBFJehZphMcr4aBlRLmq6OSPBslYe9byQPVvt/LJCOfxw8Q==}
+ '@shikijs/themes@3.22.0':
+ resolution: {integrity: sha512-o+tlOKqsr6FE4+mYJG08tfCFDS+3CG20HbldXeVoyP+cYSUxDhrFf3GPjE60U55iOkkjbpY2uC3It/eeja35/g==}
+
+ '@shikijs/transformers@3.22.0':
+ resolution: {integrity: sha512-E7eRV7mwDBjueLF6852n2oYeJYxBq3NSsDk+uyruYAXONv4U8holGmIrT+mPRJQ1J1SNOH6L8G19KRzmBawrFw==}
+
+ '@shikijs/types@3.22.0':
+ resolution: {integrity: sha512-491iAekgKDBFE67z70Ok5a8KBMsQ2IJwOWw3us/7ffQkIBCyOQfm/aNwVMBUriP02QshIfgHCBSIYAl3u2eWjg==}
+
+ '@shikijs/vscode-textmate@10.0.2':
+ resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
'@sindresorhus/merge-streams@4.0.0':
resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
@@ -3133,8 +3191,8 @@ packages:
resolution: {integrity: sha512-RL1f5ZlfZMpghrCIdzl6mLOFLTuhqmPNblZgBaeKfdtk5rfbjykurv+VfYydOFXj0vxVIoA2d/zT7xfD7Ph8fw==}
engines: {node: '>=18'}
- '@tanstack/history@1.154.14':
- resolution: {integrity: sha512-xyIfof8eHBuub1CkBnbKNKQXeRZC4dClhmzePHVOEel4G7lk/dW+TQ16da7CFdeNLv6u6Owf5VoBQxoo6DFTSA==}
+ '@tanstack/history@file:../start-rsc/packages/history':
+ resolution: {directory: ../start-rsc/packages/history, type: directory}
engines: {node: '>=12'}
'@tanstack/pacer@0.16.4':
@@ -3156,20 +3214,20 @@ packages:
peerDependencies:
react: ^18 || ^19
- '@tanstack/react-router-devtools@1.157.16':
- resolution: {integrity: sha512-g6ekyzumfLBX6T5e+Vu2r37Z2CFJKrWRFqIy3vZ6A3x7OcuPV8uXNjyrLSiT/IsGTiF8YzwI4nWJa4fyd7NlCw==}
+ '@tanstack/react-router-devtools@file:../start-rsc/packages/react-router-devtools':
+ resolution: {directory: ../start-rsc/packages/react-router-devtools, type: directory}
engines: {node: '>=12'}
peerDependencies:
- '@tanstack/react-router': ^1.157.16
- '@tanstack/router-core': ^1.157.16
+ '@tanstack/react-router': workspace:^
+ '@tanstack/router-core': workspace:^
react: '>=18.0.0 || >=19.0.0'
react-dom: '>=18.0.0 || >=19.0.0'
peerDependenciesMeta:
'@tanstack/router-core':
optional: true
- '@tanstack/react-router-ssr-query@1.157.16':
- resolution: {integrity: sha512-emvm1t2fTZk/gdctuTwbNW2LeUCpPJGttq4N9I5YdTk2QmLmCD5mgiJYB/GXWwmuSq05dmO/7W9b8HNAWSv0FQ==}
+ '@tanstack/react-router-ssr-query@file:../start-rsc/packages/react-router-ssr-query':
+ resolution: {directory: ../start-rsc/packages/react-router-ssr-query, type: directory}
engines: {node: '>=12'}
peerDependencies:
'@tanstack/query-core': '>=5.90.0'
@@ -3178,34 +3236,49 @@ packages:
react: '>=18.0.0 || >=19.0.0'
react-dom: '>=18.0.0 || >=19.0.0'
- '@tanstack/react-router@1.157.16':
- resolution: {integrity: sha512-xwFQa7S7dhBhm3aJYwU79cITEYgAKSrcL6wokaROIvl2JyIeazn8jueWqUPJzFjv+QF6Q8euKRlKUEyb5q2ymg==}
+ '@tanstack/react-router@file:../start-rsc/packages/react-router':
+ resolution: {directory: ../start-rsc/packages/react-router, type: directory}
engines: {node: '>=12'}
peerDependencies:
react: '>=18.0.0 || >=19.0.0'
react-dom: '>=18.0.0 || >=19.0.0'
- '@tanstack/react-start-client@1.157.16':
- resolution: {integrity: sha512-r3XTxYPJXZ/szhbloxqT6CQtsoEjw8DjbnZh/3ZsQv2PLKTOl925cy7YVdQc2cWZyXtn5e19Ig78R+8tsoTpig==}
+ '@tanstack/react-start-client@file:../start-rsc/packages/react-start-client':
+ resolution: {directory: ../start-rsc/packages/react-start-client, type: directory}
+ engines: {node: '>=22.12.0'}
+ peerDependencies:
+ react: '>=18.0.0 || >=19.0.0'
+ react-dom: '>=18.0.0 || >=19.0.0'
+
+ '@tanstack/react-start-rsc@file:../start-rsc/packages/react-start-rsc':
+ resolution: {directory: ../start-rsc/packages/react-start-rsc, type: directory}
engines: {node: '>=22.12.0'}
peerDependencies:
+ '@vitejs/plugin-rsc': '>=0.5.15'
react: '>=18.0.0 || >=19.0.0'
react-dom: '>=18.0.0 || >=19.0.0'
+ peerDependenciesMeta:
+ '@vitejs/plugin-rsc':
+ optional: true
- '@tanstack/react-start-server@1.157.16':
- resolution: {integrity: sha512-1YkBss4SUQ+HqVC1yGN/j7VNwjvdHHd3K58fASe0bz+uf7GrkGJlRXPkMJdxJkkmefYHQfyBL+q7o723N4CMYA==}
+ '@tanstack/react-start-server@file:../start-rsc/packages/react-start-server':
+ resolution: {directory: ../start-rsc/packages/react-start-server, type: directory}
engines: {node: '>=22.12.0'}
peerDependencies:
react: '>=18.0.0 || >=19.0.0'
react-dom: '>=18.0.0 || >=19.0.0'
- '@tanstack/react-start@1.157.16':
- resolution: {integrity: sha512-FO6UYjsZyNaC0ickSSvClqfVZemp9/HWnbRJQU2dOKYQsI+wnznhLp9IkgG90iFBLcuMAWhcNHMiIuz603GJBg==}
+ '@tanstack/react-start@file:../start-rsc/packages/react-start':
+ resolution: {directory: ../start-rsc/packages/react-start, type: directory}
engines: {node: '>=22.12.0'}
peerDependencies:
+ '@vitejs/plugin-rsc': '*'
react: '>=18.0.0 || >=19.0.0'
react-dom: '>=18.0.0 || >=19.0.0'
vite: '>=7.0.0'
+ peerDependenciesMeta:
+ '@vitejs/plugin-rsc':
+ optional: true
'@tanstack/react-store@0.8.0':
resolution: {integrity: sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow==}
@@ -3220,30 +3293,30 @@ packages:
react: '>=16.8'
react-dom: '>=16.8'
- '@tanstack/router-core@1.157.16':
- resolution: {integrity: sha512-eJuVgM7KZYTTr4uPorbUzUflmljMVcaX2g6VvhITLnHmg9SBx9RAgtQ1HmT+72mzyIbRSlQ1q0fY/m+of/fosA==}
+ '@tanstack/router-core@file:../start-rsc/packages/router-core':
+ resolution: {directory: ../start-rsc/packages/router-core, type: directory}
engines: {node: '>=12'}
- '@tanstack/router-devtools-core@1.157.16':
- resolution: {integrity: sha512-XBJTs/kMZYK6J2zhbGucHNuypwDB1t2vi8K5To+V6dUnLGBEyfQTf01fegiF4rpL1yXgomdGnP6aTiOFgldbVg==}
+ '@tanstack/router-devtools-core@file:../start-rsc/packages/router-devtools-core':
+ resolution: {directory: ../start-rsc/packages/router-devtools-core, type: directory}
engines: {node: '>=12'}
peerDependencies:
- '@tanstack/router-core': ^1.157.16
+ '@tanstack/router-core': workspace:^
csstype: ^3.0.10
peerDependenciesMeta:
csstype:
optional: true
- '@tanstack/router-generator@1.157.16':
- resolution: {integrity: sha512-Ae2M00VTFjjED7glSCi/mMLENRzhEym6NgjoOx7UVNbCC/rLU/5ASDe5VIlDa8QLEqP5Pj088Gi51gjmRuICvQ==}
+ '@tanstack/router-generator@file:../start-rsc/packages/router-generator':
+ resolution: {directory: ../start-rsc/packages/router-generator, type: directory}
engines: {node: '>=12'}
- '@tanstack/router-plugin@1.157.16':
- resolution: {integrity: sha512-YQg7L06xyCJAYyrEJNZGAnDL8oChILU+G/eSDIwEfcWn5iLk+47x1Gcdxr82++47PWmOPhzuTo8edDQXWs7kAA==}
+ '@tanstack/router-plugin@file:../start-rsc/packages/router-plugin':
+ resolution: {directory: ../start-rsc/packages/router-plugin, type: directory}
engines: {node: '>=12'}
peerDependencies:
'@rsbuild/core': '>=1.0.2'
- '@tanstack/react-router': ^1.157.16
+ '@tanstack/react-router': workspace:^
vite: '>=5.0.0 || >=6.0.0 || >=7.0.0'
vite-plugin-solid: ^2.11.10
webpack: '>=5.92.0'
@@ -3259,37 +3332,37 @@ packages:
webpack:
optional: true
- '@tanstack/router-ssr-query-core@1.157.16':
- resolution: {integrity: sha512-YuwNG4jdtn+r90yyti8yP27IKaVoflWmRezqnj0gyJxpRauBkK7MVLvWSNbJadnk88b9H+rdtNOF2k3owGaong==}
+ '@tanstack/router-ssr-query-core@file:../start-rsc/packages/router-ssr-query-core':
+ resolution: {directory: ../start-rsc/packages/router-ssr-query-core, type: directory}
engines: {node: '>=12'}
peerDependencies:
'@tanstack/query-core': '>=5.90.0'
'@tanstack/router-core': '>=1.127.0'
- '@tanstack/router-utils@1.154.7':
- resolution: {integrity: sha512-61bGx32tMKuEpVRseu2sh1KQe8CfB7793Mch/kyQt0EP3tD7X0sXmimCl3truRiDGUtI0CaSoQV1NPjAII1RBA==}
+ '@tanstack/router-utils@file:../start-rsc/packages/router-utils':
+ resolution: {directory: ../start-rsc/packages/router-utils, type: directory}
engines: {node: '>=12'}
- '@tanstack/start-client-core@1.157.16':
- resolution: {integrity: sha512-O+7H133MWQTkOxmXJNhrLXiOhDcBlxvpEcCd/N25Ga6eyZ7/P5vvFzNkSSxeQNkZV+RiPWnA5B75gT+U+buz3w==}
+ '@tanstack/start-client-core@file:../start-rsc/packages/start-client-core':
+ resolution: {directory: ../start-rsc/packages/start-client-core, type: directory}
engines: {node: '>=22.12.0'}
- '@tanstack/start-fn-stubs@1.154.7':
- resolution: {integrity: sha512-D69B78L6pcFN5X5PHaydv7CScQcKLzJeEYqs7jpuyyqGQHSUIZUjS955j+Sir8cHhuDIovCe2LmsYHeZfWf3dQ==}
+ '@tanstack/start-fn-stubs@file:../start-rsc/packages/start-fn-stubs':
+ resolution: {directory: ../start-rsc/packages/start-fn-stubs, type: directory}
engines: {node: '>=22.12.0'}
- '@tanstack/start-plugin-core@1.157.16':
- resolution: {integrity: sha512-VmRXuvP5flryUAHeBM4Xb06n544qLtyA2cwmlQLRTUYtQiQEAdd9CvCGy8CPAly3f7eeXKqC7aX0v3MwWkLR8w==}
+ '@tanstack/start-plugin-core@file:../start-rsc/packages/start-plugin-core':
+ resolution: {directory: ../start-rsc/packages/start-plugin-core, type: directory}
engines: {node: '>=22.12.0'}
peerDependencies:
vite: '>=7.0.0'
- '@tanstack/start-server-core@1.157.16':
- resolution: {integrity: sha512-PEltFleYfiqz6+KcmzNXxc1lXgT7VDNKP6G6i1TirdHBDbRJ9CIY+ASLPlhrRwqwA2PL9PpFjXZl8u5bH/+Q9A==}
+ '@tanstack/start-server-core@file:../start-rsc/packages/start-server-core':
+ resolution: {directory: ../start-rsc/packages/start-server-core, type: directory}
engines: {node: '>=22.12.0'}
- '@tanstack/start-storage-context@1.157.16':
- resolution: {integrity: sha512-56izE0oihAw2YRwYUEds2H+uO5dyT2CahXCgWX62+l+FHou09M9mSep68n1lBKPdphC2ZU3cPV7wnvgeraJWHg==}
+ '@tanstack/start-storage-context@file:../start-rsc/packages/start-storage-context':
+ resolution: {directory: ../start-rsc/packages/start-storage-context, type: directory}
engines: {node: '>=22.12.0'}
'@tanstack/store@0.8.0':
@@ -3299,8 +3372,8 @@ packages:
resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==}
engines: {node: '>=12'}
- '@tanstack/virtual-file-routes@1.154.7':
- resolution: {integrity: sha512-cHHDnewHozgjpI+MIVp9tcib6lYEQK5MyUr0ChHpHFGBl8Xei55rohFK0I0ve/GKoHeioaK42Smd8OixPp6CTg==}
+ '@tanstack/virtual-file-routes@file:../start-rsc/packages/virtual-file-routes':
+ resolution: {directory: ../start-rsc/packages/virtual-file-routes, type: directory}
engines: {node: '>=12'}
'@tweenjs/tween.js@23.1.3':
@@ -3429,6 +3502,9 @@ packages:
'@types/draco3d@1.4.10':
resolution: {integrity: sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==}
+ '@types/estree-jsx@1.0.5':
+ resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
+
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
@@ -3538,6 +3614,9 @@ packages:
'@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
+ '@types/unist@2.0.11':
+ resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
+
'@types/unist@3.0.3':
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
@@ -3659,6 +3738,17 @@ packages:
peerDependencies:
vite: ^4.2.0 || ^5.0.0 || ^6.0.0
+ '@vitejs/plugin-rsc@0.5.19':
+ resolution: {integrity: sha512-YuRKVEOYQFq4OdLKIoGpLKL0y0fyhWjjEDVHEIvPsXGk+jQ+uVbuM6hzVseb6N95x8cbdDGUe3m+qNU1dPldrg==}
+ peerDependencies:
+ react: '*'
+ react-dom: '*'
+ react-server-dom-webpack: '*'
+ vite: '*'
+ peerDependenciesMeta:
+ react-server-dom-webpack:
+ optional: true
+
'@vue/compiler-core@3.5.22':
resolution: {integrity: sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==}
@@ -4055,6 +4145,9 @@ packages:
character-entities@2.0.2:
resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==}
+ character-reference-invalid@2.0.1:
+ resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
+
cheerio-select@2.1.0:
resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==}
@@ -4857,6 +4950,9 @@ packages:
es-module-lexer@1.7.0:
resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
+ es-module-lexer@2.0.0:
+ resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==}
+
es-object-atoms@1.1.1:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'}
@@ -4996,9 +5092,15 @@ packages:
resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
engines: {node: '>=4.0'}
+ estree-util-is-identifier-name@3.0.0:
+ resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==}
+
estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
+ estree-walker@3.0.3:
+ resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
+
esutils@2.0.3:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
@@ -5356,8 +5458,8 @@ packages:
h3@1.15.4:
resolution: {integrity: sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==}
- h3@2.0.1-rc.11:
- resolution: {integrity: sha512-2myzjCqy32c1As9TjZW9fNZXtLqNedjFSrdFy2AjFBQQ3LzrnGoDdFDYfC0tV2e4vcyfJ2Sfo/F6NQhO2Ly/Mw==}
+ h3@2.0.1-rc.7:
+ resolution: {integrity: sha512-qbrRu1OLXmUYnysWOCVrYhtC/m8ZuXu/zCbo3U/KyphJxbPFiC76jHYwVrmEcss9uNAHO5BoUguQ46yEpgI2PA==}
engines: {node: '>=20.11.1'}
peerDependencies:
crossws: ^0.4.1
@@ -5423,6 +5525,9 @@ packages:
hast-util-to-html@9.0.5:
resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==}
+ hast-util-to-jsx-runtime@2.3.6:
+ resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==}
+
hast-util-to-parse5@8.0.0:
resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==}
@@ -5620,6 +5725,12 @@ packages:
iron-webcrypto@1.2.1:
resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==}
+ is-alphabetical@2.0.1:
+ resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
+
+ is-alphanumerical@2.0.1:
+ resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==}
+
is-array-buffer@3.0.4:
resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==}
engines: {node: '>= 0.4'}
@@ -5674,6 +5785,9 @@ packages:
resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==}
engines: {node: '>= 0.4'}
+ is-decimal@2.0.1:
+ resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==}
+
is-docker@2.2.1:
resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
engines: {node: '>=8'}
@@ -5708,6 +5822,9 @@ packages:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
+ is-hexadecimal@2.0.1:
+ resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==}
+
is-inside-container@1.0.0:
resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==}
engines: {node: '>=14.16'}
@@ -5759,6 +5876,9 @@ packages:
is-promise@4.0.0:
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
+ is-reference@3.0.3:
+ resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==}
+
is-regex@1.1.4:
resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==}
engines: {node: '>= 0.4'}
@@ -5909,6 +6029,9 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+ js-tokens@9.0.1:
+ resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
+
js-yaml@3.14.2:
resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==}
hasBin: true
@@ -6200,6 +6323,9 @@ packages:
magic-string@0.30.19:
resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==}
+ magic-string@0.30.21:
+ resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+
magic-string@0.30.8:
resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==}
engines: {node: '>=12'}
@@ -6247,6 +6373,15 @@ packages:
mdast-util-gfm@3.1.0:
resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==}
+ mdast-util-mdx-expression@2.0.1:
+ resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==}
+
+ mdast-util-mdx-jsx@3.2.0:
+ resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==}
+
+ mdast-util-mdxjs-esm@2.0.1:
+ resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==}
+
mdast-util-phrasing@4.1.0:
resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==}
@@ -6647,6 +6782,12 @@ packages:
resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==}
engines: {node: '>=12'}
+ oniguruma-parser@0.12.1:
+ resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==}
+
+ oniguruma-to-es@4.3.4:
+ resolution: {integrity: sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==}
+
open@7.4.2:
resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==}
engines: {node: '>=8'}
@@ -6712,6 +6853,9 @@ packages:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
+ parse-entities@4.0.2:
+ resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==}
+
parse-gitignore@2.0.0:
resolution: {integrity: sha512-RmVuCHWsfu0QPNW+mraxh/xjQVw/lhUCUru8Zni3Ctq3AoMhpDTq0OVdKS6iesd6Kqb7viCV3isAL43dciOSog==}
engines: {node: '>=14'}
@@ -6807,6 +6951,9 @@ packages:
pend@1.2.0:
resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
+ periscopic@4.0.2:
+ resolution: {integrity: sha512-sqpQDUy8vgB7ycLkendSKS6HnVz1Rneoc3Rc+ZBUCe2pbqlVuCC5vF52l0NJ1aiMg/r1qfYF9/myz8CZeI2rjA==}
+
pg-int8@1.0.1:
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
engines: {node: '>=4.0.0'}
@@ -7081,6 +7228,9 @@ packages:
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
+ react-is@19.2.4:
+ resolution: {integrity: sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==}
+
react-property@2.0.2:
resolution: {integrity: sha512-+PbtI3VuDV0l6CleQMsx2gtK0JZbZKbpdu5ynr+lbsuvtmgbNcS3VM0tuY2QjFNOcWxvXeHjDpy42RO+4U2rug==}
@@ -7176,6 +7326,15 @@ packages:
regenerator-runtime@0.14.1:
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
+ regex-recursion@6.0.2:
+ resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==}
+
+ regex-utilities@2.3.0:
+ resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==}
+
+ regex@6.1.0:
+ resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==}
+
regexp.prototype.flags@1.5.2:
resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==}
engines: {node: '>= 0.4'}
@@ -7197,6 +7356,9 @@ packages:
rehype-raw@7.0.0:
resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==}
+ rehype-react@8.0.0:
+ resolution: {integrity: sha512-vzo0YxYbB2HE+36+9HWXVdxNoNDubx63r5LBzpxBGVWM8s9mdnMdbmuJBAX6TTyuGdZjZix6qU3GcSuKCIWivw==}
+
rehype-slug@6.0.0:
resolution: {integrity: sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==}
@@ -7448,8 +7610,8 @@ packages:
shell-quote@1.8.1:
resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==}
- shiki@1.10.3:
- resolution: {integrity: sha512-eneCLncGuvPdTutJuLyUGS8QNPAVFO5Trvld2wgEq1e002mwctAhJKeMGWtWVXOIEzmlcLRqcgPSorR6AVzOmQ==}
+ shiki@3.22.0:
+ resolution: {integrity: sha512-LBnhsoYEe0Eou4e1VgJACes+O6S6QC0w71fCSp5Oya79inkwkm15gQ1UF6VtQ8j/taMDh79hAB49WUk8ALQW3g==}
side-channel-list@1.0.0:
resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
@@ -7628,6 +7790,9 @@ packages:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
+ strip-literal@3.1.0:
+ resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==}
+
style-to-js@1.1.12:
resolution: {integrity: sha512-tv+/FkgNYHI2fvCoBMsqPHh5xovwiw+C3X0Gfnss/Syau0Nr3IqGOJ9XiOYXoPnToHVbllKFf5qCNFJGwFg5mg==}
@@ -7826,6 +7991,9 @@ packages:
tunnel-rat@0.1.2:
resolution: {integrity: sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==}
+ turbo-stream@3.1.0:
+ resolution: {integrity: sha512-tVI25WEXl4fckNEmrq70xU1XumxUwEx/FZD5AgEcV8ri7Wvrg2o7GEq8U7htrNx3CajciGm+kDyhRf5JB6t7/A==}
+
type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
@@ -7935,6 +8103,9 @@ packages:
unist-util-visit@5.0.0:
resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==}
+ unist-util-visit@5.1.0:
+ resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==}
+
universal-user-agent@6.0.0:
resolution: {integrity: sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==}
@@ -8383,6 +8554,9 @@ packages:
resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==}
engines: {node: '>=18'}
+ zimmerframe@1.1.4:
+ resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==}
+
zip-stream@6.0.1:
resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==}
engines: {node: '>= 14'}
@@ -8557,7 +8731,7 @@ snapshots:
'@babel/code-frame@7.27.1':
dependencies:
- '@babel/helper-validator-identifier': 7.27.1
+ '@babel/helper-validator-identifier': 7.28.5
js-tokens: 4.0.0
picocolors: 1.1.1
@@ -9883,12 +10057,12 @@ snapshots:
'@netlify/types@2.2.0': {}
- '@netlify/vite-plugin-tanstack-start@1.0.2(@tanstack/react-start@1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)))(babel-plugin-macros@3.1.0)(rollup@4.53.3)(uploadthing@7.7.4(express@5.2.1)(h3@1.15.4)(tailwindcss@4.1.11))(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))':
+ '@netlify/vite-plugin-tanstack-start@1.0.2(@tanstack/react-start@file:../start-rsc/packages/react-start(@vitejs/plugin-rsc@0.5.19(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)))(babel-plugin-macros@3.1.0)(rollup@4.53.3)(uploadthing@7.7.4(express@5.2.1)(h3@1.15.4)(tailwindcss@4.1.11))(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))':
dependencies:
'@netlify/vite-plugin': 2.6.1(babel-plugin-macros@3.1.0)(rollup@4.53.3)(uploadthing@7.7.4(express@5.2.1)(h3@1.15.4)(tailwindcss@4.1.11))(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))
vite: 7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)
optionalDependencies:
- '@tanstack/react-start': 1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))
+ '@tanstack/react-start': file:../start-rsc/packages/react-start(@vitejs/plugin-rsc@0.5.19(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))
transitivePeerDependencies:
- '@azure/app-configuration'
- '@azure/cosmos'
@@ -9947,7 +10121,7 @@ snapshots:
'@netlify/zip-it-and-ship-it@14.1.8(rollup@4.53.3)':
dependencies:
- '@babel/parser': 7.28.4
+ '@babel/parser': 7.29.0
'@babel/types': 7.28.4
'@netlify/binary-info': 1.0.0
'@netlify/serverless-functions-api': 2.6.0
@@ -10853,6 +11027,8 @@ snapshots:
'@rolldown/pluginutils@1.0.0-beta.40': {}
+ '@rolldown/pluginutils@1.0.0-rc.2': {}
+
'@rollup/pluginutils@5.3.0(rollup@4.53.3)':
dependencies:
'@types/estree': 1.0.8
@@ -11176,13 +11352,52 @@ snapshots:
- encoding
- supports-color
- '@shikijs/core@1.10.3':
+ '@shikijs/core@3.22.0':
dependencies:
+ '@shikijs/types': 3.22.0
+ '@shikijs/vscode-textmate': 10.0.2
'@types/hast': 3.0.4
+ hast-util-to-html: 9.0.5
+
+ '@shikijs/engine-javascript@3.22.0':
+ dependencies:
+ '@shikijs/types': 3.22.0
+ '@shikijs/vscode-textmate': 10.0.2
+ oniguruma-to-es: 4.3.4
- '@shikijs/transformers@1.10.3':
+ '@shikijs/engine-oniguruma@3.22.0':
dependencies:
- shiki: 1.10.3
+ '@shikijs/types': 3.22.0
+ '@shikijs/vscode-textmate': 10.0.2
+
+ '@shikijs/langs@3.22.0':
+ dependencies:
+ '@shikijs/types': 3.22.0
+
+ '@shikijs/rehype@3.22.0':
+ dependencies:
+ '@shikijs/types': 3.22.0
+ '@types/hast': 3.0.4
+ hast-util-to-string: 3.0.1
+ shiki: 3.22.0
+ unified: 11.0.5
+ unist-util-visit: 5.1.0
+
+ '@shikijs/themes@3.22.0':
+ dependencies:
+ '@shikijs/types': 3.22.0
+
+ '@shikijs/transformers@3.22.0':
+ dependencies:
+ '@shikijs/core': 3.22.0
+ '@shikijs/types': 3.22.0
+
+ '@shikijs/types@3.22.0':
+ dependencies:
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.4
+
+ '@shikijs/vscode-textmate@10.0.2': {}
'@sindresorhus/merge-streams@4.0.0': {}
@@ -11293,7 +11508,7 @@ snapshots:
'@tanstack/devtools-event-client@0.3.5': {}
- '@tanstack/history@1.154.14': {}
+ '@tanstack/history@file:../start-rsc/packages/history': {}
'@tanstack/pacer@0.16.4':
dependencies:
@@ -11314,74 +11529,100 @@ snapshots:
'@tanstack/query-core': 5.90.12
react: 19.2.3
- '@tanstack/react-router-devtools@1.157.16(@tanstack/react-router@1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.157.16)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
+ '@tanstack/react-router-devtools@file:../start-rsc/packages/react-router-devtools(@tanstack/react-router@file:../start-rsc/packages/react-router(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies:
- '@tanstack/react-router': 1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
- '@tanstack/router-devtools-core': 1.157.16(@tanstack/router-core@1.157.16)(csstype@3.2.3)
+ '@tanstack/react-router': file:../start-rsc/packages/react-router(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@tanstack/router-devtools-core': file:../start-rsc/packages/router-devtools-core(csstype@3.2.3)
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
- optionalDependencies:
- '@tanstack/router-core': 1.157.16
transitivePeerDependencies:
- csstype
- '@tanstack/react-router-ssr-query@1.157.16(@tanstack/query-core@5.90.12)(@tanstack/react-query@5.90.12(react@19.2.3))(@tanstack/react-router@1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.157.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
+ '@tanstack/react-router-ssr-query@file:../start-rsc/packages/react-router-ssr-query(@tanstack/query-core@5.90.12)(@tanstack/react-query@5.90.12(react@19.2.3))(@tanstack/react-router@file:../start-rsc/packages/react-router(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies:
'@tanstack/query-core': 5.90.12
'@tanstack/react-query': 5.90.12(react@19.2.3)
- '@tanstack/react-router': 1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
- '@tanstack/router-ssr-query-core': 1.157.16(@tanstack/query-core@5.90.12)(@tanstack/router-core@1.157.16)
+ '@tanstack/react-router': file:../start-rsc/packages/react-router(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@tanstack/router-ssr-query-core': file:../start-rsc/packages/router-ssr-query-core(@tanstack/query-core@5.90.12)
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
transitivePeerDependencies:
- '@tanstack/router-core'
- '@tanstack/react-router@1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
+ '@tanstack/react-router@file:../start-rsc/packages/react-router(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies:
- '@tanstack/history': 1.154.14
+ '@tanstack/history': file:../start-rsc/packages/history
'@tanstack/react-store': 0.8.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
- '@tanstack/router-core': 1.157.16
+ '@tanstack/router-core': file:../start-rsc/packages/router-core
isbot: 5.1.31
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
tiny-invariant: 1.3.3
tiny-warning: 1.0.3
- '@tanstack/react-start-client@1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
+ '@tanstack/react-start-client@file:../start-rsc/packages/react-start-client(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies:
- '@tanstack/react-router': 1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
- '@tanstack/router-core': 1.157.16
- '@tanstack/start-client-core': 1.157.16
+ '@tanstack/react-router': file:../start-rsc/packages/react-router(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@tanstack/router-core': file:../start-rsc/packages/router-core
+ '@tanstack/start-client-core': file:../start-rsc/packages/start-client-core
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
tiny-invariant: 1.3.3
tiny-warning: 1.0.3
- '@tanstack/react-start-server@1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
+ '@tanstack/react-start-rsc@file:../start-rsc/packages/react-start-rsc(@vitejs/plugin-rsc@0.5.19(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))':
+ dependencies:
+ '@tanstack/react-router': file:../start-rsc/packages/react-router(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@tanstack/react-start-server': file:../start-rsc/packages/react-start-server(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@tanstack/router-core': file:../start-rsc/packages/router-core
+ '@tanstack/router-utils': file:../start-rsc/packages/router-utils
+ '@tanstack/start-client-core': file:../start-rsc/packages/start-client-core
+ '@tanstack/start-fn-stubs': file:../start-rsc/packages/start-fn-stubs
+ '@tanstack/start-plugin-core': file:../start-rsc/packages/start-plugin-core(@tanstack/react-router@file:../start-rsc/packages/react-router(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))
+ '@tanstack/start-server-core': file:../start-rsc/packages/start-server-core
+ '@tanstack/start-storage-context': file:../start-rsc/packages/start-storage-context
+ pathe: 2.0.3
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ react-is: 19.2.4
+ optionalDependencies:
+ '@vitejs/plugin-rsc': 0.5.19(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))
+ transitivePeerDependencies:
+ - '@rsbuild/core'
+ - crossws
+ - supports-color
+ - vite
+ - vite-plugin-solid
+ - webpack
+
+ '@tanstack/react-start-server@file:../start-rsc/packages/react-start-server(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies:
- '@tanstack/history': 1.154.14
- '@tanstack/react-router': 1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
- '@tanstack/router-core': 1.157.16
- '@tanstack/start-client-core': 1.157.16
- '@tanstack/start-server-core': 1.157.16
+ '@tanstack/history': file:../start-rsc/packages/history
+ '@tanstack/react-router': file:../start-rsc/packages/react-router(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@tanstack/router-core': file:../start-rsc/packages/router-core
+ '@tanstack/start-client-core': file:../start-rsc/packages/start-client-core
+ '@tanstack/start-server-core': file:../start-rsc/packages/start-server-core
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
transitivePeerDependencies:
- crossws
- '@tanstack/react-start@1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))':
+ '@tanstack/react-start@file:../start-rsc/packages/react-start(@vitejs/plugin-rsc@0.5.19(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))':
dependencies:
- '@tanstack/react-router': 1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
- '@tanstack/react-start-client': 1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
- '@tanstack/react-start-server': 1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
- '@tanstack/router-utils': 1.154.7
- '@tanstack/start-client-core': 1.157.16
- '@tanstack/start-plugin-core': 1.157.16(@tanstack/react-router@1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))
- '@tanstack/start-server-core': 1.157.16
+ '@tanstack/react-router': file:../start-rsc/packages/react-router(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@tanstack/react-start-client': file:../start-rsc/packages/react-start-client(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@tanstack/react-start-rsc': file:../start-rsc/packages/react-start-rsc(@vitejs/plugin-rsc@0.5.19(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))
+ '@tanstack/react-start-server': file:../start-rsc/packages/react-start-server(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@tanstack/router-utils': file:../start-rsc/packages/router-utils
+ '@tanstack/start-client-core': file:../start-rsc/packages/start-client-core
+ '@tanstack/start-plugin-core': file:../start-rsc/packages/start-plugin-core(@tanstack/react-router@file:../start-rsc/packages/react-router(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))
+ '@tanstack/start-server-core': file:../start-rsc/packages/start-server-core
pathe: 2.0.3
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
vite: 7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)
+ optionalDependencies:
+ '@vitejs/plugin-rsc': 0.5.19(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))
transitivePeerDependencies:
- '@rsbuild/core'
- crossws
@@ -11402,9 +11643,9 @@ snapshots:
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
- '@tanstack/router-core@1.157.16':
+ '@tanstack/router-core@file:../start-rsc/packages/router-core':
dependencies:
- '@tanstack/history': 1.154.14
+ '@tanstack/history': file:../start-rsc/packages/history
'@tanstack/store': 0.8.0
cookie-es: 2.0.0
seroval: 1.5.0
@@ -11412,20 +11653,19 @@ snapshots:
tiny-invariant: 1.3.3
tiny-warning: 1.0.3
- '@tanstack/router-devtools-core@1.157.16(@tanstack/router-core@1.157.16)(csstype@3.2.3)':
+ '@tanstack/router-devtools-core@file:../start-rsc/packages/router-devtools-core(csstype@3.2.3)':
dependencies:
- '@tanstack/router-core': 1.157.16
clsx: 2.1.1
goober: 2.1.16(csstype@3.2.3)
tiny-invariant: 1.3.3
optionalDependencies:
csstype: 3.2.3
- '@tanstack/router-generator@1.157.16':
+ '@tanstack/router-generator@file:../start-rsc/packages/router-generator':
dependencies:
- '@tanstack/router-core': 1.157.16
- '@tanstack/router-utils': 1.154.7
- '@tanstack/virtual-file-routes': 1.154.7
+ '@tanstack/router-core': file:../start-rsc/packages/router-core
+ '@tanstack/router-utils': file:../start-rsc/packages/router-utils
+ '@tanstack/virtual-file-routes': file:../start-rsc/packages/virtual-file-routes
prettier: 3.7.4
recast: 0.23.11
source-map: 0.7.6
@@ -11434,34 +11674,33 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@tanstack/router-plugin@1.157.16(@tanstack/react-router@1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))':
+ '@tanstack/router-plugin@file:../start-rsc/packages/router-plugin(@tanstack/react-router@file:../start-rsc/packages/react-router(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))':
dependencies:
'@babel/core': 7.29.0
'@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.29.0)
'@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.29.0)
- '@babel/template': 7.27.2
+ '@babel/template': 7.28.6
'@babel/traverse': 7.29.0
- '@babel/types': 7.28.5
- '@tanstack/router-core': 1.157.16
- '@tanstack/router-generator': 1.157.16
- '@tanstack/router-utils': 1.154.7
- '@tanstack/virtual-file-routes': 1.154.7
+ '@babel/types': 7.29.0
+ '@tanstack/router-core': file:../start-rsc/packages/router-core
+ '@tanstack/router-generator': file:../start-rsc/packages/router-generator
+ '@tanstack/router-utils': file:../start-rsc/packages/router-utils
+ '@tanstack/virtual-file-routes': file:../start-rsc/packages/virtual-file-routes
babel-dead-code-elimination: 1.0.12
chokidar: 3.6.0
unplugin: 2.3.10
zod: 3.25.76
optionalDependencies:
- '@tanstack/react-router': 1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@tanstack/react-router': file:../start-rsc/packages/react-router(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
vite: 7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)
transitivePeerDependencies:
- supports-color
- '@tanstack/router-ssr-query-core@1.157.16(@tanstack/query-core@5.90.12)(@tanstack/router-core@1.157.16)':
+ '@tanstack/router-ssr-query-core@file:../start-rsc/packages/router-ssr-query-core(@tanstack/query-core@5.90.12)':
dependencies:
'@tanstack/query-core': 5.90.12
- '@tanstack/router-core': 1.157.16
- '@tanstack/router-utils@1.154.7':
+ '@tanstack/router-utils@file:../start-rsc/packages/router-utils':
dependencies:
'@babel/core': 7.29.0
'@babel/generator': 7.29.0
@@ -11473,29 +11712,29 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@tanstack/start-client-core@1.157.16':
+ '@tanstack/start-client-core@file:../start-rsc/packages/start-client-core':
dependencies:
- '@tanstack/router-core': 1.157.16
- '@tanstack/start-fn-stubs': 1.154.7
- '@tanstack/start-storage-context': 1.157.16
+ '@tanstack/router-core': file:../start-rsc/packages/router-core
+ '@tanstack/start-fn-stubs': file:../start-rsc/packages/start-fn-stubs
+ '@tanstack/start-storage-context': file:../start-rsc/packages/start-storage-context
seroval: 1.5.0
tiny-invariant: 1.3.3
tiny-warning: 1.0.3
- '@tanstack/start-fn-stubs@1.154.7': {}
+ '@tanstack/start-fn-stubs@file:../start-rsc/packages/start-fn-stubs': {}
- '@tanstack/start-plugin-core@1.157.16(@tanstack/react-router@1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))':
+ '@tanstack/start-plugin-core@file:../start-rsc/packages/start-plugin-core(@tanstack/react-router@file:../start-rsc/packages/react-router(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))':
dependencies:
'@babel/code-frame': 7.27.1
'@babel/core': 7.29.0
- '@babel/types': 7.28.5
+ '@babel/types': 7.29.0
'@rolldown/pluginutils': 1.0.0-beta.40
- '@tanstack/router-core': 1.157.16
- '@tanstack/router-generator': 1.157.16
- '@tanstack/router-plugin': 1.157.16(@tanstack/react-router@1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))
- '@tanstack/router-utils': 1.154.7
- '@tanstack/start-client-core': 1.157.16
- '@tanstack/start-server-core': 1.157.16
+ '@tanstack/router-core': file:../start-rsc/packages/router-core
+ '@tanstack/router-generator': file:../start-rsc/packages/router-generator
+ '@tanstack/router-plugin': file:../start-rsc/packages/router-plugin(@tanstack/react-router@file:../start-rsc/packages/react-router(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))
+ '@tanstack/router-utils': file:../start-rsc/packages/router-utils
+ '@tanstack/start-client-core': file:../start-rsc/packages/start-client-core
+ '@tanstack/start-server-core': file:../start-rsc/packages/start-server-core
babel-dead-code-elimination: 1.0.12
cheerio: 1.1.2
exsolve: 1.0.7
@@ -11515,27 +11754,27 @@ snapshots:
- vite-plugin-solid
- webpack
- '@tanstack/start-server-core@1.157.16':
+ '@tanstack/start-server-core@file:../start-rsc/packages/start-server-core':
dependencies:
- '@tanstack/history': 1.154.14
- '@tanstack/router-core': 1.157.16
- '@tanstack/start-client-core': 1.157.16
- '@tanstack/start-storage-context': 1.157.16
- h3-v2: h3@2.0.1-rc.11
+ '@tanstack/history': file:../start-rsc/packages/history
+ '@tanstack/router-core': file:../start-rsc/packages/router-core
+ '@tanstack/start-client-core': file:../start-rsc/packages/start-client-core
+ '@tanstack/start-storage-context': file:../start-rsc/packages/start-storage-context
+ h3-v2: h3@2.0.1-rc.7
seroval: 1.5.0
tiny-invariant: 1.3.3
transitivePeerDependencies:
- crossws
- '@tanstack/start-storage-context@1.157.16':
+ '@tanstack/start-storage-context@file:../start-rsc/packages/start-storage-context':
dependencies:
- '@tanstack/router-core': 1.157.16
+ '@tanstack/router-core': file:../start-rsc/packages/router-core
'@tanstack/store@0.8.0': {}
'@tanstack/table-core@8.21.3': {}
- '@tanstack/virtual-file-routes@1.154.7': {}
+ '@tanstack/virtual-file-routes@file:../start-rsc/packages/virtual-file-routes': {}
'@tweenjs/tween.js@23.1.3': {}
@@ -11696,6 +11935,10 @@ snapshots:
'@types/draco3d@1.4.10': {}
+ '@types/estree-jsx@1.0.5':
+ dependencies:
+ '@types/estree': 1.0.8
+
'@types/estree@1.0.8': {}
'@types/express-serve-static-core@5.1.1':
@@ -11818,6 +12061,8 @@ snapshots:
'@types/trusted-types@2.0.7':
optional: true
+ '@types/unist@2.0.11': {}
+
'@types/unist@3.0.3': {}
'@types/webxr@0.5.24': {}
@@ -11999,9 +12244,24 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@vitejs/plugin-rsc@0.5.19(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))':
+ dependencies:
+ '@rolldown/pluginutils': 1.0.0-rc.2
+ es-module-lexer: 2.0.0
+ estree-walker: 3.0.3
+ magic-string: 0.30.21
+ periscopic: 4.0.2
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ srvx: 0.10.1
+ strip-literal: 3.1.0
+ turbo-stream: 3.1.0
+ vite: 7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)
+ vitefu: 1.1.1(vite@7.1.7(@types/node@24.3.0)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))
+
'@vue/compiler-core@3.5.22':
dependencies:
- '@babel/parser': 7.28.4
+ '@babel/parser': 7.29.0
'@vue/shared': 3.5.22
entities: 4.5.0
estree-walker: 2.0.2
@@ -12014,7 +12274,7 @@ snapshots:
'@vue/compiler-sfc@3.5.22':
dependencies:
- '@babel/parser': 7.28.4
+ '@babel/parser': 7.29.0
'@vue/compiler-core': 3.5.22
'@vue/compiler-dom': 3.5.22
'@vue/compiler-ssr': 3.5.22
@@ -12309,9 +12569,9 @@ snapshots:
babel-dead-code-elimination@1.0.12:
dependencies:
'@babel/core': 7.29.0
- '@babel/parser': 7.28.4
- '@babel/traverse': 7.28.4
- '@babel/types': 7.28.5
+ '@babel/parser': 7.29.0
+ '@babel/traverse': 7.29.0
+ '@babel/types': 7.29.0
transitivePeerDependencies:
- supports-color
@@ -12468,6 +12728,8 @@ snapshots:
character-entities@2.0.2: {}
+ character-reference-invalid@2.0.1: {}
+
cheerio-select@2.1.0:
dependencies:
boolbase: 1.0.0
@@ -13326,6 +13588,8 @@ snapshots:
es-module-lexer@1.7.0: {}
+ es-module-lexer@2.0.0: {}
+
es-object-atoms@1.1.1:
dependencies:
es-errors: 1.3.0
@@ -13620,8 +13884,14 @@ snapshots:
estraverse@5.3.0: {}
+ estree-util-is-identifier-name@3.0.0: {}
+
estree-walker@2.0.2: {}
+ estree-walker@3.0.3:
+ dependencies:
+ '@types/estree': 1.0.8
+
esutils@2.0.3: {}
etag@1.8.1: {}
@@ -14024,7 +14294,7 @@ snapshots:
ufo: 1.6.1
uncrypto: 0.1.3
- h3@2.0.1-rc.11:
+ h3@2.0.1-rc.7:
dependencies:
rou3: 0.7.12
srvx: 0.10.1
@@ -14119,6 +14389,26 @@ snapshots:
stringify-entities: 4.0.4
zwitch: 2.0.4
+ hast-util-to-jsx-runtime@2.3.6:
+ dependencies:
+ '@types/estree': 1.0.8
+ '@types/hast': 3.0.4
+ '@types/unist': 3.0.3
+ comma-separated-tokens: 2.0.3
+ devlop: 1.1.0
+ estree-util-is-identifier-name: 3.0.0
+ hast-util-whitespace: 3.0.0
+ mdast-util-mdx-expression: 2.0.1
+ mdast-util-mdx-jsx: 3.2.0
+ mdast-util-mdxjs-esm: 2.0.1
+ property-information: 7.1.0
+ space-separated-tokens: 2.0.2
+ style-to-js: 1.1.12
+ unist-util-position: 5.0.0
+ vfile-message: 4.0.3
+ transitivePeerDependencies:
+ - supports-color
+
hast-util-to-parse5@8.0.0:
dependencies:
'@types/hast': 3.0.4
@@ -14370,6 +14660,13 @@ snapshots:
iron-webcrypto@1.2.1: {}
+ is-alphabetical@2.0.1: {}
+
+ is-alphanumerical@2.0.1:
+ dependencies:
+ is-alphabetical: 2.0.1
+ is-decimal: 2.0.1
+
is-array-buffer@3.0.4:
dependencies:
call-bind: 1.0.7
@@ -14430,6 +14727,8 @@ snapshots:
call-bound: 1.0.4
has-tostringtag: 1.0.2
+ is-decimal@2.0.1: {}
+
is-docker@2.2.1: {}
is-docker@3.0.0: {}
@@ -14452,6 +14751,8 @@ snapshots:
dependencies:
is-extglob: 2.1.1
+ is-hexadecimal@2.0.1: {}
+
is-inside-container@1.0.0:
dependencies:
is-docker: 3.0.0
@@ -14485,6 +14786,10 @@ snapshots:
is-promise@4.0.0: {}
+ is-reference@3.0.3:
+ dependencies:
+ '@types/estree': 1.0.8
+
is-regex@1.1.4:
dependencies:
call-bind: 1.0.7
@@ -14627,6 +14932,8 @@ snapshots:
js-tokens@4.0.0: {}
+ js-tokens@9.0.1: {}
+
js-yaml@3.14.2:
dependencies:
argparse: 1.0.10
@@ -14909,6 +15216,10 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
+ magic-string@0.30.21:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+
magic-string@0.30.8:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -15007,6 +15318,45 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ mdast-util-mdx-expression@2.0.1:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ '@types/hast': 3.0.4
+ '@types/mdast': 4.0.4
+ devlop: 1.1.0
+ mdast-util-from-markdown: 2.0.2
+ mdast-util-to-markdown: 2.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-mdx-jsx@3.2.0:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ '@types/hast': 3.0.4
+ '@types/mdast': 4.0.4
+ '@types/unist': 3.0.3
+ ccount: 2.0.1
+ devlop: 1.1.0
+ mdast-util-from-markdown: 2.0.2
+ mdast-util-to-markdown: 2.1.2
+ parse-entities: 4.0.2
+ stringify-entities: 4.0.4
+ unist-util-stringify-position: 4.0.0
+ vfile-message: 4.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-mdxjs-esm@2.0.1:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ '@types/hast': 3.0.4
+ '@types/mdast': 4.0.4
+ devlop: 1.1.0
+ mdast-util-from-markdown: 2.0.2
+ mdast-util-to-markdown: 2.1.2
+ transitivePeerDependencies:
+ - supports-color
+
mdast-util-phrasing@4.1.0:
dependencies:
'@types/mdast': 4.0.4
@@ -15021,7 +15371,7 @@ snapshots:
micromark-util-sanitize-uri: 2.0.1
trim-lines: 3.0.1
unist-util-position: 5.0.0
- unist-util-visit: 5.0.0
+ unist-util-visit: 5.1.0
vfile: 6.0.3
mdast-util-to-markdown@2.1.2:
@@ -15033,7 +15383,7 @@ snapshots:
mdast-util-to-string: 4.0.0
micromark-util-classify-character: 2.0.1
micromark-util-decode-string: 2.0.1
- unist-util-visit: 5.0.0
+ unist-util-visit: 5.1.0
zwitch: 2.0.4
mdast-util-to-string@4.0.0:
@@ -15421,7 +15771,7 @@ snapshots:
node-source-walk@7.0.1:
dependencies:
- '@babel/parser': 7.28.4
+ '@babel/parser': 7.29.0
node-stream-zip@1.15.0: {}
@@ -15556,6 +15906,14 @@ snapshots:
dependencies:
mimic-fn: 4.0.0
+ oniguruma-parser@0.12.1: {}
+
+ oniguruma-to-es@4.3.4:
+ dependencies:
+ oniguruma-parser: 0.12.1
+ regex: 6.1.0
+ regex-recursion: 6.0.2
+
open@7.4.2:
dependencies:
is-docker: 2.2.1
@@ -15624,6 +15982,16 @@ snapshots:
dependencies:
callsites: 3.1.0
+ parse-entities@4.0.2:
+ dependencies:
+ '@types/unist': 2.0.11
+ character-entities-legacy: 3.0.0
+ character-reference-invalid: 2.0.1
+ decode-named-character-reference: 1.2.0
+ is-alphanumerical: 2.0.1
+ is-decimal: 2.0.1
+ is-hexadecimal: 2.0.1
+
parse-gitignore@2.0.0: {}
parse-imports@2.2.1:
@@ -15708,6 +16076,12 @@ snapshots:
pend@1.2.0: {}
+ periscopic@4.0.2:
+ dependencies:
+ '@types/estree': 1.0.8
+ is-reference: 3.0.3
+ zimmerframe: 1.1.4
+
pg-int8@1.0.1: {}
pg-numeric@1.0.2: {}
@@ -15968,6 +16342,8 @@ snapshots:
react-is@16.13.1: {}
+ react-is@19.2.4: {}
+
react-property@2.0.2: {}
react-refresh@0.14.2: {}
@@ -16082,6 +16458,16 @@ snapshots:
regenerator-runtime@0.14.1: {}
+ regex-recursion@6.0.2:
+ dependencies:
+ regex-utilities: 2.3.0
+
+ regex-utilities@2.3.0: {}
+
+ regex@6.1.0:
+ dependencies:
+ regex-utilities: 2.3.0
+
regexp.prototype.flags@1.5.2:
dependencies:
call-bind: 1.0.7
@@ -16127,6 +16513,14 @@ snapshots:
hast-util-raw: 9.1.0
vfile: 6.0.3
+ rehype-react@8.0.0:
+ dependencies:
+ '@types/hast': 3.0.4
+ hast-util-to-jsx-runtime: 2.3.6
+ unified: 11.0.5
+ transitivePeerDependencies:
+ - supports-color
+
rehype-slug@6.0.0:
dependencies:
'@types/hast': 3.0.4
@@ -16462,9 +16856,15 @@ snapshots:
shell-quote@1.8.1: {}
- shiki@1.10.3:
+ shiki@3.22.0:
dependencies:
- '@shikijs/core': 1.10.3
+ '@shikijs/core': 3.22.0
+ '@shikijs/engine-javascript': 3.22.0
+ '@shikijs/engine-oniguruma': 3.22.0
+ '@shikijs/langs': 3.22.0
+ '@shikijs/themes': 3.22.0
+ '@shikijs/types': 3.22.0
+ '@shikijs/vscode-textmate': 10.0.2
'@types/hast': 3.0.4
side-channel-list@1.0.0:
@@ -16690,6 +17090,10 @@ snapshots:
strip-json-comments@3.1.1: {}
+ strip-literal@3.1.0:
+ dependencies:
+ js-tokens: 9.0.1
+
style-to-js@1.1.12:
dependencies:
style-to-object: 1.0.6
@@ -16890,6 +17294,8 @@ snapshots:
- immer
- react
+ turbo-stream@3.1.0: {}
+
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1
@@ -17043,6 +17449,12 @@ snapshots:
unist-util-is: 6.0.1
unist-util-visit-parents: 6.0.2
+ unist-util-visit@5.1.0:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-is: 6.0.1
+ unist-util-visit-parents: 6.0.2
+
universal-user-agent@6.0.0: {}
unixify@1.0.0:
@@ -17427,6 +17839,8 @@ snapshots:
yoctocolors@2.1.2: {}
+ zimmerframe@1.1.4: {}
+
zip-stream@6.0.1:
dependencies:
archiver-utils: 5.0.2
diff --git a/src/components/Breadcrumbs.tsx b/src/components/Breadcrumbs.tsx
index 70e5ae5b0..f12f031eb 100644
--- a/src/components/Breadcrumbs.tsx
+++ b/src/components/Breadcrumbs.tsx
@@ -1,7 +1,7 @@
import { Link } from '@tanstack/react-router'
import { ChevronDown } from 'lucide-react'
import { twMerge } from 'tailwind-merge'
-import type { MarkdownHeading } from '~/utils/markdown/processor'
+import type { MarkdownHeading } from '~/utils/markdown/types'
import {
Dropdown,
DropdownTrigger,
@@ -43,7 +43,7 @@ export function Breadcrumbs({
)}
{showTocToggle && (
-
+
>
+ /** Pre-highlighted HTML by framework (from server-side Shiki) */
+ codeByFramework: Partial>
}
export function CodeExampleCard({
@@ -32,9 +33,12 @@ export function CodeExampleCard({
value={framework}
onChange={setFramework}
/>
-
- {selected.code}
-
+
diff --git a/src/components/CodeExplorer.tsx b/src/components/CodeExplorer.tsx
index 9bf364fa8..d57008f4d 100644
--- a/src/components/CodeExplorer.tsx
+++ b/src/components/CodeExplorer.tsx
@@ -1,5 +1,5 @@
import React from 'react'
-import { CodeBlock } from '~/components/markdown'
+import { HighlightedCodeBlock } from '~/components/markdown'
import { FileExplorer } from './FileExplorer'
import { InteractiveSandbox } from './InteractiveSandbox'
import { CodeExplorerTopBar } from './CodeExplorerTopBar'
@@ -7,38 +7,41 @@ import type { GitHubFileNode } from '~/utils/documents.server'
import type { Library } from '~/libraries'
import { twMerge } from 'tailwind-merge'
-function overrideExtension(ext: string | undefined) {
- if (!ext) return 'txt'
+function getLanguageFromPath(path: string): string {
+ const ext = path.split('.').pop() || ''
- // Override some extensions
- if (['cts', 'mts'].includes(ext)) return 'ts'
- if (['cjs', 'mjs'].includes(ext)) return 'js'
- if (['prettierrc', 'babelrc', 'webmanifest'].includes(ext)) return 'json'
- if (['env', 'example'].includes(ext)) return 'sh'
- if (
- [
- 'gitignore',
- 'prettierignore',
- 'log',
- 'gitattributes',
- 'editorconfig',
- 'lock',
- 'opts',
- 'Dockerfile',
- 'dockerignore',
- 'npmrc',
- 'nvmrc',
- ].includes(ext)
- )
- return 'txt'
+ const langMap: Record = {
+ ts: 'typescript',
+ tsx: 'tsx',
+ js: 'javascript',
+ jsx: 'jsx',
+ mts: 'typescript',
+ cts: 'typescript',
+ mjs: 'javascript',
+ cjs: 'javascript',
+ json: 'json',
+ md: 'markdown',
+ html: 'html',
+ css: 'css',
+ scss: 'scss',
+ yaml: 'yaml',
+ yml: 'yaml',
+ toml: 'toml',
+ sh: 'bash',
+ bash: 'bash',
+ sql: 'sql',
+ vue: 'vue',
+ svelte: 'svelte',
+ }
- return ext
+ return langMap[ext] || 'text'
}
interface CodeExplorerProps {
activeTab: 'code' | 'sandbox'
codeSandboxUrl: string
- currentCode: string
+ /** Pre-highlighted HTML from server-side Shiki */
+ currentCodeHtml: string
currentPath: string
examplePath: string
githubContents: GitHubFileNode[] | undefined
@@ -52,7 +55,7 @@ interface CodeExplorerProps {
export function CodeExplorer({
activeTab,
codeSandboxUrl,
- currentCode,
+ currentCodeHtml,
currentPath,
examplePath,
githubContents,
@@ -85,6 +88,8 @@ export function CodeExplorer({
return () => window.removeEventListener('closeSidebar', handleCloseSidebar)
}, [])
+ const lang = getLanguageFromPath(currentPath)
+
return (
-
-
- {currentCode}
-
-
+ />
renderMarkdown(content),
- [content],
- )
-
// Get current framework from prop, URL params, or local storage
const { framework: paramsFramework } = useParams({ strict: false })
const localCurrentFramework = useLocalCurrentFramework()
@@ -159,7 +155,7 @@ export function Doc({
repo={repo}
branch={branch}
filePath={filePath}
- htmlMarkup={markup}
+ contentRsc={contentRsc}
containerRef={markdownContainerRef}
libraryId={libraryId}
libraryVersion={libraryVersion}
diff --git a/src/components/DocBreadcrumb.tsx b/src/components/DocBreadcrumb.tsx
index 7d60a380d..dc3ec8ba6 100644
--- a/src/components/DocBreadcrumb.tsx
+++ b/src/components/DocBreadcrumb.tsx
@@ -1,7 +1,7 @@
import { useParams } from '@tanstack/react-router'
import { Breadcrumbs } from './Breadcrumbs'
import type { ConfigSchema } from '~/utils/config'
-import type { MarkdownHeading } from '~/utils/markdown/processor'
+import type { MarkdownHeading } from '~/utils/markdown/types'
function findSectionForDoc(
config: ConfigSchema,
diff --git a/src/components/FeedEntry.tsx b/src/components/FeedEntry.tsx
index 1e23dcc5a..90a177326 100644
--- a/src/components/FeedEntry.tsx
+++ b/src/components/FeedEntry.tsx
@@ -1,5 +1,6 @@
import { format, formatDistanceToNow } from '~/utils/dates'
-import { Markdown } from '~/components/markdown'
+// TODO: Fix feed to use server-rendered markdown
+// import { Markdown } from '~/components/markdown'
import { libraries } from '~/libraries'
import { partners } from '~/utils/partners'
import { twMerge } from 'tailwind-merge'
@@ -415,7 +416,8 @@ export function FeedEntry({
{/* Content */}
-
+ {/* TODO: Fix feed to use server-rendered markdown */}
+
{entry.content}
{/* External Link */}
diff --git a/src/components/FeedEntryTimeline.tsx b/src/components/FeedEntryTimeline.tsx
index 67ddb2b89..56dde739a 100644
--- a/src/components/FeedEntryTimeline.tsx
+++ b/src/components/FeedEntryTimeline.tsx
@@ -1,6 +1,7 @@
import * as React from 'react'
import { format, formatDistanceToNow } from '~/utils/dates'
-import { Markdown } from '~/components/markdown'
+// TODO: Fix feed to use server-rendered markdown
+// import { Markdown } from '~/components/markdown'
import { Card } from '~/components/Card'
import { libraries } from '~/libraries'
import { partners } from '~/utils/partners'
@@ -333,7 +334,8 @@ export function FeedEntryTimeline({
!expanded && 'line-clamp-6',
)}
>
-
+ {/* TODO: Fix feed to use server-rendered markdown */}
+ {entry.content}
{/* Show more/less button */}
diff --git a/src/components/SearchModal.tsx b/src/components/SearchModal.tsx
index 7b55935db..2cfc8a2ff 100644
--- a/src/components/SearchModal.tsx
+++ b/src/components/SearchModal.tsx
@@ -566,7 +566,7 @@ function LibraryRefinement() {
return (
-
+
{currentLibrary ? (
@@ -638,7 +638,7 @@ function FrameworkRefinement() {
return (
-
+
{currentFramework && (
component for documentation with code blocks.
+ * Expects pre-rendered HTML markup from the server.
*/
const markdownComponents: Record> = {
@@ -49,31 +50,15 @@ const options: HTMLReactParserOptions = {
}
type SimpleMarkdownProps = {
- rawContent?: string
- htmlMarkup?: string
+ htmlMarkup: string
}
-export function SimpleMarkdown({
- rawContent,
- htmlMarkup,
-}: SimpleMarkdownProps) {
- const rendered = React.useMemo(() => {
- if (rawContent) {
- return renderMarkdown(rawContent)
- }
-
- if (htmlMarkup) {
- return { markup: htmlMarkup, headings: [] }
- }
-
- return { markup: '', headings: [] }
- }, [rawContent, htmlMarkup])
-
+export function SimpleMarkdown({ htmlMarkup }: SimpleMarkdownProps) {
return React.useMemo(() => {
- if (!rendered.markup) {
+ if (!htmlMarkup) {
return null
}
- return parse(rendered.markup, options)
- }, [rendered.markup])
+ return parse(htmlMarkup, options)
+ }, [htmlMarkup])
}
diff --git a/src/components/ToastProvider.tsx b/src/components/ToastProvider.tsx
index e34d4889e..5edd780de 100644
--- a/src/components/ToastProvider.tsx
+++ b/src/components/ToastProvider.tsx
@@ -1,3 +1,5 @@
+'use client'
+
import * as React from 'react'
import * as Toast from '@radix-ui/react-toast'
diff --git a/src/components/Toc.tsx b/src/components/Toc.tsx
index a7f2e70a4..40c9b2823 100644
--- a/src/components/Toc.tsx
+++ b/src/components/Toc.tsx
@@ -1,7 +1,7 @@
import { Link } from '@tanstack/react-router'
import * as React from 'react'
import { twMerge } from 'tailwind-merge'
-import { MarkdownHeading } from '~/utils/markdown/processor'
+import type { MarkdownHeading } from '~/utils/markdown/types'
const headingLevels: Record = {
1: '',
diff --git a/src/components/TocMobile.tsx b/src/components/TocMobile.tsx
index 3c8d6cc62..6880f925f 100644
--- a/src/components/TocMobile.tsx
+++ b/src/components/TocMobile.tsx
@@ -1,6 +1,6 @@
import React from 'react'
import { Link } from '@tanstack/react-router'
-import { MarkdownHeading } from '~/utils/markdown/processor'
+import type { MarkdownHeading } from '~/utils/markdown/types'
import { ChevronDown, ChevronRight } from 'lucide-react'
interface TocMobileProps {
diff --git a/src/components/admin/FeedEntryEditor.tsx b/src/components/admin/FeedEntryEditor.tsx
index 8705ad5db..fd2fb351a 100644
--- a/src/components/admin/FeedEntryEditor.tsx
+++ b/src/components/admin/FeedEntryEditor.tsx
@@ -2,7 +2,8 @@ import * as React from 'react'
import { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { FeedEntry } from '~/components/FeedEntry'
-import { Markdown } from '~/components/markdown'
+// TODO: Fix feed editor to use server-rendered markdown
+// import { Markdown } from '~/components/markdown'
import { libraries } from '~/libraries'
import { partners } from '~/utils/partners'
import { currentUserQueryOptions } from '~/queries/auth'
@@ -475,7 +476,8 @@ export function FeedEntryEditor({
{excerpt}
)}
-
+ {/* TODO: Fix feed editor to use server-rendered markdown */}
+ {content || '*No content yet*'}
) : (
diff --git a/src/components/markdown/BlogContent.tsx b/src/components/markdown/BlogContent.tsx
new file mode 100644
index 000000000..50c8a0f77
--- /dev/null
+++ b/src/components/markdown/BlogContent.tsx
@@ -0,0 +1,18 @@
+// Server component for blog markdown content
+// This file does NOT have 'use client' - it's a server component.
+// The children are JSX elements produced by renderMarkdownToJsx on the server,
+// which already include client component references for interactive elements.
+
+import type { ReactNode } from 'react'
+
+type BlogContentProps = {
+ children: ReactNode
+}
+
+export function BlogContent({ children }: BlogContentProps) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/components/markdown/CodeBlock.tsx b/src/components/markdown/CodeBlock.tsx
index 2aefd0616..4fdb9f2b0 100644
--- a/src/components/markdown/CodeBlock.tsx
+++ b/src/components/markdown/CodeBlock.tsx
@@ -1,252 +1,265 @@
+'use client'
+
import * as React from 'react'
import { twMerge } from 'tailwind-merge'
import { useToast } from '~/components/ToastProvider'
import { Copy } from 'lucide-react'
-import type { Mermaid } from 'mermaid'
-import { transformerNotationDiff } from '@shikijs/transformers'
-import { createHighlighter, type HighlighterGeneric } from 'shiki'
import { Button } from '~/ui'
-// Language aliases mapping
-const LANG_ALIASES: Record
= {
- ts: 'typescript',
- js: 'javascript',
- sh: 'bash',
- shell: 'bash',
- console: 'bash',
- zsh: 'bash',
- cmd: 'bash',
- md: 'markdown',
- txt: 'plaintext',
- text: 'plaintext',
- yml: 'yaml',
- json5: 'jsonc',
- eslintrc: 'jsonc',
-}
-
-// Lazy highlighter singleton
-let highlighterPromise: Promise> | null = null
-let mermaidInstance: Mermaid | null = null
-const genSvgMap = new Map()
-const failedLanguages = new Set()
-
-async function getHighlighter(language: string): Promise<{
- highlighter: HighlighterGeneric
- effectiveLang: string
-}> {
- if (!highlighterPromise) {
- highlighterPromise = createHighlighter({
- themes: ['github-light', 'vitesse-dark'],
- langs: [
- 'typescript',
- 'javascript',
- 'tsx',
- 'jsx',
- 'bash',
- 'json',
- 'html',
- 'css',
- 'markdown',
- 'plaintext',
- 'toml',
- 'yaml',
- 'sql',
- 'diff',
- 'vue',
- 'svelte',
- 'scss',
- 'jsonc',
- 'vue-html',
- 'angular-html',
- 'angular-ts',
- ],
- })
- }
-
- const highlighter = await highlighterPromise
- const normalizedLang = LANG_ALIASES[language] || language
- const langToLoad = normalizedLang === 'mermaid' ? 'plaintext' : normalizedLang
-
- // Return plaintext for known failed languages
- if (failedLanguages.has(langToLoad)) {
- return { highlighter, effectiveLang: 'plaintext' }
- }
-
- // Load language if not already loaded
- if (!highlighter.getLoadedLanguages().includes(langToLoad as any)) {
- try {
- await highlighter.loadLanguage(langToLoad as any)
- } catch {
- console.warn(`Shiki: Language "${langToLoad}" not found, using plaintext`)
- failedLanguages.add(langToLoad)
- return { highlighter, effectiveLang: 'plaintext' }
- }
- }
-
- return { highlighter, effectiveLang: langToLoad }
-}
-
-// Lazy load mermaid only when needed
-async function getMermaid(): Promise {
- if (!mermaidInstance) {
- const { default: mermaid } = await import('mermaid')
- mermaid.initialize({ startOnLoad: false, securityLevel: 'loose' })
- mermaidInstance = mermaid
- }
- return mermaidInstance
-}
-
-function extractPreAttributes(html: string): {
- class: string | null
- style: string | null
-} {
- const match = html.match(/]*)>/i)
- if (!match) {
- return { class: null, style: null }
- }
-
- const attributes = match[1]
-
- const classMatch = attributes.match(/\bclass\s*=\s*["']([^"']*)["']/i)
- const styleMatch = attributes.match(/\bstyle\s*=\s*["']([^"']*)["']/i)
-
- return {
- class: classMatch ? classMatch[1] : null,
- style: styleMatch ? styleMatch[1] : null,
- }
+type CodeBlockProps = React.HTMLProps & {
+ /** Optional title to display above code block */
+ 'data-code-title'?: string
+ /** Whether to show the copy button */
+ showCopyButton?: boolean
+ /** Whether the code block is embedded (e.g., in a file explorer) */
+ isEmbedded?: boolean
}
+/**
+ * CodeBlock wraps pre-highlighted HTML from server-side Shiki with a copy button.
+ * Used by html-react-parser to replace elements in rendered markdown.
+ */
export function CodeBlock({
+ children,
+ className,
+ style,
+ 'data-code-title': dataCodeTitle,
+ showCopyButton = true,
isEmbedded,
- showTypeCopyButton = true,
...props
-}: React.HTMLProps & {
- isEmbedded?: boolean
- showTypeCopyButton?: boolean
- dataCodeTitle?: string
-}) {
- // Extract title from data-code-title attribute, handling both camelCase and kebab-case
- const rawTitle = ((props as any)?.dataCodeTitle ||
- (props as any)?.['data-code-title']) as string | undefined
+}: CodeBlockProps) {
+ const [copied, setCopied] = React.useState(false)
+ const ref = React.useRef(null)
+ const { notify } = useToast()
- // Filter out "undefined" strings, null, and empty strings
+ // Extract title from data attribute
const title =
- rawTitle && rawTitle !== 'undefined' && rawTitle.trim().length > 0
- ? rawTitle.trim()
+ dataCodeTitle && dataCodeTitle !== 'undefined' && dataCodeTitle.trim()
+ ? dataCodeTitle.trim()
: undefined
- const childElement = props.children as
- | undefined
- | { props?: { className?: string; children?: string } }
- const lang = childElement?.props?.className?.replace('language-', '')
+ // Try to extract language from className (e.g., "shiki shiki-themes" or "language-typescript")
+ const lang = React.useMemo(() => {
+ if (!className) return ''
+ // Look for language-* class
+ const langMatch = className.match(/language-(\w+)/)
+ if (langMatch) return langMatch[1]
+ return ''
+ }, [className])
- const children = props.children as
- | undefined
- | {
- props: {
- children: string
- }
- }
+ const handleCopy = React.useCallback(() => {
+ const copyContent = ref.current?.innerText?.trimEnd() || ''
- const [copied, setCopied] = React.useState(false)
- const ref = React.useRef(null)
- const { notify } = useToast()
+ navigator.clipboard.writeText(copyContent)
+ setCopied(true)
+ setTimeout(() => setCopied(false), 2000)
+ notify(
+
+ Copied code
+
+ Code block copied to clipboard
+
+
,
+ )
+ }, [notify])
- const code = children?.props.children
+ // Display language in header
+ const displayLang = lang?.toLowerCase() === 'bash' ? 'sh' : lang
- const [codeElement, setCodeElement] = React.useState(
-
- {lang === 'mermaid' ? : code}
- ,
- )
+ return (
+
+ {(title || showCopyButton) && (
+
+
+ {title || displayLang}
+
- React[
- typeof document !== 'undefined' ? 'useLayoutEffect' : 'useEffect'
- ](() => {
- ;(async () => {
- const themes = ['github-light', 'vitesse-dark']
- const langStr = lang || 'plaintext'
+
+ {copied ? (
+ Copied!
+ ) : (
+
+ )}
+
+
+ )}
+
+ {children}
+
+
+ )
+}
- const { highlighter, effectiveLang } = await getHighlighter(langStr)
- // Trim trailing newlines to prevent empty lines at end of code block
- const trimmedCode = (code || '').trimEnd()
+type HighlightedCodeBlockProps = {
+ /** Pre-highlighted HTML from server-side Shiki */
+ html: string
+ /** Optional title to display above code block */
+ title?: string
+ /** Language for display in header */
+ lang?: string
+ /** Whether to show the copy button */
+ showCopyButton?: boolean
+ /** Whether the code block is embedded (e.g., in a file explorer) */
+ isEmbedded?: boolean
+ className?: string
+ style?: React.CSSProperties
+}
- const htmls = await Promise.all(
- themes.map(async (theme) => {
- const output = highlighter.codeToHtml(trimmedCode, {
- lang: effectiveLang,
- theme,
- transformers: [transformerNotationDiff()],
- })
+/**
+ * HighlightedCodeBlock renders pre-highlighted HTML from server-side Shiki.
+ * Use this for code blocks outside of markdown (e.g., CodeExplorer, landing pages).
+ */
+export function HighlightedCodeBlock({
+ html,
+ title,
+ lang,
+ showCopyButton = true,
+ isEmbedded,
+ className,
+ style,
+}: HighlightedCodeBlockProps) {
+ const [copied, setCopied] = React.useState(false)
+ const ref = React.useRef(null)
+ const { notify } = useToast()
- if (lang === 'mermaid') {
- const preAttributes = extractPreAttributes(output)
- let svgHtml = genSvgMap.get(trimmedCode)
- if (!svgHtml) {
- const mermaid = await getMermaid()
- const { svg } = await mermaid.render('foo', trimmedCode)
- genSvgMap.set(trimmedCode, svg)
- svgHtml = svg
- }
- return `${svgHtml}
`
- }
+ const handleCopy = React.useCallback(() => {
+ const copyContent = ref.current?.innerText?.trimEnd() || ''
- return output
- }),
- )
+ navigator.clipboard.writeText(copyContent)
+ setCopied(true)
+ setTimeout(() => setCopied(false), 2000)
+ notify(
+
+ Copied code
+
+ Code block copied to clipboard
+
+
,
+ )
+ }, [notify])
- setCodeElement(
- pre]:h-full [&>pre]:rounded-none' : '',
- )}
- dangerouslySetInnerHTML={{ __html: htmls.join('') }}
- ref={ref}
- />,
- )
- })()
- }, [code, lang])
+ // Display language in header
+ const displayLang = lang?.toLowerCase() === 'bash' ? 'sh' : (lang ?? '')
return (
- {(title || showTypeCopyButton) && (
+ {(title || showCopyButton) && (
- {title || (lang?.toLowerCase() === 'bash' ? 'sh' : (lang ?? ''))}
+ {title || displayLang}
{
- let copyContent =
- typeof ref.current?.innerText === 'string'
- ? ref.current.innerText
- : ''
+ className="border-0 rounded-md transition-opacity"
+ onClick={handleCopy}
+ aria-label="Copy code to clipboard"
+ >
+ {copied ? (
+ Copied!
+ ) : (
+
+ )}
+
+
+ )}
+
pre]:h-full')}
+ dangerouslySetInnerHTML={{ __html: html }}
+ />
+
+ )
+}
+
+type PlainCodeBlockProps = {
+ /** Code content to display (no highlighting) */
+ code: string
+ /** Optional title to display above code block */
+ title?: string
+ /** Language for display in header */
+ lang?: string
+ /** Whether to show the copy button */
+ showCopyButton?: boolean
+ className?: string
+ style?: React.CSSProperties
+}
+
+/**
+ * PlainCodeBlock displays code without syntax highlighting.
+ * Use this for dynamically generated code (e.g., package manager commands).
+ */
+export function PlainCodeBlock({
+ code,
+ title,
+ lang,
+ showCopyButton = true,
+ className,
+ style,
+}: PlainCodeBlockProps) {
+ const [copied, setCopied] = React.useState(false)
+ const { notify } = useToast()
+
+ const handleCopy = React.useCallback(() => {
+ navigator.clipboard.writeText(code.trimEnd())
+ setCopied(true)
+ setTimeout(() => setCopied(false), 2000)
+ notify(
+
+ Copied code
+
+ Code block copied to clipboard
+
+
,
+ )
+ }, [code, notify])
+
+ // Display language in header
+ const displayLang = lang?.toLowerCase() === 'bash' ? 'sh' : (lang ?? '')
- if (copyContent.endsWith('\n')) {
- copyContent = copyContent.slice(0, -1)
- }
+ return (
+
+ {(title || showCopyButton) && (
+
+
+ {title || displayLang}
+
- navigator.clipboard.writeText(copyContent)
- setCopied(true)
- setTimeout(() => setCopied(false), 2000)
- notify(
-
- Copied code
-
- Code block copied to clipboard
-
-
,
- )
- }}
+
{copied ? (
@@ -257,7 +270,11 @@ export function CodeBlock({
)}
- {codeElement}
+
+
+ {code}
+
+
)
}
diff --git a/src/components/markdown/DocContent.tsx b/src/components/markdown/DocContent.tsx
new file mode 100644
index 000000000..5bff9f2bb
--- /dev/null
+++ b/src/components/markdown/DocContent.tsx
@@ -0,0 +1,14 @@
+// Server component for doc markdown content
+// This file does NOT have 'use client' - it's a server component.
+// The children are JSX elements produced by renderMarkdownToJsx on the server,
+// which already include client component references for interactive elements.
+
+import type { ReactNode } from 'react'
+
+type DocContentProps = {
+ children: ReactNode
+}
+
+export function DocContent({ children }: DocContentProps) {
+ return <>{children}>
+}
diff --git a/src/components/markdown/FileTabs.tsx b/src/components/markdown/FileTabs.tsx
index d29923d0d..6170fb61d 100644
--- a/src/components/markdown/FileTabs.tsx
+++ b/src/components/markdown/FileTabs.tsx
@@ -1,3 +1,5 @@
+'use client'
+
import * as React from 'react'
export type FileTabDefinition = {
diff --git a/src/components/markdown/FrameworkContent.tsx b/src/components/markdown/FrameworkContent.tsx
index 26601f0ae..bdb58d4c1 100644
--- a/src/components/markdown/FrameworkContent.tsx
+++ b/src/components/markdown/FrameworkContent.tsx
@@ -1,3 +1,5 @@
+'use client'
+
import { useLocalCurrentFramework } from '../FrameworkSelect'
import { useCurrentUserQuery } from '~/hooks/useCurrentUser'
import { useParams } from '@tanstack/react-router'
diff --git a/src/components/markdown/Markdown.tsx b/src/components/markdown/Markdown.tsx
index a5fa01be5..2315bd132 100644
--- a/src/components/markdown/Markdown.tsx
+++ b/src/components/markdown/Markdown.tsx
@@ -1,3 +1,5 @@
+'use client'
+
import type { HTMLProps } from 'react'
import * as React from 'react'
import { MarkdownLink } from './MarkdownLink'
@@ -9,7 +11,6 @@ import parse, {
HTMLReactParserOptions,
} from 'html-react-parser'
-import { renderMarkdown } from '~/utils/markdown'
import { CodeBlock } from './CodeBlock'
import { handleTabsComponent } from './MarkdownTabsHandler'
import { handleFrameworkComponent } from './MarkdownFrameworkHandler'
@@ -125,31 +126,17 @@ const options: HTMLReactParserOptions = {
}
type MarkdownProps = {
- rawContent?: string
- htmlMarkup?: string
+ htmlMarkup: string
}
export const Markdown = React.memo(function Markdown({
- rawContent,
htmlMarkup,
}: MarkdownProps) {
- const rendered = React.useMemo(() => {
- if (rawContent) {
- return renderMarkdown(rawContent)
- }
-
- if (htmlMarkup) {
- return { markup: htmlMarkup, headings: [] }
- }
-
- return { markup: '', headings: [] }
- }, [rawContent, htmlMarkup])
-
return React.useMemo(() => {
- if (!rendered.markup) {
+ if (!htmlMarkup) {
return null
}
- return parse(rendered.markup, options)
- }, [rendered.markup])
+ return parse(htmlMarkup, options)
+ }, [htmlMarkup])
})
diff --git a/src/components/markdown/MarkdownContent.tsx b/src/components/markdown/MarkdownContent.tsx
index 86553d4a3..dd9169eda 100644
--- a/src/components/markdown/MarkdownContent.tsx
+++ b/src/components/markdown/MarkdownContent.tsx
@@ -2,7 +2,6 @@ import * as React from 'react'
import { SquarePen } from 'lucide-react'
import { twMerge } from 'tailwind-merge'
import { DocTitle } from '~/components/DocTitle'
-import { Markdown } from './Markdown'
import { CopyPageDropdown } from '~/components/CopyPageDropdown'
import { DocFeedbackProvider } from '~/components/DocFeedbackProvider'
import { Button } from '~/ui'
@@ -12,10 +11,8 @@ type MarkdownContentProps = {
repo: string
branch: string
filePath: string
- /** Pre-rendered HTML markup (from renderMarkdown). If not provided, rawContent will be rendered. */
- htmlMarkup?: string
- /** Raw markdown content to render. Used if htmlMarkup is not provided. */
- rawContent?: string
+ /** RSC-rendered content */
+ contentRsc: React.ReactNode
/** Additional elements to render in the title bar (e.g., width toggle button) */
titleBarActions?: React.ReactNode
/** Additional class names for the prose container */
@@ -33,8 +30,7 @@ export function MarkdownContent({
repo,
branch,
filePath,
- htmlMarkup,
- rawContent,
+ contentRsc,
titleBarActions,
proseClassName,
containerRef,
@@ -43,12 +39,6 @@ export function MarkdownContent({
pagePath,
}: MarkdownContentProps) {
const renderMarkdownContent = () => {
- const markdownElement = htmlMarkup ? (
-
- ) : rawContent ? (
-
- ) : null
-
if (libraryId && libraryVersion && pagePath) {
return (
- {markdownElement}
+ {contentRsc}
)
}
- return markdownElement
+ return contentRsc
}
return (
diff --git a/src/components/markdown/MarkdownFrameworkHandler.tsx b/src/components/markdown/MarkdownFrameworkHandler.tsx
index de0b877a3..891066373 100644
--- a/src/components/markdown/MarkdownFrameworkHandler.tsx
+++ b/src/components/markdown/MarkdownFrameworkHandler.tsx
@@ -1,25 +1,7 @@
import * as React from 'react'
import { domToReact, Element } from 'html-react-parser'
import type { HTMLReactParserOptions } from 'html-react-parser'
-
-// Helper to resolve different module shapes (named export vs default)
-function resolveModuleDefault(mod: any, key: string): React.ComponentType
{
- if (!mod) return undefined as any
- if (mod[key] && typeof mod[key] === 'function') return mod[key]
- if (mod.default) {
- if (mod.default[key] && typeof mod.default[key] === 'function')
- return mod.default[key]
- if (typeof mod.default === 'function') return mod.default
- }
- if (typeof mod === 'function') return mod
- return (mod as any).default ?? (mod as any)
-}
-
-const FrameworkContent = React.lazy>(() =>
- import('./FrameworkContent').then((mod) => ({
- default: resolveModuleDefault(mod, 'FrameworkContent'),
- })),
-)
+import { FrameworkContent } from './FrameworkContent'
export function handleFrameworkComponent(
domNode: Element,
@@ -52,13 +34,11 @@ export function handleFrameworkComponent(
})
return (
- Loading... }>
-
-
+
)
} catch {
return null
diff --git a/src/components/markdown/MarkdownHeadingContext.tsx b/src/components/markdown/MarkdownHeadingContext.tsx
index de4a2b6a9..a1e3e4389 100644
--- a/src/components/markdown/MarkdownHeadingContext.tsx
+++ b/src/components/markdown/MarkdownHeadingContext.tsx
@@ -1,5 +1,5 @@
import * as React from 'react'
-import { MarkdownHeading } from '~/utils/markdown/processor'
+import type { MarkdownHeading } from '~/utils/markdown/types'
const MarkdownHeadingContext = React.createContext<{
headings: MarkdownHeading[]
diff --git a/src/components/markdown/MarkdownLink.tsx b/src/components/markdown/MarkdownLink.tsx
index 488724b81..1adb3f1f2 100644
--- a/src/components/markdown/MarkdownLink.tsx
+++ b/src/components/markdown/MarkdownLink.tsx
@@ -1,3 +1,5 @@
+'use client'
+
import { Link } from '@tanstack/react-router'
import type { HTMLProps } from 'react'
diff --git a/src/components/markdown/MarkdownTabsHandler.tsx b/src/components/markdown/MarkdownTabsHandler.tsx
index d453cbdb6..0facd4285 100644
--- a/src/components/markdown/MarkdownTabsHandler.tsx
+++ b/src/components/markdown/MarkdownTabsHandler.tsx
@@ -2,35 +2,9 @@ import * as React from 'react'
import { domToReact, Element } from 'html-react-parser'
import type { HTMLReactParserOptions } from 'html-react-parser'
import type { Framework } from '~/libraries/types'
-
-// Helper to resolve different module shapes (named export vs default)
-function resolveModuleDefault(mod: any, key: string): React.ComponentType
{
- if (!mod) return undefined as any
- if (mod[key] && typeof mod[key] === 'function') return mod[key]
- if (mod.default) {
- if (mod.default[key] && typeof mod.default[key] === 'function')
- return mod.default[key]
- if (typeof mod.default === 'function') return mod.default
- }
- if (typeof mod === 'function') return mod
- return (mod as any).default ?? (mod as any)
-}
-
-const Tabs = React.lazy>(() =>
- import('./Tabs').then((mod) => ({
- default: resolveModuleDefault(mod, 'Tabs'),
- })),
-)
-const PackageManagerTabs = React.lazy>(() =>
- import('./PackageManagerTabs').then((mod) => ({
- default: resolveModuleDefault(mod, 'PackageManagerTabs'),
- })),
-)
-const FileTabs = React.lazy>(() =>
- import('./FileTabs').then((mod) => ({
- default: resolveModuleDefault(mod, 'FileTabs'),
- })),
-)
+import { Tabs } from './Tabs'
+import { PackageManagerTabs } from './PackageManagerTabs'
+import { FileTabs } from './FileTabs'
export function handleTabsComponent(
domNode: Element,
@@ -46,13 +20,11 @@ export function handleTabsComponent(
const frameworks = Object.keys(packagesByFramework) as Framework[]
return (
- Loading... }>
-
-
+
)
} catch {
// Fall through to default tabs if parsing fails
@@ -74,11 +46,7 @@ export function handleTabsComponent(
domToReact(panel.children as any, options),
)
- return (
- Loading... }>
-
-
- )
+ return
} catch {
// Fall through to default tabs if parsing fails
}
@@ -102,9 +70,5 @@ export function handleTabsComponent(
return <>{result}>
})
- return (
- Loading...}>
-
-
- )
+ return
}
diff --git a/src/components/markdown/MdComponents.tsx b/src/components/markdown/MdComponents.tsx
new file mode 100644
index 000000000..6c1b2f9fe
--- /dev/null
+++ b/src/components/markdown/MdComponents.tsx
@@ -0,0 +1,184 @@
+'use client'
+
+import * as React from 'react'
+import type { Framework } from '~/libraries/types'
+import { Tabs } from './Tabs'
+import { PackageManagerTabs } from './PackageManagerTabs'
+import { FileTabs } from './FileTabs'
+import { FrameworkContent } from './FrameworkContent'
+
+type MdCommentComponentProps = {
+ 'data-component'?: string
+ 'data-attributes'?: string
+ 'data-package-manager-meta'?: string
+ 'data-files-meta'?: string
+ children?: React.ReactNode
+}
+
+/**
+ * Handles md-comment-component elements from rehype-react.
+ * Maps to the appropriate tab/framework component based on data attributes.
+ */
+export function MdCommentComponent({
+ 'data-component': componentName,
+ 'data-attributes': rawAttributes,
+ 'data-package-manager-meta': pmMeta,
+ 'data-files-meta': filesMeta,
+ children,
+}: MdCommentComponentProps) {
+ const attributes: Record = React.useMemo(() => {
+ if (typeof rawAttributes === 'string') {
+ try {
+ return JSON.parse(rawAttributes)
+ } catch {
+ return {}
+ }
+ }
+ return {}
+ }, [rawAttributes])
+
+ const normalizedComponent = componentName?.toLowerCase()
+
+ // Handle tabs component
+ if (normalizedComponent === 'tabs') {
+ // Handle package-manager variant
+ if (pmMeta) {
+ try {
+ const { packagesByFramework, mode } = JSON.parse(pmMeta)
+ const frameworks = Object.keys(packagesByFramework) as Framework[]
+
+ return (
+
+ )
+ } catch {
+ // Fall through to default tabs if parsing fails
+ }
+ }
+
+ // Handle files variant
+ if (filesMeta) {
+ try {
+ const tabs = attributes.tabs || []
+ // Children are already React nodes from rehype-react
+ const childArray = React.Children.toArray(children)
+ const panelChildren = childArray.filter(
+ (child): child is React.ReactElement =>
+ React.isValidElement(child) &&
+ (child.type === MdTabPanel || child.type === 'md-tab-panel'),
+ )
+
+ const tabContents = panelChildren.map((panel) => panel.props.children)
+
+ return
+ } catch {
+ // Fall through to default tabs if parsing fails
+ }
+ }
+
+ // Handle default tabs variant
+ const tabs = attributes.tabs
+ if (!tabs || !Array.isArray(tabs)) {
+ return {children}
+ }
+
+ // Children are already React nodes from rehype-react
+ const childArray = React.Children.toArray(children)
+ const panelChildren = childArray.filter(
+ (child): child is React.ReactElement =>
+ React.isValidElement(child) &&
+ (child.type === MdTabPanel || child.type === 'md-tab-panel'),
+ )
+
+ const tabContents = panelChildren.map((panel) => (
+ <>{panel.props.children}>
+ ))
+
+ return
+ }
+
+ // Handle framework component
+ if (normalizedComponent === 'framework') {
+ return
+ }
+
+ // Default: just render children in a div
+ return {children}
+}
+
+type MdTabPanelProps = {
+ 'data-tab-slug'?: string
+ 'data-tab-index'?: string
+ children?: React.ReactNode
+}
+
+/**
+ * Wrapper for md-tab-panel elements.
+ * These are intermediate elements that hold tab content.
+ */
+export function MdTabPanel({ children }: MdTabPanelProps) {
+ // This component is mainly used as a marker for MdCommentComponent to find
+ return <>{children}>
+}
+
+type MdFrameworkPanelProps = {
+ 'data-framework'?: string
+ children?: React.ReactNode
+}
+
+/**
+ * Wrapper for md-framework-panel elements.
+ * These hold framework-specific content.
+ */
+export function MdFrameworkPanel({ children }: MdFrameworkPanelProps) {
+ // This component is mainly used as a marker
+ return <>{children}>
+}
+
+type MdFrameworkComponentInnerProps = {
+ children?: React.ReactNode
+}
+
+/**
+ * Inner component for framework switching.
+ * Extracts framework panels from children and renders FrameworkContent.
+ */
+function MdFrameworkComponentInner({
+ children,
+}: MdFrameworkComponentInnerProps) {
+ const childArray = React.Children.toArray(children)
+
+ // Find all framework panel children
+ const panelChildren = childArray.filter(
+ (child): child is React.ReactElement =>
+ React.isValidElement(child) &&
+ (child.type === MdFrameworkPanel || child.type === 'md-framework-panel'),
+ )
+
+ // Build panelsByFramework map
+ const panelsByFramework: Record = {}
+ const availableFrameworks: string[] = []
+
+ panelChildren.forEach((panel) => {
+ const fw = panel.props['data-framework']
+ if (fw) {
+ panelsByFramework[fw] = panel.props.children
+ availableFrameworks.push(fw)
+ }
+ })
+
+ if (availableFrameworks.length === 0) {
+ return {children}
+ }
+
+ return (
+
+ )
+}
diff --git a/src/components/markdown/PackageManagerTabs.tsx b/src/components/markdown/PackageManagerTabs.tsx
index 31bade627..5faaa5027 100644
--- a/src/components/markdown/PackageManagerTabs.tsx
+++ b/src/components/markdown/PackageManagerTabs.tsx
@@ -1,9 +1,11 @@
+'use client'
+
import { useLocalCurrentFramework } from '../FrameworkSelect'
import { useCurrentUserQuery } from '~/hooks/useCurrentUser'
import { useParams } from '@tanstack/react-router'
import { create } from 'zustand'
import { Tabs, type TabDefinition } from './Tabs'
-import { CodeBlock } from './CodeBlock'
+import { PlainCodeBlock } from './CodeBlock'
import type { Framework } from '~/libraries/types'
type PackageManager = 'bun' | 'npm' | 'pnpm' | 'yarn'
@@ -190,11 +192,7 @@ export function PackageManagerTabs({
const children = PACKAGE_MANAGERS.map((pm) => {
const commands = getInstallCommand(pm, packageGroups, mode)
const commandText = commands.join('\n')
- return (
-
- {commandText}
-
- )
+ return
})
return (
diff --git a/src/components/markdown/Tabs.tsx b/src/components/markdown/Tabs.tsx
index dd2867c1a..949c30227 100644
--- a/src/components/markdown/Tabs.tsx
+++ b/src/components/markdown/Tabs.tsx
@@ -1,3 +1,5 @@
+'use client'
+
import { useParams } from '@tanstack/react-router'
import * as React from 'react'
import { frameworkOptions } from '~/libraries/frameworks'
diff --git a/src/components/markdown/index.ts b/src/components/markdown/index.ts
index f4b3dc588..e1f3f01ad 100644
--- a/src/components/markdown/index.ts
+++ b/src/components/markdown/index.ts
@@ -1,11 +1,10 @@
-export { Markdown } from './Markdown'
export { MarkdownLink } from './MarkdownLink'
export { MarkdownContent } from './MarkdownContent'
export {
MarkdownHeadingProvider,
useMarkdownHeadings,
} from './MarkdownHeadingContext'
-export { CodeBlock } from './CodeBlock'
+export { CodeBlock, HighlightedCodeBlock, PlainCodeBlock } from './CodeBlock'
export { Tabs } from './Tabs'
export { FileTabs } from './FileTabs'
export { PackageManagerTabs } from './PackageManagerTabs'
diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts
index 9a07b2e4a..bcee5ef97 100644
--- a/src/routeTree.gen.ts
+++ b/src/routeTree.gen.ts
@@ -732,9 +732,9 @@ export interface FileRoutesByFullPath {
'/admin/': typeof AdminIndexRoute
'/blog/': typeof BlogIndexRoute
'/builder/': typeof BuilderIndexRoute
- '/feed/': typeof FeedIndexRoute
- '/showcase/': typeof ShowcaseIndexRoute
- '/stats/': typeof StatsIndexRoute
+ '/feed': typeof FeedIndexRoute
+ '/showcase': typeof ShowcaseIndexRoute
+ '/stats': typeof StatsIndexRoute
'/$libraryId/$version/docs': typeof LibraryIdVersionDocsRouteWithChildren
'/admin/banners/$id': typeof AdminBannersIdRoute
'/admin/feed/$id': typeof AdminFeedIdRoute
@@ -763,14 +763,14 @@ export interface FileRoutesByFullPath {
'/showcase/edit/$id': typeof ShowcaseEditIdRoute
'/stats/npm/$packages': typeof StatsNpmPackagesRoute
'/$libraryId/$version/': typeof LibraryIdVersionIndexRoute
- '/admin/banners/': typeof AdminBannersIndexRoute
- '/admin/feed/': typeof AdminFeedIndexRoute
- '/admin/feedback/': typeof AdminFeedbackIndexRoute
- '/admin/notes/': typeof AdminNotesIndexRoute
- '/admin/roles/': typeof AdminRolesIndexRoute
- '/admin/showcases/': typeof AdminShowcasesIndexRoute
- '/api/mcp/': typeof ApiMcpIndexRoute
- '/stats/npm/': typeof StatsNpmIndexRoute
+ '/admin/banners': typeof AdminBannersIndexRoute
+ '/admin/feed': typeof AdminFeedIndexRoute
+ '/admin/feedback': typeof AdminFeedbackIndexRoute
+ '/admin/notes': typeof AdminNotesIndexRoute
+ '/admin/roles': typeof AdminRolesIndexRoute
+ '/admin/showcases': typeof AdminShowcasesIndexRoute
+ '/api/mcp': typeof ApiMcpIndexRoute
+ '/stats/npm': typeof StatsNpmIndexRoute
'/$libraryId/$version/docs/$': typeof LibraryIdVersionDocsSplatRoute
'/$libraryId/$version/docs/community-resources': typeof LibraryIdVersionDocsCommunityResourcesRoute
'/$libraryId/$version/docs/contributors': typeof LibraryIdVersionDocsContributorsRoute
@@ -780,10 +780,10 @@ export interface FileRoutesByFullPath {
'/api/builder/deploy/check-name': typeof ApiBuilderDeployCheckNameRoute
'/api/builder/deploy/github': typeof ApiBuilderDeployGithubRoute
'/$libraryId/$version/docs/': typeof LibraryIdVersionDocsIndexRoute
- '/$libraryId/$version/docs/framework/': typeof LibraryIdVersionDocsFrameworkIndexRoute
+ '/$libraryId/$version/docs/framework': typeof LibraryIdVersionDocsFrameworkIndexRoute
'/$libraryId/$version/docs/framework/$framework/$': typeof LibraryIdVersionDocsFrameworkFrameworkSplatRoute
'/$libraryId/$version/docs/framework/$framework/{$}.md': typeof LibraryIdVersionDocsFrameworkFrameworkChar123Char125DotmdRoute
- '/$libraryId/$version/docs/framework/$framework/': typeof LibraryIdVersionDocsFrameworkFrameworkIndexRoute
+ '/$libraryId/$version/docs/framework/$framework': typeof LibraryIdVersionDocsFrameworkFrameworkIndexRoute
'/$libraryId/$version/docs/framework/$framework/examples/$': typeof LibraryIdVersionDocsFrameworkFrameworkExamplesSplatRoute
}
export interface FileRoutesByTo {
@@ -1058,9 +1058,9 @@ export interface FileRouteTypes {
| '/admin/'
| '/blog/'
| '/builder/'
- | '/feed/'
- | '/showcase/'
- | '/stats/'
+ | '/feed'
+ | '/showcase'
+ | '/stats'
| '/$libraryId/$version/docs'
| '/admin/banners/$id'
| '/admin/feed/$id'
@@ -1089,14 +1089,14 @@ export interface FileRouteTypes {
| '/showcase/edit/$id'
| '/stats/npm/$packages'
| '/$libraryId/$version/'
- | '/admin/banners/'
- | '/admin/feed/'
- | '/admin/feedback/'
- | '/admin/notes/'
- | '/admin/roles/'
- | '/admin/showcases/'
- | '/api/mcp/'
- | '/stats/npm/'
+ | '/admin/banners'
+ | '/admin/feed'
+ | '/admin/feedback'
+ | '/admin/notes'
+ | '/admin/roles'
+ | '/admin/showcases'
+ | '/api/mcp'
+ | '/stats/npm'
| '/$libraryId/$version/docs/$'
| '/$libraryId/$version/docs/community-resources'
| '/$libraryId/$version/docs/contributors'
@@ -1106,10 +1106,10 @@ export interface FileRouteTypes {
| '/api/builder/deploy/check-name'
| '/api/builder/deploy/github'
| '/$libraryId/$version/docs/'
- | '/$libraryId/$version/docs/framework/'
+ | '/$libraryId/$version/docs/framework'
| '/$libraryId/$version/docs/framework/$framework/$'
| '/$libraryId/$version/docs/framework/$framework/{$}.md'
- | '/$libraryId/$version/docs/framework/$framework/'
+ | '/$libraryId/$version/docs/framework/$framework'
| '/$libraryId/$version/docs/framework/$framework/examples/$'
fileRoutesByTo: FileRoutesByTo
to:
@@ -1596,21 +1596,21 @@ declare module '@tanstack/react-router' {
'/stats/': {
id: '/stats/'
path: '/stats'
- fullPath: '/stats/'
+ fullPath: '/stats'
preLoaderRoute: typeof StatsIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/showcase/': {
id: '/showcase/'
path: '/showcase'
- fullPath: '/showcase/'
+ fullPath: '/showcase'
preLoaderRoute: typeof ShowcaseIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/feed/': {
id: '/feed/'
path: '/feed'
- fullPath: '/feed/'
+ fullPath: '/feed'
preLoaderRoute: typeof FeedIndexRouteImport
parentRoute: typeof rootRouteImport
}
@@ -1806,56 +1806,56 @@ declare module '@tanstack/react-router' {
'/stats/npm/': {
id: '/stats/npm/'
path: '/stats/npm'
- fullPath: '/stats/npm/'
+ fullPath: '/stats/npm'
preLoaderRoute: typeof StatsNpmIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/api/mcp/': {
id: '/api/mcp/'
path: '/api/mcp'
- fullPath: '/api/mcp/'
+ fullPath: '/api/mcp'
preLoaderRoute: typeof ApiMcpIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/admin/showcases/': {
id: '/admin/showcases/'
path: '/showcases'
- fullPath: '/admin/showcases/'
+ fullPath: '/admin/showcases'
preLoaderRoute: typeof AdminShowcasesIndexRouteImport
parentRoute: typeof AdminRouteRoute
}
'/admin/roles/': {
id: '/admin/roles/'
path: '/roles'
- fullPath: '/admin/roles/'
+ fullPath: '/admin/roles'
preLoaderRoute: typeof AdminRolesIndexRouteImport
parentRoute: typeof AdminRouteRoute
}
'/admin/notes/': {
id: '/admin/notes/'
path: '/notes'
- fullPath: '/admin/notes/'
+ fullPath: '/admin/notes'
preLoaderRoute: typeof AdminNotesIndexRouteImport
parentRoute: typeof AdminRouteRoute
}
'/admin/feedback/': {
id: '/admin/feedback/'
path: '/feedback'
- fullPath: '/admin/feedback/'
+ fullPath: '/admin/feedback'
preLoaderRoute: typeof AdminFeedbackIndexRouteImport
parentRoute: typeof AdminRouteRoute
}
'/admin/feed/': {
id: '/admin/feed/'
path: '/feed'
- fullPath: '/admin/feed/'
+ fullPath: '/admin/feed'
preLoaderRoute: typeof AdminFeedIndexRouteImport
parentRoute: typeof AdminRouteRoute
}
'/admin/banners/': {
id: '/admin/banners/'
path: '/banners'
- fullPath: '/admin/banners/'
+ fullPath: '/admin/banners'
preLoaderRoute: typeof AdminBannersIndexRouteImport
parentRoute: typeof AdminRouteRoute
}
@@ -2121,14 +2121,14 @@ declare module '@tanstack/react-router' {
'/$libraryId/$version/docs/framework/': {
id: '/$libraryId/$version/docs/framework/'
path: '/framework'
- fullPath: '/$libraryId/$version/docs/framework/'
+ fullPath: '/$libraryId/$version/docs/framework'
preLoaderRoute: typeof LibraryIdVersionDocsFrameworkIndexRouteImport
parentRoute: typeof LibraryIdVersionDocsRoute
}
'/$libraryId/$version/docs/framework/$framework/': {
id: '/$libraryId/$version/docs/framework/$framework/'
path: '/framework/$framework'
- fullPath: '/$libraryId/$version/docs/framework/$framework/'
+ fullPath: '/$libraryId/$version/docs/framework/$framework'
preLoaderRoute: typeof LibraryIdVersionDocsFrameworkFrameworkIndexRouteImport
parentRoute: typeof LibraryIdVersionDocsRoute
}
diff --git a/src/routes/$libraryId/$version.docs.$.tsx b/src/routes/$libraryId/$version.docs.$.tsx
index c5437ed3f..e6f50bcbc 100644
--- a/src/routes/$libraryId/$version.docs.$.tsx
+++ b/src/routes/$libraryId/$version.docs.$.tsx
@@ -85,7 +85,7 @@ export const Route = createFileRoute('/$libraryId/$version/docs/$')({
function Docs() {
const { version, libraryId, _splat } = Route.useParams()
- const { title, content, filePath } = Route.useLoaderData()
+ const { title, contentRsc, headings, filePath } = Route.useLoaderData()
const versionMatch = useMatch({ from: '/$libraryId/$version' })
const { config } = versionMatch.loaderData as { config: ConfigSchema }
const library = getLibrary(libraryId)
@@ -96,7 +96,8 @@ function Docs() {
{
+export const Route = createFileRoute('/blog/$')({
+ staleTime: Infinity,
+ loader: async ({ params }) => {
+ const docsPath = params._splat
if (!docsPath) {
throw new Error('Invalid docs path')
}
@@ -33,40 +33,43 @@ const fetchBlogPost = createServerFn({ method: 'GET' })
handleRedirects(docsPath)
const filePath = `src/blog/${docsPath}.md`
-
- const post = allPosts.find((post) => post.slug === docsPath)
+ const post = allPosts.find((p) => p.slug === docsPath)
if (!post) {
throw notFound()
}
- setResponseHeaders(
- new Headers({
- 'Cache-Control': 'public, max-age=0, must-revalidate',
- 'Netlify-CDN-Cache-Control':
- 'public, max-age=300, durable, stale-while-revalidate=300',
- }),
- )
+ const { setCacheHeaders } = await import('~/utils/headers.server')
+ setCacheHeaders()
const now = new Date()
const publishDate = new Date(post.published)
const isUnpublished = post.draft || publishDate > now
+ const blogContent = `_by ${formatAuthors(post.authors)} on ${format(
+ new Date(post.published || 0),
+ 'MMMM d, yyyy',
+ )}._
+
+${post.content}`
+
+ const { renderMarkdownToJsx } = await import('~/utils/markdown')
+ const { headings } = await renderMarkdownToJsx(blogContent)
+ const { renderBlogContent } = await import('~/utils/renderBlogContent')
+ const ContentRsc = await renderBlogContent({ data: blogContent })
+
return {
title: post.title,
description: post.description,
published: post.published,
- content: post.content,
authors: post.authors,
headerImage: post.headerImage,
filePath,
isUnpublished,
+ headings,
+ ContentRsc,
}
- })
-
-export const Route = createFileRoute('/blog/$')({
- staleTime: Infinity,
- loader: ({ params }) => fetchBlogPost({ data: params._splat }),
+ },
head: ({ loaderData }) => {
// Generate optimized social media image URL using Netlify Image CDN
const getSocialImageUrl = (headerImage?: string) => {
@@ -103,19 +106,10 @@ export const Route = createFileRoute('/blog/$')({
})
function BlogPost() {
- const { title, content, filePath, authors, published } = Route.useLoaderData()
+ const { headings, ContentRsc, title, filePath } = Route.useLoaderData()
- const blogContent = `_by ${formatAuthors(authors)} on ${format(
- new Date(published || 0),
- 'MMMM d, yyyy',
- )}._
-
-${content}`
-
- const { headings, markup } = React.useMemo(
- () => renderMarkdown(blogContent),
- [blogContent],
- )
+ const repo = 'tanstack/tanstack.com'
+ const branch = 'main'
const isTocVisible = headings.length > 1
@@ -164,9 +158,6 @@ ${content}`
return () => observer.disconnect()
}, [headings])
- const repo = 'tanstack/tanstack.com'
- const branch = 'main'
-
return (
-
-
+
+
+
+
+
+ {ContentRsc}
+
+
+
+
+
+ Edit on GitHub
+
+
{isTocVisible && (
diff --git a/src/routes/blog.index.tsx b/src/routes/blog.index.tsx
index 8db677b06..46cab45c8 100644
--- a/src/routes/blog.index.tsx
+++ b/src/routes/blog.index.tsx
@@ -2,40 +2,42 @@ import { Link, createFileRoute } from '@tanstack/react-router'
import { Card } from '~/components/Card'
import { formatAuthors, getPublishedPosts } from '~/utils/blog'
import { SimpleMarkdown } from '~/components/SimpleMarkdown'
+import { renderMarkdownAsync } from '~/utils/markdown'
import { format } from '~/utils/dates'
import { Footer } from '~/components/Footer'
import { PostNotFound } from './blog'
import { createServerFn } from '@tanstack/react-start'
-import { setResponseHeaders } from '@tanstack/react-start/server'
import { RssIcon } from 'lucide-react'
type BlogFrontMatter = {
slug: string
title: string
published: string
- excerpt: string | undefined
+ excerptHtml: string | undefined
authors: string[]
}
const fetchFrontMatters = createServerFn({ method: 'GET' }).handler(
async () => {
- setResponseHeaders(
- new Headers({
- 'Cache-Control': 'public, max-age=0, must-revalidate',
- 'Netlify-CDN-Cache-Control':
- 'public, max-age=300, durable, stale-while-revalidate=300',
+ const { setCacheHeaders } = await import('~/utils/headers.server')
+ setCacheHeaders()
+
+ const posts = getPublishedPosts()
+ const frontMatters: BlogFrontMatter[] = await Promise.all(
+ posts.map(async (post) => {
+ return {
+ slug: post.slug,
+ title: post.title,
+ published: post.published,
+ excerptHtml: post.excerpt
+ ? (await renderMarkdownAsync(post.excerpt)).markup
+ : undefined,
+ authors: post.authors,
+ }
}),
)
- return getPublishedPosts().map((post) => {
- return {
- slug: post.slug,
- title: post.title,
- published: post.published,
- excerpt: post.excerpt,
- authors: post.authors,
- }
- })
+ return frontMatters
// return json(frontMatters, {
// headers: {
@@ -84,45 +86,49 @@ function BlogIndex() {
- {frontMatters.map(({ slug, title, published, excerpt, authors }) => {
- return (
-
-
-
{title}
-
-
- by {formatAuthors(authors)}
- {published ? (
-
- {' '}
- on {format(new Date(published), 'MMM d, yyyy')}
-
+ {frontMatters.map(
+ ({ slug, title, published, excerptHtml, authors }) => {
+ return (
+
+
+
{title}
+
+
+ by {formatAuthors(authors)}
+ {published ? (
+
+ {' '}
+ on {format(new Date(published), 'MMM d, yyyy')}
+
+ ) : null}
+
+
+
+ {excerptHtml ? (
+
) : null}
-
-
-
-
+
-
-
-
-
- )
- })}
+
+ )
+ },
+ )}
diff --git a/src/routes/feed.$id.tsx b/src/routes/feed.$id.tsx
index 3c72c1aa7..b7046e5af 100644
--- a/src/routes/feed.$id.tsx
+++ b/src/routes/feed.$id.tsx
@@ -1,11 +1,13 @@
import { Link, notFound, createFileRoute } from '@tanstack/react-router'
+import { createServerFn } from '@tanstack/react-start'
+import { renderServerComponent } from '@tanstack/react-start/rsc'
import { Footer } from '~/components/Footer'
import { seo } from '~/utils/seo'
import { useCapabilities } from '~/hooks/useCapabilities'
import { isAdmin } from '~/db/types'
import * as v from 'valibot'
import { format, formatDistanceToNow } from '~/utils/dates'
-import { Markdown } from '~/components/markdown'
+import { renderMarkdownToJsx } from '~/utils/markdown'
import { libraries } from '~/libraries'
import { partners } from '~/utils/partners'
import { twMerge } from 'tailwind-merge'
@@ -15,6 +17,18 @@ import { ArrowLeft } from 'lucide-react'
const searchSchema = v.object({})
+// Server function that renders markdown content as RSC
+const renderFeedContent = createServerFn({ method: 'GET' })
+ .inputValidator((content: string) => content)
+ .handler(async ({ data: content }) => {
+ const { content: jsxContent } = await renderMarkdownToJsx(content)
+ return renderServerComponent(
+
+ {jsxContent}
+
,
+ )
+ })
+
export const Route = createFileRoute('/feed/$id')({
staleTime: 1000 * 60 * 5, // 5 minutes
loader: async ({ params, context: { queryClient } }) => {
@@ -29,12 +43,16 @@ export const Route = createFileRoute('/feed/$id')({
throw notFound()
}
+ // Render markdown content as RSC
+ const ContentRsc = await renderFeedContent({ data: entry.content })
+
// Check if entry is visible (unless admin)
// Note: We'll check capabilities client-side since they depend on auth
// For SSR, we'll allow the entry to load and handle visibility client-side
return {
entry,
+ ContentRsc,
}
},
headers: () => ({
@@ -88,7 +106,7 @@ export const Route = createFileRoute('/feed/$id')({
})
function FeedItemPage() {
- const { entry } = Route.useLoaderData()
+ const { entry, ContentRsc } = Route.useLoaderData()
const capabilities = useCapabilities()
// Show not found if entry isn't visible (unless admin)
@@ -118,10 +136,16 @@ function FeedItemPage() {
)
}
- return
+ return
}
-function FeedEntryView({ entry }: { entry: FeedEntry }) {
+function FeedEntryView({
+ entry,
+ ContentRsc,
+}: {
+ entry: FeedEntry
+ ContentRsc: React.ReactNode
+}) {
// Get library info
const entryLibraries = entry.libraryIds
.map((id) => libraries.find((lib) => lib.id === id))
@@ -366,9 +390,7 @@ function FeedEntryView({ entry }: { entry: FeedEntry }) {
)}
{/* Content */}
-
-
-
+ {ContentRsc}
{/* External Link */}
{externalLink && (
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
index 4503370ee..dfab6739e 100644
--- a/src/routes/index.tsx
+++ b/src/routes/index.tsx
@@ -18,9 +18,9 @@ import { useToast } from '~/components/ToastProvider'
import { formatAuthors, getPublishedPosts } from '~/utils/blog'
import { format } from '~/utils/dates'
import { SimpleMarkdown } from '~/components/SimpleMarkdown'
+import { renderMarkdownAsync } from '~/utils/markdown'
import { NetlifyImage } from '~/components/NetlifyImage'
import { createServerFn } from '@tanstack/react-start'
-import { setResponseHeaders } from '@tanstack/react-start/server'
import { AdGate } from '~/contexts/AdsContext'
import { GamHeader } from '~/components/Gam'
import { TrustedByMarquee } from '~/components/TrustedByMarquee'
@@ -57,31 +57,29 @@ type BlogFrontMatter = {
slug: string
title: string
published: string
- excerpt: string | undefined
+ excerptHtml: string | undefined
authors: string[]
}
const fetchRecentPosts = createServerFn({ method: 'GET' }).handler(
async (): Promise
=> {
- setResponseHeaders(
- new Headers({
- 'Cache-Control': 'public, max-age=0, must-revalidate',
- 'Netlify-CDN-Cache-Control':
- 'public, max-age=300, durable, stale-while-revalidate=300',
- }),
+ const { setCacheHeaders } = await import('~/utils/headers.server')
+ setCacheHeaders()
+
+ const posts = getPublishedPosts().slice(0, 3)
+ const postsWithMarkup = await Promise.all(
+ posts.map(async (post) => ({
+ slug: post.slug,
+ title: post.title,
+ published: post.published,
+ excerptHtml: post.excerpt
+ ? (await renderMarkdownAsync(post.excerpt)).markup
+ : undefined,
+ authors: post.authors,
+ })),
)
- return getPublishedPosts()
- .slice(0, 3)
- .map((post) => {
- return {
- slug: post.slug,
- title: post.title,
- published: post.published,
- excerpt: post.excerpt,
- authors: post.authors,
- }
- })
+ return postsWithMarkup
},
)
@@ -393,7 +391,7 @@ function Index() {
{recentPosts.map(
- ({ slug, title, published, excerpt, authors }) => {
+ ({ slug, title, published, excerptHtml, authors }) => {
return (
- {excerpt && (
+ {excerptHtml && (
-
+
)}
diff --git a/src/styles/app.css b/src/styles/app.css
index 5d87f124d..516253bad 100644
--- a/src/styles/app.css
+++ b/src/styles/app.css
@@ -669,14 +669,49 @@ pre .logger.log-log svg {
margin-right: 9px;
}
-html:not(.dark) .shiki.vitesse-dark {
+/* Hide single-theme Shiki blocks based on current theme.
+ These rules should NOT match dual-theme blocks (which have .shiki-themes class) */
+html:not(.dark) .shiki.vitesse-dark:not(.shiki-themes) {
display: none;
}
-html.dark .shiki.github-light {
+html.dark .shiki.github-light:not(.shiki-themes) {
display: none;
}
+/* Shiki CSS variable theme support (server-side rehype-shiki with defaultColor: false) */
+.shiki.shiki-themes,
+.shiki[style*='--shiki-'] {
+ /* Light mode: use --shiki-light values */
+ background-color: var(--shiki-light-bg, #fff) !important;
+ color: var(--shiki-light, inherit);
+}
+
+.shiki.shiki-themes span[style*='--shiki-'],
+.shiki[style*='--shiki-'] span[style*='--shiki-'] {
+ color: var(--shiki-light);
+ background-color: var(--shiki-light-bg);
+ font-style: var(--shiki-light-font-style);
+ font-weight: var(--shiki-light-font-weight);
+ text-decoration: var(--shiki-light-text-decoration);
+}
+
+html.dark .shiki.shiki-themes,
+html.dark .shiki[style*='--shiki-'] {
+ /* Dark mode: use --shiki-dark values */
+ background-color: var(--shiki-dark-bg, #0a0a0a) !important;
+ color: var(--shiki-dark, inherit);
+}
+
+html.dark .shiki.shiki-themes span[style*='--shiki-'],
+html.dark .shiki[style*='--shiki-'] span[style*='--shiki-'] {
+ color: var(--shiki-dark);
+ background-color: var(--shiki-dark-bg);
+ font-style: var(--shiki-dark-font-style);
+ font-weight: var(--shiki-dark-font-weight);
+ text-decoration: var(--shiki-dark-text-decoration);
+}
+
/* Improve comment contrast in dark mode */
html.dark .shiki.vitesse-dark .token.comment,
html.dark .shiki.vitesse-dark span[style*='color:#565F89'],
diff --git a/src/utils/docs.ts b/src/utils/docs.tsx
similarity index 81%
rename from src/utils/docs.ts
rename to src/utils/docs.tsx
index 7a7b177a4..b116c05bf 100644
--- a/src/utils/docs.ts
+++ b/src/utils/docs.tsx
@@ -6,8 +6,11 @@ import {
import removeMarkdown from 'remove-markdown'
import { notFound } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'
+import { renderServerComponent } from '@tanstack/react-start/rsc'
import * as v from 'valibot'
-import { setResponseHeader } from '@tanstack/react-start/server'
+import { setResponseHeader } from '~/utils/headers.server'
+import { renderMarkdownToJsx } from '~/utils/markdown'
+import { DocContent } from '~/components/markdown/DocContent'
export const loadDocs = async ({
repo,
@@ -51,6 +54,14 @@ export const fetchDocs = createServerFn({ method: 'GET' })
const frontMatter = extractFrontMatter(file)
const description = removeMarkdown(frontMatter.excerpt ?? '')
+ // Render markdown directly to JSX on the server
+ const { content, headings } = await renderMarkdownToJsx(frontMatter.content)
+
+ // Wrap in RSC stream for client hydration
+ const contentRsc = await renderServerComponent(
+ {content} ,
+ )
+
// Cache for 5 minutes on shared cache
// Revalidate in the background
setResponseHeader('Cache-Control', 'public, max-age=0, must-revalidate')
@@ -63,7 +74,9 @@ export const fetchDocs = createServerFn({ method: 'GET' })
title: frontMatter.data?.title,
description,
filePath,
- content: frontMatter.content,
+ content: frontMatter.content, // Raw markdown content for .md routes
+ contentRsc,
+ headings,
frontmatter: frontMatter.data,
}
})
diff --git a/src/utils/headers.server.ts b/src/utils/headers.server.ts
new file mode 100644
index 000000000..829ba7d89
--- /dev/null
+++ b/src/utils/headers.server.ts
@@ -0,0 +1,22 @@
+import {
+ setResponseHeaders as _setResponseHeaders,
+ setResponseHeader as _setResponseHeader,
+} from '@tanstack/react-start/server'
+
+export const setResponseHeaders = _setResponseHeaders
+export const setResponseHeader = _setResponseHeader
+
+export function setCacheHeaders(opts?: {
+ maxAge?: number
+ cdnMaxAge?: number
+ staleWhileRevalidate?: number
+}) {
+ const { maxAge = 0, cdnMaxAge = 300, staleWhileRevalidate = 300 } = opts ?? {}
+
+ setResponseHeaders(
+ new Headers({
+ 'Cache-Control': `public, max-age=${maxAge}, must-revalidate`,
+ 'Netlify-CDN-Cache-Control': `public, max-age=${cdnMaxAge}, durable, stale-while-revalidate=${staleWhileRevalidate}`,
+ }),
+ )
+}
diff --git a/src/utils/markdown/index.ts b/src/utils/markdown/index.ts
index 0aaf957fc..d683bdd05 100644
--- a/src/utils/markdown/index.ts
+++ b/src/utils/markdown/index.ts
@@ -1 +1,9 @@
-export { renderMarkdown } from './processor'
+export {
+ renderMarkdown,
+ renderMarkdownAsync,
+ renderMarkdownToJsx,
+ highlightCode,
+ type MarkdownJsxResult,
+} from './processor'
+
+export type { MarkdownHeading } from './types'
diff --git a/src/utils/markdown/processor.ts b/src/utils/markdown/processor.ts
deleted file mode 100644
index 9ea90c858..000000000
--- a/src/utils/markdown/processor.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import { unified } from 'unified'
-import remarkParse from 'remark-parse'
-import remarkGfm from 'remark-gfm'
-import remarkRehype from 'remark-rehype'
-import rehypeCallouts from 'rehype-callouts'
-import rehypeRaw from 'rehype-raw'
-import rehypeSlug from 'rehype-slug'
-import rehypeAutolinkHeadings from 'rehype-autolink-headings'
-import rehypeStringify from 'rehype-stringify'
-
-import {
- rehypeCollectHeadings,
- rehypeParseCommentComponents,
- rehypeTransformCommentComponents,
- rehypeTransformFrameworkComponents,
- type MarkdownHeading,
-} from '~/utils/markdown/plugins'
-import { extractCodeMeta } from '~/utils/markdown/plugins/extractCodeMeta'
-
-export type { MarkdownHeading } from '~/utils/markdown/plugins'
-
-export type MarkdownRenderResult = {
- markup: string
- headings: MarkdownHeading[]
-}
-
-export function renderMarkdown(content: string): MarkdownRenderResult {
- const headings: MarkdownHeading[] = []
-
- const processor = unified()
- .use(remarkParse)
- .use(remarkGfm)
- .use(remarkRehype, { allowDangerousHtml: true })
- .use(extractCodeMeta)
- .use(rehypeRaw)
- .use(rehypeParseCommentComponents)
- .use(rehypeCallouts, {
- theme: 'github',
- props: {
- containerProps: (_node: any, type: string) => ({
- className: `markdown-alert markdown-alert-${type}`,
- }),
- titleIconProps: () => ({
- className: 'octicon octicon-info mr-2',
- }),
- titleProps: () => ({
- className: 'markdown-alert-title',
- }),
- titleTextProps: () => ({
- className: 'markdown-alert-title',
- }),
- contentProps: () => ({
- className: 'markdown-alert-content',
- }),
- },
- } as any)
- .use(rehypeSlug)
- .use(rehypeTransformFrameworkComponents)
- .use(rehypeTransformCommentComponents)
- .use(rehypeAutolinkHeadings, {
- behavior: 'wrap',
- properties: {
- className: ['anchor-heading'],
- },
- })
- .use(() => rehypeCollectHeadings(headings))
- .use(rehypeStringify)
-
- const file = processor.processSync(content)
-
- return {
- markup: String(file),
- headings,
- }
-}
diff --git a/src/utils/markdown/processor.tsx b/src/utils/markdown/processor.tsx
new file mode 100644
index 000000000..ba8677d0d
--- /dev/null
+++ b/src/utils/markdown/processor.tsx
@@ -0,0 +1,432 @@
+import * as React from 'react'
+import { unified } from 'unified'
+import remarkParse from 'remark-parse'
+import remarkGfm from 'remark-gfm'
+import remarkRehype from 'remark-rehype'
+import rehypeCallouts from 'rehype-callouts'
+import rehypeRaw from 'rehype-raw'
+import rehypeSlug from 'rehype-slug'
+import rehypeAutolinkHeadings from 'rehype-autolink-headings'
+import rehypeStringify from 'rehype-stringify'
+import rehypeReact from 'rehype-react'
+import * as jsxRuntime from 'react/jsx-runtime'
+import rehypeShiki from '@shikijs/rehype'
+import { transformerNotationDiff } from '@shikijs/transformers'
+import type { RehypeShikiOptions } from '@shikijs/rehype'
+import { createHighlighter, type Highlighter } from 'shiki'
+
+import {
+ rehypeCollectHeadings,
+ rehypeParseCommentComponents,
+ rehypeTransformCommentComponents,
+ rehypeTransformFrameworkComponents,
+ type MarkdownHeading,
+} from '~/utils/markdown/plugins'
+import { extractCodeMeta } from '~/utils/markdown/plugins/extractCodeMeta'
+
+// Import markdown components for JSX rendering
+import { MarkdownLink } from '~/components/markdown/MarkdownLink'
+import { CodeBlock } from '~/components/markdown/CodeBlock'
+import { InlineCode, MarkdownImg } from '~/ui'
+import {
+ MdCommentComponent,
+ MdTabPanel,
+ MdFrameworkPanel,
+} from '~/components/markdown/MdComponents'
+
+export type { MarkdownHeading } from '~/utils/markdown/plugins'
+
+export type MarkdownRenderResult = {
+ markup: string
+ headings: MarkdownHeading[]
+}
+
+export type MarkdownJsxResult = {
+ content: React.ReactNode
+ headings: MarkdownHeading[]
+}
+
+// Language aliases to normalize common variations
+const LANG_ALIASES: Record = {
+ ts: 'typescript',
+ js: 'javascript',
+ sh: 'bash',
+ shell: 'bash',
+ console: 'bash',
+ zsh: 'bash',
+ cmd: 'bash',
+ md: 'markdown',
+ txt: 'text',
+ text: 'text',
+ plaintext: 'text',
+ yml: 'yaml',
+ json5: 'jsonc',
+ eslintrc: 'jsonc',
+}
+
+const shikiOptions: RehypeShikiOptions = {
+ themes: {
+ light: 'github-light',
+ dark: 'vitesse-dark',
+ },
+ defaultColor: false,
+ cssVariablePrefix: '--shiki-',
+ transformers: [transformerNotationDiff()],
+ defaultLanguage: 'typescript',
+ langs: [
+ 'typescript',
+ 'javascript',
+ 'tsx',
+ 'jsx',
+ 'bash',
+ 'json',
+ 'html',
+ 'css',
+ 'markdown',
+ 'toml',
+ 'yaml',
+ 'sql',
+ 'diff',
+ 'vue',
+ 'svelte',
+ 'scss',
+ 'jsonc',
+ 'vue-html',
+ 'angular-html',
+ 'angular-ts',
+ ],
+ langAlias: LANG_ALIASES,
+ // Handle unknown languages gracefully
+ onError: (error) => {
+ console.warn('Shiki highlighting error:', error)
+ },
+}
+
+export async function renderMarkdownAsync(
+ content: string,
+): Promise {
+ const headings: MarkdownHeading[] = []
+
+ const processor = unified()
+ .use(remarkParse)
+ .use(remarkGfm)
+ .use(remarkRehype, { allowDangerousHtml: true })
+ .use(extractCodeMeta)
+ .use(rehypeRaw)
+ .use(rehypeParseCommentComponents)
+ .use(rehypeCallouts, {
+ theme: 'github',
+ props: {
+ containerProps: (_node: any, type: string) => ({
+ className: `markdown-alert markdown-alert-${type}`,
+ }),
+ titleIconProps: () => ({
+ className: 'octicon octicon-info mr-2',
+ }),
+ titleProps: () => ({
+ className: 'markdown-alert-title',
+ }),
+ titleTextProps: () => ({
+ className: 'markdown-alert-title',
+ }),
+ contentProps: () => ({
+ className: 'markdown-alert-content',
+ }),
+ },
+ } as any)
+ .use(rehypeShiki, shikiOptions)
+ .use(rehypeSlug)
+ .use(rehypeTransformFrameworkComponents)
+ .use(rehypeTransformCommentComponents)
+ .use(rehypeAutolinkHeadings, {
+ behavior: 'wrap',
+ properties: {
+ className: ['anchor-heading'],
+ },
+ })
+ .use(() => rehypeCollectHeadings(headings))
+ .use(rehypeStringify)
+
+ const file = await processor.process(content)
+
+ return {
+ markup: String(file),
+ headings,
+ }
+}
+
+// Synchronous version for backwards compatibility (doesn't include syntax highlighting)
+export function renderMarkdown(content: string): MarkdownRenderResult {
+ const headings: MarkdownHeading[] = []
+
+ const processor = unified()
+ .use(remarkParse)
+ .use(remarkGfm)
+ .use(remarkRehype, { allowDangerousHtml: true })
+ .use(extractCodeMeta)
+ .use(rehypeRaw)
+ .use(rehypeParseCommentComponents)
+ .use(rehypeCallouts, {
+ theme: 'github',
+ props: {
+ containerProps: (_node: any, type: string) => ({
+ className: `markdown-alert markdown-alert-${type}`,
+ }),
+ titleIconProps: () => ({
+ className: 'octicon octicon-info mr-2',
+ }),
+ titleProps: () => ({
+ className: 'markdown-alert-title',
+ }),
+ titleTextProps: () => ({
+ className: 'markdown-alert-title',
+ }),
+ contentProps: () => ({
+ className: 'markdown-alert-content',
+ }),
+ },
+ } as any)
+ .use(rehypeSlug)
+ .use(rehypeTransformFrameworkComponents)
+ .use(rehypeTransformCommentComponents)
+ .use(rehypeAutolinkHeadings, {
+ behavior: 'wrap',
+ properties: {
+ className: ['anchor-heading'],
+ },
+ })
+ .use(() => rehypeCollectHeadings(headings))
+ .use(rehypeStringify)
+
+ const file = processor.processSync(content)
+
+ return {
+ markup: String(file),
+ headings,
+ }
+}
+
+// Lazy-loaded highlighter singleton for standalone code highlighting
+let highlighterPromise: Promise | null = null
+
+const SUPPORTED_LANGS = [
+ 'typescript',
+ 'javascript',
+ 'tsx',
+ 'jsx',
+ 'bash',
+ 'json',
+ 'html',
+ 'css',
+ 'markdown',
+ 'toml',
+ 'yaml',
+ 'sql',
+ 'diff',
+ 'vue',
+ 'svelte',
+ 'scss',
+ 'jsonc',
+ 'vue-html',
+ 'angular-html',
+ 'angular-ts',
+ 'text',
+] as const
+
+async function getHighlighter(): Promise {
+ if (!highlighterPromise) {
+ highlighterPromise = createHighlighter({
+ themes: ['github-light', 'vitesse-dark'],
+ langs: [...SUPPORTED_LANGS],
+ })
+ }
+ return highlighterPromise
+}
+
+/**
+ * Highlight code with Shiki (server-side only).
+ * Returns HTML string with dual-theme CSS variables for light/dark mode.
+ */
+export async function highlightCode(
+ code: string,
+ lang: string,
+): Promise {
+ const highlighter = await getHighlighter()
+
+ // Normalize language alias
+ const normalizedLang = LANG_ALIASES[lang] || lang
+
+ // Check if language is supported, fallback to text
+ const loadedLangs = highlighter.getLoadedLanguages()
+ const effectiveLang = loadedLangs.includes(normalizedLang as any)
+ ? normalizedLang
+ : 'text'
+
+ const html = highlighter.codeToHtml(code.trimEnd(), {
+ lang: effectiveLang,
+ themes: {
+ light: 'github-light',
+ dark: 'vitesse-dark',
+ },
+ defaultColor: false,
+ cssVariablePrefix: '--shiki-',
+ transformers: [transformerNotationDiff()],
+ })
+
+ return html
+}
+
+// Custom heading component - rehype-autolink-headings already wraps with ,
+// so we just render the heading element with proper styling
+function createHeadingComponent(
+ level: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6',
+) {
+ const HeadingComponent = ({
+ id,
+ children,
+ className,
+ ...props
+ }: React.HTMLAttributes) => {
+ const Tag = level
+ return (
+
+ {children}
+
+ )
+ }
+ HeadingComponent.displayName = `Heading${level.toUpperCase()}`
+ return HeadingComponent
+}
+
+// Iframe component for markdown
+function MarkdownIframe(props: React.IframeHTMLAttributes) {
+ return
+}
+
+// Wrapper for code elements - only style inline code, pass through code blocks
+function CodeElement({
+ children,
+ className,
+ ...props
+}: React.HTMLAttributes) {
+ // If this code element is inside a pre (code block), it will have no special styling
+ // Shiki adds no className to the inner , so we detect code blocks by checking
+ // if children are complex (contain React elements, not just text)
+ const childArray = React.Children.toArray(children)
+ const hasComplexChildren = childArray.some((child) =>
+ React.isValidElement(child),
+ )
+
+ // Code blocks (inside ) have span children from Shiki
+ // Inline code has only text children
+ if (hasComplexChildren) {
+ return (
+
+ {children}
+
+ )
+ }
+
+ // Inline code - apply InlineCode styling
+ return (
+
+ {children}
+
+ )
+}
+
+// Wrapper for anchor elements - pass through anchor-heading links, apply MarkdownLink to others
+function LinkElement(props: React.AnchorHTMLAttributes) {
+ // If this is an anchor-heading link (from rehype-autolink-headings), render as plain
+ // to avoid nested anchors when the heading content also has links
+ if (props.className?.includes('anchor-heading')) {
+ return
+ }
+ // For all other links, use MarkdownLink which handles relative links
+ return
+}
+
+// Component mapping for rehype-react
+const markdownComponents = {
+ a: LinkElement,
+ pre: CodeBlock,
+ h1: createHeadingComponent('h1'),
+ h2: createHeadingComponent('h2'),
+ h3: createHeadingComponent('h3'),
+ h4: createHeadingComponent('h4'),
+ h5: createHeadingComponent('h5'),
+ h6: createHeadingComponent('h6'),
+ code: CodeElement,
+ iframe: MarkdownIframe,
+ img: MarkdownImg,
+ // Custom markdown components
+ 'md-comment-component': MdCommentComponent,
+ 'md-tab-panel': MdTabPanel,
+ 'md-framework-panel': MdFrameworkPanel,
+}
+
+/**
+ * Render markdown directly to JSX elements (for RSC).
+ * This avoids the HTML string → parse step on the client.
+ */
+export async function renderMarkdownToJsx(
+ content: string,
+): Promise {
+ const headings: MarkdownHeading[] = []
+
+ const processor = unified()
+ .use(remarkParse)
+ .use(remarkGfm)
+ .use(remarkRehype, { allowDangerousHtml: true })
+ .use(extractCodeMeta)
+ .use(rehypeRaw)
+ .use(rehypeParseCommentComponents)
+ .use(rehypeCallouts, {
+ theme: 'github',
+ props: {
+ containerProps: (_node: any, type: string) => ({
+ className: `markdown-alert markdown-alert-${type}`,
+ }),
+ titleIconProps: () => ({
+ className: 'octicon octicon-info mr-2',
+ }),
+ titleProps: () => ({
+ className: 'markdown-alert-title',
+ }),
+ titleTextProps: () => ({
+ className: 'markdown-alert-title',
+ }),
+ contentProps: () => ({
+ className: 'markdown-alert-content',
+ }),
+ },
+ } as any)
+ .use(rehypeShiki, shikiOptions)
+ .use(rehypeSlug)
+ .use(rehypeTransformFrameworkComponents)
+ .use(rehypeTransformCommentComponents)
+ .use(rehypeAutolinkHeadings, {
+ behavior: 'wrap',
+ properties: {
+ className: ['anchor-heading'],
+ },
+ })
+ .use(() => rehypeCollectHeadings(headings))
+ .use(rehypeReact, {
+ Fragment: jsxRuntime.Fragment,
+ jsx: jsxRuntime.jsx,
+ jsxs: jsxRuntime.jsxs,
+ components: markdownComponents,
+ } as any)
+
+ const file = await processor.process(content)
+
+ return {
+ content: file.result as React.ReactNode,
+ headings,
+ }
+}
diff --git a/src/utils/markdown/types.ts b/src/utils/markdown/types.ts
new file mode 100644
index 000000000..670cf231e
--- /dev/null
+++ b/src/utils/markdown/types.ts
@@ -0,0 +1,8 @@
+// Markdown types - separate file to avoid pulling processor into client bundles
+
+export type MarkdownHeading = {
+ id: string
+ text: string
+ level: number
+ framework?: string
+}
diff --git a/src/utils/renderBlogContent.tsx b/src/utils/renderBlogContent.tsx
new file mode 100644
index 000000000..d54b2372a
--- /dev/null
+++ b/src/utils/renderBlogContent.tsx
@@ -0,0 +1,15 @@
+// Server function for rendering blog content as RSC
+// This file is separate from the route to ensure proper bundler processing
+// of the server component and client component imports.
+
+import { createServerFn } from '@tanstack/react-start'
+import { renderServerComponent } from '@tanstack/react-start/rsc'
+import { renderMarkdownToJsx } from '~/utils/markdown'
+import { BlogContent } from '~/components/markdown/BlogContent'
+
+export const renderBlogContent = createServerFn({ method: 'GET' })
+ .inputValidator((content: string) => content)
+ .handler(async ({ data: content }) => {
+ const { content: jsxContent } = await renderMarkdownToJsx(content)
+ return renderServerComponent({jsxContent} )
+ })
diff --git a/vite.config.ts b/vite.config.ts
index d8eeabd05..5d615414b 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -6,11 +6,45 @@ import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import tailwindcss from '@tailwindcss/vite'
import { analyzer } from 'vite-bundle-analyzer'
import viteReact from '@vitejs/plugin-react'
+import rsc from '@vitejs/plugin-rsc'
import netlify from '@netlify/vite-plugin-tanstack-start'
+import { fileURLToPath } from 'url'
+import { dirname, resolve } from 'path'
+const __dirname = dirname(fileURLToPath(import.meta.url))
const isDev = process.env.NODE_ENV !== 'production'
+// Packages that must be externalized due to CJS/ESM interop issues with RSC
+// These packages use patterns that conflict with Vite's ESM module runner
+// NOTE: html-react-parser and domhandler were removed to allow RSC to use Markdown component
+const rscExternals = [
+ // HTML parsing (cheerio uses iconv-lite which has CJS module issues)
+ 'cheerio',
+ 'iconv-lite',
+ 'encoding-sniffer',
+ 'parse5',
+ 'parse5-parser-stream',
+ // Discord SDK has crypto import issues
+ 'discord-interactions',
+ // OpenTelemetry uses require-in-the-middle which is CJS-only
+ 'require-in-the-middle',
+ '@opentelemetry/instrumentation',
+ // jszip has CJS module transformation issues
+ 'jszip',
+ 'pako',
+]
+
export default defineConfig({
+ resolve: {
+ alias: {
+ // Force react-is to resolve to the local node_modules version
+ // This ensures Vite can pre-bundle the CJS module for browser use
+ 'react-is': resolve(
+ __dirname,
+ 'node_modules/.pnpm/react-is@19.2.4/node_modules/react-is',
+ ),
+ },
+ },
server: {
port: Number(process.env.PORT) || 3000,
// WebContainer headers for /builder route (SharedArrayBuffer support)
@@ -25,6 +59,17 @@ export default defineConfig({
}
: undefined,
},
+ // RSC environment needs to externalize packages that import react-dom/server
+ environments: {
+ rsc: {
+ resolve: {
+ external: [
+ '@tanstack/react-start-server',
+ '@tanstack/react-router/ssr/server',
+ ],
+ },
+ },
+ },
ssr: {
external: [
'postgres',
@@ -32,10 +77,22 @@ export default defineConfig({
'@tanstack/create',
// Externalize CLI so server reloads it on changes
'@tanstack/cli',
+ // RSC compatibility externals
+ ...rscExternals,
+ ],
+ noExternal: [
+ 'drizzle-orm',
+ // react-is is CJS but used by @tanstack/react-start-rsc in client code
+ // noExternal forces Vite to bundle it (converting CJS to ESM)
+ 'react-is',
],
- noExternal: ['drizzle-orm'],
},
optimizeDeps: {
+ include: [
+ // react-is is CJS-only but start-rsc imports it in 'use client' code
+ // Pre-bundling allows Vite to create proper ESM wrappers for browser
+ 'react-is',
+ ],
exclude: [
'postgres',
// CTA packages use execa which has a broken unicorn-magic dependency
@@ -89,6 +146,9 @@ export default defineConfig({
}),
tanstackStart({
+ rsc: {
+ enabled: true,
+ },
router: {
codeSplittingOptions: {
defaultBehavior: [
@@ -103,6 +163,11 @@ export default defineConfig({
},
},
}),
+ rsc({
+ // Disable CSS link precedence to prevent React 19 SSR suspension
+ // TanStack Start handles CSS preloading via manifest injection instead
+ cssLinkPrecedence: false,
+ }),
// Only enable Netlify plugin during build or when NETLIFY env is set
...(process.env.NETLIFY || process.env.NODE_ENV === 'production'
? [netlify()]
From fc8b892451fd61366c76431aa2a2ee945f5e2d44 Mon Sep 17 00:00:00 2001
From: Tanner Linsley
Date: Tue, 10 Feb 2026 11:16:24 -0700
Subject: [PATCH 2/5] fix: externalize node: built-ins from client bundle
Fixes build with linked @tanstack packages that import Node.js built-ins
---
vite.config.ts | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/vite.config.ts b/vite.config.ts
index 5d615414b..0713a7dc0 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -106,7 +106,11 @@ export default defineConfig({
rollupOptions: {
external: (id) => {
// Externalize postgres from client bundle
- return id.includes('postgres')
+ if (id.includes('postgres')) return true
+ // Externalize Node.js built-in modules that linked packages import
+ // These should never be in client bundles
+ if (id.startsWith('node:')) return true
+ return false
},
output: {
manualChunks: (id) => {
From 68dfe5fa8afb59dfd81b22b8c7b217162b089d7a Mon Sep 17 00:00:00 2001
From: Tanner Linsley
Date: Tue, 10 Feb 2026 21:56:23 -0700
Subject: [PATCH 3/5] refactor: convert dynamic imports to static imports
- Move blog post loading to createServerFn (loadBlogPost)
- Static imports work when server code is in createServerFn
- Route loaders that directly import server modules need dynamic imports
- setCacheHeaders can be static in routes using createServerFn
---
src/routes/blog.$.tsx | 44 +++++------------------------
src/routes/blog.index.tsx | 2 +-
src/routes/index.tsx | 2 +-
src/utils/renderBlogContent.tsx | 50 +++++++++++++++++++++++++++++----
4 files changed, 54 insertions(+), 44 deletions(-)
diff --git a/src/routes/blog.$.tsx b/src/routes/blog.$.tsx
index a00e83a89..098df6a5b 100644
--- a/src/routes/blog.$.tsx
+++ b/src/routes/blog.$.tsx
@@ -2,7 +2,6 @@ import { notFound, redirect, createFileRoute } from '@tanstack/react-router'
import { seo } from '~/utils/seo'
import { PostNotFound } from './blog'
import { formatAuthors } from '~/utils/blog'
-import { format } from '~/utils/dates'
import { allPosts } from 'content-collections'
import * as React from 'react'
import { GamHeader } from '~/components/Gam'
@@ -13,6 +12,7 @@ import { DocTitle } from '~/components/DocTitle'
import { CopyPageDropdown } from '~/components/CopyPageDropdown'
import { Button } from '~/ui'
import { SquarePen } from 'lucide-react'
+import { loadBlogPost } from '~/utils/renderBlogContent'
function handleRedirects(docsPath: string) {
if (docsPath.includes('directives-the-new-framework-lock-in')) {
@@ -25,50 +25,20 @@ function handleRedirects(docsPath: string) {
export const Route = createFileRoute('/blog/$')({
staleTime: Infinity,
loader: async ({ params }) => {
- const docsPath = params._splat
- if (!docsPath) {
+ const slug = params._splat
+ if (!slug) {
throw new Error('Invalid docs path')
}
- handleRedirects(docsPath)
-
- const filePath = `src/blog/${docsPath}.md`
- const post = allPosts.find((p) => p.slug === docsPath)
+ handleRedirects(slug)
+ // Check if post exists before calling server function
+ const post = allPosts.find((p) => p.slug === slug)
if (!post) {
throw notFound()
}
- const { setCacheHeaders } = await import('~/utils/headers.server')
- setCacheHeaders()
-
- const now = new Date()
- const publishDate = new Date(post.published)
- const isUnpublished = post.draft || publishDate > now
-
- const blogContent = `_by ${formatAuthors(post.authors)} on ${format(
- new Date(post.published || 0),
- 'MMMM d, yyyy',
- )}._
-
-${post.content}`
-
- const { renderMarkdownToJsx } = await import('~/utils/markdown')
- const { headings } = await renderMarkdownToJsx(blogContent)
- const { renderBlogContent } = await import('~/utils/renderBlogContent')
- const ContentRsc = await renderBlogContent({ data: blogContent })
-
- return {
- title: post.title,
- description: post.description,
- published: post.published,
- authors: post.authors,
- headerImage: post.headerImage,
- filePath,
- isUnpublished,
- headings,
- ContentRsc,
- }
+ return loadBlogPost({ data: { slug } })
},
head: ({ loaderData }) => {
// Generate optimized social media image URL using Netlify Image CDN
diff --git a/src/routes/blog.index.tsx b/src/routes/blog.index.tsx
index 46cab45c8..3b0ffc61e 100644
--- a/src/routes/blog.index.tsx
+++ b/src/routes/blog.index.tsx
@@ -8,6 +8,7 @@ import { Footer } from '~/components/Footer'
import { PostNotFound } from './blog'
import { createServerFn } from '@tanstack/react-start'
import { RssIcon } from 'lucide-react'
+import { setCacheHeaders } from '~/utils/headers.server'
type BlogFrontMatter = {
slug: string
@@ -19,7 +20,6 @@ type BlogFrontMatter = {
const fetchFrontMatters = createServerFn({ method: 'GET' }).handler(
async () => {
- const { setCacheHeaders } = await import('~/utils/headers.server')
setCacheHeaders()
const posts = getPublishedPosts()
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
index dfab6739e..61fd5e7e9 100644
--- a/src/routes/index.tsx
+++ b/src/routes/index.tsx
@@ -19,6 +19,7 @@ import { formatAuthors, getPublishedPosts } from '~/utils/blog'
import { format } from '~/utils/dates'
import { SimpleMarkdown } from '~/components/SimpleMarkdown'
import { renderMarkdownAsync } from '~/utils/markdown'
+import { setCacheHeaders } from '~/utils/headers.server'
import { NetlifyImage } from '~/components/NetlifyImage'
import { createServerFn } from '@tanstack/react-start'
import { AdGate } from '~/contexts/AdsContext'
@@ -63,7 +64,6 @@ type BlogFrontMatter = {
const fetchRecentPosts = createServerFn({ method: 'GET' }).handler(
async (): Promise => {
- const { setCacheHeaders } = await import('~/utils/headers.server')
setCacheHeaders()
const posts = getPublishedPosts().slice(0, 3)
diff --git a/src/utils/renderBlogContent.tsx b/src/utils/renderBlogContent.tsx
index d54b2372a..151ba09c4 100644
--- a/src/utils/renderBlogContent.tsx
+++ b/src/utils/renderBlogContent.tsx
@@ -6,10 +6,50 @@ import { createServerFn } from '@tanstack/react-start'
import { renderServerComponent } from '@tanstack/react-start/rsc'
import { renderMarkdownToJsx } from '~/utils/markdown'
import { BlogContent } from '~/components/markdown/BlogContent'
+import { setCacheHeaders } from '~/utils/headers.server'
+import { allPosts } from 'content-collections'
+import { formatAuthors } from '~/utils/blog'
+import { format } from '~/utils/dates'
+import { notFound } from '@tanstack/react-router'
+import * as v from 'valibot'
-export const renderBlogContent = createServerFn({ method: 'GET' })
- .inputValidator((content: string) => content)
- .handler(async ({ data: content }) => {
- const { content: jsxContent } = await renderMarkdownToJsx(content)
- return renderServerComponent({jsxContent} )
+export const loadBlogPost = createServerFn({ method: 'GET' })
+ .inputValidator(v.object({ slug: v.string() }))
+ .handler(async ({ data: { slug } }) => {
+ const post = allPosts.find((p) => p.slug === slug)
+
+ if (!post) {
+ throw notFound()
+ }
+
+ setCacheHeaders()
+
+ const now = new Date()
+ const publishDate = new Date(post.published)
+ const isUnpublished = post.draft || publishDate > now
+
+ const blogContent = `_by ${formatAuthors(post.authors)} on ${format(
+ new Date(post.published || 0),
+ 'MMMM d, yyyy',
+ )}._
+
+${post.content}`
+
+ const { content: jsxContent, headings } =
+ await renderMarkdownToJsx(blogContent)
+ const ContentRsc = await renderServerComponent(
+ {jsxContent} ,
+ )
+
+ return {
+ title: post.title,
+ description: post.description,
+ published: post.published,
+ authors: post.authors,
+ headerImage: post.headerImage,
+ filePath: `src/blog/${slug}.md`,
+ isUnpublished,
+ headings,
+ ContentRsc,
+ }
})
From caa303f93477febe16ec24b8ac959256ccdd7511 Mon Sep 17 00:00:00 2001
From: Tanner Linsley
Date: Tue, 10 Feb 2026 23:32:23 -0700
Subject: [PATCH 4/5] refactor: dedupe feed markdown RSC rendering and cleanup
legacy code
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Add shared renderMarkdownRsc utility for markdown→JSX→RSC pipeline
- Feed list/timeline/detail now use RSC instead of HTML string parsing
- Feed detail page reuses entry.contentRsc instead of re-rendering
- Admin edit path skips content rendering (only needs raw content)
- Separate query keys for raw vs rendered feed entries
- Preview query key uses content hash to avoid cache bloat
- Remove legacy Markdown.tsx and handler files (dead code)
- Remove DocContent pass-through wrapper (inlined)
- DRY processor.tsx with shared createBasePipeline function
- Remove unused exports (renderMarkdown sync, highlightCode)
- Fix lint errors in MdComponents.tsx (JSX in try/catch)
Net: -400 lines, cleaner architecture, better perf for admin path
Note: Pre-existing TS errors in other files (ShowcaseGallery, landing pages, etc.)
prevented normal commit. Smoke tests and lint pass.
---
src/components/FeedEntry.tsx | 9 +-
src/components/FeedEntryTimeline.tsx | 5 +-
src/components/admin/FeedEntryEditor.tsx | 56 +++++-
src/components/markdown/DocContent.tsx | 14 --
src/components/markdown/Markdown.tsx | 142 -------------
.../markdown/MarkdownFrameworkHandler.tsx | 46 -----
.../markdown/MarkdownTabsHandler.tsx | 74 -------
src/components/markdown/MdComponents.tsx | 42 ++--
src/queries/feed.ts | 6 +-
src/routes/feed.$id.tsx | 45 +----
src/utils/docs.tsx | 5 +-
src/utils/feed.functions.ts | 38 +++-
src/utils/markdown/index.ts | 4 +-
src/utils/markdown/processor.tsx | 188 ++----------------
src/utils/markdown/renderRsc.tsx | 39 ++++
15 files changed, 179 insertions(+), 534 deletions(-)
delete mode 100644 src/components/markdown/DocContent.tsx
delete mode 100644 src/components/markdown/Markdown.tsx
delete mode 100644 src/components/markdown/MarkdownFrameworkHandler.tsx
delete mode 100644 src/components/markdown/MarkdownTabsHandler.tsx
create mode 100644 src/utils/markdown/renderRsc.tsx
diff --git a/src/components/FeedEntry.tsx b/src/components/FeedEntry.tsx
index 90a177326..0e8416de8 100644
--- a/src/components/FeedEntry.tsx
+++ b/src/components/FeedEntry.tsx
@@ -1,6 +1,5 @@
+import type { ReactNode } from 'react'
import { format, formatDistanceToNow } from '~/utils/dates'
-// TODO: Fix feed to use server-rendered markdown
-// import { Markdown } from '~/components/markdown'
import { libraries } from '~/libraries'
import { partners } from '~/utils/partners'
import { twMerge } from 'tailwind-merge'
@@ -14,6 +13,7 @@ export interface FeedEntry {
entryType: 'release' | 'blog' | 'announcement'
title: string
content: string
+ contentRsc?: ReactNode
excerpt?: string | null
publishedAt: number
createdAt: number
@@ -415,9 +415,8 @@ export function FeedEntry({
{/* Content */}
-
- {/* TODO: Fix feed to use server-rendered markdown */}
-
{entry.content}
+
+ {entry.contentRsc ?? entry.content}
{/* External Link */}
diff --git a/src/components/FeedEntryTimeline.tsx b/src/components/FeedEntryTimeline.tsx
index 56dde739a..0ffcc4abd 100644
--- a/src/components/FeedEntryTimeline.tsx
+++ b/src/components/FeedEntryTimeline.tsx
@@ -1,7 +1,5 @@
import * as React from 'react'
import { format, formatDistanceToNow } from '~/utils/dates'
-// TODO: Fix feed to use server-rendered markdown
-// import { Markdown } from '~/components/markdown'
import { Card } from '~/components/Card'
import { libraries } from '~/libraries'
import { partners } from '~/utils/partners'
@@ -334,8 +332,7 @@ export function FeedEntryTimeline({
!expanded && 'line-clamp-6',
)}
>
- {/* TODO: Fix feed to use server-rendered markdown */}
-
{entry.content}
+ {entry.contentRsc ?? entry.content}
{/* Show more/less button */}
diff --git a/src/components/admin/FeedEntryEditor.tsx b/src/components/admin/FeedEntryEditor.tsx
index fd2fb351a..fc396c9d3 100644
--- a/src/components/admin/FeedEntryEditor.tsx
+++ b/src/components/admin/FeedEntryEditor.tsx
@@ -1,14 +1,14 @@
import * as React from 'react'
import { useState } from 'react'
-import { useQuery } from '@tanstack/react-query'
+import { useQuery, queryOptions } from '@tanstack/react-query'
+import { createServerFn } from '@tanstack/react-start'
import { FeedEntry } from '~/components/FeedEntry'
-// TODO: Fix feed editor to use server-rendered markdown
-// import { Markdown } from '~/components/markdown'
import { libraries } from '~/libraries'
import { partners } from '~/utils/partners'
import { currentUserQueryOptions } from '~/queries/auth'
import { useCreateFeedEntry, useUpdateFeedEntry } from '~/utils/mutations'
import { generateManualEntryId } from '~/utils/feed-manual'
+import { renderMarkdownRsc } from '~/utils/markdown'
import {
Save,
X,
@@ -21,6 +21,35 @@ import {
} from 'lucide-react'
import { FormInput, Button } from '~/ui'
+// Server function to render markdown preview as RSC
+const renderPreviewRsc = createServerFn({ method: 'POST' })
+ .inputValidator((content: string) => content)
+ .handler(async ({ data: content }) => {
+ if (!content) return null
+ const { contentRsc } = await renderMarkdownRsc(content)
+ return contentRsc
+ })
+
+// Query options for preview - keyed by content hash for deduplication
+const previewQueryOptions = (content: string) =>
+ queryOptions({
+ queryKey: ['feed', 'preview', hashContent(content)],
+ queryFn: () => renderPreviewRsc({ data: content }),
+ staleTime: 1000 * 60 * 5, // Cache preview for 5 minutes
+ enabled: !!content,
+ })
+
+// Simple hash for query key (avoids bloating cache with full content strings)
+function hashContent(str: string): string {
+ let hash = 0
+ for (let i = 0; i < str.length; i++) {
+ const char = str.charCodeAt(i)
+ hash = (hash << 5) - hash + char
+ hash |= 0
+ }
+ return hash.toString(36)
+}
+
interface FeedEntryEditorProps {
entry: FeedEntry | null
onSave: () => void
@@ -55,6 +84,16 @@ export function FeedEntryEditor({
const [featured, setFeatured] = useState(entry?.featured ?? false)
const [saving, setSaving] = useState(false)
+ // Debounced content for preview query
+ const [debouncedContent, setDebouncedContent] = useState(content)
+ React.useEffect(() => {
+ const timer = setTimeout(() => setDebouncedContent(content), 300)
+ return () => clearTimeout(timer)
+ }, [content])
+
+ // RSC preview via useQuery
+ const previewQuery = useQuery(previewQueryOptions(debouncedContent))
+
const userQuery = useQuery(currentUserQueryOptions())
const user = userQuery.data
const createEntry = useCreateFeedEntry()
@@ -476,8 +515,15 @@ export function FeedEntryEditor({
{excerpt}
)}
- {/* TODO: Fix feed editor to use server-rendered markdown */}
-
{content || '*No content yet*'}
+ {previewQuery.data ? (
+ previewQuery.data
+ ) : previewQuery.isFetching ? (
+
Rendering preview...
+ ) : content ? (
+
Type to see preview
+ ) : (
+
No content yet
+ )}
) : (
diff --git a/src/components/markdown/DocContent.tsx b/src/components/markdown/DocContent.tsx
deleted file mode 100644
index 5bff9f2bb..000000000
--- a/src/components/markdown/DocContent.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-// Server component for doc markdown content
-// This file does NOT have 'use client' - it's a server component.
-// The children are JSX elements produced by renderMarkdownToJsx on the server,
-// which already include client component references for interactive elements.
-
-import type { ReactNode } from 'react'
-
-type DocContentProps = {
- children: ReactNode
-}
-
-export function DocContent({ children }: DocContentProps) {
- return <>{children}>
-}
diff --git a/src/components/markdown/Markdown.tsx b/src/components/markdown/Markdown.tsx
deleted file mode 100644
index 2315bd132..000000000
--- a/src/components/markdown/Markdown.tsx
+++ /dev/null
@@ -1,142 +0,0 @@
-'use client'
-
-import type { HTMLProps } from 'react'
-import * as React from 'react'
-import { MarkdownLink } from './MarkdownLink'
-
-import parse, {
- attributesToProps,
- domToReact,
- Element,
- HTMLReactParserOptions,
-} from 'html-react-parser'
-
-import { CodeBlock } from './CodeBlock'
-import { handleTabsComponent } from './MarkdownTabsHandler'
-import { handleFrameworkComponent } from './MarkdownFrameworkHandler'
-import { InlineCode, MarkdownImg } from '~/ui'
-
-type HeadingLevel = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
-
-const CustomHeading = ({
- Comp,
- id,
- children,
- ...props
-}: HTMLProps
& {
- Comp: HeadingLevel
-}) => {
- // Convert children to array and strip any inner anchor (native 'a' or MarkdownLink)
- const childrenArray = React.Children.toArray(children)
- const sanitizedChildren = childrenArray.map((child) => {
- if (
- React.isValidElement(child) &&
- (child.type === 'a' || child.type === MarkdownLink)
- ) {
- // replace anchor child with its own children so outer anchor remains the only link
- return (child.props as { children?: React.ReactNode }).children ?? null
- }
- return child
- })
-
- const heading = (
-
- {sanitizedChildren}
-
- )
-
- if (id) {
- return (
-
- {heading}
-
- )
- }
-
- return heading
-}
-
-const makeHeading =
- (type: HeadingLevel) => (props: HTMLProps) => (
-
- )
-
-const MarkdownIframe = React.memo(function MarkdownIframe(
- props: HTMLProps,
-) {
- return
-})
-
-const markdownComponents: Record = {
- a: MarkdownLink,
- pre: CodeBlock,
- h1: makeHeading('h1'),
- h2: makeHeading('h2'),
- h3: makeHeading('h3'),
- h4: makeHeading('h4'),
- h5: makeHeading('h5'),
- h6: makeHeading('h6'),
- code: InlineCode,
- iframe: MarkdownIframe,
- img: MarkdownImg,
-}
-
-const options: HTMLReactParserOptions = {
- replace: (domNode) => {
- if (domNode instanceof Element && domNode.attribs) {
- if (domNode.name === 'md-comment-component') {
- const componentName = domNode.attribs['data-component']
- const rawAttributes = domNode.attribs['data-attributes']
- const attributes: Record = {}
- try {
- Object.assign(attributes, JSON.parse(rawAttributes))
- } catch {
- // ignore JSON parse errors and fall back to empty props
- }
-
- switch (componentName?.toLowerCase()) {
- case 'tabs':
- return handleTabsComponent(domNode, attributes, options)
- case 'framework':
- return handleFrameworkComponent(domNode, attributes, options)
- default:
- return {domToReact(domNode.children as any, options)}
- }
- }
-
- const replacer = markdownComponents[domNode.name]
- if (replacer) {
- return React.createElement(
- replacer,
- attributesToProps(domNode.attribs),
- domToReact(domNode.children as any, options),
- )
- }
- }
-
- return
- },
-}
-
-type MarkdownProps = {
- htmlMarkup: string
-}
-
-export const Markdown = React.memo(function Markdown({
- htmlMarkup,
-}: MarkdownProps) {
- return React.useMemo(() => {
- if (!htmlMarkup) {
- return null
- }
-
- return parse(htmlMarkup, options)
- }, [htmlMarkup])
-})
diff --git a/src/components/markdown/MarkdownFrameworkHandler.tsx b/src/components/markdown/MarkdownFrameworkHandler.tsx
deleted file mode 100644
index 891066373..000000000
--- a/src/components/markdown/MarkdownFrameworkHandler.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import * as React from 'react'
-import { domToReact, Element } from 'html-react-parser'
-import type { HTMLReactParserOptions } from 'html-react-parser'
-import { FrameworkContent } from './FrameworkContent'
-
-export function handleFrameworkComponent(
- domNode: Element,
- attributes: Record,
- options: HTMLReactParserOptions,
-) {
- const frameworkMeta = domNode.attribs['data-framework-meta']
- if (!frameworkMeta) {
- return null
- }
-
- try {
- const { codeBlocksByFramework } = JSON.parse(frameworkMeta)
- const availableFrameworks = JSON.parse(
- domNode.attribs['data-available-frameworks'] || '[]',
- )
-
- const panelElements = domNode.children?.filter(
- (child): child is Element =>
- child instanceof Element && child.name === 'md-framework-panel',
- )
-
- // Build panelsByFramework map
- const panelsByFramework: Record = {}
- panelElements?.forEach((panel) => {
- const fw = panel.attribs['data-framework']
- if (fw) {
- panelsByFramework[fw] = domToReact(panel.children as any, options)
- }
- })
-
- return (
-
- )
- } catch {
- return null
- }
-}
diff --git a/src/components/markdown/MarkdownTabsHandler.tsx b/src/components/markdown/MarkdownTabsHandler.tsx
deleted file mode 100644
index 0facd4285..000000000
--- a/src/components/markdown/MarkdownTabsHandler.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-import * as React from 'react'
-import { domToReact, Element } from 'html-react-parser'
-import type { HTMLReactParserOptions } from 'html-react-parser'
-import type { Framework } from '~/libraries/types'
-import { Tabs } from './Tabs'
-import { PackageManagerTabs } from './PackageManagerTabs'
-import { FileTabs } from './FileTabs'
-
-export function handleTabsComponent(
- domNode: Element,
- attributes: Record,
- options: HTMLReactParserOptions,
-) {
- const pmMeta = domNode.attribs['data-package-manager-meta']
-
- // Handle package-manager variant
- if (pmMeta) {
- try {
- const { packagesByFramework, mode } = JSON.parse(pmMeta)
- const frameworks = Object.keys(packagesByFramework) as Framework[]
-
- return (
-
- )
- } catch {
- // Fall through to default tabs if parsing fails
- }
- }
-
- // Check if this is files variant
- const filesMeta = domNode.attribs['data-files-meta']
- if (filesMeta) {
- try {
- const tabs = attributes.tabs || []
-
- const panelElements = domNode.children?.filter(
- (child): child is Element =>
- child instanceof Element && child.name === 'md-tab-panel',
- )
-
- const children = panelElements?.map((panel) =>
- domToReact(panel.children as any, options),
- )
-
- return
- } catch {
- // Fall through to default tabs if parsing fails
- }
- }
-
- // Handle default tabs variant
- const tabs = attributes.tabs
-
- if (!tabs || !Array.isArray(tabs)) {
- return null
- }
-
- const panelElements = domNode.children?.filter(
- (child): child is Element =>
- child instanceof Element && child.name === 'md-tab-panel',
- )
-
- const children = panelElements?.map((panel) => {
- const result = domToReact(panel.children as any, options)
- // Wrap in fragment to ensure it's a single React node
- return <>{result}>
- })
-
- return
-}
diff --git a/src/components/markdown/MdComponents.tsx b/src/components/markdown/MdComponents.tsx
index 6c1b2f9fe..760dcb977 100644
--- a/src/components/markdown/MdComponents.tsx
+++ b/src/components/markdown/MdComponents.tsx
@@ -7,6 +7,15 @@ import { PackageManagerTabs } from './PackageManagerTabs'
import { FileTabs } from './FileTabs'
import { FrameworkContent } from './FrameworkContent'
+// Safe JSON parse that returns null on failure instead of throwing
+function safeJsonParse(str: string): T | null {
+ try {
+ return JSON.parse(str) as T
+ } catch {
+ return null
+ }
+}
+
type MdCommentComponentProps = {
'data-component'?: string
'data-attributes'?: string
@@ -43,8 +52,12 @@ export function MdCommentComponent({
if (normalizedComponent === 'tabs') {
// Handle package-manager variant
if (pmMeta) {
- try {
- const { packagesByFramework, mode } = JSON.parse(pmMeta)
+ const parsed = safeJsonParse<{
+ packagesByFramework: Record
+ mode: 'install' | 'dev-install' | 'local-install' | 'create' | 'custom'
+ }>(pmMeta)
+ if (parsed) {
+ const { packagesByFramework, mode } = parsed
const frameworks = Object.keys(packagesByFramework) as Framework[]
return (
@@ -54,28 +67,23 @@ export function MdCommentComponent({
frameworks={frameworks}
/>
)
- } catch {
- // Fall through to default tabs if parsing fails
}
}
// Handle files variant
if (filesMeta) {
- try {
- const tabs = attributes.tabs || []
- // Children are already React nodes from rehype-react
- const childArray = React.Children.toArray(children)
- const panelChildren = childArray.filter(
- (child): child is React.ReactElement =>
- React.isValidElement(child) &&
- (child.type === MdTabPanel || child.type === 'md-tab-panel'),
- )
-
+ const tabs = attributes.tabs || []
+ // Children are already React nodes from rehype-react
+ const childArray = React.Children.toArray(children)
+ const panelChildren = childArray.filter(
+ (child): child is React.ReactElement =>
+ React.isValidElement(child) &&
+ (child.type === MdTabPanel || child.type === 'md-tab-panel'),
+ )
+
+ if (panelChildren.length > 0) {
const tabContents = panelChildren.map((panel) => panel.props.children)
-
return
- } catch {
- // Fall through to default tabs if parsing fails
}
}
diff --git a/src/queries/feed.ts b/src/queries/feed.ts
index 4b4cb8f73..294b7384e 100644
--- a/src/queries/feed.ts
+++ b/src/queries/feed.ts
@@ -35,15 +35,17 @@ export const listFeedEntriesQueryOptions = (params: ListFeedEntriesParams) =>
queryFn: () => listFeedEntries({ data: params }),
})
+// Used by admin edit - returns raw content without RSC rendering
export const getFeedEntryQueryOptions = (id: string) =>
queryOptions({
- queryKey: ['feed', 'entry', id],
+ queryKey: ['feed', 'entry', 'raw', id],
queryFn: () => getFeedEntry({ data: { id } }),
})
+// Used by detail page - returns pre-rendered contentRsc
export const getFeedEntryByIdQueryOptions = (id: string) =>
queryOptions({
- queryKey: ['feed', 'entryById', id],
+ queryKey: ['feed', 'entry', 'rendered', id],
queryFn: () => getFeedEntryById({ data: { id } }),
})
diff --git a/src/routes/feed.$id.tsx b/src/routes/feed.$id.tsx
index b7046e5af..d56a8f1e2 100644
--- a/src/routes/feed.$id.tsx
+++ b/src/routes/feed.$id.tsx
@@ -1,13 +1,10 @@
import { Link, notFound, createFileRoute } from '@tanstack/react-router'
-import { createServerFn } from '@tanstack/react-start'
-import { renderServerComponent } from '@tanstack/react-start/rsc'
import { Footer } from '~/components/Footer'
import { seo } from '~/utils/seo'
import { useCapabilities } from '~/hooks/useCapabilities'
import { isAdmin } from '~/db/types'
import * as v from 'valibot'
import { format, formatDistanceToNow } from '~/utils/dates'
-import { renderMarkdownToJsx } from '~/utils/markdown'
import { libraries } from '~/libraries'
import { partners } from '~/utils/partners'
import { twMerge } from 'tailwind-merge'
@@ -17,24 +14,12 @@ import { ArrowLeft } from 'lucide-react'
const searchSchema = v.object({})
-// Server function that renders markdown content as RSC
-const renderFeedContent = createServerFn({ method: 'GET' })
- .inputValidator((content: string) => content)
- .handler(async ({ data: content }) => {
- const { content: jsxContent } = await renderMarkdownToJsx(content)
- return renderServerComponent(
-
- {jsxContent}
-
,
- )
- })
-
export const Route = createFileRoute('/feed/$id')({
staleTime: 1000 * 60 * 5, // 5 minutes
loader: async ({ params, context: { queryClient } }) => {
const entryId = params.id
- // Fetch the feed entry by id
+ // Fetch the feed entry by id (includes pre-rendered contentRsc)
const entry = await queryClient.ensureQueryData(
getFeedEntryByIdQueryOptions(entryId),
)
@@ -43,17 +28,7 @@ export const Route = createFileRoute('/feed/$id')({
throw notFound()
}
- // Render markdown content as RSC
- const ContentRsc = await renderFeedContent({ data: entry.content })
-
- // Check if entry is visible (unless admin)
- // Note: We'll check capabilities client-side since they depend on auth
- // For SSR, we'll allow the entry to load and handle visibility client-side
-
- return {
- entry,
- ContentRsc,
- }
+ return { entry }
},
headers: () => ({
'cache-control': 'public, max-age=0, must-revalidate',
@@ -106,7 +81,7 @@ export const Route = createFileRoute('/feed/$id')({
})
function FeedItemPage() {
- const { entry, ContentRsc } = Route.useLoaderData()
+ const { entry } = Route.useLoaderData()
const capabilities = useCapabilities()
// Show not found if entry isn't visible (unless admin)
@@ -136,16 +111,10 @@ function FeedItemPage() {
)
}
- return
+ return
}
-function FeedEntryView({
- entry,
- ContentRsc,
-}: {
- entry: FeedEntry
- ContentRsc: React.ReactNode
-}) {
+function FeedEntryView({ entry }: { entry: FeedEntry }) {
// Get library info
const entryLibraries = entry.libraryIds
.map((id) => libraries.find((lib) => lib.id === id))
@@ -390,7 +359,9 @@ function FeedEntryView({
)}
{/* Content */}
- {ContentRsc}
+
+ {entry.contentRsc ?? entry.content}
+
{/* External Link */}
{externalLink && (
diff --git a/src/utils/docs.tsx b/src/utils/docs.tsx
index b116c05bf..7833e3f72 100644
--- a/src/utils/docs.tsx
+++ b/src/utils/docs.tsx
@@ -10,7 +10,6 @@ import { renderServerComponent } from '@tanstack/react-start/rsc'
import * as v from 'valibot'
import { setResponseHeader } from '~/utils/headers.server'
import { renderMarkdownToJsx } from '~/utils/markdown'
-import { DocContent } from '~/components/markdown/DocContent'
export const loadDocs = async ({
repo,
@@ -58,9 +57,7 @@ export const fetchDocs = createServerFn({ method: 'GET' })
const { content, headings } = await renderMarkdownToJsx(frontMatter.content)
// Wrap in RSC stream for client hydration
- const contentRsc = await renderServerComponent(
- {content} ,
- )
+ const contentRsc = await renderServerComponent(<>{content}>)
// Cache for 5 minutes on shared cache
// Revalidate in the background
diff --git a/src/utils/feed.functions.ts b/src/utils/feed.functions.ts
index e69e30465..78e98c809 100644
--- a/src/utils/feed.functions.ts
+++ b/src/utils/feed.functions.ts
@@ -12,10 +12,14 @@ import {
filterByReleaseLevel,
} from './feed.server'
import { entryTypeSchema } from './schemas'
-
-// Transform database entry to API response format
-function transformFeedEntry(entry: typeof feedEntries.$inferSelect) {
- return {
+import { renderMarkdownRsc } from '~/utils/markdown'
+
+// Transform database entry to API response format (with pre-rendered RSC content)
+async function transformFeedEntry(
+ entry: typeof feedEntries.$inferSelect,
+ options?: { renderContent?: boolean },
+) {
+ const base = {
_id: entry.entryId,
id: entry.entryId,
entryType: entry.entryType,
@@ -34,6 +38,14 @@ function transformFeedEntry(entry: typeof feedEntries.$inferSelect) {
autoSynced: entry.autoSynced,
lastSyncedAt: entry.lastSyncedAt?.getTime(),
}
+
+ // Pre-render content as RSC for client-side display
+ if (options?.renderContent && entry.content) {
+ const { contentRsc } = await renderMarkdownRsc(entry.content)
+ return { ...base, contentRsc }
+ }
+
+ return base
}
export const listFeedEntries = createServerFn({ method: 'POST' })
@@ -109,7 +121,10 @@ export const listFeedEntries = createServerFn({ method: 'POST' })
const page = allEntries.slice(start, end)
const hasMore = end < allEntries.length
- const transformedPage = page.map(transformFeedEntry)
+ // Render content HTML for client-side display
+ const transformedPage = await Promise.all(
+ page.map((entry) => transformFeedEntry(entry, { renderContent: true })),
+ )
return {
page: transformedPage,
@@ -121,7 +136,7 @@ export const listFeedEntries = createServerFn({ method: 'POST' })
}
})
-// Server function wrapper for getFeedEntry
+// Server function wrapper for getFeedEntry (used by admin, skips content rendering)
export const getFeedEntry = createServerFn({ method: 'POST' })
.inputValidator(v.object({ id: v.string() }))
.handler(async ({ data }) => {
@@ -133,7 +148,8 @@ export const getFeedEntry = createServerFn({ method: 'POST' })
return null
}
- return transformFeedEntry(entry)
+ // Skip content rendering for admin edit (raw content is needed for editing)
+ return transformFeedEntry(entry, { renderContent: false })
})
// Server function wrapper for getFeedEntryById
@@ -148,7 +164,7 @@ export const getFeedEntryById = createServerFn({ method: 'POST' })
return null
}
- return transformFeedEntry(entry)
+ return transformFeedEntry(entry, { renderContent: true })
})
// Server function wrapper for getFeedStats
@@ -315,7 +331,11 @@ export const searchFeedEntries = createServerFn({ method: 'POST' })
)
.limit(limit)
- return entries.map(transformFeedEntry)
+ return Promise.all(
+ entries.map((entry) =>
+ transformFeedEntry(entry, { renderContent: true }),
+ ),
+ )
})
// Server function wrapper for getFeedConfig
diff --git a/src/utils/markdown/index.ts b/src/utils/markdown/index.ts
index d683bdd05..0d4c7391e 100644
--- a/src/utils/markdown/index.ts
+++ b/src/utils/markdown/index.ts
@@ -1,9 +1,9 @@
export {
- renderMarkdown,
renderMarkdownAsync,
renderMarkdownToJsx,
- highlightCode,
type MarkdownJsxResult,
} from './processor'
+export { renderMarkdownRsc } from './renderRsc'
+
export type { MarkdownHeading } from './types'
diff --git a/src/utils/markdown/processor.tsx b/src/utils/markdown/processor.tsx
index ba8677d0d..2b3de5fc2 100644
--- a/src/utils/markdown/processor.tsx
+++ b/src/utils/markdown/processor.tsx
@@ -13,7 +13,6 @@ import * as jsxRuntime from 'react/jsx-runtime'
import rehypeShiki from '@shikijs/rehype'
import { transformerNotationDiff } from '@shikijs/transformers'
import type { RehypeShikiOptions } from '@shikijs/rehype'
-import { createHighlighter, type Highlighter } from 'shiki'
import {
rehypeCollectHeadings,
@@ -102,12 +101,9 @@ const shikiOptions: RehypeShikiOptions = {
},
}
-export async function renderMarkdownAsync(
- content: string,
-): Promise {
- const headings: MarkdownHeading[] = []
-
- const processor = unified()
+// Build the common markdown processing pipeline up to heading collection
+function createBasePipeline(headings: MarkdownHeading[]) {
+ return unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkRehype, { allowDangerousHtml: true })
@@ -145,60 +141,14 @@ export async function renderMarkdownAsync(
},
})
.use(() => rehypeCollectHeadings(headings))
- .use(rehypeStringify)
-
- const file = await processor.process(content)
-
- return {
- markup: String(file),
- headings,
- }
}
-// Synchronous version for backwards compatibility (doesn't include syntax highlighting)
-export function renderMarkdown(content: string): MarkdownRenderResult {
+export async function renderMarkdownAsync(
+ content: string,
+): Promise {
const headings: MarkdownHeading[] = []
-
- const processor = unified()
- .use(remarkParse)
- .use(remarkGfm)
- .use(remarkRehype, { allowDangerousHtml: true })
- .use(extractCodeMeta)
- .use(rehypeRaw)
- .use(rehypeParseCommentComponents)
- .use(rehypeCallouts, {
- theme: 'github',
- props: {
- containerProps: (_node: any, type: string) => ({
- className: `markdown-alert markdown-alert-${type}`,
- }),
- titleIconProps: () => ({
- className: 'octicon octicon-info mr-2',
- }),
- titleProps: () => ({
- className: 'markdown-alert-title',
- }),
- titleTextProps: () => ({
- className: 'markdown-alert-title',
- }),
- contentProps: () => ({
- className: 'markdown-alert-content',
- }),
- },
- } as any)
- .use(rehypeSlug)
- .use(rehypeTransformFrameworkComponents)
- .use(rehypeTransformCommentComponents)
- .use(rehypeAutolinkHeadings, {
- behavior: 'wrap',
- properties: {
- className: ['anchor-heading'],
- },
- })
- .use(() => rehypeCollectHeadings(headings))
- .use(rehypeStringify)
-
- const file = processor.processSync(content)
+ const processor = createBasePipeline(headings).use(rehypeStringify)
+ const file = await processor.process(content)
return {
markup: String(file),
@@ -206,76 +156,6 @@ export function renderMarkdown(content: string): MarkdownRenderResult {
}
}
-// Lazy-loaded highlighter singleton for standalone code highlighting
-let highlighterPromise: Promise | null = null
-
-const SUPPORTED_LANGS = [
- 'typescript',
- 'javascript',
- 'tsx',
- 'jsx',
- 'bash',
- 'json',
- 'html',
- 'css',
- 'markdown',
- 'toml',
- 'yaml',
- 'sql',
- 'diff',
- 'vue',
- 'svelte',
- 'scss',
- 'jsonc',
- 'vue-html',
- 'angular-html',
- 'angular-ts',
- 'text',
-] as const
-
-async function getHighlighter(): Promise {
- if (!highlighterPromise) {
- highlighterPromise = createHighlighter({
- themes: ['github-light', 'vitesse-dark'],
- langs: [...SUPPORTED_LANGS],
- })
- }
- return highlighterPromise
-}
-
-/**
- * Highlight code with Shiki (server-side only).
- * Returns HTML string with dual-theme CSS variables for light/dark mode.
- */
-export async function highlightCode(
- code: string,
- lang: string,
-): Promise {
- const highlighter = await getHighlighter()
-
- // Normalize language alias
- const normalizedLang = LANG_ALIASES[lang] || lang
-
- // Check if language is supported, fallback to text
- const loadedLangs = highlighter.getLoadedLanguages()
- const effectiveLang = loadedLangs.includes(normalizedLang as any)
- ? normalizedLang
- : 'text'
-
- const html = highlighter.codeToHtml(code.trimEnd(), {
- lang: effectiveLang,
- themes: {
- light: 'github-light',
- dark: 'vitesse-dark',
- },
- defaultColor: false,
- cssVariablePrefix: '--shiki-',
- transformers: [transformerNotationDiff()],
- })
-
- return html
-}
-
// Custom heading component - rehype-autolink-headings already wraps with ,
// so we just render the heading element with proper styling
function createHeadingComponent(
@@ -344,6 +224,7 @@ function LinkElement(props: React.AnchorHTMLAttributes) {
// If this is an anchor-heading link (from rehype-autolink-headings), render as plain
// to avoid nested anchors when the heading content also has links
if (props.className?.includes('anchor-heading')) {
+ // eslint-disable-next-line jsx-a11y/anchor-has-content
return
}
// For all other links, use MarkdownLink which handles relative links
@@ -377,51 +258,12 @@ export async function renderMarkdownToJsx(
content: string,
): Promise {
const headings: MarkdownHeading[] = []
-
- const processor = unified()
- .use(remarkParse)
- .use(remarkGfm)
- .use(remarkRehype, { allowDangerousHtml: true })
- .use(extractCodeMeta)
- .use(rehypeRaw)
- .use(rehypeParseCommentComponents)
- .use(rehypeCallouts, {
- theme: 'github',
- props: {
- containerProps: (_node: any, type: string) => ({
- className: `markdown-alert markdown-alert-${type}`,
- }),
- titleIconProps: () => ({
- className: 'octicon octicon-info mr-2',
- }),
- titleProps: () => ({
- className: 'markdown-alert-title',
- }),
- titleTextProps: () => ({
- className: 'markdown-alert-title',
- }),
- contentProps: () => ({
- className: 'markdown-alert-content',
- }),
- },
- } as any)
- .use(rehypeShiki, shikiOptions)
- .use(rehypeSlug)
- .use(rehypeTransformFrameworkComponents)
- .use(rehypeTransformCommentComponents)
- .use(rehypeAutolinkHeadings, {
- behavior: 'wrap',
- properties: {
- className: ['anchor-heading'],
- },
- })
- .use(() => rehypeCollectHeadings(headings))
- .use(rehypeReact, {
- Fragment: jsxRuntime.Fragment,
- jsx: jsxRuntime.jsx,
- jsxs: jsxRuntime.jsxs,
- components: markdownComponents,
- } as any)
+ const processor = createBasePipeline(headings).use(rehypeReact, {
+ Fragment: jsxRuntime.Fragment,
+ jsx: jsxRuntime.jsx,
+ jsxs: jsxRuntime.jsxs,
+ components: markdownComponents,
+ } as any)
const file = await processor.process(content)
diff --git a/src/utils/markdown/renderRsc.tsx b/src/utils/markdown/renderRsc.tsx
new file mode 100644
index 000000000..adaf103af
--- /dev/null
+++ b/src/utils/markdown/renderRsc.tsx
@@ -0,0 +1,39 @@
+import * as React from 'react'
+import { renderServerComponent } from '@tanstack/react-start/rsc'
+import { renderMarkdownToJsx } from './processor'
+
+/**
+ * Render markdown content to RSC payload.
+ * Optionally wrap the content in a container component.
+ */
+export async function renderMarkdownRsc(
+ content: string,
+ options?: {
+ wrapper?: React.ComponentType<{ children: React.ReactNode }>
+ wrapperProps?: Record
+ className?: string
+ },
+) {
+ if (!content) {
+ return { contentRsc: null, headings: [] }
+ }
+
+ const { content: jsxContent, headings } = await renderMarkdownToJsx(content)
+
+ let element: React.ReactNode = jsxContent
+
+ // Apply className wrapper if provided
+ if (options?.className) {
+ element = {element}
+ }
+
+ // Apply custom wrapper component if provided
+ if (options?.wrapper) {
+ const Wrapper = options.wrapper
+ element = {element}
+ }
+
+ const contentRsc = await renderServerComponent(<>{element}>)
+
+ return { contentRsc, headings }
+}
From 1e9db0c9a4fcd3d3ca158f000a673452b0bd5f95 Mon Sep 17 00:00:00 2001
From: Tanner Linsley
Date: Wed, 11 Feb 2026 09:42:13 -0700
Subject: [PATCH 5/5] feat: migrate feed views to composite components with
slot support
- Add feed.composites.tsx with createFeedTimelineComposite and createFeedDetailComposite
- Timeline and detail views now use CompositeComponent with renderActions slot
- Admin actions wired through slots for client-side interactivity
- Table view retains contentRsc approach (too interactive for composites)
- Simplify feed.$id route by moving shell markup to server composite
---
src/components/FeedEntry.tsx | 18 +-
src/components/FeedEntryTimeline.tsx | 407 +++++-----------------
src/routes/feed.$id.tsx | 295 +++-------------
src/utils/feed.composites.tsx | 495 +++++++++++++++++++++++++++
src/utils/feed.functions.ts | 76 +++-
5 files changed, 707 insertions(+), 584 deletions(-)
create mode 100644 src/utils/feed.composites.tsx
diff --git a/src/components/FeedEntry.tsx b/src/components/FeedEntry.tsx
index 0e8416de8..32973d2a6 100644
--- a/src/components/FeedEntry.tsx
+++ b/src/components/FeedEntry.tsx
@@ -1,4 +1,5 @@
import type { ReactNode } from 'react'
+import type { AnyCompositeComponent } from '@tanstack/react-start/rsc'
import { format, formatDistanceToNow } from '~/utils/dates'
import { libraries } from '~/libraries'
import { partners } from '~/utils/partners'
@@ -18,7 +19,7 @@ export interface FeedEntry {
publishedAt: number
createdAt: number
updatedAt?: number
- metadata?: any
+ metadata?: Record
libraryIds: string[]
partnerIds?: string[]
tags: string[]
@@ -26,6 +27,9 @@ export interface FeedEntry {
featured?: boolean
autoSynced: boolean
lastSyncedAt?: number
+ // Composite sources for RSC rendering
+ timelineCompositeSrc?: AnyCompositeComponent
+ detailCompositeSrc?: AnyCompositeComponent
}
interface FeedEntryProps {
@@ -160,12 +164,18 @@ export function FeedEntry({
const releaseLevelBadge = getReleaseLevelBadge()
// Determine external link if available
- const getExternalLink = () => {
+ const getExternalLink = (): string | null => {
if (entry.metadata) {
- if (entry.entryType === 'release' && entry.metadata.url) {
+ if (
+ entry.entryType === 'release' &&
+ typeof entry.metadata.url === 'string'
+ ) {
return entry.metadata.url
}
- if (entry.entryType === 'blog' && entry.metadata.url) {
+ if (
+ entry.entryType === 'blog' &&
+ typeof entry.metadata.url === 'string'
+ ) {
return entry.metadata.url
}
}
diff --git a/src/components/FeedEntryTimeline.tsx b/src/components/FeedEntryTimeline.tsx
index 0ffcc4abd..8cc8b2bf3 100644
--- a/src/components/FeedEntryTimeline.tsx
+++ b/src/components/FeedEntryTimeline.tsx
@@ -1,12 +1,9 @@
import * as React from 'react'
-import { format, formatDistanceToNow } from '~/utils/dates'
-import { Card } from '~/components/Card'
-import { libraries } from '~/libraries'
-import { partners } from '~/utils/partners'
+import { CompositeComponent } from '@tanstack/react-start/rsc'
import { twMerge } from 'tailwind-merge'
-import { FeedEntry } from './FeedEntry'
-import { Link } from '@tanstack/react-router'
import { Eye, EyeOff, SquarePen, Star, Trash } from 'lucide-react'
+import type { FeedEntry } from './FeedEntry'
+import type { FeedActionContext } from '~/utils/feed.composites'
interface FeedEntryTimelineProps {
entry: FeedEntry
@@ -20,6 +17,18 @@ interface FeedEntryTimelineProps {
}
}
+// Type for timeline composite props (matches feed.composites.tsx)
+type TimelineCompositeProps = {
+ renderActions?: (ctx: FeedActionContext) => React.ReactNode
+ renderExpandToggle?: (ctx: { id: string }) => React.ReactNode
+ children?: React.ReactNode
+}
+
+// CompositeComponent with slot props - cast to avoid type mismatch
+const TimelineComposite = CompositeComponent as unknown as React.FC<
+ { src: FeedEntry['timelineCompositeSrc'] } & TimelineCompositeProps
+>
+
export function FeedEntryTimeline({
entry,
expanded: expandedProp = false,
@@ -31,333 +40,97 @@ export function FeedEntryTimeline({
onExpandedChange?.(value)
}
- // Get library info
- const entryLibraries = entry.libraryIds
- .map((id) => libraries.find((lib) => lib.id === id))
- .filter(Boolean)
-
- // Get partner info
- const entryPartners = entry.partnerIds
- ? entry.partnerIds
- .map((id) => partners.find((p) => p.id === id))
- .filter(Boolean)
- : []
-
- // Determine entry type badge
- const getTypeBadge = () => {
- const isPrerelease = entry.tags.includes('release:prerelease')
-
- const badgeConfigs: Record = {
- release: {
- label: 'Release',
- className:
- 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200',
- },
- prerelease: {
- label: 'Prerelease',
- className:
- 'bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200',
- },
- blog: {
- label: 'Blog',
- className:
- 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200',
- },
- announcement: {
- label: 'Announcement',
- className:
- 'bg-orange-100 dark:bg-orange-900 text-orange-800 dark:text-orange-200',
- },
- partner: {
- label: 'Partner',
- className:
- 'bg-pink-100 dark:bg-pink-900 text-pink-800 dark:text-pink-200',
- },
- update: {
- label: 'Update',
- className:
- 'bg-teal-100 dark:bg-teal-900 text-teal-800 dark:text-teal-200',
- },
- }
-
- const key =
- entry.entryType === 'release' && isPrerelease
- ? 'prerelease'
- : entry.entryType
-
- return (
- badgeConfigs[key] || {
- label: entry.entryType,
- className:
- 'bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200',
- }
- )
- }
-
- const badge = getTypeBadge()
-
- // Get release level badge if present
- const getReleaseLevelBadge = () => {
- const releaseLevelTag = entry.tags.find(
- (tag) =>
- tag.startsWith('release:') &&
- tag !== 'release:prerelease' &&
- (tag === 'release:major' ||
- tag === 'release:minor' ||
- tag === 'release:patch'),
- )
- if (!releaseLevelTag) return null
-
- const level = releaseLevelTag.replace('release:', '')
- const badgeConfigs: Record = {
- major: {
- label: 'Major',
- className:
- 'bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200',
- },
- minor: {
- label: 'Minor',
- className:
- 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200',
- },
- patch: {
- label: 'Patch',
- className:
- 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200',
- },
- }
-
- return badgeConfigs[level] || null
- }
-
- const releaseLevelBadge = getReleaseLevelBadge()
-
- // Determine external link if available
- const getExternalLink = () => {
- if (entry.metadata) {
- if (entry.entryType === 'release' && entry.metadata.url) {
- return entry.metadata.url
- }
- if (entry.entryType === 'blog' && entry.metadata.url) {
- return entry.metadata.url
- }
- }
+ // If no composite source available, don't render
+ if (!entry.timelineCompositeSrc) {
return null
}
- const externalLink = getExternalLink()
-
return (
-
- {/* Header */}
-
-
- {/* Badges and Date */}
-
-
- {badge.label}
-
- {releaseLevelBadge && (
-
- {releaseLevelBadge.label}
-
- )}
- {entry.featured && (
-
- ⭐ Featured
-
- )}
- {!entry.showInFeed && (
-
- Hidden
-
- )}
-
-
- {/* Title */}
-
+ adminActions ? (
+
+ ) : null
+ }
+ renderExpandToggle={() => (
+
setExpanded(!expanded)}
+ className="mt-3 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 font-medium"
>
- {entry.title}
-
-
- {/* Metadata */}
-
-
- {formatDistanceToNow(new Date(entry.publishedAt), {
- addSuffix: true,
- })}
-
- {entry.autoSynced && (
-
- {entry.entryType === 'release' ? 'GitHub' : entry.entryType}
-
- )}
- {entryLibraries.length > 0 && (
-
-
Libraries:
-
- {entryLibraries.map((lib) => (
-
- {lib!.name}
-
- ))}
-
-
- )}
- {entryPartners.length > 0 && (
-
-
Partners:
-
- {entryPartners.map((partner) => (
-
- {partner!.name}
-
- ))}
-
-
- )}
-
-
-
- {/* Admin Actions */}
- {adminActions && (
-
- {adminActions.onEdit && (
- adminActions.onEdit!(entry)}
- className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors text-gray-600 dark:text-gray-400"
- title="Edit"
- >
-
-
- )}
- {adminActions.onToggleVisibility && (
-
- adminActions.onToggleVisibility!(entry, !entry.showInFeed)
- }
- className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors text-gray-600 dark:text-gray-400"
- title={entry.showInFeed ? 'Hide' : 'Show'}
- >
- {entry.showInFeed ? (
-
- ) : (
-
- )}
-
- )}
- {adminActions.onToggleFeatured && (
-
- adminActions.onToggleFeatured!(entry, !entry.featured)
- }
- className={twMerge(
- 'p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors',
- entry.featured
- ? 'text-yellow-500'
- : 'text-gray-600 dark:text-gray-400',
- )}
- title="Toggle Featured"
- >
-
-
- )}
- {adminActions.onDelete && (
- adminActions.onDelete!(entry)}
- className="p-2 hover:bg-red-100 dark:hover:bg-red-900 rounded transition-colors text-red-500"
- title="Delete"
- >
-
-
- )}
-
- )}
-
-
- {/* Content */}
-
- {/* Tags - Only show when expanded */}
- {expanded && entry.tags.length > 0 && (
-
-
- Tags:
-
- {entry.tags.map((tag) => (
-
- {tag}
-
- ))}
-
+ {expanded ? 'Show less' : 'Show more'}
+
)}
+ />
+
+ )
+}
- {/* Content - Truncated when not expanded */}
-
+}) {
+ return (
+ <>
+ {adminActions.onEdit && (
+ adminActions.onEdit!(entry)}
+ className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors text-gray-600 dark:text-gray-400"
+ title="Edit"
+ >
+
+
+ )}
+ {adminActions.onToggleVisibility && (
+
+ adminActions.onToggleVisibility!(entry, !ctx.showInFeed)
+ }
+ className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors text-gray-600 dark:text-gray-400"
+ title={ctx.showInFeed ? 'Hide' : 'Show'}
+ >
+ {ctx.showInFeed ? (
+
+ ) : (
+
+ )}
+
+ )}
+ {adminActions.onToggleFeatured && (
+ adminActions.onToggleFeatured!(entry, !ctx.featured)}
className={twMerge(
- 'text-sm text-gray-900 dark:text-gray-100 leading-relaxed prose prose-sm dark:prose-invert max-w-none',
- !expanded && 'line-clamp-6',
+ 'p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors',
+ ctx.featured
+ ? 'text-yellow-500'
+ : 'text-gray-600 dark:text-gray-400',
)}
+ title="Toggle Featured"
>
- {entry.contentRsc ?? entry.content}
-
-
- {/* Show more/less button */}
+
+
+ )}
+ {adminActions.onDelete && (
setExpanded(!expanded)}
- className="mt-3 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 font-medium"
+ onClick={() => adminActions.onDelete!(entry)}
+ className="p-2 hover:bg-red-100 dark:hover:bg-red-900 rounded transition-colors text-red-500"
+ title="Delete"
>
- {expanded ? 'Show less' : 'Show more'}
+
-
-
- {/* Footer */}
- {externalLink && (
-
)}
-
+ >
)
}
diff --git a/src/routes/feed.$id.tsx b/src/routes/feed.$id.tsx
index d56a8f1e2..c450ecf2e 100644
--- a/src/routes/feed.$id.tsx
+++ b/src/routes/feed.$id.tsx
@@ -1,16 +1,25 @@
import { Link, notFound, createFileRoute } from '@tanstack/react-router'
+import { CompositeComponent } from '@tanstack/react-start/rsc'
import { Footer } from '~/components/Footer'
import { seo } from '~/utils/seo'
import { useCapabilities } from '~/hooks/useCapabilities'
import { isAdmin } from '~/db/types'
import * as v from 'valibot'
-import { format, formatDistanceToNow } from '~/utils/dates'
-import { libraries } from '~/libraries'
-import { partners } from '~/utils/partners'
-import { twMerge } from 'tailwind-merge'
import type { FeedEntry } from '~/components/FeedEntry'
import { getFeedEntryByIdQueryOptions } from '~/queries/feed'
import { ArrowLeft } from 'lucide-react'
+import type { FeedActionContext } from '~/utils/feed.composites'
+
+// Type for detail composite props
+type DetailCompositeProps = {
+ renderActions?: (ctx: FeedActionContext) => React.ReactNode
+ children?: React.ReactNode
+}
+
+// CompositeComponent with slot props - cast to avoid type mismatch
+const DetailComposite = CompositeComponent as unknown as React.FC<
+ { src: FeedEntry['detailCompositeSrc'] } & DetailCompositeProps
+>
const searchSchema = v.object({})
@@ -115,126 +124,40 @@ function FeedItemPage() {
}
function FeedEntryView({ entry }: { entry: FeedEntry }) {
- // Get library info
- const entryLibraries = entry.libraryIds
- .map((id) => libraries.find((lib) => lib.id === id))
- .filter(Boolean)
-
- // Get partner info
- const entryPartners = entry.partnerIds
- ? entry.partnerIds
- .map((id) => partners.find((p) => p.id === id))
- .filter(Boolean)
- : []
-
- // Determine entry type badge
- const getTypeBadge = () => {
- const isPrerelease = entry.tags.includes('release:prerelease')
-
- const badgeConfigs: Record
= {
- release: {
- label: 'Release',
- className:
- 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200',
- },
- prerelease: {
- label: 'Prerelease',
- className:
- 'bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200',
- },
- blog: {
- label: 'Blog',
- className:
- 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200',
- },
- announcement: {
- label: 'Announcement',
- className:
- 'bg-orange-100 dark:bg-orange-900 text-orange-800 dark:text-orange-200',
- },
- partner: {
- label: 'Partner',
- className:
- 'bg-pink-100 dark:bg-pink-900 text-pink-800 dark:text-pink-200',
- },
- update: {
- label: 'Update',
- className:
- 'bg-teal-100 dark:bg-teal-900 text-teal-800 dark:text-teal-200',
- },
- }
-
- const key =
- entry.entryType === 'release' && isPrerelease
- ? 'prerelease'
- : entry.entryType
-
+ // If composite source is available, use it
+ if (entry.detailCompositeSrc) {
return (
- badgeConfigs[key] || {
- label: entry.entryType,
- className:
- 'bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200',
- }
- )
- }
+
+
+ {/* Back Link */}
+
+
+ Back to Feed
+
- const badge = getTypeBadge()
+ {/* Feed Entry - rendered via composite */}
+
+
- // Get release level badge if present
- const getReleaseLevelBadge = () => {
- const releaseLevelTag = entry.tags.find(
- (tag) =>
- tag.startsWith('release:') &&
- tag !== 'release:prerelease' &&
- (tag === 'release:major' ||
- tag === 'release:minor' ||
- tag === 'release:patch'),
+
+
)
- if (!releaseLevelTag) return null
-
- const level = releaseLevelTag.replace('release:', '')
- const badgeConfigs: Record = {
- major: {
- label: 'Major',
- className:
- 'bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200',
- },
- minor: {
- label: 'Minor',
- className:
- 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200',
- },
- patch: {
- label: 'Patch',
- className:
- 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200',
- },
- }
-
- return badgeConfigs[level] || null
}
- const releaseLevelBadge = getReleaseLevelBadge()
-
- // Determine external link if available
- const getExternalLink = () => {
- if (entry.metadata) {
- if (entry.entryType === 'release' && entry.metadata.url) {
- return entry.metadata.url
- }
- if (entry.entryType === 'blog' && entry.metadata.url) {
- return entry.metadata.url
- }
- }
- return null
- }
-
- const externalLink = getExternalLink()
-
+ // Fallback: no composite source (shouldn't happen in practice)
return (
- {/* Back Link */}
- {/* Feed Entry */}
-
- {/* Header */}
-
- {/* Badges */}
-
-
- {badge.label}
-
- {releaseLevelBadge && (
-
- {releaseLevelBadge.label}
-
- )}
- {entry.featured && (
-
- ⭐ Featured
-
- )}
-
-
- {/* Title */}
-
- {entry.title}
-
-
- {/* Metadata */}
-
-
- {formatDistanceToNow(new Date(entry.publishedAt), {
- addSuffix: true,
- })}
-
- {entry.autoSynced && (
-
- {entry.entryType === 'release' ? 'GitHub' : entry.entryType}
-
- )}
- {entryLibraries.length > 0 && (
-
-
Libraries:
-
- {entryLibraries.map((lib) => (
-
- {lib!.name}
-
- ))}
-
-
- )}
- {entryPartners.length > 0 && (
-
-
Partners:
-
- {entryPartners.map((partner) => (
-
- {partner!.name}
-
- ))}
-
-
- )}
-
-
-
- {/* Tags */}
- {entry.tags.length > 0 && (
-
-
- Tags:
-
- {entry.tags.map((tag) => (
-
- {tag}
-
- ))}
-
- )}
-
- {/* Content */}
-
+
+
+ {entry.title}
+
+
{entry.contentRsc ?? entry.content}
-
- {/* External Link */}
- {externalLink && (
-
- )}
diff --git a/src/utils/feed.composites.tsx b/src/utils/feed.composites.tsx
new file mode 100644
index 000000000..25e0e60c7
--- /dev/null
+++ b/src/utils/feed.composites.tsx
@@ -0,0 +1,495 @@
+import * as React from 'react'
+import { createCompositeComponent } from '@tanstack/react-start/rsc'
+import { Link } from '@tanstack/react-router'
+import { twMerge } from 'tailwind-merge'
+import { format, formatDistanceToNow } from '~/utils/dates'
+import { libraries } from '~/libraries'
+import { partners } from '~/utils/partners'
+import { renderMarkdownToJsx } from '~/utils/markdown'
+import { Card } from '~/components/Card'
+
+// Serializable context passed to action slots
+export interface FeedActionContext {
+ id: string
+ entryType: 'release' | 'blog' | 'announcement'
+ showInFeed: boolean
+ featured: boolean
+ externalLink: string | null
+}
+
+// Entry data shape for composites
+interface FeedEntryData {
+ _id: string
+ id: string
+ entryType: 'release' | 'blog' | 'announcement'
+ title: string
+ content: string
+ excerpt?: string | null
+ publishedAt: number
+ metadata?: Record
+ libraryIds: string[]
+ partnerIds?: string[]
+ tags: string[]
+ showInFeed: boolean
+ featured?: boolean
+}
+
+// Badge config helpers
+function getTypeBadge(entry: FeedEntryData) {
+ const isPrerelease = entry.tags.includes('release:prerelease')
+
+ const badgeConfigs: Record = {
+ release: {
+ label: 'Release',
+ className:
+ 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200',
+ },
+ prerelease: {
+ label: 'Prerelease',
+ className:
+ 'bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200',
+ },
+ blog: {
+ label: 'Blog',
+ className:
+ 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200',
+ },
+ announcement: {
+ label: 'Announcement',
+ className:
+ 'bg-orange-100 dark:bg-orange-900 text-orange-800 dark:text-orange-200',
+ },
+ partner: {
+ label: 'Partner',
+ className:
+ 'bg-pink-100 dark:bg-pink-900 text-pink-800 dark:text-pink-200',
+ },
+ update: {
+ label: 'Update',
+ className:
+ 'bg-teal-100 dark:bg-teal-900 text-teal-800 dark:text-teal-200',
+ },
+ }
+
+ const key =
+ entry.entryType === 'release' && isPrerelease
+ ? 'prerelease'
+ : entry.entryType
+
+ return (
+ badgeConfigs[key] || {
+ label: entry.entryType,
+ className:
+ 'bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200',
+ }
+ )
+}
+
+function getReleaseLevelBadge(entry: FeedEntryData) {
+ const releaseLevelTag = entry.tags.find(
+ (tag) =>
+ tag.startsWith('release:') &&
+ tag !== 'release:prerelease' &&
+ (tag === 'release:major' ||
+ tag === 'release:minor' ||
+ tag === 'release:patch'),
+ )
+ if (!releaseLevelTag) return null
+
+ const level = releaseLevelTag.replace('release:', '')
+ const badgeConfigs: Record = {
+ major: {
+ label: 'Major',
+ className:
+ 'bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200',
+ },
+ minor: {
+ label: 'Minor',
+ className:
+ 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200',
+ },
+ patch: {
+ label: 'Patch',
+ className:
+ 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200',
+ },
+ }
+
+ return badgeConfigs[level] || null
+}
+
+function getExternalLink(entry: FeedEntryData): string | null {
+ if (entry.metadata) {
+ if (entry.entryType === 'release' && entry.metadata.url) {
+ return entry.metadata.url as string
+ }
+ if (entry.entryType === 'blog' && entry.metadata.url) {
+ return entry.metadata.url as string
+ }
+ }
+ return null
+}
+
+function getEntryLibraries(entry: FeedEntryData) {
+ return entry.libraryIds
+ .map((id) => libraries.find((lib) => lib.id === id))
+ .filter((lib): lib is (typeof libraries)[number] => lib !== undefined)
+}
+
+function getEntryPartners(entry: FeedEntryData) {
+ return entry.partnerIds
+ ? entry.partnerIds
+ .map((id) => partners.find((p) => p.id === id))
+ .filter((p): p is (typeof partners)[number] => p !== undefined)
+ : []
+}
+
+// Timeline card composite props
+type TimelineCardCompositeProps = {
+ renderActions?: (ctx: FeedActionContext) => React.ReactNode
+ renderExpandToggle?: (ctx: { id: string }) => React.ReactNode
+ children?: React.ReactNode
+}
+
+// Detail view composite props
+type DetailCompositeProps = {
+ renderActions?: (ctx: FeedActionContext) => React.ReactNode
+ children?: React.ReactNode
+}
+
+/**
+ * Create a composite for timeline card view.
+ * Content is always rendered, expand/collapse is handled via CSS (line-clamp) on client.
+ */
+export async function createFeedTimelineComposite(entry: FeedEntryData) {
+ const { content: contentJsx } = entry.content
+ ? await renderMarkdownToJsx(entry.content)
+ : { content: null }
+
+ const badge = getTypeBadge(entry)
+ const releaseLevelBadge = getReleaseLevelBadge(entry)
+ const externalLink = getExternalLink(entry)
+ const entryLibraries = getEntryLibraries(entry)
+ const entryPartners = getEntryPartners(entry)
+
+ const actionCtx: FeedActionContext = {
+ id: entry._id,
+ entryType: entry.entryType,
+ showInFeed: entry.showInFeed,
+ featured: entry.featured ?? false,
+ externalLink,
+ }
+
+ return createCompositeComponent((props: TimelineCardCompositeProps) => (
+
+ {/* Header */}
+
+
+ {/* Badges and Date */}
+
+
+ {badge.label}
+
+ {releaseLevelBadge && (
+
+ {releaseLevelBadge.label}
+
+ )}
+ {entry.featured && (
+
+ Featured
+
+ )}
+ {!entry.showInFeed && (
+
+ Hidden
+
+ )}
+
+
+ {/* Title */}
+
+ {entry.title}
+
+
+ {/* Metadata */}
+
+
+ {formatDistanceToNow(new Date(entry.publishedAt), {
+ addSuffix: true,
+ })}
+
+ {entryLibraries.length > 0 && (
+
+
Libraries:
+
+ {entryLibraries.map((lib) => (
+
+ {lib.name}
+
+ ))}
+
+
+ )}
+ {entryPartners.length > 0 && (
+
+
Partners:
+
+ {entryPartners.map((partner) => (
+
+ {partner.name}
+
+ ))}
+
+
+ )}
+
+
+
+ {/* Admin Actions */}
+
+ {props.renderActions?.(actionCtx)}
+
+
+
+ {/* Content container - expand toggle applies line-clamp via data attribute */}
+
+ {contentJsx}
+
+
+ {/* Expand toggle slot */}
+ {props.renderExpandToggle?.({ id: entry._id })}
+
+ {/* External Link */}
+ {externalLink && (
+
+ )}
+
+ {/* Client slot */}
+ {props.children}
+
+ ))
+}
+
+/**
+ * Create a composite for detail view.
+ */
+export async function createFeedDetailComposite(entry: FeedEntryData) {
+ const { content: contentJsx } = entry.content
+ ? await renderMarkdownToJsx(entry.content)
+ : { content: null }
+
+ const badge = getTypeBadge(entry)
+ const releaseLevelBadge = getReleaseLevelBadge(entry)
+ const externalLink = getExternalLink(entry)
+ const entryLibraries = getEntryLibraries(entry)
+ const entryPartners = getEntryPartners(entry)
+
+ const actionCtx: FeedActionContext = {
+ id: entry._id,
+ entryType: entry.entryType,
+ showInFeed: entry.showInFeed,
+ featured: entry.featured ?? false,
+ externalLink,
+ }
+
+ return createCompositeComponent((props: DetailCompositeProps) => (
+
+ {/* Header */}
+
+ {/* Badges */}
+
+
+ {badge.label}
+
+ {releaseLevelBadge && (
+
+ {releaseLevelBadge.label}
+
+ )}
+ {entry.featured && (
+
+ Featured
+
+ )}
+ {!entry.showInFeed && (
+
+ Hidden
+
+ )}
+
+
+ {/* Title */}
+
+ {entry.title}
+
+
+ {/* Metadata */}
+
+
+ {format(new Date(entry.publishedAt), 'MMMM d, yyyy')} (
+ {formatDistanceToNow(new Date(entry.publishedAt), {
+ addSuffix: true,
+ })}
+ )
+
+
+
+ {/* Libraries and Partners */}
+
+ {entryLibraries.length > 0 && (
+
+
+ Libraries:
+
+
+ {entryLibraries.map((lib) => (
+
+ {lib.name}
+
+ ))}
+
+
+ )}
+ {entryPartners.length > 0 && (
+
+
+ Partners:
+
+
+ {entryPartners.map((partner) => (
+
+ {partner.name}
+
+ ))}
+
+
+ )}
+
+
+ {/* Admin Actions slot */}
+ {props.renderActions?.(actionCtx)}
+
+
+ {/* Content */}
+
+ {/* Tags */}
+ {entry.tags.length > 0 && (
+
+ {entry.tags.map((tag) => (
+
+ {tag}
+
+ ))}
+
+ )}
+
+ {/* Content */}
+
+ {contentJsx}
+
+
+ {/* External Link */}
+ {externalLink && (
+
+ )}
+
+ {/* Client slot */}
+ {props.children}
+
+
+ ))
+}
+
+// Re-export types for consumers
+export type { FeedEntryData }
diff --git a/src/utils/feed.functions.ts b/src/utils/feed.functions.ts
index 78e98c809..a21e99168 100644
--- a/src/utils/feed.functions.ts
+++ b/src/utils/feed.functions.ts
@@ -13,11 +13,19 @@ import {
} from './feed.server'
import { entryTypeSchema } from './schemas'
import { renderMarkdownRsc } from '~/utils/markdown'
+import {
+ createFeedTimelineComposite,
+ createFeedDetailComposite,
+} from './feed.composites'
// Transform database entry to API response format (with pre-rendered RSC content)
async function transformFeedEntry(
entry: typeof feedEntries.$inferSelect,
- options?: { renderContent?: boolean },
+ options?: {
+ renderContent?: boolean
+ createTimelineComposite?: boolean
+ createDetailComposite?: boolean
+ },
) {
const base = {
_id: entry.entryId,
@@ -29,7 +37,10 @@ async function transformFeedEntry(
publishedAt: entry.publishedAt.getTime(),
createdAt: entry.createdAt.getTime(),
updatedAt: entry.updatedAt.getTime(),
- metadata: entry.metadata ?? {},
+ metadata: (entry.metadata ?? {}) as Record<
+ string,
+ string | number | boolean | null | undefined
+ >,
libraryIds: entry.libraryIds,
partnerIds: entry.partnerIds,
tags: entry.tags,
@@ -39,13 +50,45 @@ async function transformFeedEntry(
lastSyncedAt: entry.lastSyncedAt?.getTime(),
}
- // Pre-render content as RSC for client-side display
- if (options?.renderContent && entry.content) {
- const { contentRsc } = await renderMarkdownRsc(entry.content)
- return { ...base, contentRsc }
+ // Build composite entry data shape
+ const compositeEntryData = {
+ _id: base._id,
+ id: base.id,
+ entryType: base.entryType,
+ title: base.title,
+ content: base.content,
+ excerpt: base.excerpt,
+ publishedAt: base.publishedAt,
+ metadata: base.metadata,
+ libraryIds: base.libraryIds,
+ partnerIds: base.partnerIds,
+ tags: base.tags,
+ showInFeed: base.showInFeed,
+ featured: base.featured,
}
- return base
+ // Pre-render content as RSC for table view (legacy approach)
+ const contentRsc =
+ options?.renderContent && entry.content
+ ? (await renderMarkdownRsc(entry.content)).contentRsc
+ : undefined
+
+ // Create timeline composite for timeline view
+ const timelineCompositeSrc = options?.createTimelineComposite
+ ? await createFeedTimelineComposite(compositeEntryData)
+ : undefined
+
+ // Create detail composite for detail view
+ const detailCompositeSrc = options?.createDetailComposite
+ ? await createFeedDetailComposite(compositeEntryData)
+ : undefined
+
+ return {
+ ...base,
+ ...(contentRsc ? { contentRsc } : {}),
+ ...(timelineCompositeSrc ? { timelineCompositeSrc } : {}),
+ ...(detailCompositeSrc ? { detailCompositeSrc } : {}),
+ }
}
export const listFeedEntries = createServerFn({ method: 'POST' })
@@ -121,9 +164,14 @@ export const listFeedEntries = createServerFn({ method: 'POST' })
const page = allEntries.slice(start, end)
const hasMore = end < allEntries.length
- // Render content HTML for client-side display
+ // Transform entries with RSC content and timeline composites
const transformedPage = await Promise.all(
- page.map((entry) => transformFeedEntry(entry, { renderContent: true })),
+ page.map((entry) =>
+ transformFeedEntry(entry, {
+ renderContent: true,
+ createTimelineComposite: true,
+ }),
+ ),
)
return {
@@ -164,7 +212,10 @@ export const getFeedEntryById = createServerFn({ method: 'POST' })
return null
}
- return transformFeedEntry(entry, { renderContent: true })
+ return transformFeedEntry(entry, {
+ renderContent: true,
+ createDetailComposite: true,
+ })
})
// Server function wrapper for getFeedStats
@@ -333,7 +384,10 @@ export const searchFeedEntries = createServerFn({ method: 'POST' })
return Promise.all(
entries.map((entry) =>
- transformFeedEntry(entry, { renderContent: true }),
+ transformFeedEntry(entry, {
+ renderContent: true,
+ createTimelineComposite: true,
+ }),
),
)
})