import { initSentryServer } from "./sentry.server"; import { startCacheSubscribers } from "./cache-bootstrap.server"; // IMPORTANT: Initialize Sentry as the very first action on the server // entry so the SDK can hook into the Node runtime (HTTP, async hooks) // before any other module runs. No-op when SENTRY_DSN_WEB is unset. initSentryServer(); // Start cross-replica cache-invalidation subscribers. Idempotent + safe // in single-replica deployments (no-op when REDIS_URL is unset thanks to // SharedStore memory fallback). See docs/architecture/two-tier-cache.md. startCacheSubscribers(); import type { AppLoadContext, EntryContext } from "react-router"; import isbot from "isbot"; import { PassThrough } from "node:stream"; import { ServerRouter } from "react-router"; import { createReadableStreamFromReadable } from "@react-router/node"; import { renderToPipeableStream } from "react-dom/server"; import Backend from "i18next-fs-backend"; import { createInstance } from "i18next"; import { I18nextProvider, initReactI18next } from "react-i18next"; import { remixI18Next, serverLocalePath } from "./locale/i18next.server"; import { i18nConfig } from "./locale/i18n"; import { syncServerGlobalI18nLanguage } from "./locale/i18next.global.server"; import { routes as otherRoutes } from "./other-routes.server"; import { createScopedLogger } from "./utils/logger.server"; import { shouldNoindex, X_ROBOTS_TAG_NOINDEX } from "./utils/seo/noindex.server"; const ssrLogger = createScopedLogger("ssr.entry"); // const ABORT_DELAY = 5_000; // Reject/cancel all pending promises after 5 seconds export const streamTimeout = 5000; export default async function handleRequest( request: Request, responseStatusCode: number, responseHeaders: Headers, reactRouterContext: EntryContext, loadContext: AppLoadContext ) { const callbackName = isbot(request.headers.get("user-agent")) ? "onAllReady" : "onShellReady"; for (const handler of otherRoutes) { // eslint-disable-next-line no-await-in-loop const otherRouteResponse = await handler(request, reactRouterContext); if (otherRouteResponse) return otherRouteResponse; } // Internationalization (i18n). const i18nInstance = createInstance(); const lng = await remixI18Next.getLocale(request); const ns = remixI18Next.getRouteNamespaces(reactRouterContext); await i18nInstance .use(initReactI18next) // Tell our instance to use react-i18next. .use(Backend) // Setup backend. .init({ ...i18nConfig, // Spread configuration. lng, // Locale detected above. ns, // Namespaces detected above. backend: { localePath: serverLocalePath, loadPath: `${serverLocalePath}/{{lng}}/{{ns}}.json`, }, }); // Align the *global* i18next instance with the request locale so components // that resolve strings through the module-level `t` helper (rather than the // `useTranslation()` hook) render the same text the client hydrates with. // See ./locale/i18next.global.server.ts for the rationale. await syncServerGlobalI18nLanguage(lng); return new Promise((resolve, reject) => { let shellRendered = false; const { pipe, abort } = renderToPipeableStream( , { [callbackName]: () => { shellRendered = true; const body = new PassThrough(); const stream = createReadableStreamFromReadable(body); responseHeaders.set("Content-Type", "text/html"); // Default HTML responses to `no-store` so CDNs / browsers do not // cache rendered documents that may contain per-user data (CSRF // tokens, session-derived markup, feature flags). Loaders that // serve cacheable HTML (marketing pages, public-tenant landing) // can override by calling `headers.set("Cache-Control", ...)` // themselves — `set()` here only runs when no value is present. // See docs/ops/asset-cdn.md "Cache strategy". if (!responseHeaders.has("Cache-Control")) { responseHeaders.set("Cache-Control", "no-store"); } // AK-INFRA1 — Staging-Härtung: jede HTML-Antwort als noindex markieren, wenn WEB_NOINDEX // gesetzt ist (Staging). Greift zusätzlich zur robots.txt, auch ohne deren Abruf. if (shouldNoindex() && !responseHeaders.has("X-Robots-Tag")) { responseHeaders.set("X-Robots-Tag", X_ROBOTS_TAG_NOINDEX); } resolve( new Response(stream, { headers: responseHeaders, status: responseStatusCode, }) ); pipe(body); }, onShellError(error: unknown) { reject(error); }, onError(error: unknown) { responseStatusCode = 500; // Log streaming rendering errors from inside the shell. // Don't log errors encountered during initial shell rendering, // since they'll reject and get logged in handleDocumentRequest. if (shellRendered) { ssrLogger.error({ err: error }, "SSR streaming error"); } }, } ); // setTimeout(abort, ABORT_DELAY); // Automatically timeout the React renderer after 6 seconds, which ensures // React has enough time to flush down the rejected boundary contents setTimeout(abort, streamTimeout + 1000); }); }