fix(web): blank-screen hardening — stale chunk recovery, error boundary, no-cache index.html
- 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.
This commit is contained in:
+8
-1
@@ -34,7 +34,14 @@ server {
|
|||||||
try_files $uri $uri/ /index.html;
|
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/ {
|
location /assets/ {
|
||||||
expires 30d;
|
expires 30d;
|
||||||
add_header Cache-Control "public, immutable";
|
add_header Cache-Control "public, immutable";
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { lazy, Suspense } from "react";
|
|||||||
import { Navigate, Route, Routes } from "react-router-dom";
|
import { Navigate, Route, Routes } from "react-router-dom";
|
||||||
import { useAuthStore } from "./store/auth";
|
import { useAuthStore } from "./store/auth";
|
||||||
import { MobileTabBar } from "./components/layout/MobileTabBar";
|
import { MobileTabBar } from "./components/layout/MobileTabBar";
|
||||||
|
import { ErrorBoundary } from "./components/ErrorBoundary";
|
||||||
|
|
||||||
// هر صفحه با React.lazy لود میشود (code splitting)
|
// هر صفحه با React.lazy لود میشود (code splitting)
|
||||||
const DashboardPage = lazy(() =>
|
const DashboardPage = lazy(() =>
|
||||||
@@ -66,7 +67,7 @@ function HomeRoute() {
|
|||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<>
|
<ErrorBoundary>
|
||||||
<Suspense fallback={<Loading />}>
|
<Suspense fallback={<Loading />}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
@@ -197,6 +198,6 @@ export default function App() {
|
|||||||
</Routes>
|
</Routes>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<MobileTabBar />
|
<MobileTabBar />
|
||||||
</>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<Props, State> {
|
||||||
|
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 (
|
||||||
|
<div className="flex min-h-screen flex-col items-center justify-center gap-4 bg-bg px-6 text-center text-ink">
|
||||||
|
<div className="text-4xl">⚠️</div>
|
||||||
|
<h1 className="text-lg font-bold">خطایی در نمایش صفحه رخ داد</h1>
|
||||||
|
<p className="max-w-md text-sm text-muted">
|
||||||
|
{this.state.error.message || "مشکلی پیش آمد — لطفاً دوباره تلاش کنید."}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={this.retry}
|
||||||
|
className="cursor-pointer rounded-lg bg-accent px-5 py-2.5 text-sm font-bold text-white transition-colors hover:bg-accent-dark"
|
||||||
|
>
|
||||||
|
تلاش مجدد
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,14 @@ import App from "./App";
|
|||||||
import "./styles/globals.css";
|
import "./styles/globals.css";
|
||||||
import { queryClient } from "./lib/queryClient";
|
import { queryClient } from "./lib/queryClient";
|
||||||
|
|
||||||
|
// بعد از استقرار نسخهی جدید، مرورگری که index.html قدیمی را باز کرده برای
|
||||||
|
// بارگذاری chunkهای حذفشده 404 میگیرد و صفحه خالی میماند. این شنونده،
|
||||||
|
// همان لحظه صفحه را دوباره بارگذاری میکند تا نسخهی تازه (با نام فایلهای
|
||||||
|
// درست) گرفته شود.
|
||||||
|
window.addEventListener("vite:preloadError", () => {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
|
|||||||
Reference in New Issue
Block a user