diff --git a/examples/react-router-framework-and-antd/.dockerignore b/examples/react-router-framework-and-antd/.dockerignore
new file mode 100644
index 0000000..9b8d514
--- /dev/null
+++ b/examples/react-router-framework-and-antd/.dockerignore
@@ -0,0 +1,4 @@
+.react-router
+build
+node_modules
+README.md
\ No newline at end of file
diff --git a/examples/react-router-framework-and-antd/.gitignore b/examples/react-router-framework-and-antd/.gitignore
new file mode 100644
index 0000000..510be82
--- /dev/null
+++ b/examples/react-router-framework-and-antd/.gitignore
@@ -0,0 +1,6 @@
+.DS_Store
+/node_modules/
+
+# React Router
+/.react-router/
+/build/
\ No newline at end of file
diff --git a/examples/react-router-framework-and-antd/Dockerfile b/examples/react-router-framework-and-antd/Dockerfile
new file mode 100644
index 0000000..207bf93
--- /dev/null
+++ b/examples/react-router-framework-and-antd/Dockerfile
@@ -0,0 +1,22 @@
+FROM node:20-alpine AS development-dependencies-env
+COPY . /app
+WORKDIR /app
+RUN npm ci
+
+FROM node:20-alpine AS production-dependencies-env
+COPY ./package.json package-lock.json /app/
+WORKDIR /app
+RUN npm ci --omit=dev
+
+FROM node:20-alpine AS build-env
+COPY . /app/
+COPY --from=development-dependencies-env /app/node_modules /app/node_modules
+WORKDIR /app
+RUN npm run build
+
+FROM node:20-alpine
+COPY ./package.json package-lock.json /app/
+COPY --from=production-dependencies-env /app/node_modules /app/node_modules
+COPY --from=build-env /app/build /app/build
+WORKDIR /app
+CMD ["npm", "run", "start"]
\ No newline at end of file
diff --git a/examples/react-router-framework-and-antd/README.md b/examples/react-router-framework-and-antd/README.md
new file mode 100644
index 0000000..a676264
--- /dev/null
+++ b/examples/react-router-framework-and-antd/README.md
@@ -0,0 +1,14 @@
+# React Router + TypeScript + Vite
+
+## Development
+
+```bash
+npm install && npm run dev
+```
+
+## Docs
+
+- [Vite](https://vitejs.dev/)
+- [React](https://react.dev/)
+- [Ant Design](https://ant.design/)
+- [React Router docs](https://reactrouter.com/)
\ No newline at end of file
diff --git a/examples/react-router-framework-and-antd/app/app.css b/examples/react-router-framework-and-antd/app/app.css
new file mode 100644
index 0000000..99345d8
--- /dev/null
+++ b/examples/react-router-framework-and-antd/app/app.css
@@ -0,0 +1,15 @@
+@import "tailwindcss";
+
+@theme {
+ --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif,
+ "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+}
+
+html,
+body {
+ @apply bg-white dark:bg-gray-950;
+
+ @media (prefers-color-scheme: dark) {
+ color-scheme: dark;
+ }
+}
diff --git a/examples/react-router-framework-and-antd/app/entry.client.tsx b/examples/react-router-framework-and-antd/app/entry.client.tsx
new file mode 100644
index 0000000..f8d06ad
--- /dev/null
+++ b/examples/react-router-framework-and-antd/app/entry.client.tsx
@@ -0,0 +1,18 @@
+import { createCache, StyleProvider } from "@ant-design/cssinjs";
+import { startTransition, StrictMode } from "react";
+import { hydrateRoot } from "react-dom/client";
+import { HydratedRouter } from "react-router/dom";
+
+// used to ensure proper hydration and consistent styling between server and client for Ant Design components
+const cache = createCache();
+
+startTransition(() => {
+ hydrateRoot(
+ document,
+
+
+
+
+
+ );
+});
diff --git a/examples/react-router-framework-and-antd/app/entry.server.tsx b/examples/react-router-framework-and-antd/app/entry.server.tsx
new file mode 100644
index 0000000..7f31215
--- /dev/null
+++ b/examples/react-router-framework-and-antd/app/entry.server.tsx
@@ -0,0 +1,97 @@
+import { PassThrough } from "node:stream";
+
+import type { AppLoadContext, EntryContext } from "react-router";
+import { createReadableStreamFromReadable } from "@react-router/node";
+import { ServerRouter } from "react-router";
+import { isbot } from "isbot";
+import type { RenderToPipeableStreamOptions } from "react-dom/server";
+import { renderToPipeableStream } from "react-dom/server";
+import { createCache, extractStyle, StyleProvider } from "@ant-design/cssinjs";
+
+export const streamTimeout = 5_000;
+
+export default function handleRequest(
+ request: Request,
+ responseStatusCode: number,
+ responseHeaders: Headers,
+ routerContext: EntryContext,
+ loadContext: AppLoadContext
+ // If you have middleware enabled:
+ // loadContext: unstable_RouterContextProvider
+) {
+ return new Promise((resolve, reject) => {
+ let shellRendered = false;
+ let userAgent = request.headers.get("user-agent");
+ const cache = createCache();
+ let styles = "";
+
+ // Ensure requests from bots and SPA Mode renders wait for all content to load before responding
+ // https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
+ let readyOption: keyof RenderToPipeableStreamOptions =
+ (userAgent && isbot(userAgent)) || routerContext.isSpaMode
+ ? "onAllReady"
+ : "onShellReady";
+
+ const { pipe, abort } = renderToPipeableStream(
+
+
+ ,
+ {
+ [readyOption]() {
+ shellRendered = true;
+ const body = new PassThrough();
+ const stream = createReadableStreamFromReadable(body);
+
+ responseHeaders.set("Content-Type", "text/html");
+
+ // Extract styles from the cache
+ styles = extractStyle(cache);
+
+ // We need to transform the output to inject the Ant Design styles
+ let didInjectStyles = false;
+ const wrappedStream = new TransformStream({
+ transform(chunk, controller) {
+ const chunkString = new TextDecoder().decode(chunk);
+ if (!didInjectStyles && chunkString.includes("")) {
+ // Inject the styles before the closing head tag
+ const modifiedChunk = chunkString.replace(
+ "",
+ ``
+ );
+ didInjectStyles = true;
+ controller.enqueue(new TextEncoder().encode(modifiedChunk));
+ } else {
+ controller.enqueue(chunk);
+ }
+ },
+ });
+
+ resolve(
+ new Response(stream.pipeThrough(wrappedStream), {
+ headers: responseHeaders,
+ status: responseStatusCode,
+ })
+ );
+
+ pipe(body);
+ },
+ onShellError(error: unknown) {
+ reject(error);
+ },
+ onError(error: unknown) {
+ responseStatusCode = 500;
+ // Log streaming rendering errors from inside the shell. Don't log
+ // errors encountered during initial shell rendering since they'll
+ // reject and get logged in handleDocumentRequest.
+ if (shellRendered) {
+ console.error(error);
+ }
+ },
+ }
+ );
+
+ // Abort the rendering stream after the `streamTimeout` so it has time to
+ // flush down the rejected boundaries
+ setTimeout(abort, streamTimeout + 1000);
+ });
+}
diff --git a/examples/react-router-framework-and-antd/app/root.tsx b/examples/react-router-framework-and-antd/app/root.tsx
new file mode 100644
index 0000000..fab9ab9
--- /dev/null
+++ b/examples/react-router-framework-and-antd/app/root.tsx
@@ -0,0 +1,92 @@
+import {
+ isRouteErrorResponse,
+ Links,
+ Meta,
+ Outlet,
+ Scripts,
+ ScrollRestoration,
+} from "react-router";
+import { ConfigProvider } from "antd";
+
+import type { Route } from "./+types/root";
+import "./app.css";
+
+export const links: Route.LinksFunction = () => [
+ { rel: "preconnect", href: "https://fonts.googleapis.com" },
+ {
+ rel: "preconnect",
+ href: "https://fonts.gstatic.com",
+ crossOrigin: "anonymous",
+ },
+ {
+ rel: "stylesheet",
+ href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
+ },
+ {
+ rel: "icon",
+ href: "/public/favicon.ico",
+ sizes: "48x48",
+ type: "image/vnd.microsoft.icon",
+ },
+];
+
+export function Layout({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+ );
+}
+
+export default function App() {
+ return ;
+}
+
+export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
+ let message = "Oops!";
+ let details = "An unexpected error occurred.";
+ let stack: string | undefined;
+
+ if (isRouteErrorResponse(error)) {
+ message = error.status === 404 ? "404" : "Error";
+ details =
+ error.status === 404
+ ? "The requested page could not be found."
+ : error.statusText || details;
+ } else if (import.meta.env.DEV && error && error instanceof Error) {
+ details = error.message;
+ stack = error.stack;
+ }
+
+ return (
+
+ {message}
+ {details}
+ {stack && (
+
+ {stack}
+
+ )}
+
+ );
+}
diff --git a/examples/react-router-framework-and-antd/app/routes.ts b/examples/react-router-framework-and-antd/app/routes.ts
new file mode 100644
index 0000000..102b402
--- /dev/null
+++ b/examples/react-router-framework-and-antd/app/routes.ts
@@ -0,0 +1,3 @@
+import { type RouteConfig, index } from "@react-router/dev/routes";
+
+export default [index("routes/home.tsx")] satisfies RouteConfig;
diff --git a/examples/react-router-framework-and-antd/app/routes/home.tsx b/examples/react-router-framework-and-antd/app/routes/home.tsx
new file mode 100644
index 0000000..398e47c
--- /dev/null
+++ b/examples/react-router-framework-and-antd/app/routes/home.tsx
@@ -0,0 +1,13 @@
+import type { Route } from "./+types/home";
+import { Welcome } from "../welcome/welcome";
+
+export function meta({}: Route.MetaArgs) {
+ return [
+ { title: "New React Router App" },
+ { name: "description", content: "Welcome to React Router!" },
+ ];
+}
+
+export default function Home() {
+ return ;
+}
diff --git a/examples/react-router-framework-and-antd/app/welcome/logo-dark.svg b/examples/react-router-framework-and-antd/app/welcome/logo-dark.svg
new file mode 100644
index 0000000..dd82028
--- /dev/null
+++ b/examples/react-router-framework-and-antd/app/welcome/logo-dark.svg
@@ -0,0 +1,23 @@
+
diff --git a/examples/react-router-framework-and-antd/app/welcome/logo-light.svg b/examples/react-router-framework-and-antd/app/welcome/logo-light.svg
new file mode 100644
index 0000000..7328492
--- /dev/null
+++ b/examples/react-router-framework-and-antd/app/welcome/logo-light.svg
@@ -0,0 +1,23 @@
+
diff --git a/examples/react-router-framework-and-antd/app/welcome/welcome.tsx b/examples/react-router-framework-and-antd/app/welcome/welcome.tsx
new file mode 100644
index 0000000..8f386e4
--- /dev/null
+++ b/examples/react-router-framework-and-antd/app/welcome/welcome.tsx
@@ -0,0 +1,95 @@
+import { Button, Col, Row } from "antd";
+import logoDark from "./logo-dark.svg";
+import logoLight from "./logo-light.svg";
+
+export function Welcome() {
+ return (
+
+
+
+
+

+

+
+
+
+
+
+
+
+ );
+}
+
+const resources = [
+ {
+ href: "https://reactrouter.com/docs",
+ text: "React Router Docs",
+ icon: (
+
+ ),
+ },
+ {
+ href: "https://rmx.as/discord",
+ text: "Join Discord",
+ icon: (
+
+ ),
+ },
+];
diff --git a/examples/react-router-framework-and-antd/package.json b/examples/react-router-framework-and-antd/package.json
new file mode 100644
index 0000000..45462a2
--- /dev/null
+++ b/examples/react-router-framework-and-antd/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "react-router-framework-and-antd",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "build": "react-router build",
+ "dev": "react-router dev",
+ "start": "react-router-serve ./build/server/index.js",
+ "typecheck": "react-router typegen && tsc"
+ },
+ "dependencies": {
+ "@ant-design/cssinjs": "^1.23.0",
+ "@react-router/node": "^7.3.0",
+ "@react-router/serve": "^7.3.0",
+ "antd": "^5.24.3",
+ "isbot": "^5.1.17",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "react-router": "^7.3.0"
+ },
+ "devDependencies": {
+ "@react-router/dev": "^7.3.0",
+ "@tailwindcss/vite": "^4.0.0",
+ "@types/node": "^20",
+ "@types/react": "^19.0.1",
+ "@types/react-dom": "^19.0.1",
+ "react-router-devtools": "^1.1.0",
+ "tailwindcss": "^4.0.0",
+ "typescript": "^5.7.2",
+ "vite": "^5.4.11",
+ "vite-tsconfig-paths": "^5.1.4"
+ }
+}
diff --git a/examples/react-router-framework-and-antd/public/favicon.ico b/examples/react-router-framework-and-antd/public/favicon.ico
new file mode 100644
index 0000000..5dbdfcd
Binary files /dev/null and b/examples/react-router-framework-and-antd/public/favicon.ico differ
diff --git a/examples/react-router-framework-and-antd/react-router.config.ts b/examples/react-router-framework-and-antd/react-router.config.ts
new file mode 100644
index 0000000..6ff16f9
--- /dev/null
+++ b/examples/react-router-framework-and-antd/react-router.config.ts
@@ -0,0 +1,7 @@
+import type { Config } from "@react-router/dev/config";
+
+export default {
+ // Config options...
+ // Server-side render by default, to enable SPA mode set this to `false`
+ ssr: true,
+} satisfies Config;
diff --git a/examples/react-router-framework-and-antd/tsconfig.json b/examples/react-router-framework-and-antd/tsconfig.json
new file mode 100644
index 0000000..dc391a4
--- /dev/null
+++ b/examples/react-router-framework-and-antd/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "include": [
+ "**/*",
+ "**/.server/**/*",
+ "**/.client/**/*",
+ ".react-router/types/**/*"
+ ],
+ "compilerOptions": {
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
+ "types": ["node", "vite/client"],
+ "target": "ES2022",
+ "module": "ES2022",
+ "moduleResolution": "bundler",
+ "jsx": "react-jsx",
+ "rootDirs": [".", "./.react-router/types"],
+ "baseUrl": ".",
+ "paths": {
+ "~/*": ["./app/*"]
+ },
+ "esModuleInterop": true,
+ "verbatimModuleSyntax": true,
+ "noEmit": true,
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "strict": true
+ }
+}
diff --git a/examples/react-router-framework-and-antd/vite.config.ts b/examples/react-router-framework-and-antd/vite.config.ts
new file mode 100644
index 0000000..eb8a1e3
--- /dev/null
+++ b/examples/react-router-framework-and-antd/vite.config.ts
@@ -0,0 +1,11 @@
+import { reactRouter } from "@react-router/dev/vite";
+import tailwindcss from "@tailwindcss/vite";
+import { defineConfig } from "vite";
+import tsconfigPaths from "vite-tsconfig-paths";
+
+export default defineConfig({
+ plugins: [tailwindcss(), reactRouter(), tsconfigPaths()],
+ server: {
+ open: true,
+ },
+});