├── public
├── .assetsignore
└── favicon.ico
├── src
├── style.css
├── cloudflare-workers.tsx
├── routes
│ ├── home
│ │ ├── index.tsx
│ │ ├── page.tsx
│ │ └── components
│ │ │ └── Counter.tsx
│ ├── _404.tsx
│ └── _error.tsx
├── rsc
│ ├── entry.rsc.tsx
│ ├── README.md
│ ├── entry.ssr.tsx
│ ├── entry.browser.tsx
│ └── rsc-renderer.tsx
├── server.ts
└── components
│ └── Layout.tsx
├── wrangler.jsonc
├── .gitignore
├── tsconfig.json
├── package.json
├── LICENSE
├── vite.config.ts
└── README.md
/public/.assetsignore:
--------------------------------------------------------------------------------
1 | .vite
--------------------------------------------------------------------------------
/src/style.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kfly8/boilerplate-hono-vite-rsc/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/cloudflare-workers.tsx:
--------------------------------------------------------------------------------
1 | import rscHandler from './rsc/entry.rsc'
2 |
3 | export default {
4 | fetch: rscHandler,
5 | }
6 |
--------------------------------------------------------------------------------
/src/routes/home/index.tsx:
--------------------------------------------------------------------------------
1 | import { Hono } from 'hono'
2 | import { HomePage } from './page'
3 |
4 | const app = new Hono()
5 |
6 | app.get('/', (c) => {
7 | return c.render(, { title: 'Home' })
8 | })
9 |
10 | export default app
11 |
--------------------------------------------------------------------------------
/wrangler.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "node_modules/wrangler/config-schema.json",
3 | "name": "boilerplate-hono-vite-rsc",
4 | "compatibility_date": "2025-08-03",
5 | "compatibility_flags": [ "nodejs_als" ],
6 | "main": "./dist/rsc/workers.js",
7 | "assets": {
8 | "directory": "dist/client"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/rsc/entry.rsc.tsx:
--------------------------------------------------------------------------------
1 | import app from '../server'
2 | import type { ReactFormState } from 'react-dom/client'
3 |
4 | export default app.fetch
5 |
6 | export type RscPayload = {
7 | root: React.ReactNode
8 | returnValue?: unknown
9 | formState?: ReactFormState
10 | }
11 |
12 | if (import.meta.hot) {
13 | import.meta.hot.accept()
14 | }
15 |
--------------------------------------------------------------------------------
/src/routes/home/page.tsx:
--------------------------------------------------------------------------------
1 | import { Counter } from './components/Counter'
2 |
3 | export function HomePage() {
4 | return (
5 |
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/src/routes/home/components/Counter.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useState } from 'react'
4 |
5 | export function Counter() {
6 | const [count, setCount] = useState(0)
7 | return (
8 |
9 |
{count}
10 |
14 |
15 | )
16 | }
17 |
18 |
--------------------------------------------------------------------------------
/src/routes/_404.tsx:
--------------------------------------------------------------------------------
1 | import type { NotFoundHandler } from 'hono'
2 |
3 | const notFound: NotFoundHandler = (c) => {
4 | c.status(404)
5 | return c.render(
6 | ,
13 | {
14 | title: '404 - Page Not Found',
15 | },
16 | )
17 | }
18 |
19 | export default notFound
--------------------------------------------------------------------------------
/src/routes/_error.tsx:
--------------------------------------------------------------------------------
1 | import type { ErrorHandler } from 'hono'
2 |
3 | const onError: ErrorHandler = (err, c) => {
4 | console.error(err)
5 |
6 | c.status(500)
7 | return c.render(
8 | ,
15 | {
16 | title: 'Error - Hono RSC App',
17 | }
18 | )
19 | }
20 |
21 | export default onError
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # prod
2 | dist/
3 | dist-server/
4 |
5 | # dev
6 | .yarn/
7 | !.yarn/releases
8 | .vscode/*
9 | !.vscode/launch.json
10 | !.vscode/*.code-snippets
11 | .idea/workspace.xml
12 | .idea/usage.statistics.xml
13 | .idea/shelf
14 |
15 | # deps
16 | node_modules/
17 | .wrangler
18 |
19 | # env
20 | .env
21 | .env.production
22 | .dev.vars
23 |
24 | # logs
25 | logs/
26 | *.log
27 | npm-debug.log*
28 | yarn-debug.log*
29 | yarn-error.log*
30 | pnpm-debug.log*
31 | lerna-debug.log*
32 |
33 | # misc
34 | .DS_Store
35 |
36 | # claude
37 | .claude/settings.local.json
38 |
39 | *storybook.log
40 | storybook-static
41 |
--------------------------------------------------------------------------------
/src/rsc/README.md:
--------------------------------------------------------------------------------
1 | # React Server Components Setup
2 |
3 | This directory contains the React Server Components (RSC) implementation for the application.
4 |
5 | ## Files
6 |
7 | - `entry.browser.tsx` - Client-side entry point for browser rendering
8 | - `entry.rsc.tsx` - React Server Components entry point
9 | - `entry.ssr.tsx` - Server-side rendering entry point
10 | - `rsc-renderer.tsx` - Hono middleware for RSC rendering
11 |
12 | ## License
13 |
14 | The code in this directory is adapted from [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react) and is licensed under the MIT License.
15 |
16 | Copyright (c) 2019-present, Yuxi (Evan) You and Vite contributors
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "module": "ESNext",
5 | "moduleResolution": "Bundler",
6 | "allowSyntheticDefaultImports": true,
7 | "esModuleInterop": true,
8 | "isolatedModules": true,
9 | "strict": true,
10 | "skipLibCheck": true,
11 | "lib": [
12 | "DOM",
13 | "DOM.Iterable",
14 | "ESNext"
15 | ],
16 | "types": [
17 | "vite/client",
18 | "@vitejs/plugin-rsc/types",
19 | "react",
20 | "react-dom",
21 | "vitest/globals"
22 | ],
23 | "jsx": "react-jsx",
24 | "jsxImportSource": "react",
25 | "baseUrl": ".",
26 | "paths": {
27 | "@/*": ["./src/*"]
28 | }
29 | },
30 | "include": [
31 | "src/**/*",
32 | "vite-env.d.ts",
33 | "worker-configuration.d.ts"
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------
1 | import { Hono } from 'hono'
2 | import { Layout } from './components/Layout'
3 | import { rscRenderer } from './rsc/rsc-renderer'
4 | import { logger } from 'hono/logger'
5 |
6 | import notFound from './routes/_404'
7 | import onError from './routes/_error'
8 |
9 | const app = new Hono()
10 |
11 | app.use(rscRenderer({ Layout }))
12 | app.use(logger())
13 |
14 | const modules = import.meta.glob([
15 | './routes/**/index.(ts|tsx)',
16 | '!./routes/**/*.test.(ts|tsx)',
17 | '!./routes/**/*.spec.(ts|tsx)',
18 | '!./routes/**/*.stories.(ts|tsx)'
19 | ], { eager: true })
20 |
21 | for (const path in modules) {
22 | const module = modules[path] as any
23 | if (module.default) {
24 | app.route('/', module.default)
25 | }
26 | }
27 |
28 | app.notFound(notFound)
29 | app.onError(onError)
30 |
31 | export default app;
32 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "boilerplate-hono-vite-rsc",
3 | "type": "module",
4 | "scripts": {
5 | "dev": "vite",
6 | "build": "vite build",
7 | "preview": "$npm_execpath run build && vite preview",
8 | "deploy": "$npm_execpath run build && wrangler deploy --minify",
9 | "cf-preview": "wrangler dev",
10 | "cf-typegen": "wrangler types --env-interface CloudflareBindings"
11 | },
12 | "dependencies": {
13 | "hono": "^4.9.1",
14 | "react": "^19.1.1",
15 | "react-dom": "^19.1.1",
16 | "tailwindcss": "^4.1.11"
17 | },
18 | "devDependencies": {
19 | "@tailwindcss/vite": "^4.1.11",
20 | "@cloudflare/vite-plugin": "^1.2.3",
21 | "@types/react": "^19.1.10",
22 | "@types/react-dom": "^19.1.7",
23 | "@vitejs/plugin-react": "latest",
24 | "@vitejs/plugin-rsc": "latest",
25 | "rsc-html-stream": "^0.0.7",
26 | "vite": "^7.1.3",
27 | "vite-ssr-components": "^0.3.0",
28 | "wrangler": "^4.32.0"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 kfly8
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/src/components/Layout.tsx:
--------------------------------------------------------------------------------
1 | import { ViteClient } from 'vite-ssr-components/react'
2 | import React from 'react'
3 | import type { Props } from '../rsc/rsc-renderer'
4 | import '../style.css'
5 |
6 | declare module '../rsc/rsc-renderer' {
7 | interface Props {
8 | title?: string
9 | }
10 | }
11 |
12 | export const Layout: React.FC = ({ children, title }) => {
13 | return (
14 |
15 |
16 |
17 |
18 | {title ?? "hono-vite-rsc-shadcnui" }
19 | {/*
20 | WORKAROUND: Manually check for development environment before rendering ViteClient.
21 | ViteClient is expected to handle this branching internally, but due to compatibility
22 | issues with RSC (React Server Components) build process, it doesn't work as expected.
23 | This explicit environment check ensures ViteClient only renders in development.
24 | */}
25 | {import.meta.env.DEV && }
26 |
27 | {children}
28 |
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/src/rsc/entry.ssr.tsx:
--------------------------------------------------------------------------------
1 | import * as ReactClient from '@vitejs/plugin-rsc/ssr'
2 | import React from 'react'
3 | import type { ReactFormState } from 'react-dom/client'
4 | import * as ReactDOMServer from 'react-dom/server.edge'
5 | import { injectRSCPayload } from 'rsc-html-stream/server'
6 | import type { RscPayload } from './entry.rsc'
7 |
8 | export async function renderHTML(
9 | rscStream: ReadableStream,
10 | options: {
11 | formState?: ReactFormState
12 | nonce?: string
13 | debugNojs?: boolean
14 | },
15 | ) {
16 | const [rscStream1, rscStream2] = rscStream.tee()
17 |
18 | let payload: Promise | undefined
19 | function SsrRoot() {
20 | payload ??= ReactClient.createFromReadableStream(rscStream1)
21 | return {React.use(payload).root}
22 | }
23 |
24 | function FixSsrThenable(props: React.PropsWithChildren) {
25 | return props.children
26 | }
27 |
28 | const bootstrapScriptContent =
29 | await import.meta.viteRsc.loadBootstrapScriptContent('index')
30 | const htmlStream = await ReactDOMServer.renderToReadableStream(, {
31 | bootstrapScriptContent: options?.debugNojs
32 | ? undefined
33 | : bootstrapScriptContent,
34 | nonce: options?.nonce,
35 | ...{ formState: options?.formState },
36 | })
37 |
38 | let responseStream: ReadableStream = htmlStream
39 | if (!options?.debugNojs) {
40 | responseStream = responseStream.pipeThrough(
41 | injectRSCPayload(rscStream2, {
42 | nonce: options?.nonce,
43 | }),
44 | )
45 | }
46 |
47 | return responseStream
48 | }
49 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import rsc from '@vitejs/plugin-rsc'
2 | import react from '@vitejs/plugin-react'
3 | import { defineConfig } from 'vite'
4 | import tailwindcss from '@tailwindcss/vite'
5 |
6 | export default defineConfig(({ mode }) => ({
7 | plugins: [
8 | rsc(),
9 | react(),
10 | tailwindcss(),
11 | ],
12 |
13 | // The @vitejs/plugin-rsc bundles React's react-server-dom implementation,
14 | // which relies on process.env.NODE_ENV to determine whether to use
15 | // development or production builds. While Vite uses 'mode' internally,
16 | // we need to define process.env.NODE_ENV to ensure React RSC components
17 | // use the correct build variant. Without this, the server and client
18 | // may use mismatched React versions, causing runtime errors.
19 | // Note: This workaround may become unnecessary in future versions as
20 | // Vite's RSC support matures or React adopts Vite's native environment variables.
21 | define: {
22 | 'process.env.NODE_ENV': JSON.stringify(mode),
23 | },
24 |
25 | environments: {
26 | rsc: {
27 | build: {
28 | rollupOptions: {
29 | input: {
30 | index: './src/rsc/entry.rsc.tsx',
31 | workers: './src/cloudflare-workers.tsx',
32 | },
33 | },
34 | },
35 | },
36 |
37 | ssr: {
38 | build: {
39 | rollupOptions: {
40 | input: {
41 | index: './src/rsc/entry.ssr.tsx',
42 | },
43 | },
44 | },
45 | },
46 |
47 | client: {
48 | build: {
49 | rollupOptions: {
50 | input: {
51 | index: './src/rsc/entry.browser.tsx',
52 | },
53 | },
54 | },
55 | }
56 | },
57 | }))
58 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Hono + Vite + React Server Components Boilerplate
2 |
3 | A modern web application boilerplate using Hono framework with Vite and React Server Components (RSC).
4 |
5 | ## Tech Stack
6 |
7 | - [Hono](https://hono.dev/) - Lightweight, fast web framework
8 | - [Vite](https://vitejs.dev/) - Fast development and build tooling
9 | - [React 19](https://react.dev/) - UI library with Server Components
10 | - [Tailwind CSS v4](https://tailwindcss.com/) - Utility-first CSS framework
11 |
12 | ## Getting Started
13 |
14 | ### Prerequisites
15 |
16 | - Node.js 18+
17 | - npm, yarn, pnpm or bun
18 |
19 | ### Setup
20 |
21 | 1. Clone or use this template
22 |
23 | ```sh
24 | git clone https://github.com/kfly8/boilerplate-hono-vite-rsc.git
25 | cd boilerplate-hono-vite-rsc
26 | ```
27 |
28 | 2. Install dependencies
29 |
30 | ```sh
31 | npm install
32 | ```
33 |
34 | 3. Start development server
35 |
36 | ```sh
37 | npm run dev
38 | ```
39 |
40 | Open http://localhost:5173 to view the application.
41 |
42 | ### Available Scripts
43 |
44 | - `npm run dev` - Start development server
45 | - `npm run build` - Build for production
46 | - `npm run preview` - Preview production build locally
47 | - `npm run deploy` - Deploy to Cloudflare Workers
48 | - `npm run cf-preview` - Preview with Wrangler locally
49 | - `npm run cf-typegen` - Generate Cloudflare binding types
50 |
51 | ## Project Structure Summary
52 |
53 | ```
54 | src/
55 | ├── cloudflare-workers.tsx # Cloudflare Workers entry point
56 | ├── server.ts # Hono server configuration
57 | ├── components/ # Shared React components
58 | │ └── Layout.tsx # Main layout component
59 | ├── routes/ # File-based routing
60 | │ ├── _404.tsx # 404 page
61 | │ ├── _error.tsx # Error page
62 | │ └── home/ # Home route
63 | │ ├── index.tsx # Route handler
64 | │ ├── page.tsx # Page component
65 | │ └── components/ # Route-specific components
66 | └── rsc/ # React Server Components setup
67 | ├── entry.browser.tsx # Browser entry point
68 | ├── entry.rsc.tsx # RSC entry point
69 | ├── entry.ssr.tsx # SSR entry point
70 | └── rsc-renderer.tsx # RSC renderer middleware
71 | ```
72 |
73 | ## Examples
74 |
75 | Looking for a more feature-rich example? Check out [hono-vite-rsc-shadcnui](https://github.com/kfly8-sandbox/hono-vite-rsc-shadcnui) for an implementation with shadcn/ui components.
76 |
77 | ## License
78 |
79 | [MIT](./LICENSE)
80 |
81 |
--------------------------------------------------------------------------------
/src/rsc/entry.browser.tsx:
--------------------------------------------------------------------------------
1 | import * as ReactClient from '@vitejs/plugin-rsc/browser'
2 | import React from 'react'
3 | import * as ReactDOMClient from 'react-dom/client'
4 | import { rscStream } from 'rsc-html-stream/client'
5 | import type { RscPayload } from './entry.rsc'
6 |
7 | async function main() {
8 | let setPayload: (v: RscPayload) => void
9 |
10 | const initialPayload = await ReactClient.createFromReadableStream(
11 | rscStream,
12 | )
13 |
14 | function BrowserRoot() {
15 | const [payload, setPayload_] = React.useState(initialPayload)
16 |
17 | React.useEffect(() => {
18 | setPayload = (v) => React.startTransition(() => setPayload_(v))
19 | }, [setPayload_])
20 |
21 | React.useEffect(() => {
22 | return listenNavigation(() => fetchRscPayload())
23 | }, [])
24 |
25 | return payload.root
26 | }
27 |
28 | async function fetchRscPayload() {
29 | const payload = await ReactClient.createFromFetch(
30 | fetch(window.location.href),
31 | )
32 | setPayload(payload)
33 | }
34 |
35 | ReactClient.setServerCallback(async (id, args) => {
36 | const url = new URL(window.location.href)
37 | const temporaryReferences = ReactClient.createTemporaryReferenceSet()
38 | const payload = await ReactClient.createFromFetch(
39 | fetch(url, {
40 | method: 'POST',
41 | body: await ReactClient.encodeReply(args, { temporaryReferences }),
42 | headers: {
43 | 'x-rsc-action': id,
44 | },
45 | }),
46 | { temporaryReferences },
47 | )
48 | setPayload(payload)
49 | return payload.returnValue
50 | })
51 |
52 | const browserRoot = (
53 |
54 |
55 |
56 | )
57 | ReactDOMClient.hydrateRoot(document, browserRoot, {
58 | formState: initialPayload.formState,
59 | })
60 |
61 | if (import.meta.hot) {
62 | import.meta.hot.on('rsc:update', () => {
63 | fetchRscPayload()
64 | })
65 | }
66 | }
67 |
68 | function listenNavigation(onNavigation: () => void) {
69 | window.addEventListener('popstate', onNavigation)
70 |
71 | const oldPushState = window.history.pushState
72 | window.history.pushState = function (...args) {
73 | const res = oldPushState.apply(this, args)
74 | onNavigation()
75 | return res
76 | }
77 |
78 | const oldReplaceState = window.history.replaceState
79 | window.history.replaceState = function (...args) {
80 | const res = oldReplaceState.apply(this, args)
81 | onNavigation()
82 | return res
83 | }
84 |
85 | function onClick(e: MouseEvent) {
86 | let link = (e.target as Element).closest('a')
87 | if (
88 | link &&
89 | link instanceof HTMLAnchorElement &&
90 | link.href &&
91 | (!link.target || link.target === '_self') &&
92 | link.origin === location.origin &&
93 | !link.hasAttribute('download') &&
94 | e.button === 0 &&
95 | !e.metaKey &&
96 | !e.ctrlKey &&
97 | !e.altKey &&
98 | !e.shiftKey &&
99 | !e.defaultPrevented
100 | ) {
101 | e.preventDefault()
102 | history.pushState(null, '', link.href)
103 | }
104 | }
105 | document.addEventListener('click', onClick)
106 |
107 | return () => {
108 | document.removeEventListener('click', onClick)
109 | window.removeEventListener('popstate', onNavigation)
110 | window.history.pushState = oldPushState
111 | window.history.replaceState = oldReplaceState
112 | }
113 | }
114 |
115 | main()
116 |
--------------------------------------------------------------------------------
/src/rsc/rsc-renderer.tsx:
--------------------------------------------------------------------------------
1 | import { createMiddleware } from 'hono/factory'
2 | import * as ReactServer from '@vitejs/plugin-rsc/rsc'
3 | import type { RscPayload } from './entry.rsc'
4 | import type { ReactFormState } from 'react-dom/client'
5 |
6 | // Props interface that can be extended by users
7 | export interface Props {
8 | children?: React.ReactNode
9 | }
10 |
11 | export type RscRendererOptions = {
12 | Layout: React.FC
13 | }
14 |
15 | // This declaration is necessary to type the c.render() method in Hono
16 | declare module 'hono' {
17 | interface ContextRenderer {
18 | (component: React.ReactNode, props?: Props): Response | Promise
19 | }
20 | }
21 |
22 | export const rscRenderer = ({ Layout }: RscRendererOptions) => {
23 | return createMiddleware(async (c, next) => {
24 | const request = c.req.raw
25 |
26 | // Handle server actions (POST requests)
27 | if (request.method === 'POST') {
28 | let returnValue: unknown | undefined
29 | let formState: ReactFormState | undefined
30 | let temporaryReferences: unknown | undefined
31 |
32 | const actionId = request.headers.get('x-rsc-action')
33 | if (actionId) {
34 | const contentType = request.headers.get('content-type')
35 | const body = contentType?.startsWith('multipart/form-data')
36 | ? await request.formData()
37 | : await request.text()
38 | temporaryReferences = ReactServer.createTemporaryReferenceSet()
39 | const args = await ReactServer.decodeReply(body, { temporaryReferences })
40 | const action = await ReactServer.loadServerAction(actionId)
41 | returnValue = await action.apply(null, args)
42 | } else {
43 | const formData = await request.formData()
44 | const decodedAction = await ReactServer.decodeAction(formData)
45 | const result = await decodedAction()
46 | formState = await ReactServer.decodeFormState(result, formData)
47 | }
48 |
49 | // Store action results in context for render
50 | c.set('rscActionResult', { returnValue, formState, temporaryReferences })
51 | }
52 |
53 | // Set up the render function
54 | c.setRenderer(async (component: React.ReactNode, props?: Props) => {
55 | // Get action results if they exist
56 | const actionResult = c.get('rscActionResult') || {}
57 | const { returnValue, formState, temporaryReferences } = actionResult
58 |
59 | // Create RSC payload with the component wrapped in Layout
60 | const rscPayload: RscPayload = {
61 | root: {component},
62 | formState,
63 | returnValue
64 | }
65 |
66 | const rscOptions = temporaryReferences ? { temporaryReferences } : {}
67 | const rscStream = ReactServer.renderToReadableStream(
68 | rscPayload,
69 | rscOptions,
70 | )
71 |
72 | // Check if this is an RSC request or HTML request
73 | const url = new URL(request.url)
74 | const isRscRequest =
75 | (!request.headers.get('accept')?.includes('text/html') &&
76 | !url.searchParams.has('__html')) ||
77 | url.searchParams.has('__rsc')
78 |
79 | if (isRscRequest) {
80 | return c.body(rscStream, 200, {
81 | 'content-type': 'text/x-component;charset=utf-8',
82 | vary: 'accept',
83 | })
84 | }
85 |
86 | // Delegate to SSR for HTML rendering
87 | const ssrEntryModule = await import.meta.viteRsc.loadModule<
88 | typeof import('./entry.ssr.js')
89 | >('ssr', 'index')
90 | const htmlStream = await ssrEntryModule.renderHTML(rscStream, {
91 | formState,
92 | debugNojs: url.searchParams.has('__nojs'),
93 | })
94 |
95 | return c.body(htmlStream, 200, {
96 | 'Content-Type': 'text/html; charset=utf-8',
97 | vary: 'accept',
98 | })
99 | })
100 |
101 | await next()
102 | })
103 | }
104 |
105 | if (import.meta.hot) {
106 | import.meta.hot.accept()
107 | }
108 |
--------------------------------------------------------------------------------