Files
homepage/src/routes/errors/[status=httpStatus]/+page.svelte
T
Alexander b10634f831
CI / update (push) Successful in 39s
feat(errors): per-status static error pages for nginx fallback
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.
2026-05-02 20:11:34 +02:00

172 lines
4.4 KiB
Svelte

<script lang="ts">
import Header from '$lib/components/Header.svelte';
import Lock from '@lucide/svelte/icons/lock';
import Ban from '@lucide/svelte/icons/ban';
import SearchX from '@lucide/svelte/icons/search-x';
import TriangleAlert from '@lucide/svelte/icons/triangle-alert';
import CircleAlert from '@lucide/svelte/icons/circle-alert';
import { getErrorTitle, getErrorDescription, getErrorBibleQuote } from '$lib/js/errorStrings';
let { data } = $props();
const status = data.status;
const Icon = (() => {
switch (status) {
case 401: return Lock;
case 403: return Ban;
case 404: return SearchX;
case 500: return TriangleAlert;
default: return CircleAlert;
}
})();
const title = getErrorTitle(status, false);
const description = getErrorDescription(status, false);
const quote = getErrorBibleQuote(status, false);
const otherLangHref = `https://bocken.org/errors/en/${status}.html`;
</script>
<svelte:head>
<title>{title} — Alexander's Website</title>
<meta name="robots" content="noindex" />
<link rel="alternate" hreflang="en" href={otherLangHref} />
<link rel="alternate" hreflang="de" href={`https://bocken.org/errors/${status}.html`} />
</svelte:head>
<Header>
{#snippet links()}
<ul class="site_header"></ul>
{/snippet}
{#snippet language_selector_desktop()}
<a class="lang-toggle" href={otherLangHref} hreflang="en" aria-label="Switch to English">EN</a>
{/snippet}
<main class="error-page" lang="de">
<article class="error-article">
<header class="eyebrow">
<Icon size={14} strokeWidth={1.5} aria-hidden="true" />
<span class="eyebrow-label">Fehler</span>
</header>
<div class="code" aria-hidden="true">{status}</div>
<h1 class="title">{title}</h1>
<p class="description">{description}</p>
{#if quote}
<figure class="quote">
<blockquote class="quote-text">{quote.text}</blockquote>
<figcaption class="quote-reference">{quote.reference}</figcaption>
</figure>
{/if}
</article>
</main>
</Header>
<style>
.error-page {
min-height: calc(100vh - 6rem);
display: flex;
align-items: flex-start;
justify-content: center;
padding: clamp(3rem, 10vh, 8rem) 1.5rem 4rem;
background: var(--color-bg-primary);
}
.error-article {
width: 100%;
max-width: 640px;
}
.eyebrow {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: var(--color-text-tertiary);
margin-bottom: 1rem;
}
.eyebrow-label {
font-size: 0.75rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
}
.code {
font-size: clamp(7rem, 22vw, 14rem);
font-weight: 200;
line-height: 0.9;
letter-spacing: -0.05em;
color: var(--color-text-primary);
margin: 0 0 2rem;
font-variant-numeric: lining-nums tabular-nums;
}
.title {
font-size: clamp(1.5rem, 3vw, 2rem);
font-weight: 500;
letter-spacing: -0.01em;
color: var(--color-text-primary);
margin: 0 0 0.5rem;
}
.description {
font-size: 1.0625rem;
line-height: 1.55;
color: var(--color-text-secondary);
margin: 0;
max-width: 44ch;
}
.quote {
margin: 2rem 0 0;
padding-left: 1rem;
border-left: 2px solid var(--color-border);
max-width: 44ch;
}
.quote-text {
font-family: Georgia, "Times New Roman", Cambria, serif;
font-style: italic;
font-size: 1.0625rem;
line-height: 1.5;
color: var(--color-text-primary);
margin: 0 0 0.5rem;
text-wrap: balance;
hyphens: auto;
}
.quote-reference {
font-size: 0.7rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--color-text-tertiary);
}
.lang-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 2rem;
height: 1.85rem;
padding: 0 0.6rem;
border-radius: 100px;
border: 1px solid var(--nav-btn-border, rgba(255,255,255,0.2));
color: var(--nav-text, #999);
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.05em;
text-decoration: none;
transition: all 0.15s;
}
.lang-toggle:hover,
.lang-toggle:focus-visible {
color: var(--nav-text-hover, white);
background: var(--nav-hover-bg, rgba(255,255,255,0.1));
border-color: var(--nav-btn-border-hover, rgba(255,255,255,0.4));
}
</style>