/* ============================================================================ 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 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"; import { getServerEnv } from "~/lib/env.server"; 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; } } 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}' 'strict-dynamic' 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("; "); } export default function handleRequest( request: Request, responseStatusCode: number, responseHeaders: Headers, routerContext: EntryContext, loadContext: RouterContextProvider, ) { const nonce = readCspNonce(routerContext); if (nonce) { responseHeaders.set("Content-Security-Policy", contentSecurityPolicy(nonce)); } // 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 | undefined = setTimeout( () => abort(), streamTimeout + 1000, ); const { pipe, abort } = renderToPipeableStream( , { nonce, [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); }