From 1f123a0a051406806444cf27fe212e5e4e186244 Mon Sep 17 00:00:00 2001 From: "reza.g" Date: Sun, 6 Sep 2026 19:03:27 +0330 Subject: [PATCH] =?UTF-8?q?fix(web):=20blank-screen=20hardening=20?= =?UTF-8?q?=E2=80=94=20stale=20chunk=20recovery,=20error=20boundary,=20no-?= =?UTF-8?q?cache=20index.html?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - main.tsx: on vite:preloadError (stale index.html referencing removed hashed chunks after a redeploy) reload the page immediately. - New ErrorBoundary around the whole router: render errors show a Persian fallback with «تلاش مجدد» instead of wiping the DOM. - nginx: index.html served with Cache-Control no-cache,no-store, must-revalidate (verified live); /assets/* stays public, immutable. --- frontend/nginx.conf | 9 ++++- frontend/src/App.tsx | 5 ++- frontend/src/components/ErrorBoundary.tsx | 48 +++++++++++++++++++++++ frontend/src/main.tsx | 8 ++++ 4 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/ErrorBoundary.tsx diff --git a/frontend/nginx.conf b/frontend/nginx.conf index e3f8d02..59228a6 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -34,7 +34,14 @@ server { try_files $uri $uri/ /index.html; } - # Static assets cache + # index.html itself must never be cached: after a redeploy the hashed + # /assets/* names change, and a stale cached HTML would 404 on the old + # chunks (blank page). Content-hashed assets below stay immutable. + location = /index.html { + add_header Cache-Control "no-cache, no-store, must-revalidate"; + } + + # Static assets cache — hashed filenames are content-addressed location /assets/ { expires 30d; add_header Cache-Control "public, immutable"; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index fdf1f7b..3cace90 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,6 +2,7 @@ import { lazy, Suspense } from "react"; import { Navigate, Route, Routes } from "react-router-dom"; import { useAuthStore } from "./store/auth"; import { MobileTabBar } from "./components/layout/MobileTabBar"; +import { ErrorBoundary } from "./components/ErrorBoundary"; // هر صفحه با React.lazy لود می‌شود (code splitting) const DashboardPage = lazy(() => @@ -66,7 +67,7 @@ function HomeRoute() { export default function App() { return ( - <> + }> } /> @@ -197,6 +198,6 @@ export default function App() { - + ); } diff --git a/frontend/src/components/ErrorBoundary.tsx b/frontend/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..f263a55 --- /dev/null +++ b/frontend/src/components/ErrorBoundary.tsx @@ -0,0 +1,48 @@ +import { Component, type ErrorInfo, type ReactNode } from "react"; + +interface Props { + children: ReactNode; +} + +interface State { + error: Error | null; +} + +// خطای رندر/لود ماژول نباید کل DOM را خالی کند — با این باکس، به‌جای صفحه‌ی +// سفید، پیام + دکمه‌ی «تلاش مجدد» (بارگذاری مجدد صفحه) نمایش داده می‌شود. +export class ErrorBoundary extends Component { + state: State = { error: null }; + + static getDerivedStateFromError(error: Error): State { + return { error }; + } + + componentDidCatch(error: Error, info: ErrorInfo) { + console.error("ErrorBoundary caught:", error, info); + } + + private retry = () => { + this.setState({ error: null }); + window.location.reload(); + }; + + render() { + if (!this.state.error) return this.props.children; + return ( +
+
⚠️
+

خطایی در نمایش صفحه رخ داد

+

+ {this.state.error.message || "مشکلی پیش آمد — لطفاً دوباره تلاش کنید."} +

+ +
+ ); + } +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 40ae341..20e4b71 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -7,6 +7,14 @@ import App from "./App"; import "./styles/globals.css"; import { queryClient } from "./lib/queryClient"; +// بعد از استقرار نسخه‌ی جدید، مرورگری که index.html قدیمی را باز کرده برای +// بارگذاری chunkهای حذف‌شده 404 می‌گیرد و صفحه خالی می‌ماند. این شنونده، +// همان لحظه صفحه را دوباره بارگذاری می‌کند تا نسخه‌ی تازه (با نام فایل‌های +// درست) گرفته شود. +window.addEventListener("vite:preloadError", () => { + window.location.reload(); +}); + createRoot(document.getElementById("root")!).render(