├── .assets └── starter-kits-solid.png ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .nvmrc ├── .tests ├── .gitignore ├── package.json ├── playwright.config.ts ├── test.cloudflare-d1.ts ├── test.cloudflare.ts ├── test.default.ts ├── test.deno.ts ├── test.javascript.ts ├── test.minimal.ts ├── test.netlify.ts ├── test.node-custom-server.ts ├── test.node-postgres.ts ├── test.vercel.ts └── utils.ts ├── README.md ├── cloudflare-d1 ├── .gitignore ├── README.md ├── app │ ├── app.css │ ├── entry.server.tsx │ ├── root.tsx │ ├── routes.ts │ ├── routes │ │ └── home.tsx │ └── welcome │ │ ├── logo-dark.svg │ │ ├── logo-light.svg │ │ └── welcome.tsx ├── database │ └── schema.ts ├── drizzle.config.ts ├── drizzle │ ├── 0000_outstanding_trauma.sql │ └── meta │ │ ├── 0000_snapshot.json │ │ └── _journal.json ├── package.json ├── public │ └── favicon.ico ├── react-router.config.ts ├── tsconfig.cloudflare.json ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts ├── worker-configuration.d.ts ├── workers │ └── app.ts └── wrangler.jsonc ├── cloudflare ├── .gitignore ├── README.md ├── app │ ├── app.css │ ├── entry.server.tsx │ ├── root.tsx │ ├── routes.ts │ ├── routes │ │ └── home.tsx │ └── welcome │ │ ├── logo-dark.svg │ │ ├── logo-light.svg │ │ └── welcome.tsx ├── package.json ├── public │ └── favicon.ico ├── react-router.config.ts ├── tsconfig.cloudflare.json ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts ├── worker-configuration.d.ts ├── workers │ └── app.ts └── wrangler.jsonc ├── default ├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.md ├── app │ ├── app.css │ ├── root.tsx │ ├── routes.ts │ ├── routes │ │ └── home.tsx │ └── welcome │ │ ├── logo-dark.svg │ │ ├── logo-light.svg │ │ └── welcome.tsx ├── package.json ├── public │ └── favicon.ico ├── react-router.config.ts ├── tsconfig.json └── vite.config.ts ├── deno ├── .gitignore ├── .vscode │ ├── extensions.json │ └── settings.json ├── README.md ├── app │ ├── app.css │ ├── entry.server.tsx │ ├── root.tsx │ ├── routes.ts │ ├── routes │ │ └── home.tsx │ └── welcome │ │ ├── logo-dark.svg │ │ ├── logo-light.svg │ │ └── welcome.tsx ├── deno.jsonc ├── deno.lock ├── package.json ├── public │ └── favicon.ico ├── react-router.config.ts ├── server.ts └── vite.config.ts ├── javascript ├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.md ├── app │ ├── app.css │ ├── root.jsx │ ├── routes.js │ ├── routes │ │ └── home.jsx │ └── welcome │ │ ├── logo-dark.svg │ │ ├── logo-light.svg │ │ └── welcome.jsx ├── package.json ├── public │ └── favicon.ico ├── react-router.config.js └── vite.config.js ├── minimal ├── .gitignore ├── README.md ├── app │ ├── app.css │ ├── root.tsx │ ├── routes.ts │ └── routes │ │ └── home.tsx ├── package.json ├── public │ └── favicon.ico ├── react-router.config.ts ├── tsconfig.json └── vite.config.ts ├── netlify ├── .gitignore ├── README.md ├── app │ ├── app.css │ ├── root.tsx │ ├── routes.ts │ ├── routes │ │ └── home.tsx │ └── welcome │ │ ├── logo-dark.svg │ │ ├── logo-light.svg │ │ └── welcome.tsx ├── dev-server.js ├── netlify.toml ├── netlify │ └── prepare.js ├── package.json ├── public │ └── favicon.ico ├── react-router.config.ts ├── server │ └── app.ts ├── tsconfig.json └── vite.config.ts ├── node-custom-server ├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.md ├── app │ ├── app.css │ ├── root.tsx │ ├── routes.ts │ ├── routes │ │ └── home.tsx │ └── welcome │ │ ├── logo-dark.svg │ │ ├── logo-light.svg │ │ └── welcome.tsx ├── package.json ├── public │ └── favicon.ico ├── react-router.config.ts ├── server.js ├── server │ └── app.ts ├── tsconfig.json ├── tsconfig.node.json ├── tsconfig.vite.json └── vite.config.ts ├── node-postgres ├── .dockerignore ├── .env.example ├── .gitignore ├── Dockerfile ├── README.md ├── app │ ├── app.css │ ├── root.tsx │ ├── routes.ts │ ├── routes │ │ └── home.tsx │ └── welcome │ │ ├── logo-dark.svg │ │ ├── logo-light.svg │ │ └── welcome.tsx ├── database │ ├── context.ts │ └── schema.ts ├── drizzle.config.ts ├── drizzle │ ├── 0000_short_donald_blake.sql │ └── meta │ │ ├── 0000_snapshot.json │ │ └── _journal.json ├── package.json ├── public │ └── favicon.ico ├── react-router.config.ts ├── server.js ├── server │ └── app.ts ├── tsconfig.json ├── tsconfig.node.json ├── tsconfig.vite.json └── vite.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── unstable_rsc-parcel ├── .gitignore ├── README.md ├── package.json ├── public │ └── favicon.ico ├── src │ ├── entry.browser.tsx │ ├── entry.rsc.tsx │ ├── entry.ssr.tsx │ └── routes │ │ ├── about │ │ └── route.tsx │ │ ├── config.ts │ │ ├── home │ │ └── route.tsx │ │ └── root │ │ ├── client.tsx │ │ ├── route.tsx │ │ └── styles.css └── tsconfig.json ├── unstable_rsc-vite ├── .gitignore ├── README.md ├── package.json ├── public │ └── favicon.ico ├── server.js ├── src │ ├── entry.browser.tsx │ ├── entry.rsc.tsx │ ├── entry.ssr.tsx │ └── routes │ │ ├── about │ │ └── route.tsx │ │ ├── config.ts │ │ ├── home │ │ └── route.tsx │ │ └── root │ │ ├── client.tsx │ │ ├── route.tsx │ │ └── styles.css ├── tsconfig.json └── vite.config.ts └── vercel ├── .gitignore ├── README.md ├── app ├── app.css ├── root.tsx ├── routes.ts ├── routes │ └── home.tsx └── welcome │ ├── logo-dark.svg │ ├── logo-light.svg │ └── welcome.tsx ├── package.json ├── public └── favicon.ico ├── react-router.config.ts ├── tsconfig.json └── vite.config.ts /.assets/starter-kits-solid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remix-run/react-router-templates/56789beb24c7fd0da5c5681e7bfcbaa4ef03f758/.assets/starter-kits-solid.png -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | jobs: 10 | test: 11 | strategy: 12 | matrix: 13 | include: 14 | - os: ubuntu-latest 15 | browser: firefox 16 | - os: ubuntu-latest 17 | browser: chromium 18 | - os: macos-latest 19 | browser: webkit 20 | - os: windows-latest 21 | browser: msedge 22 | fail-fast: false 23 | runs-on: ${{ matrix.os }} 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: pnpm/action-setup@v4 27 | with: 28 | version: 10 29 | - uses: actions/setup-node@v4 30 | with: 31 | node-version: 22 32 | cache: "pnpm" 33 | - uses: denoland/setup-deno@v2 34 | with: 35 | deno-version: v2.3.3 36 | - name: Install dependencies 37 | run: pnpm install 38 | - name: Install Playwright Browsers 39 | working-directory: .tests 40 | run: pnpm playwright install --with-deps ${{ matrix.browser }} 41 | - name: Run Playwright tests 42 | working-directory: .tests 43 | run: pnpm test --project=${{ matrix.browser }} 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules/ 3 | 4 | # https://github.com/netlify/cli/issues/6958 5 | .netlify 6 | # Parcel cache 7 | .parcel-cache 8 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 2 | -------------------------------------------------------------------------------- /.tests/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /tmp/ 3 | 4 | # playwright 5 | /blob-report/ 6 | /playwright-report/ 7 | /playwright/.cache/ 8 | /test-results/ 9 | -------------------------------------------------------------------------------- /.tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "test": "playwright test", 6 | "clean": "rm -rf ./test-results && rm -rf ./tmp" 7 | }, 8 | "devDependencies": { 9 | "@playwright/test": "^1.52.0", 10 | "@types/fs-extra": "^11.0.4", 11 | "@types/node": "^22.15.3", 12 | "execa": "^9.5.2", 13 | "fs-extra": "^11.3.0", 14 | "get-port": "^7.1.0", 15 | "pathe": "^2.0.3" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.tests/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from "@playwright/test"; 2 | 3 | export default defineConfig({ 4 | testDir: ".", 5 | testMatch: ["**/test.*.ts"], 6 | fullyParallel: true, 7 | forbidOnly: !!process.env.CI, 8 | retries: process.env.CI ? 2 : 0, 9 | workers: process.env.CI ? 1 : undefined, 10 | reporter: "line", 11 | use: { 12 | trace: "on-first-retry", 13 | }, 14 | projects: [ 15 | { 16 | name: "chromium", 17 | use: devices["Desktop Chrome"], 18 | }, 19 | { 20 | name: "webkit", 21 | use: devices["Desktop Safari"], 22 | }, 23 | { 24 | name: "msedge", 25 | use: { 26 | ...devices["Desktop Edge"], 27 | // Desktop Edge uses chromium by default 28 | // https://github.com/microsoft/playwright/blob/993546c1bc3267fb72eddaf8cf003cb2e1519598/packages/playwright-core/src/server/deviceDescriptorsSource.json#L1652 29 | channel: "msedge", 30 | }, 31 | }, 32 | { 33 | name: "firefox", 34 | use: devices["Desktop Firefox"], 35 | }, 36 | ], 37 | }); 38 | -------------------------------------------------------------------------------- /.tests/test.cloudflare-d1.ts: -------------------------------------------------------------------------------- 1 | import { expect, Page } from "@playwright/test"; 2 | import getPort from "get-port"; 3 | 4 | import { matchLine, testTemplate, urlRegex } from "./utils.js"; 5 | 6 | const test = testTemplate("cloudflare-d1"); 7 | 8 | test("typecheck", async ({ $ }) => { 9 | await $(`pnpm typecheck`); 10 | }); 11 | 12 | test("dev", async ({ page, $ }) => { 13 | await $(`pnpm db:migrate`); 14 | 15 | const port = await getPort(); 16 | const dev = $(`pnpm dev --port ${port}`); 17 | 18 | const url = await matchLine(dev.stdout, urlRegex.viteDev); 19 | await workflow({ page, url }); 20 | 21 | const ignoredLines = [ 22 | /Default inspector port \d{4} not available, using \d{4} instead/, 23 | ]; 24 | const filteredStderr = dev.buffer.stderr 25 | .split("\n") 26 | .filter( 27 | (line) => 28 | line && !ignoredLines.some((ignoredLine) => ignoredLine.test(line)), 29 | ) 30 | .join("\n"); 31 | expect(filteredStderr).toBe(""); 32 | }); 33 | 34 | test("build + start", async ({ page, $ }) => { 35 | await $(`pnpm db:migrate`); 36 | 37 | const port = await getPort(); 38 | const preview = $(`pnpm preview --port ${port}`); 39 | 40 | const url = await matchLine(preview.stdout, urlRegex.viteDev); 41 | await workflow({ page, url }); 42 | 43 | const ignoredLines = [ 44 | /The build was canceled/, 45 | /Error running vite-plugin-cloudflare:nodejs-compat on Tailwind CSS output\. Skipping\./, 46 | /Default inspector port \d{4} not available, using \d{4} instead/, 47 | ]; 48 | const filteredStderr = preview.buffer.stderr 49 | .split("\n") 50 | .filter( 51 | (line) => 52 | line && !ignoredLines.some((ignoredLine) => ignoredLine.test(line)), 53 | ) 54 | .join("\n"); 55 | expect(filteredStderr).toBe(""); 56 | }); 57 | 58 | async function workflow({ page, url }: { page: Page; url: string }) { 59 | await page.goto(url); 60 | await expect(page).toHaveTitle(/New React Router App/); 61 | 62 | await page.getByRole("link", { name: "React Router Docs" }).waitFor(); 63 | await page.getByRole("link", { name: "Join Discord" }).waitFor(); 64 | 65 | const randomText = Math.random().toString(36).substring(7); 66 | await page.getByRole("textbox", { name: "Name" }).fill(randomText); 67 | await page 68 | .getByRole("textbox", { name: "Email" }) 69 | .fill(`email${randomText}@example.com`); 70 | await page.getByRole("button", { name: "Sign Guest Book" }).click(); 71 | await page.getByText(randomText).waitFor(); 72 | 73 | await page.goto(url); 74 | await page.getByText(randomText).waitFor(); 75 | expect(page.errors).toStrictEqual([]); 76 | } 77 | -------------------------------------------------------------------------------- /.tests/test.cloudflare.ts: -------------------------------------------------------------------------------- 1 | import { expect, Page } from "@playwright/test"; 2 | import getPort from "get-port"; 3 | 4 | import { matchLine, testTemplate, urlRegex } from "./utils"; 5 | 6 | const test = testTemplate("cloudflare"); 7 | 8 | test("typecheck", async ({ $ }) => { 9 | await $(`pnpm typecheck`); 10 | }); 11 | 12 | test("dev", async ({ page, $ }) => { 13 | const port = await getPort(); 14 | const dev = $(`pnpm dev --port ${port}`); 15 | 16 | const url = await matchLine(dev.stdout, urlRegex.viteDev); 17 | await workflow({ page, url }); 18 | const ignoredLines = [ 19 | /Default inspector port \d{4} not available, using \d{4} instead/, 20 | ]; 21 | const filteredStderr = dev.buffer.stderr 22 | .split("\n") 23 | .filter( 24 | (line) => 25 | line && !ignoredLines.some((ignoredLine) => ignoredLine.test(line)), 26 | ) 27 | .join("\n"); 28 | expect(filteredStderr).toBe(""); 29 | }); 30 | 31 | test("preview", async ({ page, $ }) => { 32 | const port = await getPort(); 33 | const preview = $(`pnpm preview --port ${port}`); 34 | 35 | const url = await matchLine(preview.stdout, urlRegex.viteDev); 36 | await workflow({ page, url }); 37 | 38 | const ignoredLines = [ 39 | /The build was canceled/, 40 | /Error running vite-plugin-cloudflare:nodejs-compat on Tailwind CSS output\. Skipping\./, 41 | /Default inspector port \d{4} not available, using \d{4} instead/, 42 | ]; 43 | const filteredStderr = preview.buffer.stderr 44 | .split("\n") 45 | .filter( 46 | (line) => 47 | line && !ignoredLines.some((ignoredLine) => ignoredLine.test(line)), 48 | ) 49 | .join("\n"); 50 | expect(filteredStderr).toBe(""); 51 | }); 52 | 53 | async function workflow({ page, url }: { page: Page; url: string }) { 54 | await page.goto(url); 55 | await expect(page).toHaveTitle(/New React Router App/); 56 | await page.getByRole("link", { name: "React Router Docs" }).waitFor(); 57 | await page.getByRole("link", { name: "Join Discord" }).waitFor(); 58 | expect(page.errors).toStrictEqual([]); 59 | } 60 | -------------------------------------------------------------------------------- /.tests/test.default.ts: -------------------------------------------------------------------------------- 1 | import { expect, Page } from "@playwright/test"; 2 | import getPort from "get-port"; 3 | 4 | import { matchLine, testTemplate, urlRegex } from "./utils"; 5 | 6 | const test = testTemplate("default"); 7 | 8 | test("typecheck", async ({ $ }) => { 9 | await $(`pnpm typecheck`); 10 | }); 11 | 12 | test("dev", async ({ page, $ }) => { 13 | const port = await getPort(); 14 | const dev = $(`pnpm dev --port ${port}`); 15 | 16 | const url = await matchLine(dev.stdout, urlRegex.viteDev); 17 | await workflow({ page, url }); 18 | expect(dev.buffer.stderr).toBe(""); 19 | }); 20 | 21 | test("build + start", async ({ page, $ }) => { 22 | await $(`pnpm build`); 23 | 24 | const port = await getPort(); 25 | const start = $(`pnpm start`, { env: { PORT: String(port) } }); 26 | 27 | const url = await matchLine(start.stdout, urlRegex.reactRouterServe); 28 | await workflow({ page, url }); 29 | expect(start.buffer.stderr).toBe(""); 30 | }); 31 | 32 | async function workflow({ page, url }: { page: Page; url: string }) { 33 | await page.goto(url); 34 | await expect(page).toHaveTitle(/New React Router App/); 35 | await page.getByRole("link", { name: "React Router Docs" }).waitFor(); 36 | await page.getByRole("link", { name: "Join Discord" }).waitFor(); 37 | expect(page.errors).toStrictEqual([]); 38 | } 39 | -------------------------------------------------------------------------------- /.tests/test.deno.ts: -------------------------------------------------------------------------------- 1 | import { expect, Page } from "@playwright/test"; 2 | import getPort from "get-port"; 3 | 4 | import { matchLine, testTemplate, urlRegex } from "./utils"; 5 | 6 | const test = testTemplate("deno", "deno install"); 7 | 8 | test("typecheck", async ({ $ }) => { 9 | await $(`deno task typecheck`); 10 | }); 11 | 12 | test("dev", async ({ page, $ }) => { 13 | const port = await getPort(); 14 | const dev = $(`deno task dev --port ${port}`); 15 | 16 | const url = await matchLine(dev.stdout, urlRegex.viteDev); 17 | 18 | await workflow({ page, url }); 19 | const [, ...restLines] = dev.buffer.stderr.split("\n"); 20 | expect(restLines.join("\n")).toBe(""); 21 | }); 22 | 23 | test("build + start", async ({ page, $ }) => { 24 | await $(`deno task build`); 25 | 26 | const port = await getPort(); 27 | const start = $(`deno task start`, { env: { PORT: String(port) } }); 28 | 29 | const url = await matchLine(start.stderr, urlRegex.deno); 30 | const localURL = new URL(url); 31 | localURL.hostname = "localhost"; 32 | 33 | await workflow({ page, url: localURL.href }); 34 | const [, ...restLines] = start.buffer.stderr.split("\n"); 35 | expect(restLines.join("\n")).toBe( 36 | `Listening on ${url} (${localURL})\n`, 37 | ); 38 | }); 39 | 40 | async function workflow({ page, url }: { page: Page; url: string }) { 41 | await page.goto(url); 42 | await page.getByRole("link", { name: "React Router Docs" }).waitFor(); 43 | await page.getByRole("link", { name: "Join Discord" }).waitFor(); 44 | await expect(page).toHaveTitle(/New React Router App/); 45 | expect(page.errors).toStrictEqual([]); 46 | } 47 | -------------------------------------------------------------------------------- /.tests/test.javascript.ts: -------------------------------------------------------------------------------- 1 | import { expect, Page } from "@playwright/test"; 2 | import getPort from "get-port"; 3 | 4 | import { matchLine, testTemplate, urlRegex } from "./utils"; 5 | 6 | const test = testTemplate("javascript"); 7 | 8 | test("dev", async ({ page, $ }) => { 9 | const port = await getPort(); 10 | const dev = $(`pnpm dev --port ${port}`); 11 | 12 | const url = await matchLine(dev.stdout, urlRegex.viteDev); 13 | await workflow({ page, url }); 14 | expect(dev.buffer.stderr).toBe(""); 15 | }); 16 | 17 | test("build + start", async ({ page, $ }) => { 18 | await $(`pnpm build`); 19 | 20 | const port = await getPort(); 21 | const start = $(`pnpm start`, { env: { PORT: String(port) } }); 22 | 23 | const url = await matchLine(start.stdout, urlRegex.reactRouterServe); 24 | await workflow({ page, url }); 25 | expect(start.buffer.stderr).toBe(""); 26 | }); 27 | 28 | async function workflow({ page, url }: { page: Page; url: string }) { 29 | await page.goto(url); 30 | await expect(page).toHaveTitle(/New React Router App/); 31 | await page.getByRole("link", { name: "React Router Docs" }).waitFor(); 32 | await page.getByRole("link", { name: "Join Discord" }).waitFor(); 33 | expect(page.errors).toStrictEqual([]); 34 | } 35 | -------------------------------------------------------------------------------- /.tests/test.minimal.ts: -------------------------------------------------------------------------------- 1 | import { expect, Page } from "@playwright/test"; 2 | import getPort from "get-port"; 3 | 4 | import { matchLine, testTemplate, urlRegex } from "./utils"; 5 | 6 | const test = testTemplate("minimal"); 7 | 8 | test("typecheck", async ({ $ }) => { 9 | await $(`pnpm typecheck`); 10 | }); 11 | 12 | test("dev", async ({ page, $ }) => { 13 | const port = await getPort(); 14 | const dev = $(`pnpm dev --port ${port}`); 15 | 16 | const url = await matchLine(dev.stdout, urlRegex.viteDev); 17 | await workflow({ page, url }); 18 | expect(dev.buffer.stderr).toBe(""); 19 | }); 20 | 21 | test("build + start", async ({ page, $ }) => { 22 | await $(`pnpm build`); 23 | 24 | const port = await getPort(); 25 | const start = $(`pnpm start`, { env: { PORT: String(port) } }); 26 | 27 | const url = await matchLine(start.stdout, urlRegex.reactRouterServe); 28 | await workflow({ page, url }); 29 | expect(start.buffer.stderr).toBe(""); 30 | }); 31 | 32 | async function workflow({ page, url }: { page: Page; url: string }) { 33 | await page.goto(url); 34 | await page.getByRole("heading", { name: "Hello, React Router" }).waitFor(); 35 | await page.getByRole("link", { name: "React Router Docs" }).waitFor(); 36 | expect(page.errors).toStrictEqual([]); 37 | } 38 | -------------------------------------------------------------------------------- /.tests/test.netlify.ts: -------------------------------------------------------------------------------- 1 | import { expect, Page } from "@playwright/test"; 2 | import getPort from "get-port"; 3 | 4 | import { 5 | matchLine, 6 | testTemplate, 7 | urlRegex, 8 | withoutHmrPortError, 9 | } from "./utils"; 10 | 11 | const test = testTemplate("netlify"); 12 | 13 | test("typecheck", async ({ $ }) => { 14 | await $(`pnpm typecheck`); 15 | }); 16 | 17 | test("dev", async ({ page, $ }) => { 18 | const port = await getPort(); 19 | const dev = $(`pnpm dev`, { env: { PORT: String(port) } }); 20 | 21 | const url = await matchLine(dev.stdout, urlRegex.custom); 22 | await workflow({ page, url }); 23 | expect(withoutHmrPortError(dev.buffer.stderr)).toBe(""); 24 | }); 25 | 26 | test("build", async ({ $ }) => { 27 | await $(`pnpm build`); 28 | }); 29 | 30 | // For some reason, `netlify serve` can't find our `build` script 🤷 31 | // This happens when running the template within this monorepo, not just in tests 32 | // It does not happen when the template is initialized via `pnpm create react-router --template` 33 | test.skip("build + start", async ({ page, edit, $ }) => { 34 | await edit("netlify.toml", (txt) => 35 | txt 36 | .replaceAll("[dev]", "[dev]\nautoLaunch = false") 37 | .replaceAll("npm run", "pnpm"), 38 | ); 39 | 40 | const port1 = await getPort(); 41 | const port2 = await getPort(); 42 | const port3 = await getPort(); 43 | const start = $( 44 | `pnpm start --port ${port1} --functionsPort ${port2} --staticServerPort ${port3}`, 45 | ); 46 | 47 | const url = await matchLine(start.stdout, urlRegex.netlify); 48 | await workflow({ page, url }); 49 | expect(start.buffer.stderr).toBe(""); 50 | }); 51 | 52 | // Helper function to filter out expected WebSocket errors 53 | function filterExpectedErrors(errors: Error[]) { 54 | return errors.filter( 55 | (error) => 56 | !error.message.includes("WebSocket closed without opened") && 57 | !error.message.includes("WebSocket server error"), 58 | ); 59 | } 60 | 61 | async function workflow({ page, url }: { page: Page; url: string }) { 62 | await page.goto(url); 63 | await expect(page).toHaveTitle(/New React Router App/); 64 | await page.getByRole("link", { name: "React Router Docs" }).waitFor(); 65 | await page.getByRole("link", { name: "Join Discord" }).waitFor(); 66 | expect(filterExpectedErrors(page.errors)).toStrictEqual([]); 67 | } 68 | -------------------------------------------------------------------------------- /.tests/test.node-custom-server.ts: -------------------------------------------------------------------------------- 1 | import { expect, Page } from "@playwright/test"; 2 | import getPort from "get-port"; 3 | 4 | import { 5 | matchLine, 6 | testTemplate, 7 | urlRegex, 8 | withoutHmrPortError, 9 | } from "./utils"; 10 | 11 | const test = testTemplate("node-custom-server"); 12 | 13 | // test("typecheck", async ({ $ }) => { 14 | // await $(`pnpm typecheck`); 15 | // }); 16 | 17 | test("dev", async ({ page, $ }) => { 18 | const port = await getPort(); 19 | const dev = $(`pnpm dev`, { env: { PORT: String(port) } }); 20 | 21 | const url = await matchLine(dev.stdout, urlRegex.custom); 22 | await workflow({ page, url }); 23 | 24 | expect(withoutHmrPortError(dev.buffer.stderr)).toBe(""); 25 | }); 26 | 27 | test("build + start", async ({ page, $ }) => { 28 | await $(`pnpm build`); 29 | 30 | const port = await getPort(); 31 | const start = $(`pnpm start`, { env: { PORT: String(port) } }); 32 | 33 | const url = await matchLine(start.stdout, urlRegex.custom); 34 | await workflow({ page, url }); 35 | expect(start.buffer.stderr).toBe(""); 36 | }); 37 | 38 | // Helper function to filter out expected WebSocket errors 39 | function filterExpectedErrors(errors: Error[]) { 40 | return errors.filter( 41 | (error) => 42 | !error.message.includes("WebSocket closed without opened") && 43 | !error.message.includes("WebSocket server error"), 44 | ); 45 | } 46 | 47 | async function workflow({ page, url }: { page: Page; url: string }) { 48 | await page.goto(url); 49 | await expect(page).toHaveTitle(/New React Router App/); 50 | await page.getByRole("link", { name: "React Router Docs" }).waitFor(); 51 | await page.getByRole("link", { name: "Join Discord" }).waitFor(); 52 | expect(filterExpectedErrors(page.errors)).toStrictEqual([]); 53 | } 54 | -------------------------------------------------------------------------------- /.tests/test.node-postgres.ts: -------------------------------------------------------------------------------- 1 | import { testTemplate } from "./utils"; 2 | 3 | const test = testTemplate("node-postgres"); 4 | 5 | test("typecheck", async ({ $ }) => { 6 | await $(`pnpm typecheck`); 7 | }); 8 | -------------------------------------------------------------------------------- /.tests/test.vercel.ts: -------------------------------------------------------------------------------- 1 | import { expect, Page } from "@playwright/test"; 2 | import getPort from "get-port"; 3 | 4 | import { 5 | matchLine, 6 | testTemplate, 7 | urlRegex, 8 | withoutHmrPortError, 9 | } from "./utils"; 10 | 11 | const test = testTemplate("vercel"); 12 | 13 | test("typecheck", async ({ $ }) => { 14 | await $(`pnpm typecheck`); 15 | }); 16 | 17 | test("dev", async ({ page, $ }) => { 18 | const port = await getPort(); 19 | const dev = $(`pnpm dev`, { env: { PORT: String(port) } }); 20 | 21 | const url = await matchLine(dev.stdout, urlRegex.viteDev); 22 | await workflow({ page, url }); 23 | expect(withoutHmrPortError(dev.buffer.stderr)).toBe(""); 24 | }); 25 | 26 | test("build", async ({ $ }) => { 27 | await $(`pnpm build`); 28 | }); 29 | 30 | async function workflow({ page, url }: { page: Page; url: string }) { 31 | await page.goto(url); 32 | await expect(page).toHaveTitle(/New React Router App/); 33 | await page.getByRole("link", { name: "React Router Docs" }).waitFor(); 34 | await page.getByRole("link", { name: "Join Discord" }).waitFor(); 35 | expect(page.errors).toStrictEqual([]); 36 | } 37 | -------------------------------------------------------------------------------- /.tests/utils.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "url"; 2 | 3 | import { test as playwrightTest } from "@playwright/test"; 4 | import { 5 | execa, 6 | ExecaError, 7 | Options, 8 | parseCommandString, 9 | ResultPromise, 10 | } from "execa"; 11 | import fs from "fs-extra"; 12 | import * as Path from "pathe"; 13 | import { Readable } from "stream"; 14 | import { ChildProcess } from "child_process"; 15 | 16 | const __filename = fileURLToPath(import.meta.url); 17 | const ROOT = Path.join(__filename, "../.."); 18 | const TMP = Path.join(ROOT, ".tests/tmp"); 19 | 20 | declare module "@playwright/test" { 21 | interface Page { 22 | errors: Error[]; 23 | } 24 | } 25 | 26 | type Edit = ( 27 | file: string, 28 | transform: (contents: string) => string, 29 | ) => Promise; 30 | 31 | type Command = ( 32 | command: string, 33 | options?: Pick, 34 | ) => ResultPromise<{ reject: false }> & { 35 | buffer: { stdout: string; stderr: string }; 36 | }; 37 | 38 | export const testTemplate = (template: string, installCommand?: string) => 39 | playwrightTest.extend<{ 40 | cwd: string; 41 | edit: Edit; 42 | $: Command; 43 | }>({ 44 | page: async ({ page }, use) => { 45 | page.errors = []; 46 | page.on("pageerror", (error: Error) => page.errors.push(error)); 47 | await use(page); 48 | }, 49 | cwd: async ({}, use, testInfo) => { 50 | await fs.ensureDir(TMP); 51 | const cwd = await fs.mkdtemp(Path.join(TMP, template + "-")); 52 | await fs.mkdirp(cwd); 53 | 54 | const templatePath = Path.join(ROOT, template); 55 | const nodeModulesPath = Path.join(templatePath, "node_modules"); 56 | fs.copySync(templatePath, cwd, { 57 | errorOnExist: true, 58 | filter: (src) => Path.normalize(src) !== nodeModulesPath, 59 | }); 60 | 61 | if (installCommand) { 62 | const spawn = execa({ 63 | cwd, 64 | env: { 65 | NO_COLOR: "1", 66 | FORCE_COLOR: "0", 67 | }, 68 | reject: false, 69 | }); 70 | 71 | const [file, ...args] = parseCommandString(installCommand); 72 | 73 | await spawn(file, args); 74 | } else { 75 | fs.symlinkSync(nodeModulesPath, Path.join(cwd, "node_modules")); 76 | } 77 | 78 | await use(cwd); 79 | 80 | const testPassed = testInfo.errors.length === 0; 81 | if (!testPassed) console.log("cwd: ", cwd); 82 | }, 83 | edit: async ({ cwd }, use) => { 84 | await use(async (file, transform) => { 85 | let filepath = Path.join(cwd, file); 86 | let contents = fs.readFileSync(filepath, "utf8"); 87 | return fs.writeFileSync(filepath, transform(contents), "utf8"); 88 | }); 89 | }, 90 | $: async ({ cwd }, use) => { 91 | const spawn = execa({ 92 | cwd, 93 | env: { 94 | NO_COLOR: "1", 95 | FORCE_COLOR: "0", 96 | }, 97 | reject: false, 98 | }); 99 | 100 | let testHasEnded = false; 101 | const processes: Array = []; 102 | await use((command, options = {}) => { 103 | const [file, ...args] = parseCommandString(command); 104 | 105 | const p = spawn(file, args, options); 106 | if (p instanceof ChildProcess) { 107 | processes.push(p); 108 | } 109 | 110 | p.then((result) => { 111 | if (!(result instanceof Error)) return result; 112 | 113 | // Once the test has ended, this process will be killed as part of its teardown resulting in an ExecaError. 114 | // We only care about surfacing errors that occurred during test execution, not during teardown. 115 | const expectedError = testHasEnded && result instanceof ExecaError; 116 | if (expectedError) return result; 117 | 118 | throw result; 119 | }); 120 | 121 | const buffer = { stdout: "", stderr: "" }; 122 | p.stdout.on("data", (data) => (buffer.stdout += data.toString())); 123 | p.stderr.on("data", (data) => (buffer.stderr += data.toString())); 124 | return Object.assign(p, { buffer }); 125 | }); 126 | 127 | testHasEnded = true; 128 | processes.forEach((p) => p.kill()); 129 | }, 130 | }); 131 | 132 | export function matchLine( 133 | stream: Readable, 134 | pattern: RegExp, 135 | options: { timeout?: number } = {}, 136 | ) { 137 | // Prepare error outside of promise so that stacktrace points to caller of `matchLine` 138 | const timeout = new Error(`Timed out - Could not find pattern: ${pattern}`); 139 | return new Promise(async (resolve, reject) => { 140 | setTimeout(() => reject(timeout), options.timeout ?? 10_000); 141 | stream.on("data", (data) => { 142 | const line = data.toString(); 143 | const matches = line.match(pattern); 144 | if (matches) resolve(matches[1]); 145 | }); 146 | }); 147 | } 148 | 149 | const urlMatch = ({ prefix }: { prefix: RegExp }) => 150 | new RegExp(`${prefix.source}(${/http:\/\/\S+/.source})`); 151 | export const urlRegex = { 152 | viteDev: urlMatch({ prefix: /Local:\s+/ }), 153 | reactRouterServe: urlMatch({ prefix: /\[react-router-serve\]\s+/ }), 154 | custom: urlMatch({ prefix: /Server is running on / }), 155 | deno: urlMatch({ prefix: /Listening on / }), 156 | netlify: urlMatch({ prefix: /◈ Server now ready on / }), 157 | wrangler: urlMatch({ prefix: /Ready on / }), 158 | }; 159 | 160 | // `vite.createServer` always tries to use the same HMR port 161 | // unless `server.hmr.port` is configured. 162 | // Ultimately, we should provide better primitives for building custom servers 163 | // something like `createRequestHandler(pathToBuild)`. 164 | export const withoutHmrPortError = (stderr: string) => 165 | stderr 166 | .replace(/WebSocket server error: Port \d+ is already in use/, "") 167 | .trim(); 168 | -------------------------------------------------------------------------------- /cloudflare-d1/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules/ 3 | *.tsbuildinfo 4 | 5 | # React Router 6 | /.react-router/ 7 | /build/ 8 | 9 | # Cloudflare 10 | .mf 11 | .wrangler 12 | -------------------------------------------------------------------------------- /cloudflare-d1/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to React Router! 2 | 3 | A modern, production-ready template for building full-stack React applications using React Router. 4 | 5 | ## Features 6 | 7 | - 🚀 Server-side rendering 8 | - ⚡️ Hot Module Replacement (HMR) 9 | - 📦 Asset bundling and optimization 10 | - 🔄 Data loading and mutations 11 | - 🔒 TypeScript by default 12 | - 🎉 TailwindCSS for styling 13 | - 📖 [React Router docs](https://reactrouter.com/) 14 | 15 | ## Getting Started 16 | 17 | ### Installation 18 | 19 | Install the dependencies: 20 | 21 | ```bash 22 | npm install 23 | ``` 24 | 25 | ### Development 26 | 27 | Run an initial database migration: 28 | 29 | ```bash 30 | npm run db:migrate 31 | ``` 32 | 33 | Start the development server with HMR: 34 | 35 | ```bash 36 | npm run dev 37 | ``` 38 | 39 | Your application will be available at `http://localhost:5173`. 40 | 41 | ## Building for Production 42 | 43 | Create a production build: 44 | 45 | ```bash 46 | npm run build 47 | ``` 48 | 49 | ## Deployment 50 | 51 | Deployment is done using the Wrangler CLI. 52 | 53 | First, you need to create a d1 database in Cloudflare. 54 | 55 | ```sh 56 | npx wrangler d1 create 57 | ``` 58 | 59 | Be sure to update the `wrangler.toml` file with the correct database name and id. 60 | 61 | You will also need to [update the `drizzle.config.ts` file](https://orm.drizzle.team/docs/guides/d1-http-with-drizzle-kit), and then run the production migration: 62 | 63 | ```sh 64 | npm run db:migrate-production 65 | ``` 66 | 67 | To build and deploy directly to production: 68 | 69 | ```sh 70 | npm run deploy 71 | ``` 72 | 73 | To deploy a preview URL: 74 | 75 | ```sh 76 | npx wrangler versions upload 77 | ``` 78 | 79 | You can then promote a version to production after verification or roll it out progressively. 80 | 81 | ```sh 82 | npx wrangler versions deploy 83 | ``` 84 | 85 | ## Styling 86 | 87 | This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer. 88 | 89 | --- 90 | 91 | Built with ❤️ using React Router. 92 | -------------------------------------------------------------------------------- /cloudflare-d1/app/app.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | @theme { 4 | --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif, 5 | "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 6 | } 7 | 8 | html, 9 | body { 10 | @apply bg-white dark:bg-gray-950; 11 | 12 | @media (prefers-color-scheme: dark) { 13 | color-scheme: dark; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /cloudflare-d1/app/entry.server.tsx: -------------------------------------------------------------------------------- 1 | import type { AppLoadContext, EntryContext } from "react-router"; 2 | import { ServerRouter } from "react-router"; 3 | import { isbot } from "isbot"; 4 | import { renderToReadableStream } from "react-dom/server"; 5 | 6 | export default async function handleRequest( 7 | request: Request, 8 | responseStatusCode: number, 9 | responseHeaders: Headers, 10 | routerContext: EntryContext, 11 | _loadContext: AppLoadContext 12 | ) { 13 | let shellRendered = false; 14 | const userAgent = request.headers.get("user-agent"); 15 | 16 | const body = await renderToReadableStream( 17 | , 18 | { 19 | onError(error: unknown) { 20 | responseStatusCode = 500; 21 | // Log streaming rendering errors from inside the shell. Don't log 22 | // errors encountered during initial shell rendering since they'll 23 | // reject and get logged in handleDocumentRequest. 24 | if (shellRendered) { 25 | console.error(error); 26 | } 27 | }, 28 | } 29 | ); 30 | shellRendered = true; 31 | 32 | // Ensure requests from bots and SPA Mode renders wait for all content to load before responding 33 | // https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation 34 | if ((userAgent && isbot(userAgent)) || routerContext.isSpaMode) { 35 | await body.allReady; 36 | } 37 | 38 | responseHeaders.set("Content-Type", "text/html"); 39 | return new Response(body, { 40 | headers: responseHeaders, 41 | status: responseStatusCode, 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /cloudflare-d1/app/root.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | isRouteErrorResponse, 3 | Links, 4 | Meta, 5 | Outlet, 6 | Scripts, 7 | ScrollRestoration, 8 | } from "react-router"; 9 | 10 | import type { Route } from "./+types/root"; 11 | import "./app.css"; 12 | 13 | export const links: Route.LinksFunction = () => [ 14 | { rel: "preconnect", href: "https://fonts.googleapis.com" }, 15 | { 16 | rel: "preconnect", 17 | href: "https://fonts.gstatic.com", 18 | crossOrigin: "anonymous", 19 | }, 20 | { 21 | rel: "stylesheet", 22 | href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", 23 | }, 24 | ]; 25 | 26 | export function Layout({ children }: { children: React.ReactNode }) { 27 | return ( 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {children} 37 | 38 | 39 | 40 | 41 | ); 42 | } 43 | 44 | export default function App() { 45 | return ; 46 | } 47 | 48 | export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { 49 | let message = "Oops!"; 50 | let details = "An unexpected error occurred."; 51 | let stack: string | undefined; 52 | 53 | if (isRouteErrorResponse(error)) { 54 | message = error.status === 404 ? "404" : "Error"; 55 | details = 56 | error.status === 404 57 | ? "The requested page could not be found." 58 | : error.statusText || details; 59 | } else if (import.meta.env.DEV && error && error instanceof Error) { 60 | details = error.message; 61 | stack = error.stack; 62 | } 63 | 64 | return ( 65 |
66 |

