Files
sharper.ly/apps/web/app/entry.server.tsx
T

158 lines
5.6 KiB
TypeScript
Raw Normal View History

2026-09-11 02:46:56 +02:00
/* ============================================================================
SERVER ENTRY
----------------------------------------------------------------------------
The stock React Router streaming entry plus two SHARP-ER additions:
- `Content-Language` is stamped from the URL's locale prefix, so the
header agrees with <html lang> that root.tsx renders. `Vary` is NOT set
globally: every page URL carries its own locale, so only the bare "/"
redirect varies on Accept-Language and it sets the header itself.
- `handleError` logs server errors without the noise of client aborts.
========================================================================= */
import { PassThrough } from "node:stream";
import type { EntryContext, RouterContextProvider } from "react-router";
import { createReadableStreamFromReadable } from "@react-router/node";
import { ServerRouter } from "react-router";
import { isbot } from "isbot";
import type { RenderToPipeableStreamOptions } from "react-dom/server";
import { renderToPipeableStream } from "react-dom/server";
import { DEFAULT_LOCALE, getLocaleFromPathname } from "~/i18n/config";
2026-10-15 23:44:56 +02:00
import { getServerEnv } from "~/lib/env.server";
2026-09-11 02:46:56 +02:00
export const streamTimeout = 5_000;
/** The locale a URL addresses, for the `Content-Language` response header. */
function contentLanguage(request: Request): string {
try {
return getLocaleFromPathname(new URL(request.url).pathname) ?? DEFAULT_LOCALE;
} catch {
return DEFAULT_LOCALE;
}
}
2026-10-15 23:44:56 +02:00
function readCspNonce(routerContext: EntryContext): string | undefined {
const rootData = routerContext.staticHandlerContext.loaderData["root"] as
| { nonce?: unknown }
| undefined;
return typeof rootData?.nonce === "string" ? rootData.nonce : undefined;
}
function contentSecurityPolicy(nonce: string): string {
const { STRAPI_URL, isProduction } = getServerEnv();
const mediaOrigin = new URL(STRAPI_URL).origin;
const directives = [
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}' https://www.googletagmanager.com`,
"script-src-attr 'none'",
"style-src 'self' 'unsafe-inline'",
`img-src 'self' ${mediaOrigin} https://www.google-analytics.com data: blob:`,
`media-src 'self' ${mediaOrigin} data: blob:`,
"font-src 'self' data:",
"connect-src 'self' https://www.googletagmanager.com https://www.google-analytics.com https://region1.google-analytics.com",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'none'",
...(isProduction ? ["upgrade-insecure-requests"] : []),
];
return directives.join("; ");
}
2026-09-11 02:46:56 +02:00
export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
routerContext: EntryContext,
loadContext: RouterContextProvider,
) {
2026-10-15 23:44:56 +02:00
const nonce = readCspNonce(routerContext);
if (nonce) {
responseHeaders.set("Content-Security-Policy", contentSecurityPolicy(nonce));
}
2026-09-11 02:46:56 +02:00
// https://httpwg.org/specs/rfc9110.html#HEAD
if (request.method.toUpperCase() === "HEAD") {
return new Response(null, {
status: responseStatusCode,
headers: responseHeaders,
});
}
return new Promise((resolve, reject) => {
let shellRendered = false;
let userAgent = request.headers.get("user-agent");
// Ensure requests from bots and SPA Mode renders wait for all content to load before responding
// https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
let readyOption: keyof RenderToPipeableStreamOptions =
(userAgent && isbot(userAgent)) || routerContext.isSpaMode
? "onAllReady"
: "onShellReady";
// Abort the rendering stream after the `streamTimeout` so it has time to
// flush down the rejected boundaries
let timeoutId: ReturnType<typeof setTimeout> | undefined = setTimeout(
() => abort(),
streamTimeout + 1000,
);
const { pipe, abort } = renderToPipeableStream(
<ServerRouter context={routerContext} url={request.url} />,
{
2026-10-15 23:44:56 +02:00
nonce,
2026-09-11 02:46:56 +02:00
[readyOption]() {
shellRendered = true;
const body = new PassThrough({
final(callback) {
// Clear the timeout to prevent retaining the closure and memory leak
clearTimeout(timeoutId);
timeoutId = undefined;
callback();
},
});
const stream = createReadableStreamFromReadable(body);
responseHeaders.set("Content-Type", "text/html");
responseHeaders.set("Content-Language", contentLanguage(request));
pipe(body);
resolve(
new Response(stream, {
headers: responseHeaders,
status: responseStatusCode,
}),
);
},
onShellError(error: unknown) {
reject(error);
},
onError(error: unknown) {
responseStatusCode = 500;
// Log streaming rendering errors from inside the shell. Don't log
// errors encountered during initial shell rendering since they'll
// reject and get logged in handleDocumentRequest.
if (shellRendered) {
console.error(error);
}
},
},
);
});
}
/**
* Central server-side error logging. A request the client walked away from is
* not an application error, so it is not logged as one.
*/
export function handleError(
error: unknown,
{ request }: { request: Request },
): void {
if (request.signal.aborted) return;
console.error(`[sharp-er] ${request.method} ${request.url}`, error);
}