feat(maintenance): show maintenance page when the API is in maintenance

The backend now answers 503 with code MAINTENANCE_MODE while maintenance is
on. Without this change a visitor got a red error toast over a broken page
client-side, and a silently empty page server-side, because fetchReq discards
the status and returns null on any failure.

- lib/maintenance.js detects the state by BOTH status 503 and the error code;
  a bare 503 can come from a reverse proxy and is not maintenance
- The axios interceptor checks it before the 401 branch, so a maintenance
  response never triggers the refresh-token path or logs the user out
- fetchReq redirects to /maintenance, with a silentMaintenance opt-out used by
  getStateInfo: that one runs inside generateMetadata and while rendering the
  maintenance page itself, where a redirect is either ineffective or loops
- redirect() works by throwing, so the try/catch blocks in the doctors,
  clinics and specialties pages now rethrow NEXT_REDIRECT instead of
  swallowing it
- clinicApi.js handles 503 too; it previously rendered maintenance as a clinic
  with zero doctors
- The page reuses the existing 404 design and is marked noindex

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-07-19 22:01:45 +03:30
co-authored by Claude Fable 5
parent 18484f974d
commit daf38c8631
11 changed files with 393 additions and 4 deletions
+5 -1
View File
@@ -1,4 +1,5 @@
import { fetchReq } from "@/lib/req";
import { isNextRedirectError } from "@/lib/maintenance";
import ClinicsPage from "@/components/clinics";
import Layout from "@/components/layout/StLayout";
import { getStateInfo } from "@/lib/getStateInfo";
@@ -62,7 +63,10 @@ export default async function Clinics({ searchParams }) {
clinics = await fetchReq(`${API_URL}/api/v1/clinics`, {
params,
});
} catch (err) { }
} catch (err) {
// ریدایرکت نکست با throw کار می‌کند؛ بدون این rethrow، ریدایرکت حالت تعمیرات بلعیده می‌شود.
if (isNextRedirectError(err)) throw err;
}
return (
<Layout name="/clinics">
+3
View File
@@ -3,6 +3,7 @@ import Layout from "@/components/layout/StLayout";
import { buildDoctorParams } from "@/helper";
import { getStateInfo } from "@/lib/getStateInfo";
import { fetchReq } from "@/lib/req";
import { isNextRedirectError } from "@/lib/maintenance";
import { listingRobots } from "@/lib/listingRobots";
export async function generateMetadata({ searchParams }) {
@@ -62,6 +63,8 @@ async function Doctors({ searchParams }) {
params,
});
} catch (error) {
// ریدایرکت نکست با throw کار می‌کند؛ بدون این rethrow، ریدایرکت حالت تعمیرات بلعیده می‌شود.
if (isNextRedirectError(error)) throw error;
console.error("Error fetching doctors:", error);
}
+23
View File
@@ -0,0 +1,23 @@
import Layout from "@/components/layout/StLayout";
import MaintenancePage from "@/components/maintenance";
// وضعیت تعمیرات هر لحظه ممکن است عوض شود؛ این صفحه هرگز نباید کش شود.
export const dynamic = "force-dynamic";
export async function generateMetadata() {
return {
title: "در حال به‌روزرسانی سیستم",
description: "سامانه موقتاً برای انجام عملیات فنی در دسترس نیست.",
robots: { index: false, follow: false },
};
}
function Maintenance() {
return (
<Layout title="در حال به‌روزرسانی سیستم" disableFooter={true}>
<MaintenancePage />
</Layout>
);
}
export default Maintenance;
+4 -1
View File
@@ -9,6 +9,7 @@ import { resolveCityDisplayName } from "@/lib/domainHelpers";
import { buildSpecialtyFaq, buildSpecialtyIntro } from "@/lib/specialtyContent";
import { safeJsonLd } from "@/lib/sanitize";
import { fetchReq } from "@/lib/req";
import { isNextRedirectError } from "@/lib/maintenance";
const API_URL = process.env.NEXT_PUBLIC_API_URL;
const DOCTOR_LIMIT = 12;
@@ -32,7 +33,9 @@ const getDoctors = cache(async (specialtyId, cityId) => {
items: res?.data?.data ?? res?.data ?? [],
total: Number(res?.data?.meta?.totalRecords ?? res?.meta?.totalRecords ?? 0),
};
} catch {
} catch (error) {
// ریدایرکت نکست با throw کار می‌کند؛ بدون این rethrow، ریدایرکت حالت تعمیرات بلعیده می‌شود.
if (isNextRedirectError(error)) throw error;
return { items: [], total: 0 };
}
});