feat(maintenance): static fallback when the CMS is unreachable
Adds a self-contained maintenance page that the SvelteKit handle hook serves before the first CMS call when `isCmsAvailable()` returns false. The probe hits `/api-docs/openapi.json` with a short timeout and caches the result (10s healthy, 3s unhealthy) so we don't pay a round-trip per request. `/maintenance` and `?cms_down=1` simulate the page without taking the CMS down — useful for previewing the copy. The maintenance HTML is fully inline (no CMS calls, no layout, no external CSS) so it survives even when every dependency is down. Returned with 503 + Retry-After:60 + noindex headers. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import { getTranslationBundleBySlug } from '$lib/cms';
|
||||
import { isCmsAvailable } from '$lib/cms-health.server';
|
||||
import { maintenanceResponse } from '$lib/maintenance.server';
|
||||
import { TRANSLATION_BUNDLE_SLUG } from '$lib/constants';
|
||||
import type { Handle } from '@sveltejs/kit';
|
||||
|
||||
@@ -9,6 +11,8 @@ import type { Handle } from '@sveltejs/kit';
|
||||
const PAGE_CACHE_CONTROL =
|
||||
'public, max-age=0, s-maxage=30, stale-while-revalidate=300';
|
||||
|
||||
const ASSET_EXT_RE = /\.(ico|png|jpe?g|webp|gif|svg|css|js|mjs|map|woff2?|ttf|otf|txt|xml|json|pdf)$/i;
|
||||
|
||||
function isCacheablePath(pathname: string): boolean {
|
||||
if (pathname.startsWith('/api/')) return false;
|
||||
if (pathname.startsWith('/cms-images/')) return false;
|
||||
@@ -16,7 +20,37 @@ function isCacheablePath(pathname: string): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Pfade auf denen wir vor dem Resolve einen CMS-Health-Check machen. */
|
||||
function shouldHealthCheck(pathname: string): boolean {
|
||||
if (pathname.startsWith('/api/')) return false;
|
||||
if (pathname.startsWith('/_app/')) return false;
|
||||
if (pathname.startsWith('/cms-images/')) return false;
|
||||
if (pathname.startsWith('/cms-files/')) return false;
|
||||
if (pathname === '/sitemap.xml' || pathname === '/robots.txt') return false;
|
||||
if (ASSET_EXT_RE.test(pathname)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function isMaintenanceSimulation(pathname: string, search: URLSearchParams): boolean {
|
||||
if (pathname === '/maintenance' || pathname === '/maintenance/') return true;
|
||||
if (search.has('cms_down')) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
// Simulation: erlaubt manuelles Triggern der Maintenance-Seite ohne
|
||||
// CMS abzuschalten. /maintenance oder ?cms_down=1.
|
||||
if (isMaintenanceSimulation(event.url.pathname, event.url.searchParams)) {
|
||||
return maintenanceResponse(200);
|
||||
}
|
||||
|
||||
// Echter Ausfall: vor dem ersten CMS-Call prüfen ob die API antwortet.
|
||||
// Cached, daher kein Roundtrip pro Request.
|
||||
if (event.request.method === 'GET' && shouldHealthCheck(event.url.pathname)) {
|
||||
const ok = await isCmsAvailable();
|
||||
if (!ok) return maintenanceResponse(503);
|
||||
}
|
||||
|
||||
try {
|
||||
const bundle = await getTranslationBundleBySlug(TRANSLATION_BUNDLE_SLUG, { locale: 'de' });
|
||||
if (bundle?.strings && typeof bundle.strings === 'object') {
|
||||
|
||||
Reference in New Issue
Block a user