···11+# Welcome to Remix!
22+33+- 📖 [Remix docs](https://remix.run/docs)
44+55+## Development
66+77+Run the dev server:
88+99+```shellscript
1010+npm run dev
1111+```
1212+1313+## Deployment
1414+1515+First, build your app for production:
1616+1717+```sh
1818+npm run build
1919+```
2020+2121+Then run the app in production mode:
2222+2323+```sh
2424+npm start
2525+```
2626+2727+Now you'll need to pick a host to deploy it to.
2828+2929+### DIY
3030+3131+If you're familiar with deploying Node applications, the built-in Remix app server is production-ready.
3232+3333+Make sure to deploy the output of `npm run build`
3434+3535+- `build/server`
3636+- `build/client`
3737+3838+## Styling
3939+4040+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. See the [Vite docs on css](https://vitejs.dev/guide/features.html#css) for more information.
+18
app/entry.client.tsx
···11+/**
22+ * By default, Remix will handle hydrating your app on the client for you.
33+ * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
44+ * For more information, see https://remix.run/file-conventions/entry.client
55+ */
66+77+import { RemixBrowser } from "@remix-run/react";
88+import { startTransition, StrictMode } from "react";
99+import { hydrateRoot } from "react-dom/client";
1010+1111+startTransition(() => {
1212+ hydrateRoot(
1313+ document,
1414+ <StrictMode>
1515+ <RemixBrowser />
1616+ </StrictMode>
1717+ );
1818+});
+140
app/entry.server.tsx
···11+/**
22+ * By default, Remix will handle generating the HTTP Response for you.
33+ * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
44+ * For more information, see https://remix.run/file-conventions/entry.server
55+ */
66+77+import { PassThrough } from "node:stream";
88+99+import type { AppLoadContext, EntryContext } from "@remix-run/node";
1010+import { createReadableStreamFromReadable } from "@remix-run/node";
1111+import { RemixServer } from "@remix-run/react";
1212+import { isbot } from "isbot";
1313+import { renderToPipeableStream } from "react-dom/server";
1414+1515+const ABORT_DELAY = 5_000;
1616+1717+export default function handleRequest(
1818+ request: Request,
1919+ responseStatusCode: number,
2020+ responseHeaders: Headers,
2121+ remixContext: EntryContext,
2222+ // This is ignored so we can keep it in the template for visibility. Feel
2323+ // free to delete this parameter in your app if you're not using it!
2424+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
2525+ loadContext: AppLoadContext
2626+) {
2727+ return isbot(request.headers.get("user-agent") || "")
2828+ ? handleBotRequest(
2929+ request,
3030+ responseStatusCode,
3131+ responseHeaders,
3232+ remixContext
3333+ )
3434+ : handleBrowserRequest(
3535+ request,
3636+ responseStatusCode,
3737+ responseHeaders,
3838+ remixContext
3939+ );
4040+}
4141+4242+function handleBotRequest(
4343+ request: Request,
4444+ responseStatusCode: number,
4545+ responseHeaders: Headers,
4646+ remixContext: EntryContext
4747+) {
4848+ return new Promise((resolve, reject) => {
4949+ let shellRendered = false;
5050+ const { pipe, abort } = renderToPipeableStream(
5151+ <RemixServer
5252+ context={remixContext}
5353+ url={request.url}
5454+ abortDelay={ABORT_DELAY}
5555+ />,
5656+ {
5757+ onAllReady() {
5858+ shellRendered = true;
5959+ const body = new PassThrough();
6060+ const stream = createReadableStreamFromReadable(body);
6161+6262+ responseHeaders.set("Content-Type", "text/html");
6363+6464+ resolve(
6565+ new Response(stream, {
6666+ headers: responseHeaders,
6767+ status: responseStatusCode,
6868+ })
6969+ );
7070+7171+ pipe(body);
7272+ },
7373+ onShellError(error: unknown) {
7474+ reject(error);
7575+ },
7676+ onError(error: unknown) {
7777+ responseStatusCode = 500;
7878+ // Log streaming rendering errors from inside the shell. Don't log
7979+ // errors encountered during initial shell rendering since they'll
8080+ // reject and get logged in handleDocumentRequest.
8181+ if (shellRendered) {
8282+ console.error(error);
8383+ }
8484+ },
8585+ }
8686+ );
8787+8888+ setTimeout(abort, ABORT_DELAY);
8989+ });
9090+}
9191+9292+function handleBrowserRequest(
9393+ request: Request,
9494+ responseStatusCode: number,
9595+ responseHeaders: Headers,
9696+ remixContext: EntryContext
9797+) {
9898+ return new Promise((resolve, reject) => {
9999+ let shellRendered = false;
100100+ const { pipe, abort } = renderToPipeableStream(
101101+ <RemixServer
102102+ context={remixContext}
103103+ url={request.url}
104104+ abortDelay={ABORT_DELAY}
105105+ />,
106106+ {
107107+ onShellReady() {
108108+ shellRendered = true;
109109+ const body = new PassThrough();
110110+ const stream = createReadableStreamFromReadable(body);
111111+112112+ responseHeaders.set("Content-Type", "text/html");
113113+114114+ resolve(
115115+ new Response(stream, {
116116+ headers: responseHeaders,
117117+ status: responseStatusCode,
118118+ })
119119+ );
120120+121121+ pipe(body);
122122+ },
123123+ onShellError(error: unknown) {
124124+ reject(error);
125125+ },
126126+ onError(error: unknown) {
127127+ responseStatusCode = 500;
128128+ // Log streaming rendering errors from inside the shell. Don't log
129129+ // errors encountered during initial shell rendering since they'll
130130+ // reject and get logged in handleDocumentRequest.
131131+ if (shellRendered) {
132132+ console.error(error);
133133+ }
134134+ },
135135+ }
136136+ );
137137+138138+ setTimeout(abort, ABORT_DELAY);
139139+ });
140140+}