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;
|
||||
}
|
||||
|
||||
# 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";
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<ErrorBoundary>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Routes>
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
@@ -197,6 +198,6 @@ export default function App() {
|
||||
</Routes>
|
||||
</Suspense>
|
||||
<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 { queryClient } from "./lib/queryClient";
|
||||
|
||||
// بعد از استقرار نسخهی جدید، مرورگری که index.html قدیمی را باز کرده برای
|
||||
// بارگذاری chunkهای حذفشده 404 میگیرد و صفحه خالی میماند. این شنونده،
|
||||
// همان لحظه صفحه را دوباره بارگذاری میکند تا نسخهی تازه (با نام فایلهای
|
||||
// درست) گرفته شود.
|
||||
window.addEventListener("vite:preloadError", () => {
|
||||
window.location.reload();
|
||||
});
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
|
||||
Reference in New Issue
Block a user