Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/entry.server.tsx
T

139 lines
5.4 KiB
TypeScript

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(
<I18nextProvider i18n={i18nInstance}>
<ServerRouter context={reactRouterContext} url={request.url} />
</I18nextProvider>,
{
[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);
});
}