Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/entry.server.tsx
T

155 lines
6.2 KiB
TypeScript

import { initSentryServer } from "./sentry.server";
import { startEntryRuntimeServices } from "./entry-runtime-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();
// Vite resolves this import to a surface-specific boot module. Core keeps its
// cache subscribers, while the Portal artifact never imports that Core graph.
startEntryRuntimeServices();
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 { createInstance } from "i18next";
import { I18nextProvider, initReactI18next } from "react-i18next";
import { remixI18Next } from "./locale/i18next.server";
import { i18nConfig } from "./locale/i18n";
import { getServerI18nResourcesSnapshot, getServerRequestI18nLanguage, runWithServerI18nLanguage } 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";
import { getRequestCspNonce } from "./utils/security/contentSecurityPolicy";
const ssrLogger = createScopedLogger("ssr.entry");
// const ABORT_DELAY = 5_000;
// Reject/cancel all pending promises after 5 seconds
export const streamTimeout = 5000;
export async function runWithRequestI18nScope<T>(request: Request, callback: () => T | Promise<T>): Promise<T> {
const language = await remixI18Next.getLocale(request);
return runWithServerI18nLanguage(language, callback);
}
export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
reactRouterContext: EntryContext,
loadContext: AppLoadContext
) {
if (getServerRequestI18nLanguage()) {
return handleRequestWithinI18nScope(request, responseStatusCode, responseHeaders, reactRouterContext, loadContext);
}
// The production server wraps React Router before loaders/actions execute.
// Keep this entry-level fallback for development servers and direct tests.
return runWithRequestI18nScope(request, () => handleRequestWithinI18nScope(request, responseStatusCode, responseHeaders, reactRouterContext, loadContext));
}
async function handleRequestWithinI18nScope(
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 = getServerRequestI18nLanguage() ?? (await remixI18Next.getLocale(request));
const ns = remixI18Next.getRouteNamespaces(reactRouterContext);
const resources = getServerI18nResourcesSnapshot();
const cspNonce = getRequestCspNonce(request);
await i18nInstance
.use(initReactI18next) // Tell our instance to use react-i18next.
.init({
...i18nConfig, // Spread configuration.
lng, // Locale detected above.
ns, // Namespaces detected above.
resources,
// The process snapshot is already in memory; avoid scheduling another
// async initialization turn for every SSR request.
initImmediate: false,
});
return new Promise((resolve, reject) => {
let shellRendered = false;
const { pipe, abort } = renderToPipeableStream(
<I18nextProvider i18n={i18nInstance}>
<ServerRouter context={reactRouterContext} url={request.url} nonce={cspNonce} />
</I18nextProvider>,
{
nonce: cspNonce,
[callbackName]: () => {
shellRendered = true;
const body = new PassThrough();
const stream = createReadableStreamFromReadable(body);
responseHeaders.set("Content-Type", "text/html; charset=utf-8");
// 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);
});
}