From bab609024939afc5ca8f593efa329a335baefdce Mon Sep 17 00:00:00 2001 From: Kai Wei Mo Date: Thu, 30 Jul 2026 22:04:17 -0500 Subject: [PATCH] fix: stop nginx :3000 redirects and web HydrateFallback #418 Disable absolute_redirect/port_in_redirect in web, admin, and space nginx configs so reverse proxies no longer receive Location headers with the container port. Gate web HydrateFallback on mount so SPA prerender matches the first client render (React #418). --- apps/admin/nginx/nginx.conf | 4 ++++ apps/space/nginx/nginx.conf | 4 ++++ apps/web/app/root.tsx | 14 ++++++++++++-- apps/web/nginx/nginx.conf | 4 ++++ 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/apps/admin/nginx/nginx.conf b/apps/admin/nginx/nginx.conf index 36f62812868..b6d2aa152a3 100644 --- a/apps/admin/nginx/nginx.conf +++ b/apps/admin/nginx/nginx.conf @@ -20,6 +20,10 @@ http { server { listen 3000; + # Behind reverse proxies, keep redirects relative and never leak :3000. + absolute_redirect off; + port_in_redirect off; + # Security headers add_header X-Frame-Options "DENY" always; add_header X-Content-Type-Options "nosniff" always; diff --git a/apps/space/nginx/nginx.conf b/apps/space/nginx/nginx.conf index e6f15b1ee3a..9c55c9b46a9 100644 --- a/apps/space/nginx/nginx.conf +++ b/apps/space/nginx/nginx.conf @@ -20,6 +20,10 @@ http { server { listen 3000; + # Behind reverse proxies, keep redirects relative and never leak :3000. + absolute_redirect off; + port_in_redirect off; + location / { root /usr/share/nginx/html; index index.html index.htm; diff --git a/apps/web/app/root.tsx b/apps/web/app/root.tsx index e9f46d014c1..758a84e2c83 100644 --- a/apps/web/app/root.tsx +++ b/apps/web/app/root.tsx @@ -5,6 +5,7 @@ */ import type { ReactNode } from "react"; +import { useEffect, useState } from "react"; import Script from "next/script"; import { Links, Meta, Outlet, Scripts } from "react-router"; import type { LinksFunction } from "react-router"; @@ -135,8 +136,17 @@ export default function Root() { export function HydrateFallback() { const { resolvedTheme } = useTheme(); - // if we are on the server or the theme is not resolved, return an empty div - if (typeof window === "undefined" || resolvedTheme === undefined) return
; + // SPA build (ssr: false) prerenders this on the server as an empty
. + // On the first client hydration render, next-themes can already resolve the + // theme synchronously from localStorage, so rendering LogoSpinner here + // mismatches the prerendered HTML and throws React #418 on every load. + // Gate on `mounted` so hydration matches the prerender; show the spinner after mount. + const [mounted, setMounted] = useState(false); + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted || resolvedTheme === undefined) return
; return (
diff --git a/apps/web/nginx/nginx.conf b/apps/web/nginx/nginx.conf index b6719971cde..c8e9146f24f 100644 --- a/apps/web/nginx/nginx.conf +++ b/apps/web/nginx/nginx.conf @@ -20,6 +20,10 @@ http { server { listen 3000; + # Behind reverse proxies, keep redirects relative and never leak :3000. + absolute_redirect off; + port_in_redirect off; + # Security headers add_header X-Frame-Options "DENY" always; add_header X-Content-Type-Options "nosniff" always;