{message}

67 |

{details}

68 | {stack && ( 69 |
70 |           {stack}
71 |         
72 | )} 73 |
74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /cloudflare-d1/app/routes.ts: -------------------------------------------------------------------------------- 1 | import { type RouteConfig, index } from "@react-router/dev/routes"; 2 | 3 | export default [index("routes/home.tsx")] satisfies RouteConfig; 4 | -------------------------------------------------------------------------------- /cloudflare-d1/app/routes/home.tsx: -------------------------------------------------------------------------------- 1 | import * as schema from "~/database/schema"; 2 | 3 | import type { Route } from "./+types/home"; 4 | import { Welcome } from "../welcome/welcome"; 5 | 6 | export function meta({}: Route.MetaArgs) { 7 | return [ 8 | { title: "New React Router App" }, 9 | { name: "description", content: "Welcome to React Router!" }, 10 | ]; 11 | } 12 | 13 | export async function action({ request, context }: Route.ActionArgs) { 14 | const formData = await request.formData(); 15 | let name = formData.get("name"); 16 | let email = formData.get("email"); 17 | if (typeof name !== "string" || typeof email !== "string") { 18 | return { guestBookError: "Name and email are required" }; 19 | } 20 | 21 | name = name.trim(); 22 | email = email.trim(); 23 | if (!name || !email) { 24 | return { guestBookError: "Name and email are required" }; 25 | } 26 | 27 | try { 28 | await context.db.insert(schema.guestBook).values({ name, email }); 29 | } catch (error) { 30 | return { guestBookError: "Error adding to guest book" }; 31 | } 32 | } 33 | 34 | export async function loader({ context }: Route.LoaderArgs) { 35 | const guestBook = await context.db.query.guestBook.findMany({ 36 | columns: { 37 | id: true, 38 | name: true, 39 | }, 40 | }); 41 | 42 | return { 43 | guestBook, 44 | message: context.cloudflare.env.VALUE_FROM_CLOUDFLARE, 45 | }; 46 | } 47 | 48 | export default function Home({ actionData, loaderData }: Route.ComponentProps) { 49 | return ( 50 | 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /cloudflare-d1/database/schema.ts: -------------------------------------------------------------------------------- 1 | import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; 2 | 3 | export const guestBook = sqliteTable("guestBook", { 4 | id: integer().primaryKey({ autoIncrement: true }), 5 | name: text().notNull(), 6 | email: text().notNull().unique(), 7 | }); 8 | -------------------------------------------------------------------------------- /cloudflare-d1/drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "drizzle-kit"; 2 | 3 | export default { 4 | out: "./drizzle", 5 | schema: "./database/schema.ts", 6 | dialect: "sqlite", 7 | driver: "d1-http", 8 | dbCredentials: { 9 | databaseId: "your-database-id", 10 | accountId: process.env.CLOUDFLARE_ACCOUNT_ID!, 11 | token: process.env.CLOUDFLARE_TOKEN!, 12 | }, 13 | } satisfies Config; 14 | -------------------------------------------------------------------------------- /cloudflare-d1/drizzle/0000_outstanding_trauma.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `guestBook` ( 2 | `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, 3 | `name` text NOT NULL, 4 | `email` text NOT NULL 5 | ); 6 | --> statement-breakpoint 7 | CREATE UNIQUE INDEX `guestBook_email_unique` ON `guestBook` (`email`); -------------------------------------------------------------------------------- /cloudflare-d1/drizzle/meta/0000_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "6", 3 | "dialect": "sqlite", 4 | "id": "008ee181-36ed-4cc3-b593-481f13e2254a", 5 | "prevId": "00000000-0000-0000-0000-000000000000", 6 | "tables": { 7 | "guestBook": { 8 | "name": "guestBook", 9 | "columns": { 10 | "id": { 11 | "name": "id", 12 | "type": "integer", 13 | "primaryKey": true, 14 | "notNull": true, 15 | "autoincrement": true 16 | }, 17 | "name": { 18 | "name": "name", 19 | "type": "text", 20 | "primaryKey": false, 21 | "notNull": true, 22 | "autoincrement": false 23 | }, 24 | "email": { 25 | "name": "email", 26 | "type": "text", 27 | "primaryKey": false, 28 | "notNull": true, 29 | "autoincrement": false 30 | } 31 | }, 32 | "indexes": { 33 | "guestBook_email_unique": { 34 | "name": "guestBook_email_unique", 35 | "columns": [ 36 | "email" 37 | ], 38 | "isUnique": true 39 | } 40 | }, 41 | "foreignKeys": {}, 42 | "compositePrimaryKeys": {}, 43 | "uniqueConstraints": {}, 44 | "checkConstraints": {} 45 | } 46 | }, 47 | "views": {}, 48 | "enums": {}, 49 | "_meta": { 50 | "schemas": {}, 51 | "tables": {}, 52 | "columns": {} 53 | }, 54 | "internal": { 55 | "indexes": {} 56 | } 57 | } -------------------------------------------------------------------------------- /cloudflare-d1/drizzle/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "sqlite", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "6", 8 | "when": 1732083994982, 9 | "tag": "0000_outstanding_trauma", 10 | "breakpoints": true 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /cloudflare-d1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "dev": "react-router dev", 6 | "build": "react-router build", 7 | "preview": "npm run build && vite preview", 8 | "deploy": "npm run build && wrangler deploy", 9 | "db:generate": "dotenv -- drizzle-kit generate", 10 | "db:migrate": "wrangler d1 migrations apply --local DB", 11 | "db:migrate-production": "dotenv -- drizzle-kit migrate", 12 | "cf-typegen": "wrangler types", 13 | "typecheck": "npm run cf-typegen && react-router typegen && tsc -b", 14 | "postinstall": "npm run cf-typegen" 15 | }, 16 | "dependencies": { 17 | "drizzle-orm": "~0.36.3", 18 | "isbot": "^5.1.27", 19 | "react": "^19.1.0", 20 | "react-dom": "^19.1.0", 21 | "react-router": "^7.7.0" 22 | }, 23 | "devDependencies": { 24 | "@cloudflare/vite-plugin": "^1.0.12", 25 | "@react-router/dev": "^7.7.0", 26 | "@tailwindcss/vite": "^4.1.4", 27 | "@types/node": "^20", 28 | "@types/react": "^19.1.2", 29 | "@types/react-dom": "^19.1.2", 30 | "dotenv-cli": "^7.4.3", 31 | "drizzle-kit": "~0.28.1", 32 | "tailwindcss": "^4.1.4", 33 | "typescript": "^5.8.3", 34 | "vite": "^6.3.3", 35 | "vite-tsconfig-paths": "^5.1.4", 36 | "wrangler": "^4.13.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /cloudflare-d1/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remix-run/react-router-templates/56789beb24c7fd0da5c5681e7bfcbaa4ef03f758/cloudflare-d1/public/favicon.ico -------------------------------------------------------------------------------- /cloudflare-d1/react-router.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "@react-router/dev/config"; 2 | 3 | export default { 4 | ssr: true, 5 | future: { 6 | unstable_viteEnvironmentApi: true, 7 | }, 8 | } satisfies Config; 9 | -------------------------------------------------------------------------------- /cloudflare-d1/tsconfig.cloudflare.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": [ 4 | ".react-router/types/**/*", 5 | "app/**/*", 6 | "app/**/.server/**/*", 7 | "app/**/.client/**/*", 8 | "database/**/*", 9 | "workers/**/*", 10 | "worker-configuration.d.ts" 11 | ], 12 | "compilerOptions": { 13 | "composite": true, 14 | "strict": true, 15 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 16 | "types": [ "node", "vite/client"], 17 | "target": "ES2022", 18 | "module": "ES2022", 19 | "moduleResolution": "bundler", 20 | "jsx": "react-jsx", 21 | "baseUrl": ".", 22 | "rootDirs": [".", "./.react-router/types"], 23 | "paths": { 24 | "~/database/*": ["./database/*"], 25 | "~/*": ["./app/*"] 26 | }, 27 | "esModuleInterop": true, 28 | "resolveJsonModule": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /cloudflare-d1/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.node.json" }, 5 | { "path": "./tsconfig.cloudflare.json" } 6 | ], 7 | "compilerOptions": { 8 | "checkJs": true, 9 | "verbatimModuleSyntax": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "noEmit": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /cloudflare-d1/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["vite.config.ts"], 4 | "compilerOptions": { 5 | "composite": true, 6 | "strict": true, 7 | "types": ["node"], 8 | "lib": ["ES2022"], 9 | "target": "ES2022", 10 | "module": "ES2022", 11 | "moduleResolution": "bundler" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /cloudflare-d1/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { reactRouter } from "@react-router/dev/vite"; 2 | import { cloudflare } from "@cloudflare/vite-plugin"; 3 | import tailwindcss from "@tailwindcss/vite"; 4 | import { defineConfig } from "vite"; 5 | import tsconfigPaths from "vite-tsconfig-paths"; 6 | 7 | export default defineConfig({ 8 | plugins: [ 9 | cloudflare({ viteEnvironment: { name: "ssr" } }), 10 | tailwindcss(), 11 | reactRouter(), 12 | tsconfigPaths(), 13 | ], 14 | }); 15 | -------------------------------------------------------------------------------- /cloudflare-d1/workers/app.ts: -------------------------------------------------------------------------------- 1 | import { drizzle, type DrizzleD1Database } from "drizzle-orm/d1"; 2 | import { createRequestHandler } from "react-router"; 3 | import * as schema from "../database/schema"; 4 | 5 | declare module "react-router" { 6 | export interface AppLoadContext { 7 | cloudflare: { 8 | env: Env; 9 | ctx: ExecutionContext; 10 | }; 11 | db: DrizzleD1Database; 12 | } 13 | } 14 | 15 | const requestHandler = createRequestHandler( 16 | () => import("virtual:react-router/server-build"), 17 | import.meta.env.MODE 18 | ); 19 | 20 | export default { 21 | async fetch(request, env, ctx) { 22 | const db = drizzle(env.DB, { schema }); 23 | 24 | return requestHandler(request, { 25 | cloudflare: { env, ctx }, 26 | db, 27 | }); 28 | }, 29 | } satisfies ExportedHandler; 30 | -------------------------------------------------------------------------------- /cloudflare-d1/wrangler.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/wrangler/config-schema.json", 3 | "name": "react-router-app", 4 | "compatibility_date": "2025-04-04", 5 | "main": "./workers/app.ts", 6 | "vars": { 7 | "VALUE_FROM_CLOUDFLARE": "Hello from Cloudflare" 8 | }, 9 | "d1_databases": [ 10 | { 11 | "binding": "DB", 12 | "database_name": "your-database-name", 13 | "database_id": "your-database-id", 14 | "migrations_dir": "drizzle" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /cloudflare/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules/ 3 | *.tsbuildinfo 4 | 5 | # React Router 6 | /.react-router/ 7 | /build/ 8 | 9 | # Cloudflare 10 | .mf 11 | .wrangler 12 | .dev.vars* 13 | 14 | -------------------------------------------------------------------------------- /cloudflare/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to React Router! 2 | 3 | A modern, production-ready template for building full-stack React applications using React Router. 4 | 5 | ## Features 6 | 7 | - 🚀 Server-side rendering 8 | - ⚡️ Hot Module Replacement (HMR) 9 | - 📦 Asset bundling and optimization 10 | - 🔄 Data loading and mutations 11 | - 🔒 TypeScript by default 12 | - 🎉 TailwindCSS for styling 13 | - 📖 [React Router docs](https://reactrouter.com/) 14 | 15 | ## Getting Started 16 | 17 | ### Installation 18 | 19 | Install the dependencies: 20 | 21 | ```bash 22 | npm install 23 | ``` 24 | 25 | ### Development 26 | 27 | Start the development server with HMR: 28 | 29 | ```bash 30 | npm run dev 31 | ``` 32 | 33 | Your application will be available at `http://localhost:5173`. 34 | 35 | ## Previewing the Production Build 36 | 37 | Preview the production build locally: 38 | 39 | ```bash 40 | npm run preview 41 | ``` 42 | 43 | ## Building for Production 44 | 45 | Create a production build: 46 | 47 | ```bash 48 | npm run build 49 | ``` 50 | 51 | ## Deployment 52 | 53 | Deployment is done using the Wrangler CLI. 54 | 55 | To build and deploy directly to production: 56 | 57 | ```sh 58 | npm run deploy 59 | ``` 60 | 61 | To deploy a preview URL: 62 | 63 | ```sh 64 | npx wrangler versions upload 65 | ``` 66 | 67 | You can then promote a version to production after verification or roll it out progressively. 68 | 69 | ```sh 70 | npx wrangler versions deploy 71 | ``` 72 | 73 | ## Styling 74 | 75 | This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer. 76 | 77 | --- 78 | 79 | Built with ❤️ using React Router. 80 | -------------------------------------------------------------------------------- /cloudflare/app/app.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss" source("."); 2 | 3 | @theme { 4 | --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif, 5 | "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 6 | } 7 | 8 | html, 9 | body { 10 | @apply bg-white dark:bg-gray-950; 11 | 12 | @media (prefers-color-scheme: dark) { 13 | color-scheme: dark; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /cloudflare/app/entry.server.tsx: -------------------------------------------------------------------------------- 1 | import type { AppLoadContext, EntryContext } from "react-router"; 2 | import { ServerRouter } from "react-router"; 3 | import { isbot } from "isbot"; 4 | import { renderToReadableStream } from "react-dom/server"; 5 | 6 | export default async function handleRequest( 7 | request: Request, 8 | responseStatusCode: number, 9 | responseHeaders: Headers, 10 | routerContext: EntryContext, 11 | _loadContext: AppLoadContext 12 | ) { 13 | let shellRendered = false; 14 | const userAgent = request.headers.get("user-agent"); 15 | 16 | const body = await renderToReadableStream( 17 | , 18 | { 19 | onError(error: unknown) { 20 | responseStatusCode = 500; 21 | // Log streaming rendering errors from inside the shell. Don't log 22 | // errors encountered during initial shell rendering since they'll 23 | // reject and get logged in handleDocumentRequest. 24 | if (shellRendered) { 25 | console.error(error); 26 | } 27 | }, 28 | } 29 | ); 30 | shellRendered = true; 31 | 32 | // Ensure requests from bots and SPA Mode renders wait for all content to load before responding 33 | // https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation 34 | if ((userAgent && isbot(userAgent)) || routerContext.isSpaMode) { 35 | await body.allReady; 36 | } 37 | 38 | responseHeaders.set("Content-Type", "text/html"); 39 | return new Response(body, { 40 | headers: responseHeaders, 41 | status: responseStatusCode, 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /cloudflare/app/root.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | isRouteErrorResponse, 3 | Links, 4 | Meta, 5 | Outlet, 6 | Scripts, 7 | ScrollRestoration, 8 | } from "react-router"; 9 | 10 | import type { Route } from "./+types/root"; 11 | import "./app.css"; 12 | 13 | export const links: Route.LinksFunction = () => [ 14 | { rel: "preconnect", href: "https://fonts.googleapis.com" }, 15 | { 16 | rel: "preconnect", 17 | href: "https://fonts.gstatic.com", 18 | crossOrigin: "anonymous", 19 | }, 20 | { 21 | rel: "stylesheet", 22 | href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", 23 | }, 24 | ]; 25 | 26 | export function Layout({ children }: { children: React.ReactNode }) { 27 | return ( 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {children} 37 | 38 | 39 | 40 | 41 | ); 42 | } 43 | 44 | export default function App() { 45 | return ; 46 | } 47 | 48 | export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { 49 | let message = "Oops!"; 50 | let details = "An unexpected error occurred."; 51 | let stack: string | undefined; 52 | 53 | if (isRouteErrorResponse(error)) { 54 | message = error.status === 404 ? "404" : "Error"; 55 | details = 56 | error.status === 404 57 | ? "The requested page could not be found." 58 | : error.statusText || details; 59 | } else if (import.meta.env.DEV && error && error instanceof Error) { 60 | details = error.message; 61 | stack = error.stack; 62 | } 63 | 64 | return ( 65 |
66 |

{message}

67 |

{details}

68 | {stack && ( 69 |
70 |           {stack}
71 |         
72 | )} 73 |
74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /cloudflare/app/routes.ts: -------------------------------------------------------------------------------- 1 | import { type RouteConfig, index } from "@react-router/dev/routes"; 2 | 3 | export default [index("routes/home.tsx")] satisfies RouteConfig; 4 | -------------------------------------------------------------------------------- /cloudflare/app/routes/home.tsx: -------------------------------------------------------------------------------- 1 | import type { Route } from "./+types/home"; 2 | import { Welcome } from "../welcome/welcome"; 3 | 4 | export function meta({}: Route.MetaArgs) { 5 | return [ 6 | { title: "New React Router App" }, 7 | { name: "description", content: "Welcome to React Router!" }, 8 | ]; 9 | } 10 | 11 | export function loader({ context }: Route.LoaderArgs) { 12 | return { message: context.cloudflare.env.VALUE_FROM_CLOUDFLARE }; 13 | } 14 | 15 | export default function Home({ loaderData }: Route.ComponentProps) { 16 | return ; 17 | } 18 | -------------------------------------------------------------------------------- /cloudflare/app/welcome/welcome.tsx: -------------------------------------------------------------------------------- 1 | import logoDark from "./logo-dark.svg"; 2 | import logoLight from "./logo-light.svg"; 3 | 4 | export function Welcome({ message }: { message: string }) { 5 | return ( 6 |
7 |
8 |
9 |
10 | React Router 15 | React Router 20 |
21 |
22 |
23 | 44 |
45 |
46 |
47 | ); 48 | } 49 | 50 | const resources = [ 51 | { 52 | href: "https://reactrouter.com/docs", 53 | text: "React Router Docs", 54 | icon: ( 55 | 63 | 68 | 69 | ), 70 | }, 71 | { 72 | href: "https://rmx.as/discord", 73 | text: "Join Discord", 74 | icon: ( 75 | 83 | 87 | 88 | ), 89 | }, 90 | ]; 91 | -------------------------------------------------------------------------------- /cloudflare/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "dev": "react-router dev", 6 | "build": "react-router build", 7 | "preview": "npm run build && vite preview", 8 | "deploy": "npm run build && wrangler deploy", 9 | "cf-typegen": "wrangler types", 10 | "typecheck": "npm run cf-typegen && react-router typegen && tsc -b", 11 | "postinstall": "npm run cf-typegen" 12 | }, 13 | "dependencies": { 14 | "isbot": "^5.1.27", 15 | "react": "^19.1.0", 16 | "react-dom": "^19.1.0", 17 | "react-router": "^7.7.0" 18 | }, 19 | "devDependencies": { 20 | "@cloudflare/vite-plugin": "^1.0.12", 21 | "@react-router/dev": "^7.7.0", 22 | "@tailwindcss/vite": "^4.1.4", 23 | "@types/node": "^20", 24 | "@types/react": "^19.1.2", 25 | "@types/react-dom": "^19.1.2", 26 | "tailwindcss": "^4.1.4", 27 | "typescript": "^5.8.3", 28 | "vite": "^6.3.3", 29 | "vite-tsconfig-paths": "^5.1.4", 30 | "wrangler": "^4.13.2" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /cloudflare/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remix-run/react-router-templates/56789beb24c7fd0da5c5681e7bfcbaa4ef03f758/cloudflare/public/favicon.ico -------------------------------------------------------------------------------- /cloudflare/react-router.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "@react-router/dev/config"; 2 | 3 | export default { 4 | ssr: true, 5 | future: { 6 | unstable_viteEnvironmentApi: true, 7 | }, 8 | } satisfies Config; 9 | -------------------------------------------------------------------------------- /cloudflare/tsconfig.cloudflare.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": [ 4 | ".react-router/types/**/*", 5 | "app/**/*", 6 | "app/**/.server/**/*", 7 | "app/**/.client/**/*", 8 | "workers/**/*", 9 | "worker-configuration.d.ts" 10 | ], 11 | "compilerOptions": { 12 | "composite": true, 13 | "strict": true, 14 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 15 | "types": ["vite/client"], 16 | "target": "ES2022", 17 | "module": "ES2022", 18 | "moduleResolution": "bundler", 19 | "jsx": "react-jsx", 20 | "baseUrl": ".", 21 | "rootDirs": [".", "./.react-router/types"], 22 | "paths": { 23 | "~/*": ["./app/*"] 24 | }, 25 | "esModuleInterop": true, 26 | "resolveJsonModule": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /cloudflare/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.node.json" }, 5 | { "path": "./tsconfig.cloudflare.json" } 6 | ], 7 | "compilerOptions": { 8 | "checkJs": true, 9 | "verbatimModuleSyntax": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "noEmit": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /cloudflare/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["vite.config.ts"], 4 | "compilerOptions": { 5 | "composite": true, 6 | "strict": true, 7 | "types": ["node"], 8 | "lib": ["ES2022"], 9 | "target": "ES2022", 10 | "module": "ES2022", 11 | "moduleResolution": "bundler" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /cloudflare/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { reactRouter } from "@react-router/dev/vite"; 2 | import { cloudflare } from "@cloudflare/vite-plugin"; 3 | import tailwindcss from "@tailwindcss/vite"; 4 | import { defineConfig } from "vite"; 5 | import tsconfigPaths from "vite-tsconfig-paths"; 6 | 7 | export default defineConfig({ 8 | plugins: [ 9 | cloudflare({ viteEnvironment: { name: "ssr" } }), 10 | tailwindcss(), 11 | reactRouter(), 12 | tsconfigPaths(), 13 | ], 14 | }); 15 | -------------------------------------------------------------------------------- /cloudflare/workers/app.ts: -------------------------------------------------------------------------------- 1 | import { createRequestHandler } from "react-router"; 2 | 3 | declare module "react-router" { 4 | export interface AppLoadContext { 5 | cloudflare: { 6 | env: Env; 7 | ctx: ExecutionContext; 8 | }; 9 | } 10 | } 11 | 12 | const requestHandler = createRequestHandler( 13 | () => import("virtual:react-router/server-build"), 14 | import.meta.env.MODE 15 | ); 16 | 17 | export default { 18 | async fetch(request, env, ctx) { 19 | return requestHandler(request, { 20 | cloudflare: { env, ctx }, 21 | }); 22 | }, 23 | } satisfies ExportedHandler; 24 | -------------------------------------------------------------------------------- /cloudflare/wrangler.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/wrangler/config-schema.json", 3 | "name": "react-router-app", 4 | "compatibility_date": "2025-04-04", 5 | "main": "./workers/app.ts", 6 | "vars": { 7 | "VALUE_FROM_CLOUDFLARE": "Hello from Cloudflare" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /default/.dockerignore: -------------------------------------------------------------------------------- 1 | .react-router 2 | build 3 | node_modules 4 | README.md -------------------------------------------------------------------------------- /default/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules/ 3 | 4 | # React Router 5 | /.react-router/ 6 | /build/ 7 | -------------------------------------------------------------------------------- /default/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine AS development-dependencies-env 2 | COPY . /app 3 | WORKDIR /app 4 | RUN npm ci 5 | 6 | FROM node:20-alpine AS production-dependencies-env 7 | COPY ./package.json package-lock.json /app/ 8 | WORKDIR /app 9 | RUN npm ci --omit=dev 10 | 11 | FROM node:20-alpine AS build-env 12 | COPY . /app/ 13 | COPY --from=development-dependencies-env /app/node_modules /app/node_modules 14 | WORKDIR /app 15 | RUN npm run build 16 | 17 | FROM node:20-alpine 18 | COPY ./package.json package-lock.json /app/ 19 | COPY --from=production-dependencies-env /app/node_modules /app/node_modules 20 | COPY --from=build-env /app/build /app/build 21 | WORKDIR /app 22 | CMD ["npm", "run", "start"] -------------------------------------------------------------------------------- /default/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to React Router! 2 | 3 | A modern, production-ready template for building full-stack React applications using React Router. 4 | 5 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router-templates/tree/main/default) 6 | 7 | ## Features 8 | 9 | - 🚀 Server-side rendering 10 | - ⚡️ Hot Module Replacement (HMR) 11 | - 📦 Asset bundling and optimization 12 | - 🔄 Data loading and mutations 13 | - 🔒 TypeScript by default 14 | - 🎉 TailwindCSS for styling 15 | - 📖 [React Router docs](https://reactrouter.com/) 16 | 17 | ## Getting Started 18 | 19 | ### Installation 20 | 21 | Install the dependencies: 22 | 23 | ```bash 24 | npm install 25 | ``` 26 | 27 | ### Development 28 | 29 | Start the development server with HMR: 30 | 31 | ```bash 32 | npm run dev 33 | ``` 34 | 35 | Your application will be available at `http://localhost:5173`. 36 | 37 | ## Building for Production 38 | 39 | Create a production build: 40 | 41 | ```bash 42 | npm run build 43 | ``` 44 | 45 | ## Deployment 46 | 47 | ### Docker Deployment 48 | 49 | To build and run using Docker: 50 | 51 | ```bash 52 | docker build -t my-app . 53 | 54 | # Run the container 55 | docker run -p 3000:3000 my-app 56 | ``` 57 | 58 | The containerized application can be deployed to any platform that supports Docker, including: 59 | 60 | - AWS ECS 61 | - Google Cloud Run 62 | - Azure Container Apps 63 | - Digital Ocean App Platform 64 | - Fly.io 65 | - Railway 66 | 67 | ### DIY Deployment 68 | 69 | If you're familiar with deploying Node applications, the built-in app server is production-ready. 70 | 71 | Make sure to deploy the output of `npm run build` 72 | 73 | ``` 74 | ├── package.json 75 | ├── package-lock.json (or pnpm-lock.yaml, or bun.lockb) 76 | ├── build/ 77 | │ ├── client/ # Static assets 78 | │ └── server/ # Server-side code 79 | ``` 80 | 81 | ## Styling 82 | 83 | This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer. 84 | 85 | --- 86 | 87 | Built with ❤️ using React Router. 88 | -------------------------------------------------------------------------------- /default/app/app.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | @theme { 4 | --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif, 5 | "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 6 | } 7 | 8 | html, 9 | body { 10 | @apply bg-white dark:bg-gray-950; 11 | 12 | @media (prefers-color-scheme: dark) { 13 | color-scheme: dark; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /default/app/root.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | isRouteErrorResponse, 3 | Links, 4 | Meta, 5 | Outlet, 6 | Scripts, 7 | ScrollRestoration, 8 | } from "react-router"; 9 | 10 | import type { Route } from "./+types/root"; 11 | import "./app.css"; 12 | 13 | export const links: Route.LinksFunction = () => [ 14 | { rel: "preconnect", href: "https://fonts.googleapis.com" }, 15 | { 16 | rel: "preconnect", 17 | href: "https://fonts.gstatic.com", 18 | crossOrigin: "anonymous", 19 | }, 20 | { 21 | rel: "stylesheet", 22 | href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", 23 | }, 24 | ]; 25 | 26 | export function Layout({ children }: { children: React.ReactNode }) { 27 | return ( 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {children} 37 | 38 | 39 | 40 | 41 | ); 42 | } 43 | 44 | export default function App() { 45 | return ; 46 | } 47 | 48 | export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { 49 | let message = "Oops!"; 50 | let details = "An unexpected error occurred."; 51 | let stack: string | undefined; 52 | 53 | if (isRouteErrorResponse(error)) { 54 | message = error.status === 404 ? "404" : "Error"; 55 | details = 56 | error.status === 404 57 | ? "The requested page could not be found." 58 | : error.statusText || details; 59 | } else if (import.meta.env.DEV && error && error instanceof Error) { 60 | details = error.message; 61 | stack = error.stack; 62 | } 63 | 64 | return ( 65 |
66 |

{message}

67 |

{details}

68 | {stack && ( 69 |
70 |           {stack}
71 |         
72 | )} 73 |
74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /default/app/routes.ts: -------------------------------------------------------------------------------- 1 | import { type RouteConfig, index } from "@react-router/dev/routes"; 2 | 3 | export default [index("routes/home.tsx")] satisfies RouteConfig; 4 | -------------------------------------------------------------------------------- /default/app/routes/home.tsx: -------------------------------------------------------------------------------- 1 | import type { Route } from "./+types/home"; 2 | import { Welcome } from "../welcome/welcome"; 3 | 4 | export function meta({}: Route.MetaArgs) { 5 | return [ 6 | { title: "New React Router App" }, 7 | { name: "description", content: "Welcome to React Router!" }, 8 | ]; 9 | } 10 | 11 | export default function Home() { 12 | return ; 13 | } 14 | -------------------------------------------------------------------------------- /default/app/welcome/logo-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /default/app/welcome/welcome.tsx: -------------------------------------------------------------------------------- 1 | import logoDark from "./logo-dark.svg"; 2 | import logoLight from "./logo-light.svg"; 3 | 4 | export function Welcome() { 5 | return ( 6 |
7 |
8 |
9 |
10 | React Router 15 | React Router 20 |
21 |
22 |
23 | 43 |
44 |
45 |
46 | ); 47 | } 48 | 49 | const resources = [ 50 | { 51 | href: "https://reactrouter.com/docs", 52 | text: "React Router Docs", 53 | icon: ( 54 | 62 | 67 | 68 | ), 69 | }, 70 | { 71 | href: "https://rmx.as/discord", 72 | text: "Join Discord", 73 | icon: ( 74 | 82 | 86 | 87 | ), 88 | }, 89 | ]; 90 | -------------------------------------------------------------------------------- /default/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "build": "react-router build", 6 | "dev": "react-router dev", 7 | "start": "react-router-serve ./build/server/index.js", 8 | "typecheck": "react-router typegen && tsc" 9 | }, 10 | "dependencies": { 11 | "@react-router/node": "^7.7.0", 12 | "@react-router/serve": "^7.7.0", 13 | "isbot": "^5.1.27", 14 | "react": "^19.1.0", 15 | "react-dom": "^19.1.0", 16 | "react-router": "^7.7.0" 17 | }, 18 | "devDependencies": { 19 | "@react-router/dev": "^7.7.0", 20 | "@tailwindcss/vite": "^4.1.4", 21 | "@types/node": "^20", 22 | "@types/react": "^19.1.2", 23 | "@types/react-dom": "^19.1.2", 24 | "tailwindcss": "^4.1.4", 25 | "typescript": "^5.8.3", 26 | "vite": "^6.3.3", 27 | "vite-tsconfig-paths": "^5.1.4" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /default/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remix-run/react-router-templates/56789beb24c7fd0da5c5681e7bfcbaa4ef03f758/default/public/favicon.ico -------------------------------------------------------------------------------- /default/react-router.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "@react-router/dev/config"; 2 | 3 | export default { 4 | // Config options... 5 | // Server-side render by default, to enable SPA mode set this to `false` 6 | ssr: true, 7 | } satisfies Config; 8 | -------------------------------------------------------------------------------- /default/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "**/*", 4 | "**/.server/**/*", 5 | "**/.client/**/*", 6 | ".react-router/types/**/*" 7 | ], 8 | "compilerOptions": { 9 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 10 | "types": ["node", "vite/client"], 11 | "target": "ES2022", 12 | "module": "ES2022", 13 | "moduleResolution": "bundler", 14 | "jsx": "react-jsx", 15 | "rootDirs": [".", "./.react-router/types"], 16 | "baseUrl": ".", 17 | "paths": { 18 | "~/*": ["./app/*"] 19 | }, 20 | "esModuleInterop": true, 21 | "verbatimModuleSyntax": true, 22 | "noEmit": true, 23 | "resolveJsonModule": true, 24 | "skipLibCheck": true, 25 | "strict": true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /default/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { reactRouter } from "@react-router/dev/vite"; 2 | import tailwindcss from "@tailwindcss/vite"; 3 | import { defineConfig } from "vite"; 4 | import tsconfigPaths from "vite-tsconfig-paths"; 5 | 6 | export default defineConfig({ 7 | plugins: [tailwindcss(), reactRouter(), tsconfigPaths()], 8 | }); 9 | -------------------------------------------------------------------------------- /deno/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .env 3 | /node_modules/ 4 | 5 | # React Router 6 | /.react-router/ 7 | /build/ 8 | -------------------------------------------------------------------------------- /deno/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["denoland.vscode-deno"] 3 | } 4 | -------------------------------------------------------------------------------- /deno/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll": "explicit" 6 | }, 7 | 8 | "editor.defaultFormatter": "denoland.vscode-deno", 9 | "editor.formatOnSave": true, 10 | 11 | "[javascript]": { 12 | "editor.defaultFormatter": "denoland.vscode-deno" 13 | }, 14 | "[typescript]": { 15 | "editor.defaultFormatter": "denoland.vscode-deno" 16 | }, 17 | "[jsx]": { 18 | "editor.defaultFormatter": "denoland.vscode-deno" 19 | }, 20 | "[tsx]": { 21 | "editor.defaultFormatter": "denoland.vscode-deno" 22 | }, 23 | "[json]": { 24 | "editor.defaultFormatter": "denoland.vscode-deno" 25 | }, 26 | "[jsonc]": { 27 | "editor.defaultFormatter": "denoland.vscode-deno" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /deno/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to React Router! 2 | 3 | A modern, production-ready template for building full-stack React applications 4 | using React Router. 5 | 6 | ## Features 7 | 8 | - 🚀 Server-side rendering 9 | - ⚡️ Hot Module Replacement (HMR) 10 | - 📦 Asset bundling and optimization 11 | - 🔄 Data loading and mutations 12 | - 🔒 TypeScript by default 13 | - 🎉 TailwindCSS for styling 14 | - 📖 [React Router docs](https://reactrouter.com/) 15 | 16 | ## Getting Started 17 | 18 | ### Installation 19 | 20 | Install the dependencies: 21 | 22 | ```bash 23 | deno install 24 | ``` 25 | 26 | ### Development 27 | 28 | Start the development server with HMR: 29 | 30 | ```bash 31 | deno task dev 32 | ``` 33 | 34 | Your application will be available at `http://localhost:5173`. 35 | 36 | ## Building for Production 37 | 38 | Create a production build: 39 | 40 | ```bash 41 | deno task build 42 | ``` 43 | 44 | ## Deployment 45 | 46 | ### Deno Deploy 47 | 48 | After running a build, deploy to https://deno.com/deploy with the following command: 49 | 50 | ```bash 51 | deno run -A jsr:@deno/deployctl deploy --entrypoint server.ts 52 | ``` 53 | 54 | ### DIY Deployment 55 | 56 | If you're familiar with deploying Deno applications, the built-in app server is 57 | production-ready. 58 | 59 | Make sure to deploy the output of `deno task build` 60 | 61 | ``` 62 | ├── deno.jsonc 63 | ├── deno.lock 64 | ├── server.ts 65 | ├── build/ 66 | │ ├── client/ # Static assets 67 | │ └── server/ # Server-side code 68 | ``` 69 | 70 | ## Styling 71 | 72 | This template comes with [Tailwind CSS](https://tailwindcss.com/) already 73 | configured for a simple default starting experience. You can use whatever CSS 74 | framework you prefer. 75 | 76 | --- 77 | 78 | Built with ❤️ using React Router. 79 | -------------------------------------------------------------------------------- /deno/app/app.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | @theme { 4 | --font-sans: 5 | "Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", 6 | "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 7 | } 8 | 9 | html, 10 | body { 11 | @apply bg-white dark:bg-gray-950; 12 | 13 | @media (prefers-color-scheme: dark) { 14 | color-scheme: dark; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /deno/app/entry.server.tsx: -------------------------------------------------------------------------------- 1 | import type { AppLoadContext, EntryContext } from "react-router"; 2 | import { ServerRouter } from "react-router"; 3 | import { isbot } from "isbot"; 4 | import { renderToReadableStream } from "react-dom/server"; 5 | 6 | export default async function handleRequest( 7 | request: Request, 8 | responseStatusCode: number, 9 | responseHeaders: Headers, 10 | routerContext: EntryContext, 11 | _loadContext: AppLoadContext, 12 | ) { 13 | let shellRendered = false; 14 | const userAgent = request.headers.get("user-agent"); 15 | 16 | const body = await renderToReadableStream( 17 | , 18 | { 19 | onError(error: unknown) { 20 | responseStatusCode = 500; 21 | // Log streaming rendering errors from inside the shell. Don't log 22 | // errors encountered during initial shell rendering since they'll 23 | // reject and get logged in handleDocumentRequest. 24 | if (shellRendered) { 25 | console.error(error); 26 | } 27 | }, 28 | }, 29 | ); 30 | shellRendered = true; 31 | 32 | // Ensure requests from bots and SPA Mode renders wait for all content to load before responding 33 | // https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation 34 | if ((userAgent && isbot(userAgent)) || routerContext.isSpaMode) { 35 | await body.allReady; 36 | } 37 | 38 | responseHeaders.set("Content-Type", "text/html"); 39 | return new Response(body, { 40 | headers: responseHeaders, 41 | status: responseStatusCode, 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /deno/app/root.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | isRouteErrorResponse, 3 | Links, 4 | Meta, 5 | Outlet, 6 | Scripts, 7 | ScrollRestoration, 8 | } from "react-router"; 9 | 10 | import type { Route } from "./+types/root.ts"; 11 | import "./app.css"; 12 | 13 | export const links: Route.LinksFunction = () => [ 14 | { rel: "preconnect", href: "https://fonts.googleapis.com" }, 15 | { 16 | rel: "preconnect", 17 | href: "https://fonts.gstatic.com", 18 | crossOrigin: "anonymous", 19 | }, 20 | { 21 | rel: "stylesheet", 22 | href: 23 | "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", 24 | }, 25 | ]; 26 | 27 | export function Layout({ children }: { children: React.ReactNode }) { 28 | return ( 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {children} 38 | 39 | 40 | 41 | 42 | ); 43 | } 44 | 45 | export default function App() { 46 | return ; 47 | } 48 | 49 | export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { 50 | let message = "Oops!"; 51 | let details = "An unexpected error occurred."; 52 | let stack: string | undefined; 53 | 54 | if (isRouteErrorResponse(error)) { 55 | message = error.status === 404 ? "404" : "Error"; 56 | details = error.status === 404 57 | ? "The requested page could not be found." 58 | : error.statusText || details; 59 | } else if (import.meta.env.DEV && error && error instanceof Error) { 60 | details = error.message; 61 | stack = error.stack; 62 | } 63 | 64 | return ( 65 |
66 |

{message}

67 |

{details}

68 | {stack && ( 69 |
70 |           {stack}
71 |         
72 | )} 73 |
74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /deno/app/routes.ts: -------------------------------------------------------------------------------- 1 | import { index, type RouteConfig } from "@react-router/dev/routes"; 2 | 3 | export default [index("routes/home.tsx")] satisfies RouteConfig; 4 | -------------------------------------------------------------------------------- /deno/app/routes/home.tsx: -------------------------------------------------------------------------------- 1 | import type { Route } from "./+types/home.ts"; 2 | import { Welcome } from "../welcome/welcome.tsx"; 3 | 4 | export function meta({}: Route.MetaArgs) { 5 | return [ 6 | { title: "New React Router App" }, 7 | { name: "description", content: "Welcome to React Router!" }, 8 | ]; 9 | } 10 | 11 | export function loader() { 12 | return { 13 | denoVersion: Deno.version.deno, 14 | }; 15 | } 16 | 17 | export default function Home({ loaderData }: Route.ComponentProps) { 18 | return ; 19 | } 20 | -------------------------------------------------------------------------------- /deno/app/welcome/logo-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /deno/app/welcome/welcome.tsx: -------------------------------------------------------------------------------- 1 | import logoDark from "./logo-dark.svg"; 2 | import logoLight from "./logo-light.svg"; 3 | 4 | export function Welcome({ denoVersion }: { denoVersion: string }) { 5 | return ( 6 |
7 |
8 |
9 |
10 | React Router 15 | React Router 20 |
21 |
22 |
23 | 43 |
44 |
45 |
46 | ); 47 | } 48 | 49 | const resources = [ 50 | { 51 | href: "https://reactrouter.com/docs", 52 | text: "React Router Docs", 53 | icon: ( 54 | 62 | 67 | 68 | ), 69 | }, 70 | { 71 | href: "https://rmx.as/discord", 72 | text: "Join Discord", 73 | icon: ( 74 | 82 | 86 | 87 | ), 88 | }, 89 | ]; 90 | -------------------------------------------------------------------------------- /deno/deno.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/denoland/deno/refs/heads/main/cli/schemas/config-file.v1.json", 3 | "nodeModulesDir": "auto", 4 | "unstable": [ 5 | "sloppy-imports" 6 | ], 7 | "tasks": { 8 | "build": "deno run -A npm:@react-router/dev@7.6.1 build", 9 | "dev": "deno run -A npm:@react-router/dev@7.6.1 dev", 10 | "start": "deno run --allow-env --allow-net --allow-read ./server.ts", 11 | "typegen": "deno run -A npm:@react-router/dev@7.6.1 typegen", 12 | "typecheck": { 13 | "command": "deno check .", 14 | "dependencies": [ 15 | "typegen" 16 | ] 17 | } 18 | }, 19 | "imports": { 20 | "@std/http": "jsr:@std/http@^1.0.16", 21 | "~/": "./app/", 22 | "@deno/vite-plugin": "npm:@deno/vite-plugin@1.0.4", 23 | "@react-router/dev": "npm:@react-router/dev@7.6.1", 24 | "@react-router/serve": "npm:@react-router/serve@7.6.1", 25 | "@std/assert": "jsr:@std/assert@1", 26 | "@tailwindcss/vite": "npm:@tailwindcss/vite@4.1.7", 27 | "@types/react-dom": "npm:@types/react-dom@19.1.5", 28 | "@types/react": "npm:@types/react@19.1.5", 29 | "isbot": "npm:isbot@5.1.28", 30 | "react-dom": "npm:react-dom@19.1.0", 31 | "react-router": "npm:react-router@7.6.1", 32 | "react": "npm:react@19.1.0", 33 | "tailwindcss": "npm:tailwindcss@4.1.7", 34 | "vite": "npm:vite@6.3.5" 35 | }, 36 | "compilerOptions": { 37 | "verbatimModuleSyntax": true, 38 | "strict": true, 39 | "jsx": "react-jsx", 40 | "jsxImportSource": "react", 41 | "types": [ 42 | "vite/client" 43 | ], 44 | "rootDirs": [ 45 | ".", 46 | "./.react-router/types" 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /deno/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "comment": "This only exists to help vite module resolution. Do not use this file, instread use deno.json." 3 | } 4 | -------------------------------------------------------------------------------- /deno/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remix-run/react-router-templates/56789beb24c7fd0da5c5681e7bfcbaa4ef03f758/deno/public/favicon.ico -------------------------------------------------------------------------------- /deno/react-router.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "@react-router/dev/config"; 2 | 3 | export default { 4 | ssr: true, 5 | future: { 6 | unstable_viteEnvironmentApi: true, 7 | }, 8 | } satisfies Config; 9 | -------------------------------------------------------------------------------- /deno/server.ts: -------------------------------------------------------------------------------- 1 | import { serveDir, serveFile } from "@std/http/file-server"; 2 | import type { ServerBuild } from "react-router"; 3 | import { createRequestHandler } from "react-router"; 4 | 5 | const handler = createRequestHandler( 6 | () => import("./build/server/index.js") as Promise, 7 | "production", 8 | ); 9 | 10 | const PORT = parseInt(Deno.env.get("PORT") ?? "8000", 10); 11 | 12 | Deno.serve({ port: PORT }, async (request: Request): Promise => { 13 | const pathname = new URL(request.url).pathname; 14 | 15 | if (pathname === "/favicon.ico") { 16 | return serveFile(request, "build/client/favicon.ico"); 17 | } 18 | 19 | if (pathname.startsWith("/assets/")) { 20 | return serveDir(request, { 21 | fsRoot: "build/client/assets", 22 | urlRoot: "assets", 23 | headers: [ 24 | "Cache-Control: public, max-age=31536000, immutable", 25 | ], 26 | }); 27 | } 28 | 29 | return await handler(request); 30 | }); 31 | -------------------------------------------------------------------------------- /deno/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { reactRouter } from "@react-router/dev/vite"; 2 | import deno from "@deno/vite-plugin"; 3 | import tailwindcss from "@tailwindcss/vite"; 4 | import { defineConfig } from "vite"; 5 | 6 | export default defineConfig({ 7 | plugins: [reactRouter(), deno(), tailwindcss()], 8 | environments: { 9 | ssr: { 10 | build: { 11 | target: "ESNext", 12 | }, 13 | resolve: { 14 | conditions: ["deno"], 15 | externalConditions: ["deno"], 16 | }, 17 | }, 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /javascript/.dockerignore: -------------------------------------------------------------------------------- 1 | .react-router 2 | build 3 | node_modules 4 | README.md -------------------------------------------------------------------------------- /javascript/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules/ 3 | 4 | # React Router 5 | /.react-router/ 6 | /build/ 7 | -------------------------------------------------------------------------------- /javascript/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine AS development-dependencies-env 2 | COPY . /app 3 | WORKDIR /app 4 | RUN npm ci 5 | 6 | FROM node:20-alpine AS production-dependencies-env 7 | COPY ./package.json package-lock.json /app/ 8 | WORKDIR /app 9 | RUN npm ci --omit=dev 10 | 11 | FROM node:20-alpine AS build-env 12 | COPY . /app/ 13 | COPY --from=development-dependencies-env /app/node_modules /app/node_modules 14 | WORKDIR /app 15 | RUN npm run build 16 | 17 | FROM node:20-alpine 18 | COPY ./package.json package-lock.json /app/ 19 | COPY --from=production-dependencies-env /app/node_modules /app/node_modules 20 | COPY --from=build-env /app/build /app/build 21 | WORKDIR /app 22 | CMD ["npm", "run", "start"] -------------------------------------------------------------------------------- /javascript/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to React Router! 2 | 3 | A modern, production-ready template for building full-stack React applications using React Router. 4 | 5 | ## Features 6 | 7 | - 🚀 Server-side rendering 8 | - ⚡️ Hot Module Replacement (HMR) 9 | - 📦 Asset bundling and optimization 10 | - 🔄 Data loading and mutations 11 | - 🎉 TailwindCSS for styling 12 | - 📖 [React Router docs](https://reactrouter.com/) 13 | 14 | ## Getting Started 15 | 16 | ### Installation 17 | 18 | Install the dependencies: 19 | 20 | ```bash 21 | npm install 22 | ``` 23 | 24 | ### Development 25 | 26 | Start the development server with HMR: 27 | 28 | ```bash 29 | npm run dev 30 | ``` 31 | 32 | Your application will be available at `http://localhost:5173`. 33 | 34 | ## Building for Production 35 | 36 | Create a production build: 37 | 38 | ```bash 39 | npm run build 40 | ``` 41 | 42 | ## Deployment 43 | 44 | ### Docker Deployment 45 | 46 | To build and run using Docker: 47 | 48 | ```bash 49 | docker build -t my-app . 50 | 51 | # Run the container 52 | docker run -p 3000:3000 my-app 53 | ``` 54 | 55 | The containerized application can be deployed to any platform that supports Docker, including: 56 | 57 | - AWS ECS 58 | - Google Cloud Run 59 | - Azure Container Apps 60 | - Digital Ocean App Platform 61 | - Fly.io 62 | - Railway 63 | 64 | ### DIY Deployment 65 | 66 | If you're familiar with deploying Node applications, the built-in app server is production-ready. 67 | 68 | Make sure to deploy the output of `npm run build` 69 | 70 | ``` 71 | ├── package.json 72 | ├── package-lock.json (or pnpm-lock.yaml, or bun.lockb) 73 | ├── build/ 74 | │ ├── client/ # Static assets 75 | │ └── server/ # Server-side code 76 | ``` 77 | 78 | ## Styling 79 | 80 | This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer. 81 | 82 | --- 83 | 84 | Built with ❤️ using React Router. 85 | -------------------------------------------------------------------------------- /javascript/app/app.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | @theme { 4 | --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif, 5 | "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 6 | } 7 | 8 | html, 9 | body { 10 | @apply bg-white dark:bg-gray-950; 11 | 12 | @media (prefers-color-scheme: dark) { 13 | color-scheme: dark; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /javascript/app/root.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | isRouteErrorResponse, 3 | Links, 4 | Meta, 5 | Outlet, 6 | Scripts, 7 | ScrollRestoration, 8 | } from "react-router"; 9 | 10 | import "./app.css"; 11 | 12 | export const links = () => [ 13 | { rel: "preconnect", href: "https://fonts.googleapis.com" }, 14 | { 15 | rel: "preconnect", 16 | href: "https://fonts.gstatic.com", 17 | crossOrigin: "anonymous", 18 | }, 19 | { 20 | rel: "stylesheet", 21 | href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", 22 | }, 23 | ]; 24 | 25 | export function Layout({ children }) { 26 | return ( 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | {children} 36 | 37 | 38 | 39 | 40 | ); 41 | } 42 | 43 | export default function App() { 44 | return ; 45 | } 46 | 47 | export function ErrorBoundary({ error }) { 48 | let message = "Oops!"; 49 | let details = "An unexpected error occurred."; 50 | let stack; 51 | 52 | if (isRouteErrorResponse(error)) { 53 | message = error.status === 404 ? "404" : "Error"; 54 | details = 55 | error.status === 404 56 | ? "The requested page could not be found." 57 | : error.statusText || details; 58 | } else if (import.meta.env.DEV && error && error instanceof Error) { 59 | details = error.message; 60 | stack = error.stack; 61 | } 62 | 63 | return ( 64 |
65 |

{message}

66 |

{details}

67 | {stack && ( 68 |
69 |           {stack}
70 |         
71 | )} 72 |
73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /javascript/app/routes.js: -------------------------------------------------------------------------------- 1 | import { index } from "@react-router/dev/routes"; 2 | 3 | export default [index("routes/home.jsx")]; 4 | -------------------------------------------------------------------------------- /javascript/app/routes/home.jsx: -------------------------------------------------------------------------------- 1 | import { Welcome } from "../welcome/welcome"; 2 | 3 | export function meta() { 4 | return [ 5 | { title: "New React Router App" }, 6 | { name: "description", content: "Welcome to React Router!" }, 7 | ]; 8 | } 9 | 10 | export default function Home() { 11 | return ; 12 | } 13 | -------------------------------------------------------------------------------- /javascript/app/welcome/welcome.jsx: -------------------------------------------------------------------------------- 1 | import logoDark from "./logo-dark.svg"; 2 | import logoLight from "./logo-light.svg"; 3 | 4 | export function Welcome() { 5 | return ( 6 |
7 |
8 |
9 |
10 | React Router 15 | React Router 20 |
21 |
22 |
23 | 43 |
44 |
45 |
46 | ); 47 | } 48 | 49 | const resources = [ 50 | { 51 | href: "https://reactrouter.com/docs", 52 | text: "React Router Docs", 53 | icon: ( 54 | 62 | 67 | 68 | ), 69 | }, 70 | { 71 | href: "https://rmx.as/discord", 72 | text: "Join Discord", 73 | icon: ( 74 | 82 | 86 | 87 | ), 88 | }, 89 | ]; 90 | -------------------------------------------------------------------------------- /javascript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "build": "react-router build", 6 | "dev": "react-router dev", 7 | "start": "react-router-serve ./build/server/index.js" 8 | }, 9 | "dependencies": { 10 | "@react-router/node": "^7.7.0", 11 | "@react-router/serve": "^7.7.0", 12 | "isbot": "^5.1.27", 13 | "react": "^19.1.0", 14 | "react-dom": "^19.1.0", 15 | "react-router": "^7.7.0" 16 | }, 17 | "devDependencies": { 18 | "@react-router/dev": "^7.7.0", 19 | "@tailwindcss/vite": "^4.1.4", 20 | "tailwindcss": "^4.1.4", 21 | "vite": "^6.3.3" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /javascript/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remix-run/react-router-templates/56789beb24c7fd0da5c5681e7bfcbaa4ef03f758/javascript/public/favicon.ico -------------------------------------------------------------------------------- /javascript/react-router.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // Config options... 3 | // Server-side render by default, to enable SPA mode set this to `false` 4 | ssr: true, 5 | }; 6 | -------------------------------------------------------------------------------- /javascript/vite.config.js: -------------------------------------------------------------------------------- 1 | import { reactRouter } from "@react-router/dev/vite"; 2 | import tailwindcss from "@tailwindcss/vite"; 3 | import { defineConfig } from "vite"; 4 | 5 | export default defineConfig({ 6 | plugins: [reactRouter(), tailwindcss()], 7 | }); 8 | -------------------------------------------------------------------------------- /minimal/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules/ 3 | 4 | # React Router 5 | /.react-router/ 6 | /build/ 7 | -------------------------------------------------------------------------------- /minimal/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to React Router! 2 | 3 | A minimal template for experimenting with React Router v7. 4 | 5 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router-templates/tree/main/minimal) 6 | 7 | > ![NOTE] 8 | > This template should not be used for production apps and is intended more for experimentation and demo applications. Please see the [default](https://github.com/remix-run/react-router-templates/tree/main/default) template for a more full-featured template. 9 | 10 | ## Getting Started 11 | 12 | ### Installation 13 | 14 | Install the dependencies: 15 | 16 | ```bash 17 | npm install 18 | ``` 19 | 20 | ### Development 21 | 22 | Start the development server with HMR: 23 | 24 | ```bash 25 | npm run dev 26 | ``` 27 | 28 | Your application will be available at `http://localhost:5173`. 29 | 30 | --- 31 | 32 | Built with ❤️ using React Router. 33 | -------------------------------------------------------------------------------- /minimal/app/app.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | -------------------------------------------------------------------------------- /minimal/app/root.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | isRouteErrorResponse, 3 | Links, 4 | Meta, 5 | Outlet, 6 | Scripts, 7 | ScrollRestoration, 8 | } from "react-router"; 9 | 10 | import type { Route } from "./+types/root"; 11 | import "./app.css"; 12 | 13 | export function Layout({ children }: { children: React.ReactNode }) { 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {children} 24 | 25 | 26 | 27 | 28 | ); 29 | } 30 | 31 | export default function App() { 32 | return ; 33 | } 34 | 35 | export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { 36 | let message = "Oops!"; 37 | let details = "An unexpected error occurred."; 38 | let stack: string | undefined; 39 | 40 | if (isRouteErrorResponse(error)) { 41 | message = error.status === 404 ? "404" : "Error"; 42 | details = 43 | error.status === 404 44 | ? "The requested page could not be found." 45 | : error.statusText || details; 46 | } else if (import.meta.env.DEV && error && error instanceof Error) { 47 | details = error.message; 48 | stack = error.stack; 49 | } 50 | 51 | return ( 52 |
53 |

{message}

54 |

{details}

55 | {stack && ( 56 |
57 |           {stack}
58 |         
59 | )} 60 |
61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /minimal/app/routes.ts: -------------------------------------------------------------------------------- 1 | import { type RouteConfig, index } from "@react-router/dev/routes"; 2 | 3 | export default [index("routes/home.tsx")] satisfies RouteConfig; 4 | -------------------------------------------------------------------------------- /minimal/app/routes/home.tsx: -------------------------------------------------------------------------------- 1 | import type { Route } from "./+types/home"; 2 | 3 | export function loader() { 4 | return { name: "React Router" }; 5 | } 6 | 7 | export default function Home({ loaderData }: Route.ComponentProps) { 8 | return ( 9 |
10 |

Hello, {loaderData.name}

11 | 15 | React Router Docs 16 | 17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /minimal/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "build": "react-router build", 6 | "dev": "react-router dev", 7 | "start": "react-router-serve ./build/server/index.js", 8 | "typecheck": "react-router typegen && tsc" 9 | }, 10 | "dependencies": { 11 | "@react-router/node": "^7.7.0", 12 | "@react-router/serve": "^7.7.0", 13 | "isbot": "^5.1.27", 14 | "react": "^19.1.0", 15 | "react-dom": "^19.1.0", 16 | "react-router": "^7.7.0" 17 | }, 18 | "devDependencies": { 19 | "@react-router/dev": "^7.7.0", 20 | "@tailwindcss/vite": "^4.1.4", 21 | "@types/node": "^20", 22 | "@types/react": "^19.1.2", 23 | "@types/react-dom": "^19.1.2", 24 | "tailwindcss": "^4.1.4", 25 | "typescript": "^5.8.3", 26 | "vite": "^6.3.3", 27 | "vite-tsconfig-paths": "^5.1.4" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /minimal/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remix-run/react-router-templates/56789beb24c7fd0da5c5681e7bfcbaa4ef03f758/minimal/public/favicon.ico -------------------------------------------------------------------------------- /minimal/react-router.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "@react-router/dev/config"; 2 | 3 | export default { 4 | // Config options... 5 | // Server-side render by default, to enable SPA mode set this to `false` 6 | ssr: true, 7 | } satisfies Config; 8 | -------------------------------------------------------------------------------- /minimal/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "**/*", 4 | "**/.server/**/*", 5 | "**/.client/**/*", 6 | ".react-router/types/**/*" 7 | ], 8 | "compilerOptions": { 9 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 10 | "types": ["node", "vite/client"], 11 | "target": "ES2022", 12 | "module": "ES2022", 13 | "moduleResolution": "bundler", 14 | "jsx": "react-jsx", 15 | "rootDirs": [".", "./.react-router/types"], 16 | "baseUrl": ".", 17 | "paths": { 18 | "~/*": ["./app/*"] 19 | }, 20 | "esModuleInterop": true, 21 | "verbatimModuleSyntax": true, 22 | "noEmit": true, 23 | "resolveJsonModule": true, 24 | "skipLibCheck": true, 25 | "strict": true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /minimal/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { reactRouter } from "@react-router/dev/vite"; 2 | import tailwindcss from "@tailwindcss/vite"; 3 | import { defineConfig } from "vite"; 4 | import tsconfigPaths from "vite-tsconfig-paths"; 5 | 6 | export default defineConfig({ 7 | plugins: [tailwindcss(), reactRouter(), tsconfigPaths()], 8 | }); 9 | -------------------------------------------------------------------------------- /netlify/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules/ 3 | 4 | # React Router 5 | /.react-router/ 6 | /build/ 7 | 8 | # Netlify 9 | .netlify 10 | -------------------------------------------------------------------------------- /netlify/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to React Router! 2 | 3 | A modern, production-ready template for building full-stack React applications using React Router. 4 | 5 | ## Features 6 | 7 | - 🚀 Server-side rendering 8 | - ⚡️ Hot Module Replacement (HMR) 9 | - 📦 Asset bundling and optimization 10 | - 🔄 Data loading and mutations 11 | - 🔒 TypeScript by default 12 | - 🎉 TailwindCSS for styling 13 | - 📖 [React Router docs](https://reactrouter.com/) 14 | 15 | ## Getting Started 16 | 17 | ### Installation 18 | 19 | Install the dependencies: 20 | 21 | ```bash 22 | npm install 23 | ``` 24 | 25 | ### Development 26 | 27 | Start the development server with HMR: 28 | 29 | ```bash 30 | npm run dev 31 | ``` 32 | 33 | Your application will be available at `http://localhost:3000`. 34 | 35 | ## Building for Production 36 | 37 | Create a production build: 38 | 39 | ```bash 40 | npm run build 41 | ``` 42 | 43 | ## Deployment 44 | 45 | [![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/remix-run/react-router-templates&create_from_path=netlify) 46 | 47 | ## Styling 48 | 49 | This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer. 50 | 51 | --- 52 | 53 | Built with ❤️ using React Router. 54 | -------------------------------------------------------------------------------- /netlify/app/app.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | @theme { 4 | --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif, 5 | "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 6 | } 7 | 8 | html, 9 | body { 10 | @apply bg-white dark:bg-gray-950; 11 | 12 | @media (prefers-color-scheme: dark) { 13 | color-scheme: dark; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /netlify/app/root.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | isRouteErrorResponse, 3 | Links, 4 | Meta, 5 | Outlet, 6 | Scripts, 7 | ScrollRestoration, 8 | } from "react-router"; 9 | 10 | import type { Route } from "./+types/root"; 11 | import "./app.css"; 12 | 13 | export const links: Route.LinksFunction = () => [ 14 | { rel: "preconnect", href: "https://fonts.googleapis.com" }, 15 | { 16 | rel: "preconnect", 17 | href: "https://fonts.gstatic.com", 18 | crossOrigin: "anonymous", 19 | }, 20 | { 21 | rel: "stylesheet", 22 | href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", 23 | }, 24 | ]; 25 | 26 | export function Layout({ children }: { children: React.ReactNode }) { 27 | return ( 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {children} 37 | 38 | 39 | 40 | 41 | ); 42 | } 43 | 44 | export default function App() { 45 | return ; 46 | } 47 | 48 | export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { 49 | let message = "Oops!"; 50 | let details = "An unexpected error occurred."; 51 | let stack: string | undefined; 52 | 53 | if (isRouteErrorResponse(error)) { 54 | message = error.status === 404 ? "404" : "Error"; 55 | details = 56 | error.status === 404 57 | ? "The requested page could not be found." 58 | : error.statusText || details; 59 | } else if (import.meta.env.DEV && error && error instanceof Error) { 60 | details = error.message; 61 | stack = error.stack; 62 | } 63 | 64 | return ( 65 |
66 |

{message}

67 |

{details}

68 | {stack && ( 69 |
70 |           {stack}
71 |         
72 | )} 73 |
74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /netlify/app/routes.ts: -------------------------------------------------------------------------------- 1 | import { type RouteConfig, index } from "@react-router/dev/routes"; 2 | 3 | export default [index("routes/home.tsx")] satisfies RouteConfig; 4 | -------------------------------------------------------------------------------- /netlify/app/routes/home.tsx: -------------------------------------------------------------------------------- 1 | import type { Route } from "./+types/home"; 2 | import { Welcome } from "../welcome/welcome"; 3 | 4 | export function meta({}: Route.MetaArgs) { 5 | return [ 6 | { title: "New React Router App" }, 7 | { name: "description", content: "Welcome to React Router!" }, 8 | ]; 9 | } 10 | 11 | export function loader({ context }: Route.LoaderArgs) { 12 | return { message: context.VALUE_FROM_NETLIFY }; 13 | } 14 | 15 | export default function Home({ loaderData }: Route.ComponentProps) { 16 | return ; 17 | } 18 | -------------------------------------------------------------------------------- /netlify/app/welcome/welcome.tsx: -------------------------------------------------------------------------------- 1 | import logoDark from "./logo-dark.svg"; 2 | import logoLight from "./logo-light.svg"; 3 | 4 | export function Welcome({ message }: { message: string }) { 5 | return ( 6 |
7 |
8 |
9 |
10 | React Router 15 | React Router 20 |
21 |
22 |
23 | 44 |
45 |
46 |
47 | ); 48 | } 49 | 50 | const resources = [ 51 | { 52 | href: "https://reactrouter.com/docs", 53 | text: "React Router Docs", 54 | icon: ( 55 | 63 | 68 | 69 | ), 70 | }, 71 | { 72 | href: "https://rmx.as/discord", 73 | text: "Join Discord", 74 | icon: ( 75 | 83 | 87 | 88 | ), 89 | }, 90 | ]; 91 | -------------------------------------------------------------------------------- /netlify/dev-server.js: -------------------------------------------------------------------------------- 1 | import { createRequestListener } from "@mjackson/node-fetch-server"; 2 | import express from "express"; 3 | 4 | const PORT = Number.parseInt(process.env.PORT || "3000"); 5 | 6 | const app = express(); 7 | app.disable("x-powered-by"); 8 | 9 | console.log("Starting development server"); 10 | const viteDevServer = await import("vite").then((vite) => 11 | vite.createServer({ 12 | server: { middlewareMode: true }, 13 | }) 14 | ); 15 | app.use(viteDevServer.middlewares); 16 | app.use(async (req, res, next) => { 17 | try { 18 | return await createRequestListener(async (request) => { 19 | const source = await viteDevServer.ssrLoadModule("./server/app.ts"); 20 | return await source.default(request, { 21 | // TODO: Mock any required netlify functions context 22 | }); 23 | })(req, res); 24 | } catch (error) { 25 | if (typeof error === "object" && error instanceof Error) { 26 | viteDevServer.ssrFixStacktrace(error); 27 | } 28 | next(error); 29 | } 30 | }); 31 | 32 | app.listen(PORT, () => { 33 | console.log(`Server is running on http://localhost:${PORT}`); 34 | }); 35 | -------------------------------------------------------------------------------- /netlify/netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "npm run build" 3 | publish = "build/client" 4 | 5 | [dev] 6 | command = "npm run dev" 7 | framework = "vite" 8 | 9 | # Set immutable caching for static files, because they have fingerprinted filenames 10 | 11 | [[headers]] 12 | for = "/assets/*" 13 | [headers.values] 14 | "Cache-Control" = "public, max-age=31560000, immutable" 15 | -------------------------------------------------------------------------------- /netlify/netlify/prepare.js: -------------------------------------------------------------------------------- 1 | import * as fsp from "node:fs/promises"; 2 | 3 | await fsp 4 | .rm(".netlify/functions-internal", { recursive: true }) 5 | .catch(() => {}); 6 | await fsp.mkdir(".netlify/functions-internal", { recursive: true }); 7 | await fsp.cp("build/server/", ".netlify/functions-internal/handler", { 8 | recursive: true, 9 | }); 10 | 11 | // .netlify/functions-internal 12 | -------------------------------------------------------------------------------- /netlify/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "build": "react-router build && node netlify/prepare.js", 6 | "dev": "cross-env NODE_ENV=development node ./dev-server.js", 7 | "start": "netlify serve", 8 | "typecheck": "react-router typegen && tsc" 9 | }, 10 | "dependencies": { 11 | "@netlify/functions": "3.1.2", 12 | "@react-router/node": "^7.7.0", 13 | "isbot": "^5.1.27", 14 | "react": "^19.1.0", 15 | "react-dom": "^19.1.0", 16 | "react-router": "^7.7.0" 17 | }, 18 | "devDependencies": { 19 | "@mjackson/node-fetch-server": "0.6.1", 20 | "@react-router/dev": "^7.7.0", 21 | "@tailwindcss/vite": "^4.1.4", 22 | "@types/express": "^5.0.1", 23 | "@types/react": "^19.1.2", 24 | "@types/react-dom": "^19.1.2", 25 | "cross-env": "^7.0.3", 26 | "express": "^5.1.0", 27 | "netlify-cli": "^20.1.1", 28 | "tailwindcss": "^4.1.4", 29 | "typescript": "^5.8.3", 30 | "vite": "^6.3.3", 31 | "vite-tsconfig-paths": "^5.1.4" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /netlify/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remix-run/react-router-templates/56789beb24c7fd0da5c5681e7bfcbaa4ef03f758/netlify/public/favicon.ico -------------------------------------------------------------------------------- /netlify/react-router.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "@react-router/dev/config"; 2 | 3 | export default { 4 | // Config options... 5 | // Server-side render by default, to enable SPA mode set this to `false` 6 | ssr: true, 7 | } satisfies Config; 8 | -------------------------------------------------------------------------------- /netlify/server/app.ts: -------------------------------------------------------------------------------- 1 | import type { Config, Context } from "@netlify/functions"; 2 | import { createRequestHandler } from "react-router"; 3 | 4 | declare module "react-router" { 5 | interface AppLoadContext { 6 | VALUE_FROM_NETLIFY: string; 7 | } 8 | } 9 | 10 | const requestHandler = createRequestHandler( 11 | () => import("virtual:react-router/server-build"), 12 | import.meta.env.MODE, 13 | ); 14 | 15 | export default async (request: Request, context: Context) => { 16 | return requestHandler(request, { 17 | VALUE_FROM_NETLIFY: "Hello from Netlify", 18 | }); 19 | }; 20 | 21 | export const config: Config = { 22 | path: "/*", 23 | preferStatic: true, 24 | }; 25 | -------------------------------------------------------------------------------- /netlify/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "**/*", 4 | "**/.server/**/*", 5 | "**/.client/**/*", 6 | ".react-router/types/**/*" 7 | ], 8 | "compilerOptions": { 9 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 10 | "types": ["vite/client"], 11 | "target": "ES2022", 12 | "module": "ES2022", 13 | "moduleResolution": "bundler", 14 | "jsx": "react-jsx", 15 | "rootDirs": [".", "./.react-router/types"], 16 | "baseUrl": ".", 17 | "paths": { 18 | "~/*": ["./app/*"] 19 | }, 20 | "esModuleInterop": true, 21 | "verbatimModuleSyntax": true, 22 | "noEmit": true, 23 | "resolveJsonModule": true, 24 | "skipLibCheck": true, 25 | "strict": true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /netlify/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { reactRouter } from "@react-router/dev/vite"; 2 | import tailwindcss from "@tailwindcss/vite"; 3 | import { defineConfig } from "vite"; 4 | import tsconfigPaths from "vite-tsconfig-paths"; 5 | 6 | export default defineConfig(({ isSsrBuild }) => ({ 7 | build: { 8 | rollupOptions: isSsrBuild 9 | ? { 10 | input: "./server/app.ts", 11 | } 12 | : undefined, 13 | }, 14 | plugins: [tailwindcss(), reactRouter(), tsconfigPaths()], 15 | })); 16 | -------------------------------------------------------------------------------- /node-custom-server/.dockerignore: -------------------------------------------------------------------------------- 1 | .react-router 2 | build 3 | node_modules 4 | README.md -------------------------------------------------------------------------------- /node-custom-server/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules/ 3 | *.tsbuildinfo 4 | 5 | # React Router 6 | /.react-router/ 7 | /build/ 8 | -------------------------------------------------------------------------------- /node-custom-server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine AS development-dependencies-env 2 | COPY . /app 3 | WORKDIR /app 4 | RUN npm ci 5 | 6 | FROM node:20-alpine AS production-dependencies-env 7 | COPY ./package.json package-lock.json /app/ 8 | WORKDIR /app 9 | RUN npm ci --omit=dev 10 | 11 | FROM node:20-alpine AS build-env 12 | COPY . /app/ 13 | COPY --from=development-dependencies-env /app/node_modules /app/node_modules 14 | WORKDIR /app 15 | RUN npm run build 16 | 17 | FROM node:20-alpine 18 | COPY ./package.json package-lock.json server.js /app/ 19 | COPY --from=production-dependencies-env /app/node_modules /app/node_modules 20 | COPY --from=build-env /app/build /app/build 21 | WORKDIR /app 22 | CMD ["npm", "run", "start"] -------------------------------------------------------------------------------- /node-custom-server/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to React Router! 2 | 3 | A modern, production-ready template for building full-stack React applications using React Router. 4 | 5 | ## Features 6 | 7 | - 🚀 Server-side rendering 8 | - ⚡️ Hot Module Replacement (HMR) 9 | - 📦 Asset bundling and optimization 10 | - 🔄 Data loading and mutations 11 | - 🔒 TypeScript by default 12 | - 🎉 TailwindCSS for styling 13 | - 📖 [React Router docs](https://reactrouter.com/) 14 | 15 | ## Getting Started 16 | 17 | ### Installation 18 | 19 | Install the dependencies: 20 | 21 | ```bash 22 | npm install 23 | ``` 24 | 25 | ### Development 26 | 27 | Start the development server with HMR: 28 | 29 | ```bash 30 | npm run dev 31 | ``` 32 | 33 | Your application will be available at `http://localhost:5173`. 34 | 35 | ## Building for Production 36 | 37 | Create a production build: 38 | 39 | ```bash 40 | npm run build 41 | ``` 42 | 43 | ## Deployment 44 | 45 | ### Docker Deployment 46 | 47 | To build and run using Docker: 48 | 49 | ```bash 50 | docker build -t my-app . 51 | 52 | # Run the container 53 | docker run -p 3000:3000 my-app 54 | ``` 55 | 56 | The containerized application can be deployed to any platform that supports Docker, including: 57 | 58 | - AWS ECS 59 | - Google Cloud Run 60 | - Azure Container Apps 61 | - Digital Ocean App Platform 62 | - Fly.io 63 | - Railway 64 | 65 | ### DIY Deployment 66 | 67 | If you're familiar with deploying Node applications, the built-in app server is production-ready. 68 | 69 | Make sure to deploy the output of `npm run build` 70 | 71 | ``` 72 | ├── package.json 73 | ├── package-lock.json (or pnpm-lock.yaml, or bun.lockb) 74 | ├── server.js 75 | ├── build/ 76 | │ ├── client/ # Static assets 77 | │ └── server/ # Server-side code 78 | ``` 79 | 80 | ## Styling 81 | 82 | This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer. 83 | 84 | --- 85 | 86 | Built with ❤️ using React Router. 87 | -------------------------------------------------------------------------------- /node-custom-server/app/app.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | @theme { 4 | --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif, 5 | "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 6 | } 7 | 8 | html, 9 | body { 10 | @apply bg-white dark:bg-gray-950; 11 | 12 | @media (prefers-color-scheme: dark) { 13 | color-scheme: dark; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /node-custom-server/app/root.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | isRouteErrorResponse, 3 | Links, 4 | Meta, 5 | Outlet, 6 | Scripts, 7 | ScrollRestoration, 8 | } from "react-router"; 9 | 10 | import type { Route } from "./+types/root"; 11 | import "./app.css"; 12 | 13 | export const links: Route.LinksFunction = () => [ 14 | { rel: "preconnect", href: "https://fonts.googleapis.com" }, 15 | { 16 | rel: "preconnect", 17 | href: "https://fonts.gstatic.com", 18 | crossOrigin: "anonymous", 19 | }, 20 | { 21 | rel: "stylesheet", 22 | href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", 23 | }, 24 | ]; 25 | 26 | export function Layout({ children }: { children: React.ReactNode }) { 27 | return ( 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {children} 37 | 38 | 39 | 40 | 41 | ); 42 | } 43 | 44 | export default function App() { 45 | return ; 46 | } 47 | 48 | export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { 49 | let message = "Oops!"; 50 | let details = "An unexpected error occurred."; 51 | let stack: string | undefined; 52 | 53 | if (isRouteErrorResponse(error)) { 54 | message = error.status === 404 ? "404" : "Error"; 55 | details = 56 | error.status === 404 57 | ? "The requested page could not be found." 58 | : error.statusText || details; 59 | } else if (import.meta.env.DEV && error && error instanceof Error) { 60 | details = error.message; 61 | stack = error.stack; 62 | } 63 | 64 | return ( 65 |
66 |

{message}

67 |

{details}

68 | {stack && ( 69 |
70 |           {stack}
71 |         
72 | )} 73 |
74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /node-custom-server/app/routes.ts: -------------------------------------------------------------------------------- 1 | import { type RouteConfig, index } from "@react-router/dev/routes"; 2 | 3 | export default [index("routes/home.tsx")] satisfies RouteConfig; 4 | -------------------------------------------------------------------------------- /node-custom-server/app/routes/home.tsx: -------------------------------------------------------------------------------- 1 | import type { Route } from "./+types/home"; 2 | import { Welcome } from "../welcome/welcome"; 3 | 4 | export function meta({}: Route.MetaArgs) { 5 | return [ 6 | { title: "New React Router App" }, 7 | { name: "description", content: "Welcome to React Router!" }, 8 | ]; 9 | } 10 | 11 | export function loader({ context }: Route.LoaderArgs) { 12 | return { message: context.VALUE_FROM_EXPRESS }; 13 | } 14 | 15 | export default function Home({ loaderData }: Route.ComponentProps) { 16 | return ; 17 | } 18 | -------------------------------------------------------------------------------- /node-custom-server/app/welcome/welcome.tsx: -------------------------------------------------------------------------------- 1 | import logoDark from "./logo-dark.svg"; 2 | import logoLight from "./logo-light.svg"; 3 | 4 | export function Welcome({ message }: { message: string }) { 5 | return ( 6 |
7 |
8 |
9 |

10 | {message} 11 |

12 |
13 | React Router 18 | React Router 23 |
24 |
25 | 45 |
46 |
47 | ); 48 | } 49 | 50 | const resources = [ 51 | { 52 | href: "https://reactrouter.com/docs", 53 | text: "React Router Docs", 54 | icon: ( 55 | 63 | 68 | 69 | ), 70 | }, 71 | { 72 | href: "https://rmx.as/discord", 73 | text: "Join Discord", 74 | icon: ( 75 | 83 | 87 | 88 | ), 89 | }, 90 | ]; 91 | -------------------------------------------------------------------------------- /node-custom-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "build": "react-router build", 6 | "dev": "cross-env NODE_ENV=development node server.js", 7 | "start": "node server.js", 8 | "typecheck": "react-router typegen && tsc -b" 9 | }, 10 | "dependencies": { 11 | "@react-router/express": "^7.7.0", 12 | "@react-router/node": "^7.7.0", 13 | "compression": "^1.7.5", 14 | "express": "^5.1.0", 15 | "isbot": "^5.1.27", 16 | "morgan": "^1.10.0", 17 | "react": "^19.1.0", 18 | "react-dom": "^19.1.0", 19 | "react-router": "^7.7.0" 20 | }, 21 | "devDependencies": { 22 | "@react-router/dev": "^7.7.0", 23 | "@tailwindcss/vite": "^4.1.4", 24 | "@types/compression": "^1.7.5", 25 | "@types/express": "^5.0.1", 26 | "@types/express-serve-static-core": "^5.0.6", 27 | "@types/morgan": "^1.9.9", 28 | "@types/node": "^20", 29 | "@types/react": "^19.1.2", 30 | "@types/react-dom": "^19.1.2", 31 | "cross-env": "^7.0.3", 32 | "tailwindcss": "^4.1.4", 33 | "typescript": "^5.8.3", 34 | "vite": "^6.3.3", 35 | "vite-tsconfig-paths": "^5.1.4" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /node-custom-server/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remix-run/react-router-templates/56789beb24c7fd0da5c5681e7bfcbaa4ef03f758/node-custom-server/public/favicon.ico -------------------------------------------------------------------------------- /node-custom-server/react-router.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "@react-router/dev/config"; 2 | 3 | export default { 4 | // Config options... 5 | // Server-side render by default, to enable SPA mode set this to `false` 6 | ssr: true, 7 | } satisfies Config; 8 | -------------------------------------------------------------------------------- /node-custom-server/server.js: -------------------------------------------------------------------------------- 1 | import compression from "compression"; 2 | import express from "express"; 3 | import morgan from "morgan"; 4 | 5 | // Short-circuit the type-checking of the built output. 6 | const BUILD_PATH = "./build/server/index.js"; 7 | const DEVELOPMENT = process.env.NODE_ENV === "development"; 8 | const PORT = Number.parseInt(process.env.PORT || "3000"); 9 | 10 | const app = express(); 11 | 12 | app.use(compression()); 13 | app.disable("x-powered-by"); 14 | 15 | if (DEVELOPMENT) { 16 | console.log("Starting development server"); 17 | const viteDevServer = await import("vite").then((vite) => 18 | vite.createServer({ 19 | server: { middlewareMode: true }, 20 | }), 21 | ); 22 | app.use(viteDevServer.middlewares); 23 | app.use(async (req, res, next) => { 24 | try { 25 | const source = await viteDevServer.ssrLoadModule("./server/app.ts"); 26 | return await source.app(req, res, next); 27 | } catch (error) { 28 | if (typeof error === "object" && error instanceof Error) { 29 | viteDevServer.ssrFixStacktrace(error); 30 | } 31 | next(error); 32 | } 33 | }); 34 | } else { 35 | console.log("Starting production server"); 36 | app.use( 37 | "/assets", 38 | express.static("build/client/assets", { immutable: true, maxAge: "1y" }), 39 | ); 40 | app.use(morgan("tiny")); 41 | app.use(express.static("build/client", { maxAge: "1h" })); 42 | app.use(await import(BUILD_PATH).then((mod) => mod.app)); 43 | } 44 | 45 | app.listen(PORT, () => { 46 | console.log(`Server is running on http://localhost:${PORT}`); 47 | }); 48 | -------------------------------------------------------------------------------- /node-custom-server/server/app.ts: -------------------------------------------------------------------------------- 1 | import "react-router"; 2 | import { createRequestHandler } from "@react-router/express"; 3 | import express from "express"; 4 | 5 | declare module "react-router" { 6 | interface AppLoadContext { 7 | VALUE_FROM_EXPRESS: string; 8 | } 9 | } 10 | 11 | export const app = express(); 12 | 13 | app.use( 14 | createRequestHandler({ 15 | build: () => import("virtual:react-router/server-build"), 16 | getLoadContext() { 17 | return { 18 | VALUE_FROM_EXPRESS: "Hello from Express", 19 | }; 20 | }, 21 | }), 22 | ); 23 | -------------------------------------------------------------------------------- /node-custom-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.node.json" }, 5 | { "path": "./tsconfig.vite.json" } 6 | ], 7 | "compilerOptions": { 8 | "checkJs": true, 9 | "verbatimModuleSyntax": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "noEmit": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /node-custom-server/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["server.js", "vite.config.ts"], 4 | "compilerOptions": { 5 | "composite": true, 6 | "strict": true, 7 | "types": ["node"], 8 | "lib": ["ES2022"], 9 | "target": "ES2022", 10 | "module": "ES2022", 11 | "moduleResolution": "bundler" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /node-custom-server/tsconfig.vite.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": [ 4 | ".react-router/types/**/*", 5 | "app/**/*", 6 | "app/**/.server/**/*", 7 | "app/**/.client/**/*", 8 | "server/**/*" 9 | ], 10 | "compilerOptions": { 11 | "composite": true, 12 | "strict": true, 13 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 14 | "types": ["vite/client"], 15 | "target": "ES2022", 16 | "module": "ES2022", 17 | "moduleResolution": "bundler", 18 | "jsx": "react-jsx", 19 | "baseUrl": ".", 20 | "rootDirs": [".", "./.react-router/types"], 21 | "paths": { 22 | "~/*": ["./app/*"] 23 | }, 24 | "esModuleInterop": true, 25 | "resolveJsonModule": true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /node-custom-server/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { reactRouter } from "@react-router/dev/vite"; 2 | import tailwindcss from "@tailwindcss/vite"; 3 | import { defineConfig } from "vite"; 4 | import tsconfigPaths from "vite-tsconfig-paths"; 5 | 6 | export default defineConfig(({ isSsrBuild }) => ({ 7 | build: { 8 | rollupOptions: isSsrBuild 9 | ? { 10 | input: "./server/app.ts", 11 | } 12 | : undefined, 13 | }, 14 | plugins: [tailwindcss(), reactRouter(), tsconfigPaths()], 15 | })); 16 | -------------------------------------------------------------------------------- /node-postgres/.dockerignore: -------------------------------------------------------------------------------- 1 | .react-router 2 | build 3 | node_modules 4 | README.md -------------------------------------------------------------------------------- /node-postgres/.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV="development" 2 | DATABASE_URL="" 3 | -------------------------------------------------------------------------------- /node-postgres/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .env 3 | /node_modules/ 4 | *.tsbuildinfo 5 | 6 | # React Router 7 | /.react-router/ 8 | /build/ 9 | -------------------------------------------------------------------------------- /node-postgres/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine AS development-dependencies-env 2 | COPY . /app 3 | WORKDIR /app 4 | RUN npm ci 5 | 6 | FROM node:20-alpine AS production-dependencies-env 7 | COPY ./package.json package-lock.json /app/ 8 | WORKDIR /app 9 | RUN npm ci --omit=dev 10 | 11 | FROM node:20-alpine AS build-env 12 | COPY . /app/ 13 | COPY --from=development-dependencies-env /app/node_modules /app/node_modules 14 | WORKDIR /app 15 | RUN npm run build 16 | 17 | FROM node:20-alpine 18 | COPY ./package.json package-lock.json server.js /app/ 19 | COPY --from=production-dependencies-env /app/node_modules /app/node_modules 20 | COPY --from=build-env /app/build /app/build 21 | WORKDIR /app 22 | CMD ["npm", "run", "start"] -------------------------------------------------------------------------------- /node-postgres/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to React Router! 2 | 3 | A modern, production-ready template for building full-stack React applications using React Router. 4 | 5 | ## Features 6 | 7 | - 🚀 Server-side rendering 8 | - ⚡️ Hot Module Replacement (HMR) 9 | - 📦 Asset bundling and optimization 10 | - 🔄 Data loading and mutations 11 | - 🔒 TypeScript by default 12 | - 🎉 TailwindCSS for styling 13 | - 💾 PostgreSQL + DrizzleORM 14 | - 📖 [React Router docs](https://reactrouter.com/) 15 | 16 | ## Getting Started 17 | 18 | ### Installation 19 | 20 | Install the dependencies: 21 | 22 | ```bash 23 | npm install 24 | ``` 25 | 26 | ### Development 27 | 28 | Copy `.env.example` to `.env` and provide a `DATABASE_URL` with your connection string. 29 | 30 | Run an initial database migration: 31 | 32 | ```bash 33 | npm run db:migrate 34 | ``` 35 | 36 | Start the development server with HMR: 37 | 38 | ```bash 39 | npm run dev 40 | ``` 41 | 42 | Your application will be available at `http://localhost:5173`. 43 | 44 | ## Building for Production 45 | 46 | Create a production build: 47 | 48 | ```bash 49 | npm run build 50 | ``` 51 | 52 | ## Deployment 53 | 54 | ### Docker Deployment 55 | 56 | To build and run using Docker: 57 | 58 | ```bash 59 | # For npm 60 | docker build -t my-app . 61 | 62 | # Run the container 63 | docker run -p 3000:3000 my-app 64 | ``` 65 | 66 | The containerized application can be deployed to any platform that supports Docker, including: 67 | 68 | - AWS ECS 69 | - Google Cloud Run 70 | - Azure Container Apps 71 | - Digital Ocean App Platform 72 | - Fly.io 73 | - Railway 74 | 75 | ### DIY Deployment 76 | 77 | If you're familiar with deploying Node applications, the built-in app server is production-ready. 78 | 79 | Make sure to deploy the output of `npm run build` 80 | 81 | ``` 82 | ├── package.json 83 | ├── package-lock.json (or pnpm-lock.yaml, or bun.lockb) 84 | ├── server.js 85 | ├── build/ 86 | │ ├── client/ # Static assets 87 | │ └── server/ # Server-side code 88 | ``` 89 | 90 | ## Styling 91 | 92 | This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer. 93 | 94 | --- 95 | 96 | Built with ❤️ using React Router. 97 | -------------------------------------------------------------------------------- /node-postgres/app/app.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | @theme { 4 | --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif, 5 | "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 6 | } 7 | 8 | html, 9 | body { 10 | @apply bg-white dark:bg-gray-950; 11 | 12 | @media (prefers-color-scheme: dark) { 13 | color-scheme: dark; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /node-postgres/app/root.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | isRouteErrorResponse, 3 | Links, 4 | Meta, 5 | Outlet, 6 | Scripts, 7 | ScrollRestoration, 8 | } from "react-router"; 9 | 10 | import type { Route } from "./+types/root"; 11 | import "./app.css"; 12 | 13 | export const links: Route.LinksFunction = () => [ 14 | { rel: "preconnect", href: "https://fonts.googleapis.com" }, 15 | { 16 | rel: "preconnect", 17 | href: "https://fonts.gstatic.com", 18 | crossOrigin: "anonymous", 19 | }, 20 | { 21 | rel: "stylesheet", 22 | href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", 23 | }, 24 | ]; 25 | 26 | export function Layout({ children }: { children: React.ReactNode }) { 27 | return ( 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {children} 37 | 38 | 39 | 40 | 41 | ); 42 | } 43 | 44 | export default function App() { 45 | return ; 46 | } 47 | 48 | export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { 49 | let message = "Oops!"; 50 | let details = "An unexpected error occurred."; 51 | let stack: string | undefined; 52 | 53 | if (isRouteErrorResponse(error)) { 54 | message = error.status === 404 ? "404" : "Error"; 55 | details = 56 | error.status === 404 57 | ? "The requested page could not be found." 58 | : error.statusText || details; 59 | } else if (import.meta.env.DEV && error && error instanceof Error) { 60 | details = error.message; 61 | stack = error.stack; 62 | } 63 | 64 | return ( 65 |
66 |

{message}

67 |

{details}

68 | {stack && ( 69 |
70 |           {stack}
71 |         
72 | )} 73 |
74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /node-postgres/app/routes.ts: -------------------------------------------------------------------------------- 1 | import { type RouteConfig, index } from "@react-router/dev/routes"; 2 | 3 | export default [index("routes/home.tsx")] satisfies RouteConfig; 4 | -------------------------------------------------------------------------------- /node-postgres/app/routes/home.tsx: -------------------------------------------------------------------------------- 1 | import { database } from "~/database/context"; 2 | import * as schema from "~/database/schema"; 3 | 4 | import type { Route } from "./+types/home"; 5 | import { Welcome } from "../welcome/welcome"; 6 | 7 | export function meta({}: Route.MetaArgs) { 8 | return [ 9 | { title: "New React Router App" }, 10 | { name: "description", content: "Welcome to React Router!" }, 11 | ]; 12 | } 13 | 14 | export async function action({ request }: Route.ActionArgs) { 15 | const formData = await request.formData(); 16 | let name = formData.get("name"); 17 | let email = formData.get("email"); 18 | if (typeof name !== "string" || typeof email !== "string") { 19 | return { guestBookError: "Name and email are required" }; 20 | } 21 | 22 | name = name.trim(); 23 | email = email.trim(); 24 | if (!name || !email) { 25 | return { guestBookError: "Name and email are required" }; 26 | } 27 | 28 | const db = database(); 29 | try { 30 | await db.insert(schema.guestBook).values({ name, email }); 31 | } catch (error) { 32 | return { guestBookError: "Error adding to guest book" }; 33 | } 34 | } 35 | 36 | export async function loader({ context }: Route.LoaderArgs) { 37 | const db = database(); 38 | 39 | const guestBook = await db.query.guestBook.findMany({ 40 | columns: { 41 | id: true, 42 | name: true, 43 | }, 44 | }); 45 | 46 | return { 47 | guestBook, 48 | message: context.VALUE_FROM_EXPRESS, 49 | }; 50 | } 51 | 52 | export default function Home({ actionData, loaderData }: Route.ComponentProps) { 53 | return ( 54 | 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /node-postgres/database/context.ts: -------------------------------------------------------------------------------- 1 | import { AsyncLocalStorage } from "node:async_hooks"; 2 | 3 | import type { PostgresJsDatabase } from "drizzle-orm/postgres-js"; 4 | 5 | import * as schema from "./schema"; 6 | 7 | export const DatabaseContext = new AsyncLocalStorage< 8 | PostgresJsDatabase 9 | >(); 10 | 11 | export function database() { 12 | const db = DatabaseContext.getStore(); 13 | if (!db) { 14 | throw new Error("DatabaseContext not set"); 15 | } 16 | return db; 17 | } 18 | -------------------------------------------------------------------------------- /node-postgres/database/schema.ts: -------------------------------------------------------------------------------- 1 | import { integer, pgTable, varchar } from "drizzle-orm/pg-core"; 2 | 3 | export const guestBook = pgTable("guestBook", { 4 | id: integer().primaryKey().generatedAlwaysAsIdentity(), 5 | name: varchar({ length: 255 }).notNull(), 6 | email: varchar({ length: 255 }).notNull().unique(), 7 | }); 8 | -------------------------------------------------------------------------------- /node-postgres/drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "drizzle-kit"; 2 | 3 | if (!process.env.DATABASE_URL) { 4 | throw new Error("DATABASE_URL is required"); 5 | } 6 | 7 | export default defineConfig({ 8 | out: "./drizzle", 9 | schema: "./database/schema.ts", 10 | dialect: "postgresql", 11 | dbCredentials: { 12 | url: process.env.DATABASE_URL, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /node-postgres/drizzle/0000_short_donald_blake.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS "guestBook" ( 2 | "id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "guestBook_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), 3 | "name" varchar(255) NOT NULL, 4 | "email" varchar(255) NOT NULL, 5 | CONSTRAINT "guestBook_email_unique" UNIQUE("email") 6 | ); 7 | -------------------------------------------------------------------------------- /node-postgres/drizzle/meta/0000_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "6bf145c1-851c-4a50-a085-9306f05abb25", 3 | "prevId": "00000000-0000-0000-0000-000000000000", 4 | "version": "7", 5 | "dialect": "postgresql", 6 | "tables": { 7 | "public.guestBook": { 8 | "name": "guestBook", 9 | "schema": "", 10 | "columns": { 11 | "id": { 12 | "name": "id", 13 | "type": "integer", 14 | "primaryKey": true, 15 | "notNull": true, 16 | "identity": { 17 | "type": "always", 18 | "name": "guestBook_id_seq", 19 | "schema": "public", 20 | "increment": "1", 21 | "startWith": "1", 22 | "minValue": "1", 23 | "maxValue": "2147483647", 24 | "cache": "1", 25 | "cycle": false 26 | } 27 | }, 28 | "name": { 29 | "name": "name", 30 | "type": "varchar(255)", 31 | "primaryKey": false, 32 | "notNull": true 33 | }, 34 | "email": { 35 | "name": "email", 36 | "type": "varchar(255)", 37 | "primaryKey": false, 38 | "notNull": true 39 | } 40 | }, 41 | "indexes": {}, 42 | "foreignKeys": {}, 43 | "compositePrimaryKeys": {}, 44 | "uniqueConstraints": { 45 | "guestBook_email_unique": { 46 | "name": "guestBook_email_unique", 47 | "nullsNotDistinct": false, 48 | "columns": [ 49 | "email" 50 | ] 51 | } 52 | }, 53 | "policies": {}, 54 | "checkConstraints": {}, 55 | "isRLSEnabled": false 56 | } 57 | }, 58 | "enums": {}, 59 | "schemas": {}, 60 | "sequences": {}, 61 | "roles": {}, 62 | "policies": {}, 63 | "views": {}, 64 | "_meta": { 65 | "columns": {}, 66 | "schemas": {}, 67 | "tables": {} 68 | } 69 | } -------------------------------------------------------------------------------- /node-postgres/drizzle/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "postgresql", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "7", 8 | "when": 1732076135211, 9 | "tag": "0000_short_donald_blake", 10 | "breakpoints": true 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /node-postgres/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "build": "react-router build", 6 | "db:generate": "dotenv -- drizzle-kit generate", 7 | "db:migrate": "dotenv -- drizzle-kit migrate", 8 | "dev": "dotenv -- node server.js", 9 | "start": "node server.js", 10 | "typecheck": "react-router typegen && tsc -b" 11 | }, 12 | "dependencies": { 13 | "@react-router/express": "^7.7.0", 14 | "@react-router/node": "^7.7.0", 15 | "compression": "^1.8.0", 16 | "drizzle-orm": "~0.36.3", 17 | "express": "^5.1.0", 18 | "isbot": "^5.1.27", 19 | "morgan": "^1.10.0", 20 | "postgres": "^3.4.5", 21 | "react": "^19.1.0", 22 | "react-dom": "^19.1.0", 23 | "react-router": "^7.7.0" 24 | }, 25 | "devDependencies": { 26 | "@react-router/dev": "^7.7.0", 27 | "@tailwindcss/vite": "^4.1.4", 28 | "@types/compression": "^1.7.5", 29 | "@types/express": "^5.0.1", 30 | "@types/express-serve-static-core": "^5.0.6", 31 | "@types/morgan": "^1.9.9", 32 | "@types/node": "^20", 33 | "@types/pg": "^8.11.14", 34 | "@types/react": "^19.1.2", 35 | "@types/react-dom": "^19.1.2", 36 | "dotenv-cli": "^8.0.0", 37 | "drizzle-kit": "~0.28.1", 38 | "tailwindcss": "^4.1.4", 39 | "tsx": "^4.19.2", 40 | "typescript": "^5.8.3", 41 | "vite": "^6.3.3", 42 | "vite-tsconfig-paths": "^5.1.4" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /node-postgres/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remix-run/react-router-templates/56789beb24c7fd0da5c5681e7bfcbaa4ef03f758/node-postgres/public/favicon.ico -------------------------------------------------------------------------------- /node-postgres/react-router.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "@react-router/dev/config"; 2 | 3 | export default { 4 | // Config options... 5 | // Server-side render by default, to enable SPA mode set this to `false` 6 | ssr: true, 7 | } satisfies Config; 8 | -------------------------------------------------------------------------------- /node-postgres/server.js: -------------------------------------------------------------------------------- 1 | import compression from "compression"; 2 | import express from "express"; 3 | import morgan from "morgan"; 4 | 5 | // Short-circuit the type-checking of the built output. 6 | const BUILD_PATH = "./build/server/index.js"; 7 | const DEVELOPMENT = process.env.NODE_ENV === "development"; 8 | const PORT = Number.parseInt(process.env.PORT || "3000"); 9 | 10 | const app = express(); 11 | 12 | app.use(compression()); 13 | app.disable("x-powered-by"); 14 | 15 | if (DEVELOPMENT) { 16 | console.log("Starting development server"); 17 | const viteDevServer = await import("vite").then((vite) => 18 | vite.createServer({ 19 | server: { middlewareMode: true }, 20 | }), 21 | ); 22 | app.use(viteDevServer.middlewares); 23 | app.use(async (req, res, next) => { 24 | try { 25 | const source = await viteDevServer.ssrLoadModule("./server/app.ts"); 26 | return await source.app(req, res, next); 27 | } catch (error) { 28 | if (typeof error === "object" && error instanceof Error) { 29 | viteDevServer.ssrFixStacktrace(error); 30 | } 31 | next(error); 32 | } 33 | }); 34 | } else { 35 | console.log("Starting production server"); 36 | app.use( 37 | "/assets", 38 | express.static("build/client/assets", { immutable: true, maxAge: "1y" }), 39 | ); 40 | app.use(morgan("tiny")); 41 | app.use(express.static("build/client", { maxAge: "1h" })); 42 | app.use(await import(BUILD_PATH).then((mod) => mod.app)); 43 | } 44 | 45 | app.listen(PORT, () => { 46 | console.log(`Server is running on http://localhost:${PORT}`); 47 | }); 48 | -------------------------------------------------------------------------------- /node-postgres/server/app.ts: -------------------------------------------------------------------------------- 1 | import { createRequestHandler } from "@react-router/express"; 2 | import { drizzle } from "drizzle-orm/postgres-js"; 3 | import express from "express"; 4 | import postgres from "postgres"; 5 | import "react-router"; 6 | 7 | import { DatabaseContext } from "~/database/context"; 8 | import * as schema from "~/database/schema"; 9 | 10 | declare module "react-router" { 11 | interface AppLoadContext { 12 | VALUE_FROM_EXPRESS: string; 13 | } 14 | } 15 | 16 | export const app = express(); 17 | 18 | if (!process.env.DATABASE_URL) throw new Error("DATABASE_URL is required"); 19 | 20 | const client = postgres(process.env.DATABASE_URL); 21 | const db = drizzle(client, { schema }); 22 | app.use((_, __, next) => DatabaseContext.run(db, next)); 23 | 24 | app.use( 25 | createRequestHandler({ 26 | build: () => import("virtual:react-router/server-build"), 27 | getLoadContext() { 28 | return { 29 | VALUE_FROM_EXPRESS: "Hello from Express", 30 | }; 31 | }, 32 | }), 33 | ); 34 | -------------------------------------------------------------------------------- /node-postgres/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.node.json" }, 5 | { "path": "./tsconfig.vite.json" } 6 | ], 7 | "compilerOptions": { 8 | "checkJs": true, 9 | "verbatimModuleSyntax": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "noEmit": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /node-postgres/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["server.js", "vite.config.ts"], 4 | "compilerOptions": { 5 | "composite": true, 6 | "strict": true, 7 | "types": ["node"], 8 | "lib": ["ES2022"], 9 | "target": "ES2022", 10 | "module": "ES2022", 11 | "moduleResolution": "bundler" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /node-postgres/tsconfig.vite.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": [ 4 | ".react-router/types/**/*", 5 | "app/**/*", 6 | "app/**/.server/**/*", 7 | "app/**/.client/**/*", 8 | "database/**/*", 9 | "server/**/*" 10 | ], 11 | "compilerOptions": { 12 | "composite": true, 13 | "strict": true, 14 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 15 | "types": ["vite/client"], 16 | "target": "ES2022", 17 | "module": "ES2022", 18 | "moduleResolution": "bundler", 19 | "jsx": "react-jsx", 20 | "baseUrl": ".", 21 | "rootDirs": [".", "./.react-router/types"], 22 | "paths": { 23 | "~/database/*": ["./database/*"], 24 | "~/*": ["./app/*"] 25 | }, 26 | "esModuleInterop": true, 27 | "resolveJsonModule": true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /node-postgres/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { reactRouter } from "@react-router/dev/vite"; 2 | import tailwindcss from "@tailwindcss/vite"; 3 | import { defineConfig } from "vite"; 4 | import tsconfigPaths from "vite-tsconfig-paths"; 5 | 6 | export default defineConfig(({ isSsrBuild }) => ({ 7 | build: { 8 | rollupOptions: isSsrBuild 9 | ? { 10 | input: "./server/app.ts", 11 | } 12 | : undefined, 13 | }, 14 | plugins: [tailwindcss(), reactRouter(), tsconfigPaths()], 15 | })); 16 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - ".tests" 3 | - "cloudflare" 4 | - "cloudflare-d1" 5 | - "default" 6 | - "javascript" 7 | - "minimal" 8 | - "netlify" 9 | - "node-custom-server" 10 | - "node-postgres" 11 | - "vercel" 12 | - "unstable_rsc-parcel" 13 | - "unstable_rsc-vite" 14 | -------------------------------------------------------------------------------- /unstable_rsc-parcel/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .parcel-cache 3 | node_modules 4 | dist 5 | -------------------------------------------------------------------------------- /unstable_rsc-parcel/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to React Router! (Experimental RSC) 2 | 3 | ⚠️ **EXPERIMENTAL**: This template demonstrates React Server Components with React Router. This is experimental technology and not recommended for production use. 4 | 5 | A modern template for exploring React Server Components (RSC) with React Router, powered by Parcel. 6 | 7 | ## Features 8 | 9 | - 🧪 **Experimental React Server Components** 10 | - 🚀 Server-side rendering with RSC 11 | - ⚡️ Hot Module Replacement (HMR) 12 | - 📦 Asset bundling and optimization with Parcel 13 | - 🔄 Data loading and mutations 14 | - 🔒 TypeScript by default 15 | - 🎉 TailwindCSS for styling 16 | - 📖 [React Router docs](https://reactrouter.com/) 17 | - 📚 [React Server Components guide](https://reactrouter.com/how-to/react-server-components) 18 | 19 | ## Getting Started 20 | 21 | ### Installation 22 | 23 | Install the dependencies: 24 | 25 | ```bash 26 | npm install 27 | ``` 28 | 29 | ### Development 30 | 31 | Start the development server with HMR: 32 | 33 | ```bash 34 | npm run dev 35 | ``` 36 | 37 | Your application will be available at `http://localhost:1234` (Parcel default). 38 | 39 | ## Building for Production 40 | 41 | Create a production build: 42 | 43 | ```bash 44 | npm run build 45 | ``` 46 | 47 | ## Running Production Build 48 | 49 | Run the production server: 50 | 51 | ```bash 52 | npm start 53 | ``` 54 | 55 | ## Understanding React Server Components 56 | 57 | This template includes three entry points: 58 | 59 | - **`entry.rsc.tsx`** - React Server Components entry point 60 | - **`entry.ssr.tsx`** - Server-side rendering entry point 61 | - **`entry.browser.tsx`** - Client-side hydration entry point 62 | 63 | Learn more about React Server Components with React Router in our [comprehensive guide](https://reactrouter.com/how-to/react-server-components). 64 | 65 | ## Styling 66 | 67 | This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer. 68 | 69 | --- 70 | 71 | Built with ❤️ using React Router. 72 | -------------------------------------------------------------------------------- /unstable_rsc-parcel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "targets": { 4 | "react-server": { 5 | "context": "react-server", 6 | "source": "src/entry.rsc.tsx", 7 | "scopeHoist": false, 8 | "includeNodeModules": { 9 | "@mjackson/node-fetch-server": false, 10 | "compression": false, 11 | "express": false 12 | } 13 | } 14 | }, 15 | "postcss": { 16 | "plugins": { 17 | "@tailwindcss/postcss": {} 18 | } 19 | }, 20 | "scripts": { 21 | "dev": "cross-env NODE_ENV=development parcel --no-autoinstall --no-cache", 22 | "build": "parcel build --no-autoinstall", 23 | "start": "cross-env NODE_ENV=production node dist/server/entry.rsc.js", 24 | "typecheck": "tsc --noEmit" 25 | }, 26 | "dependencies": { 27 | "@mjackson/node-fetch-server": "0.7.0", 28 | "@parcel/runtime-rsc": "^2.15.4", 29 | "buffer": "^6.0.3", 30 | "compression": "^1.8.0", 31 | "cross-env": "^7.0.3", 32 | "express": "^5.1.0", 33 | "react": "19.1.0", 34 | "react-dom": "19.1.0", 35 | "react-router": "7.7.0", 36 | "react-server-dom-parcel": "19.1.0" 37 | }, 38 | "devDependencies": { 39 | "@tailwindcss/postcss": "^4.1.10", 40 | "@tailwindcss/typography": "0.5.16", 41 | "@types/compression": "^1.8.1", 42 | "@types/express": "^5.0.3", 43 | "@types/node": "^24.0.3", 44 | "@types/react": "^19.1.8", 45 | "@types/react-dom": "^19.1.6", 46 | "parcel": "^2.15.4", 47 | "postcss": "^8.5.6", 48 | "tailwindcss": "^4.1.10", 49 | "typescript": "^5.8.3" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /unstable_rsc-parcel/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remix-run/react-router-templates/56789beb24c7fd0da5c5681e7bfcbaa4ef03f758/unstable_rsc-parcel/public/favicon.ico -------------------------------------------------------------------------------- /unstable_rsc-parcel/src/entry.browser.tsx: -------------------------------------------------------------------------------- 1 | "use client-entry"; 2 | 3 | import { startTransition, StrictMode } from "react"; 4 | import { hydrateRoot } from "react-dom/client"; 5 | import { 6 | unstable_createCallServer as createCallServer, 7 | unstable_getRSCStream as getRSCStream, 8 | unstable_RSCHydratedRouter as RSCHydratedRouter, 9 | type unstable_RSCPayload as RSCServerPayload, 10 | } from "react-router"; 11 | import { 12 | createFromReadableStream, 13 | createTemporaryReferenceSet, 14 | encodeReply, 15 | setServerCallback, 16 | // @ts-expect-error - no types for this yet 17 | } from "react-server-dom-parcel/client"; 18 | 19 | // Create and set the callServer function to support post-hydration server actions. 20 | setServerCallback( 21 | createCallServer({ 22 | createFromReadableStream, 23 | createTemporaryReferenceSet, 24 | encodeReply, 25 | }), 26 | ); 27 | 28 | // Get and decode the initial server payload 29 | createFromReadableStream(getRSCStream()).then((payload: RSCServerPayload) => { 30 | startTransition(async () => { 31 | const formState = 32 | payload.type === "render" ? await payload.formState : undefined; 33 | 34 | hydrateRoot( 35 | document, 36 | 37 | 41 | , 42 | { 43 | // @ts-expect-error - no types for this yet 44 | formState, 45 | }, 46 | ); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /unstable_rsc-parcel/src/entry.rsc.tsx: -------------------------------------------------------------------------------- 1 | import { createRequestListener } from "@mjackson/node-fetch-server"; 2 | import compression from "compression"; 3 | import express from "express"; 4 | import { unstable_matchRSCServerRequest as matchRSCServerRequest } from "react-router"; 5 | import { 6 | createTemporaryReferenceSet, 7 | decodeAction, 8 | decodeFormState, 9 | decodeReply, 10 | loadServerAction, 11 | renderToReadableStream, 12 | // @ts-expect-error - no types for this yet 13 | } from "react-server-dom-parcel/server.edge"; 14 | 15 | // Import the generateHTML function from the client environment 16 | import { generateHTML } from "./entry.ssr" with { env: "react-client" }; 17 | import { routes } from "./routes/config"; 18 | 19 | function fetchServer(request: Request) { 20 | return matchRSCServerRequest({ 21 | // Provide the React Server touchpoints. 22 | createTemporaryReferenceSet, 23 | decodeAction, 24 | decodeFormState, 25 | decodeReply, 26 | loadServerAction, 27 | // The incoming request. 28 | request, 29 | // The app routes. 30 | routes: routes(), 31 | // Encode the match with the React Server implementation. 32 | generateResponse(match) { 33 | return new Response(renderToReadableStream(match.payload), { 34 | status: match.statusCode, 35 | headers: match.headers, 36 | }); 37 | }, 38 | }); 39 | } 40 | 41 | const app = express(); 42 | 43 | // Serve static assets with compression and long cache lifetime. 44 | app.use( 45 | "/client", 46 | compression(), 47 | express.static("dist/client", { 48 | immutable: true, 49 | maxAge: "1y", 50 | }) 51 | ); 52 | app.use(compression(), express.static("public")); 53 | 54 | // Ignore Chrome extension requests. 55 | app.get("/.well-known/appspecific/com.chrome.devtools.json", (_, res) => { 56 | res.status(404); 57 | res.end(); 58 | }); 59 | 60 | // Hookup our application. 61 | app.use( 62 | createRequestListener((request) => 63 | generateHTML( 64 | request, 65 | fetchServer, 66 | (routes as unknown as { bootstrapScript?: string }).bootstrapScript 67 | ) 68 | ) 69 | ); 70 | 71 | const PORT = Number.parseInt(process.env.PORT || "3000"); 72 | app.listen(PORT, () => { 73 | console.log(`Server listening on port ${PORT} (http://localhost:${PORT})`); 74 | }); 75 | -------------------------------------------------------------------------------- /unstable_rsc-parcel/src/entry.ssr.tsx: -------------------------------------------------------------------------------- 1 | import { renderToReadableStream as renderHTMLToReadableStream } from "react-dom/server.edge"; 2 | import { 3 | unstable_routeRSCServerRequest as routeRSCServerRequest, 4 | unstable_RSCStaticRouter as RSCStaticRouter, 5 | } from "react-router"; 6 | // @ts-expect-error - no types for this yet 7 | import { createFromReadableStream } from "react-server-dom-parcel/client.edge"; 8 | 9 | export async function generateHTML( 10 | request: Request, 11 | fetchServer: (request: Request) => Promise, 12 | bootstrapScriptContent: string | undefined, 13 | ): Promise { 14 | return await routeRSCServerRequest({ 15 | // The incoming request. 16 | request, 17 | // How to call the React Server. 18 | fetchServer, 19 | // Provide the React Server touchpoints. 20 | createFromReadableStream, 21 | // Render the router to HTML. 22 | async renderHTML(getPayload) { 23 | const payload = await getPayload(); 24 | const formState = 25 | payload.type === "render" ? await payload.formState : undefined; 26 | 27 | return await renderHTMLToReadableStream( 28 | , 29 | { 30 | bootstrapScriptContent, 31 | // @ts-expect-error - no types for this yet 32 | formState, 33 | }, 34 | ); 35 | }, 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /unstable_rsc-parcel/src/routes/about/route.tsx: -------------------------------------------------------------------------------- 1 | export default function About() { 2 | return ( 3 |
4 |
5 |

About Page

6 |

This is the about page of our application.

7 |
8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /unstable_rsc-parcel/src/routes/config.ts: -------------------------------------------------------------------------------- 1 | "use server-entry"; 2 | 3 | import type { unstable_RSCRouteConfig as RSCRouteConfig } from "react-router"; 4 | 5 | import "../entry.browser"; 6 | 7 | export function routes() { 8 | return [ 9 | { 10 | id: "root", 11 | path: "", 12 | lazy: () => import("./root/route"), 13 | children: [ 14 | { 15 | id: "home", 16 | index: true, 17 | lazy: () => import("./home/route"), 18 | }, 19 | { 20 | id: "about", 21 | path: "about", 22 | lazy: () => import("./about/route"), 23 | }, 24 | ], 25 | }, 26 | ] satisfies RSCRouteConfig; 27 | } 28 | -------------------------------------------------------------------------------- /unstable_rsc-parcel/src/routes/home/route.tsx: -------------------------------------------------------------------------------- 1 | export default function Home() { 2 | return ( 3 |
4 |
5 |

Welcome to React Router RSC

6 |

7 | This is a simple example of a React Router application using React 8 | Server Components (RSC) with Parcel. It demonstrates how to set up a 9 | basic routing structure and render components server-side. 10 |

11 |
12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /unstable_rsc-parcel/src/routes/root/client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | isRouteErrorResponse, 5 | Link, 6 | NavLink, 7 | useNavigation, 8 | useRouteError, 9 | } from "react-router"; 10 | 11 | export function Layout({ children }: { children: React.ReactNode }) { 12 | const navigation = useNavigation(); 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |
23 |
24 | React Router 🚀 25 | 45 |
{navigation.state !== "idle" &&

Loading...

}
46 |
47 |
48 |
49 | {children} 50 | 51 | 52 | ); 53 | } 54 | 55 | export function ErrorBoundary() { 56 | const error = useRouteError(); 57 | let status = 500; 58 | let message = "An unexpected error occurred."; 59 | 60 | if (isRouteErrorResponse(error)) { 61 | status = error.status; 62 | message = status === 404 ? "Page not found." : error.statusText || message; 63 | } 64 | 65 | return ( 66 |
67 |
68 |

{status}

69 |

{message}

70 |
71 |
72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /unstable_rsc-parcel/src/routes/root/route.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from "react-router"; 2 | 3 | import { Layout as ClientLayout } from "./client"; 4 | import "./styles.css"; 5 | 6 | export { ErrorBoundary } from "./client"; 7 | 8 | export function Layout({ children }: { children: React.ReactNode }) { 9 | // This is necessary for the bundler to inject the needed CSS assets. 10 | return {children}; 11 | } 12 | 13 | export default function Component() { 14 | return ; 15 | } 16 | -------------------------------------------------------------------------------- /unstable_rsc-parcel/src/routes/root/styles.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @plugin "@tailwindcss/typography"; 3 | -------------------------------------------------------------------------------- /unstable_rsc-parcel/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowImportingTsExtensions": true, 4 | "strict": true, 5 | "noUnusedLocals": true, 6 | "noUnusedParameters": true, 7 | "skipLibCheck": true, 8 | "verbatimModuleSyntax": true, 9 | "noEmit": true, 10 | "moduleResolution": "Bundler", 11 | "module": "ESNext", 12 | "target": "ESNext", 13 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 14 | "jsx": "react-jsx" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /unstable_rsc-vite/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | -------------------------------------------------------------------------------- /unstable_rsc-vite/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to React Router! (Experimental RSC) 2 | 3 | ⚠️ **EXPERIMENTAL**: This template demonstrates React Server Components with React Router. This is experimental technology and not recommended for production use. 4 | 5 | A modern template for exploring React Server Components (RSC) with React Router, powered by Vite. 6 | 7 | ## Features 8 | 9 | - 🧪 **Experimental React Server Components** 10 | - 🚀 Server-side rendering with RSC 11 | - ⚡️ Hot Module Replacement (HMR) 12 | - 📦 Asset bundling and optimization with Vite 13 | - 🔄 Data loading and mutations 14 | - 🔒 TypeScript by default 15 | - 🎉 TailwindCSS for styling 16 | - 📖 [React Router docs](https://reactrouter.com/) 17 | - 📚 [React Server Components guide](https://reactrouter.com/how-to/react-server-components) 18 | 19 | ## Getting Started 20 | 21 | ### Installation 22 | 23 | Install the dependencies: 24 | 25 | ```bash 26 | npm install 27 | ``` 28 | 29 | ### Development 30 | 31 | Start the development server with HMR: 32 | 33 | ```bash 34 | npm run dev 35 | ``` 36 | 37 | Your application will be available at `http://localhost:5173`. 38 | 39 | ## Building for Production 40 | 41 | Create a production build: 42 | 43 | ```bash 44 | npm run build 45 | ``` 46 | 47 | ## Running Production Build 48 | 49 | Run the production server: 50 | 51 | ```bash 52 | npm start 53 | ``` 54 | 55 | ## Understanding React Server Components 56 | 57 | This template includes three entry points: 58 | 59 | - **`entry.rsc.tsx`** - React Server Components entry point 60 | - **`entry.ssr.tsx`** - Server-side rendering entry point 61 | - **`entry.browser.tsx`** - Client-side hydration entry point 62 | 63 | Learn more about React Server Components with React Router in our [comprehensive guide](https://reactrouter.com/how-to/react-server-components). 64 | 65 | ## Styling 66 | 67 | This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer. 68 | 69 | --- 70 | 71 | Built with ❤️ using React Router. -------------------------------------------------------------------------------- /unstable_rsc-vite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "build": "vite build", 6 | "dev": "cross-env NODE_ENV=development vite", 7 | "start": "cross-env NODE_ENV=production node server.js", 8 | "typecheck": "tsc --noEmit" 9 | }, 10 | "dependencies": { 11 | "@mjackson/node-fetch-server": "0.7.0", 12 | "compression": "^1.8.0", 13 | "cross-env": "^7.0.3", 14 | "express": "^5.1.0", 15 | "react": "19.1.0", 16 | "react-dom": "19.1.0", 17 | "react-router": "7.7.0" 18 | }, 19 | "devDependencies": { 20 | "@tailwindcss/typography": "0.5.16", 21 | "@tailwindcss/vite": "^4.1.10", 22 | "@types/compression": "^1.8.1", 23 | "@types/express": "^5.0.3", 24 | "@types/node": "^24.0.3", 25 | "@types/react": "^19.1.8", 26 | "@types/react-dom": "^19.1.6", 27 | "@vitejs/plugin-react": "^4.5.2", 28 | "@vitejs/plugin-rsc": "^0.4.11", 29 | "tailwindcss": "^4.1.10", 30 | "typescript": "^5.8.3", 31 | "vite": "^6.3.5", 32 | "vite-plugin-devtools-json": "0.2.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /unstable_rsc-vite/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remix-run/react-router-templates/56789beb24c7fd0da5c5681e7bfcbaa4ef03f758/unstable_rsc-vite/public/favicon.ico -------------------------------------------------------------------------------- /unstable_rsc-vite/server.js: -------------------------------------------------------------------------------- 1 | import { createRequestListener } from "@mjackson/node-fetch-server"; 2 | import compression from "compression"; 3 | import express from "express"; 4 | 5 | import build from "./dist/rsc/index.js"; 6 | 7 | const app = express(); 8 | 9 | app.use( 10 | "/assets", 11 | compression(), 12 | express.static("dist/client/assets", { 13 | immutable: true, 14 | maxAge: "1y", 15 | }) 16 | ); 17 | app.use(compression(), express.static("dist/client")); 18 | 19 | app.get("/.well-known/appspecific/com.chrome.devtools.json", (_, res) => { 20 | res.status(404); 21 | res.end(); 22 | }); 23 | 24 | app.use(createRequestListener(build)); 25 | 26 | const PORT = Number.parseInt(process.env.PORT || "3000"); 27 | app.listen(PORT, () => { 28 | console.log(`Server listening on port ${PORT} (http://localhost:${PORT})`); 29 | }); 30 | -------------------------------------------------------------------------------- /unstable_rsc-vite/src/entry.browser.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | createFromReadableStream, 3 | createTemporaryReferenceSet, 4 | encodeReply, 5 | setServerCallback, 6 | } from "@vitejs/plugin-rsc/browser"; 7 | import { startTransition, StrictMode } from "react"; 8 | import { hydrateRoot } from "react-dom/client"; 9 | import { 10 | unstable_createCallServer as createCallServer, 11 | unstable_getRSCStream as getRSCStream, 12 | unstable_RSCHydratedRouter as RSCHydratedRouter, 13 | type unstable_RSCPayload as RSCServerPayload, 14 | } from "react-router"; 15 | 16 | // Create and set the callServer function to support post-hydration server actions. 17 | setServerCallback( 18 | createCallServer({ 19 | createFromReadableStream, 20 | createTemporaryReferenceSet, 21 | encodeReply, 22 | }), 23 | ); 24 | 25 | // Get and decode the initial server payload 26 | createFromReadableStream(getRSCStream()).then((payload) => { 27 | startTransition(async () => { 28 | const formState = 29 | payload.type === "render" ? await payload.formState : undefined; 30 | 31 | hydrateRoot( 32 | document, 33 | 34 | 38 | , 39 | { 40 | // @ts-expect-error - no types for this yet 41 | formState, 42 | }, 43 | ); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /unstable_rsc-vite/src/entry.rsc.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | createTemporaryReferenceSet, 3 | decodeAction, 4 | decodeFormState, 5 | decodeReply, 6 | loadServerAction, 7 | renderToReadableStream, 8 | } from "@vitejs/plugin-rsc/rsc"; 9 | import { unstable_matchRSCServerRequest as matchRSCServerRequest } from "react-router"; 10 | 11 | import { routes } from "./routes/config"; 12 | 13 | function fetchServer(request: Request) { 14 | return matchRSCServerRequest({ 15 | // Provide the React Server touchpoints. 16 | createTemporaryReferenceSet, 17 | decodeAction, 18 | decodeFormState, 19 | decodeReply, 20 | loadServerAction, 21 | // The incoming request. 22 | request, 23 | // The app routes. 24 | routes: routes(), 25 | // Encode the match with the React Server implementation. 26 | generateResponse(match) { 27 | return new Response(renderToReadableStream(match.payload), { 28 | status: match.statusCode, 29 | headers: match.headers, 30 | }); 31 | }, 32 | }); 33 | } 34 | 35 | export default async function handler(request: Request) { 36 | // Import the generateHTML function from the client environment 37 | const ssr = await import.meta.viteRsc.loadModule< 38 | typeof import("./entry.ssr") 39 | >("ssr", "index"); 40 | 41 | return ssr.generateHTML(request, fetchServer); 42 | } 43 | 44 | if (import.meta.hot) { 45 | import.meta.hot.accept(); 46 | } 47 | -------------------------------------------------------------------------------- /unstable_rsc-vite/src/entry.ssr.tsx: -------------------------------------------------------------------------------- 1 | import { createFromReadableStream } from "@vitejs/plugin-rsc/ssr"; 2 | import { renderToReadableStream as renderHTMLToReadableStream } from "react-dom/server.edge"; 3 | import { 4 | unstable_routeRSCServerRequest as routeRSCServerRequest, 5 | unstable_RSCStaticRouter as RSCStaticRouter, 6 | } from "react-router"; 7 | 8 | export async function generateHTML( 9 | request: Request, 10 | fetchServer: (request: Request) => Promise, 11 | ): Promise { 12 | return await routeRSCServerRequest({ 13 | // The incoming request. 14 | request, 15 | // How to call the React Server. 16 | fetchServer, 17 | // Provide the React Server touchpoints. 18 | createFromReadableStream, 19 | // Render the router to HTML. 20 | async renderHTML(getPayload) { 21 | const payload = await getPayload(); 22 | const formState = 23 | payload.type === "render" ? await payload.formState : undefined; 24 | 25 | const bootstrapScriptContent = 26 | await import.meta.viteRsc.loadBootstrapScriptContent("index"); 27 | 28 | return await renderHTMLToReadableStream( 29 | , 30 | { 31 | bootstrapScriptContent, 32 | // @ts-expect-error - no types for this yet 33 | formState, 34 | }, 35 | ); 36 | }, 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /unstable_rsc-vite/src/routes/about/route.tsx: -------------------------------------------------------------------------------- 1 | export default function About() { 2 | return ( 3 |
4 |
5 |

About Page

6 |

This is the about page of our application.

7 |
8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /unstable_rsc-vite/src/routes/config.ts: -------------------------------------------------------------------------------- 1 | import type { unstable_RSCRouteConfig as RSCRouteConfig } from "react-router"; 2 | 3 | export function routes() { 4 | return [ 5 | { 6 | id: "root", 7 | path: "", 8 | lazy: () => import("./root/route"), 9 | children: [ 10 | { 11 | id: "home", 12 | index: true, 13 | lazy: () => import("./home/route"), 14 | }, 15 | { 16 | id: "about", 17 | path: "about", 18 | lazy: () => import("./about/route"), 19 | }, 20 | ], 21 | }, 22 | ] satisfies RSCRouteConfig; 23 | } 24 | -------------------------------------------------------------------------------- /unstable_rsc-vite/src/routes/home/route.tsx: -------------------------------------------------------------------------------- 1 | export default function Home() { 2 | return ( 3 |
4 |
5 |

Welcome to React Router RSC

6 |

7 | This is a simple example of a React Router application using React 8 | Server Components (RSC) with Vite. It demonstrates how to set up a 9 | basic routing structure and render components server-side. 10 |

11 |
12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /unstable_rsc-vite/src/routes/root/client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | isRouteErrorResponse, 5 | Link, 6 | NavLink, 7 | useNavigation, 8 | useRouteError, 9 | } from "react-router"; 10 | 11 | export function Layout({ children }: { children: React.ReactNode }) { 12 | const navigation = useNavigation(); 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |
23 |
24 | React Router 🚀 25 | 45 |
{navigation.state !== "idle" &&

Loading...

}
46 |
47 |
48 |
49 | {children} 50 | 51 | 52 | ); 53 | } 54 | 55 | export function ErrorBoundary() { 56 | const error = useRouteError(); 57 | let status = 500; 58 | let message = "An unexpected error occurred."; 59 | 60 | if (isRouteErrorResponse(error)) { 61 | status = error.status; 62 | message = status === 404 ? "Page not found." : error.statusText || message; 63 | } 64 | 65 | return ( 66 |
67 |
68 |

{status}

69 |

{message}

70 |
71 |
72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /unstable_rsc-vite/src/routes/root/route.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from "react-router"; 2 | 3 | import { Layout as ClientLayout } from "./client"; 4 | import "./styles.css"; 5 | 6 | export { ErrorBoundary } from "./client"; 7 | 8 | export function Layout({ children }: { children: React.ReactNode }) { 9 | // This is necessary for the bundler to inject the needed CSS assets. 10 | return {children}; 11 | } 12 | 13 | export default function Component() { 14 | return ; 15 | } 16 | -------------------------------------------------------------------------------- /unstable_rsc-vite/src/routes/root/styles.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @plugin "@tailwindcss/typography"; 3 | -------------------------------------------------------------------------------- /unstable_rsc-vite/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowImportingTsExtensions": true, 4 | "strict": true, 5 | "noUnusedLocals": true, 6 | "noUnusedParameters": true, 7 | "skipLibCheck": true, 8 | "verbatimModuleSyntax": true, 9 | "noEmit": true, 10 | "moduleResolution": "Bundler", 11 | "module": "ESNext", 12 | "target": "ESNext", 13 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 14 | "types": ["vite/client", "@vitejs/plugin-rsc/types"], 15 | "jsx": "react-jsx" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /unstable_rsc-vite/vite.config.ts: -------------------------------------------------------------------------------- 1 | import rsc from "@vitejs/plugin-rsc/plugin"; 2 | import react from "@vitejs/plugin-react"; 3 | import tailwindcss from "@tailwindcss/vite"; 4 | import { defineConfig } from "vite"; 5 | import devtoolsJson from "vite-plugin-devtools-json"; 6 | 7 | export default defineConfig({ 8 | plugins: [ 9 | tailwindcss(), 10 | react(), 11 | rsc({ 12 | entries: { 13 | client: "src/entry.browser.tsx", 14 | rsc: "src/entry.rsc.tsx", 15 | ssr: "src/entry.ssr.tsx", 16 | }, 17 | }), 18 | devtoolsJson(), 19 | ], 20 | }); 21 | -------------------------------------------------------------------------------- /vercel/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules/ 3 | 4 | # React Router 5 | /.react-router/ 6 | /build/ 7 | 8 | # Vercel 9 | /.vercel/ 10 | -------------------------------------------------------------------------------- /vercel/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to React Router! 2 | 3 | A modern, production-ready template for building full-stack React applications using React Router. 4 | 5 | ## Features 6 | 7 | - 🚀 Server-side rendering 8 | - ⚡️ Hot Module Replacement (HMR) 9 | - 📦 Asset bundling and optimization 10 | - 🔄 Data loading and mutations 11 | - 🔒 TypeScript by default 12 | - 🎉 TailwindCSS for styling 13 | - 📖 [React Router docs](https://reactrouter.com/) 14 | 15 | ## Getting Started 16 | 17 | ### Installation 18 | 19 | Install the dependencies: 20 | 21 | ```bash 22 | npm install 23 | ``` 24 | 25 | ### Development 26 | 27 | Start the development server with HMR: 28 | 29 | ```bash 30 | npm run dev 31 | ``` 32 | 33 | Your application will be available at `http://localhost:3000`. 34 | 35 | ## Building for Production 36 | 37 | Create a production build: 38 | 39 | ```bash 40 | npm run build 41 | ``` 42 | 43 | ## Deployment 44 | 45 | [![Deploy to Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fremix-run%2Freact-router-templates%2Ftree%2Fmain%2Fvercel&project-name=my-react-router-app&repository-name=my-react-router-app) 46 | 47 | ## Styling 48 | 49 | This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer. 50 | 51 | --- 52 | 53 | Built with ❤️ using React Router. 54 | -------------------------------------------------------------------------------- /vercel/app/app.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | @theme { 4 | --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif, 5 | "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 6 | } 7 | 8 | html, 9 | body { 10 | @apply bg-white dark:bg-gray-950; 11 | 12 | @media (prefers-color-scheme: dark) { 13 | color-scheme: dark; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /vercel/app/root.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | isRouteErrorResponse, 3 | Links, 4 | Meta, 5 | Outlet, 6 | Scripts, 7 | ScrollRestoration, 8 | } from "react-router"; 9 | 10 | import type { Route } from "./+types/root"; 11 | import "./app.css"; 12 | 13 | export const links: Route.LinksFunction = () => [ 14 | { rel: "preconnect", href: "https://fonts.googleapis.com" }, 15 | { 16 | rel: "preconnect", 17 | href: "https://fonts.gstatic.com", 18 | crossOrigin: "anonymous", 19 | }, 20 | { 21 | rel: "stylesheet", 22 | href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", 23 | }, 24 | ]; 25 | 26 | export function Layout({ children }: { children: React.ReactNode }) { 27 | return ( 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {children} 37 | 38 | 39 | 40 | 41 | ); 42 | } 43 | 44 | export default function App() { 45 | return ; 46 | } 47 | 48 | export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { 49 | let message = "Oops!"; 50 | let details = "An unexpected error occurred."; 51 | let stack: string | undefined; 52 | 53 | if (isRouteErrorResponse(error)) { 54 | message = error.status === 404 ? "404" : "Error"; 55 | details = 56 | error.status === 404 57 | ? "The requested page could not be found." 58 | : error.statusText || details; 59 | } else if (import.meta.env.DEV && error && error instanceof Error) { 60 | details = error.message; 61 | stack = error.stack; 62 | } 63 | 64 | return ( 65 |
66 |

{message}

67 |

{details}

68 | {stack && ( 69 |
70 |           {stack}
71 |         
72 | )} 73 |
74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /vercel/app/routes.ts: -------------------------------------------------------------------------------- 1 | import { type RouteConfig, index } from "@react-router/dev/routes"; 2 | 3 | export default [index("routes/home.tsx")] satisfies RouteConfig; 4 | -------------------------------------------------------------------------------- /vercel/app/routes/home.tsx: -------------------------------------------------------------------------------- 1 | import type { Route } from "./+types/home"; 2 | import { Welcome } from "../welcome/welcome"; 3 | 4 | export function meta({}: Route.MetaArgs) { 5 | return [ 6 | { title: "New React Router App" }, 7 | { name: "description", content: "Welcome to React Router!" }, 8 | ]; 9 | } 10 | 11 | export function loader({ context }: Route.LoaderArgs) { 12 | return { message: "Hello from Vercel" }; 13 | } 14 | 15 | export default function Home({ loaderData }: Route.ComponentProps) { 16 | return ; 17 | } 18 | -------------------------------------------------------------------------------- /vercel/app/welcome/welcome.tsx: -------------------------------------------------------------------------------- 1 | import logoDark from "./logo-dark.svg"; 2 | import logoLight from "./logo-light.svg"; 3 | 4 | export function Welcome({ message }: { message: string }) { 5 | return ( 6 |
7 |
8 |
9 |
10 | React Router 15 | React Router 20 |
21 |
22 |
23 | 44 |
45 |
46 |
47 | ); 48 | } 49 | 50 | const resources = [ 51 | { 52 | href: "https://reactrouter.com/docs", 53 | text: "React Router Docs", 54 | icon: ( 55 | 63 | 68 | 69 | ), 70 | }, 71 | { 72 | href: "https://rmx.as/discord", 73 | text: "Join Discord", 74 | icon: ( 75 | 83 | 87 | 88 | ), 89 | }, 90 | ]; 91 | -------------------------------------------------------------------------------- /vercel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "build": "react-router build", 6 | "dev": "react-router dev", 7 | "start": "react-router-serve ./build/server/index.js", 8 | "typecheck": "react-router typegen && tsc" 9 | }, 10 | "dependencies": { 11 | "@react-router/node": "^7.7.0", 12 | "isbot": "^5.1.27", 13 | "react": "^19.1.0", 14 | "react-dom": "^19.1.0", 15 | "react-router": "^7.7.0" 16 | }, 17 | "devDependencies": { 18 | "@react-router/dev": "^7.7.0", 19 | "@tailwindcss/vite": "^4.1.4", 20 | "@types/node": "^20", 21 | "@types/react": "^19.1.2", 22 | "@types/react-dom": "^19.1.2", 23 | "@vercel/react-router": "^1.2.2", 24 | "tailwindcss": "^4.1.4", 25 | "typescript": "^5.8.3", 26 | "vite": "^6.3.3", 27 | "vite-tsconfig-paths": "^5.1.4" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /vercel/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remix-run/react-router-templates/56789beb24c7fd0da5c5681e7bfcbaa4ef03f758/vercel/public/favicon.ico -------------------------------------------------------------------------------- /vercel/react-router.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "@react-router/dev/config"; 2 | import { vercelPreset } from "@vercel/react-router/vite"; 3 | 4 | export default { 5 | // Config options... 6 | // Server-side render by default, to enable SPA mode set this to `false` 7 | ssr: true, 8 | presets: [vercelPreset()], 9 | } satisfies Config; 10 | -------------------------------------------------------------------------------- /vercel/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "**/*", 4 | "**/.server/**/*", 5 | "**/.client/**/*", 6 | ".react-router/types/**/*" 7 | ], 8 | "compilerOptions": { 9 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 10 | "types": ["vite/client"], 11 | "target": "ES2022", 12 | "module": "ES2022", 13 | "moduleResolution": "bundler", 14 | "jsx": "react-jsx", 15 | "rootDirs": [".", "./.react-router/types"], 16 | "baseUrl": ".", 17 | "paths": { 18 | "~/*": ["./app/*"] 19 | }, 20 | "esModuleInterop": true, 21 | "verbatimModuleSyntax": true, 22 | "noEmit": true, 23 | "resolveJsonModule": true, 24 | "skipLibCheck": true, 25 | "strict": true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /vercel/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { reactRouter } from "@react-router/dev/vite"; 2 | import tailwindcss from "@tailwindcss/vite"; 3 | import { defineConfig } from "vite"; 4 | import tsconfigPaths from "vite-tsconfig-paths"; 5 | 6 | export default defineConfig({ 7 | plugins: [tailwindcss(), reactRouter(), tsconfigPaths()], 8 | }); 9 | --------------------------------------------------------------------------------