feat(errors): per-status static error pages for nginx fallback
CI / update (push) Successful in 39s

Adds prerendered, JS-less, self-contained error pages for nginx
error_page use — served directly from /var/www/errors/ when the
SvelteKit upstream is unreachable or any nginx-originated 4xx/5xx
fires (including the catch-all default_server for unknown hosts).

- /errors/[status] (DE default) + /errors/en/[status] (EN), each
  with a header language toggle linking absolute to bocken.org so
  the switch works even on unknown-host fallbacks.
- httpStatus param matcher restricts entries to 401/403/404/500/
  502/503/504; entries() drives prerender output.
- generate-error-quotes.ts looks up curated bilingual references
  in the existing allioli/drb TSV bibles at prebuild time and
  writes src/lib/data/errorQuotes.json.
- build-error-page.ts (postbuild) inlines all CSS, strips module
  preloads/scripts, rewrites the home-link to canonical https URL,
  and emits .html + .gz + .br per status under build/client/errors.
- deploy.sh syncs build/client/errors → /var/www/errors with
  http:http ownership for nginx access.
This commit is contained in:
2026-05-02 20:11:34 +02:00
parent e85a2508e8
commit b10634f831
11 changed files with 652 additions and 3 deletions
+72
View File
@@ -0,0 +1,72 @@
{
"401": {
"de": {
"text": "Bittet, und es wird euch gegeben werden; suchet, und ihr werdet finden; klopfet an, und es wird euch aufgetan werden",
"reference": "Mt 7,7"
},
"en": {
"text": "Ask, and it shall be given you: seek, and you shall find: knock, and it shall be opened to you.",
"reference": "Mt 7:7"
}
},
"403": {
"de": {
"text": "Wie eng ist die Pforte, und wie schmal der Weg, der zum Leben führt; und wenige sind, die ihn finden!",
"reference": "Mt 7,14"
},
"en": {
"text": "How narrow is the gate, and strait is the way that leadeth to life: and few there are that find it!",
"reference": "Mt 7:14"
}
},
"404": {
"de": {
"text": "Denn ein jeder, der bittet, der empfängt; und wer suchet, findet; und dem, der anklopft, wird aufgetan werden.",
"reference": "Mt 7,8"
},
"en": {
"text": "For every one that asketh, receiveth: and he that seeketh, findeth: and to him that knocketh, it shall be opened.",
"reference": "Mt 7:8"
}
},
"500": {
"de": {
"text": "Wir haben aber diesen Schatz in irdenen Gefäßen, damit die Überschwenglichkeit der Kraft nicht uns, sondern Gott beigemessen werde.",
"reference": "2Kor 4,7"
},
"en": {
"text": "But we have this treasure in earthen vessels, that the excellency may be of the power of God and not of us.",
"reference": "2Cor 4:7"
}
},
"502": {
"de": {
"text": "Darum ward ihr Name Babel genannt, weil daselbst sie Sprache der ganzen Menschheit verwirrt ward; und von da zerstreute sie der Herr über alle Lande.",
"reference": "1Mo 11,9"
},
"en": {
"text": "And therefore the name thereof was called Babel, because there the language of the whole earth was confounded: and from thence the Lord scattered them abroad upon the face of all countries.",
"reference": "Gn 11:9"
}
},
"503": {
"de": {
"text": "Sei dem Herrn untergeben und bete zu ihm. Ereifere dich nicht über den, der glücklich ist auf seinem Wege, über den Mann, der Unrecht tut.",
"reference": "Ps 37,7"
},
"en": {
"text": "I am become miserable, and am bowed down even to the end: I walked sorrowful all the day long.",
"reference": "Ps 37:7"
}
},
"504": {
"de": {
"text": "die aber auf den Herrn hoffen, erneuern ihre Kraft, heben ihre Schwingen gleich Adlern, laufen und werden nicht müde, schreiten voran und werden nicht matt.",
"reference": "Jes 40,31"
},
"en": {
"text": "But they that hope in the Lord shall renew their strength, they shall take wings as eagles, they shall run and not be weary, they shall walk and not faint.",
"reference": "Is 40:31"
}
}
}
+29
View File
@@ -5,6 +5,9 @@ export function getErrorTitle(status: number, isEnglish: boolean): string {
case 403: return 'Access Denied';
case 404: return 'Page Not Found';
case 500: return 'Server Error';
case 502: return 'Bad Gateway';
case 503: return 'Service Temporarily Unavailable';
case 504: return 'Gateway Timeout';
default: return 'Error';
}
}
@@ -13,6 +16,9 @@ export function getErrorTitle(status: number, isEnglish: boolean): string {
case 403: return 'Zugriff verweigert';
case 404: return 'Seite nicht gefunden';
case 500: return 'Serverfehler';
case 502: return 'Bad Gateway';
case 503: return 'Server vorübergehend nicht erreichbar';
case 504: return 'Gateway-Zeitüberschreitung';
default: return 'Fehler';
}
}
@@ -24,6 +30,9 @@ export function getErrorDescription(status: number, isEnglish: boolean): string
case 403: return 'You do not have permission for this area.';
case 404: return 'The requested page could not be found.';
case 500: return 'An unexpected error occurred. Please try again later.';
case 502: return 'The site is briefly offline. Please try again in a few minutes.';
case 503: return 'The site is briefly offline. Please try again in a few minutes.';
case 504: return 'The upstream is taking too long to respond. Please try again shortly.';
default: return 'An unexpected error occurred.';
}
}
@@ -32,10 +41,30 @@ export function getErrorDescription(status: number, isEnglish: boolean): string
case 403: return 'Du hast keine Berechtigung für diesen Bereich.';
case 404: return 'Die angeforderte Seite konnte nicht gefunden werden.';
case 500: return 'Es ist ein unerwarteter Fehler aufgetreten. Bitte versuche es später erneut.';
case 502: return 'Die Seite ist gerade kurz offline. Bitte in ein paar Minuten erneut probieren.';
case 503: return 'Die Seite ist gerade kurz offline. Bitte in ein paar Minuten erneut probieren.';
case 504: return 'Der Server braucht zu lange zum Antworten. Bitte gleich nochmal probieren.';
default: return 'Es ist ein unerwarteter Fehler aufgetreten.';
}
}
export interface BibleQuote {
text: string;
reference: string;
}
// Generated at prebuild time by scripts/generate-error-quotes.ts from the
// allioli (DE) + drb (EN) TSV bibles. Curated reference list lives in that
// script; this JSON is the resolved verse text + display reference.
import errorQuotesData from '$lib/data/errorQuotes.json';
const errorQuotes = errorQuotesData as Record<string, { de: BibleQuote; en: BibleQuote }>;
export function getErrorBibleQuote(status: number, isEnglish: boolean): BibleQuote | null {
const entry = errorQuotes[String(status)];
if (!entry) return null;
return isEnglish ? entry.en : entry.de;
}
export const errorLabels = {
login: { en: 'Log in', de: 'Anmelden' },
tryAgain: { en: 'Try again', de: 'Erneut versuchen' },