mirror of
https://github.com/Xevion/xevion.dev.git
synced 2026-01-31 08:26:41 -06:00
feat: add prerendered error pages with Rust integration
- Prerender 20+ HTTP error codes (4xx/5xx) at build time - Embed error pages in binary and serve via Axum - Intercept error responses for HTML requests - Add transient flag for retry-worthy errors (502/503/504)
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Single source of truth for all HTTP error codes.
|
||||
* Used by:
|
||||
* - SvelteKit EntryGenerator (prerendering)
|
||||
* - Error page components (rendering)
|
||||
* - Rust assets.rs (validation only)
|
||||
*/
|
||||
export const ERROR_CODES = {
|
||||
// 4xx Client Errors
|
||||
400: { message: "Bad request", transient: false },
|
||||
401: { message: "Unauthorized", transient: false },
|
||||
403: { message: "Forbidden", transient: false },
|
||||
404: { message: "Page not found", transient: false },
|
||||
405: { message: "Method not allowed", transient: false },
|
||||
406: { message: "Not acceptable", transient: false },
|
||||
408: { message: "Request timeout", transient: true },
|
||||
409: { message: "Conflict", transient: false },
|
||||
410: { message: "Gone", transient: false },
|
||||
413: { message: "Payload too large", transient: false },
|
||||
414: { message: "URI too long", transient: false },
|
||||
415: { message: "Unsupported media type", transient: false },
|
||||
418: { message: "I'm a teapot", transient: false }, // RFC 2324 Easter egg
|
||||
422: { message: "Unprocessable entity", transient: false },
|
||||
429: { message: "Too many requests", transient: true },
|
||||
451: { message: "Unavailable for legal reasons", transient: false },
|
||||
|
||||
// 5xx Server Errors
|
||||
500: { message: "Internal server error", transient: false },
|
||||
501: { message: "Not implemented", transient: false },
|
||||
502: { message: "Bad gateway", transient: true },
|
||||
503: { message: "Service unavailable", transient: true },
|
||||
504: { message: "Gateway timeout", transient: true },
|
||||
505: { message: "HTTP version not supported", transient: false },
|
||||
} as const;
|
||||
|
||||
export type ErrorCode = keyof typeof ERROR_CODES;
|
||||
|
||||
// Helper to check if error code is defined
|
||||
export function isDefinedErrorCode(code: number): code is ErrorCode {
|
||||
return code in ERROR_CODES;
|
||||
}
|
||||
+13
-12
@@ -16,18 +16,19 @@ export type OGImageSpec =
|
||||
* @returns Full URL to the R2-hosted image
|
||||
*/
|
||||
export function getOGImageUrl(spec: OGImageSpec): string {
|
||||
const R2_BASE = import.meta.env.VITE_OG_R2_BASE_URL;
|
||||
const R2_BASE = import.meta.env.VITE_OG_R2_BASE_URL;
|
||||
|
||||
if (!R2_BASE) {
|
||||
throw new Error("VITE_OG_R2_BASE_URL environment variable is not set");
|
||||
}
|
||||
if (!R2_BASE) {
|
||||
// During prerendering or development, use a fallback placeholder
|
||||
return "/og/placeholder.png";
|
||||
}
|
||||
|
||||
switch (spec.type) {
|
||||
case "index":
|
||||
return `${R2_BASE}/og/index.png`;
|
||||
case "projects":
|
||||
return `${R2_BASE}/og/projects.png`;
|
||||
case "project":
|
||||
return `${R2_BASE}/og/project/${spec.id}.png`;
|
||||
}
|
||||
switch (spec.type) {
|
||||
case "index":
|
||||
return `${R2_BASE}/og/index.png`;
|
||||
case "projects":
|
||||
return `${R2_BASE}/og/projects.png`;
|
||||
case "project":
|
||||
return `${R2_BASE}/og/project/${spec.id}.png`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import AppWrapper from "$lib/components/AppWrapper.svelte";
|
||||
import { page } from "$app/stores";
|
||||
|
||||
const status = $derived($page.status);
|
||||
|
||||
const messages: Record<number, string> = {
|
||||
404: "Page not found",
|
||||
405: "Method not allowed",
|
||||
500: "Something went wrong",
|
||||
502: "Service temporarily unavailable",
|
||||
503: "Service temporarily unavailable",
|
||||
};
|
||||
|
||||
const message = $derived(messages[status] || "An error occurred");
|
||||
const showHomeLink = $derived(![502, 503].includes(status));
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{status} - {message}</title>
|
||||
</svelte:head>
|
||||
|
||||
<AppWrapper>
|
||||
<div class="flex min-h-screen items-center justify-center">
|
||||
<div class="mx-4 max-w-2xl text-center">
|
||||
<h1 class="mb-4 font-hanken text-8xl text-zinc-200">{status}</h1>
|
||||
<p class="mb-8 text-2xl text-zinc-400">{message}</p>
|
||||
{#if showHomeLink}
|
||||
<a
|
||||
href="/"
|
||||
class="inline-block rounded-sm bg-zinc-900 px-4 py-2 text-zinc-100 transition-colors hover:bg-zinc-800"
|
||||
>
|
||||
Return home
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</AppWrapper>
|
||||
@@ -0,0 +1,2 @@
|
||||
// Reset layout chain - no server-side data loading for error pages
|
||||
export const prerender = true;
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ERROR_CODES } from "$lib/error-codes";
|
||||
import type { EntryGenerator, PageServerLoad } from "./$types";
|
||||
|
||||
/**
|
||||
* Tell SvelteKit to prerender all defined error codes.
|
||||
* This generates static HTML files at build time.
|
||||
*/
|
||||
export const entries: EntryGenerator = () => {
|
||||
return Object.keys(ERROR_CODES).map((code) => ({ code }));
|
||||
};
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
/**
|
||||
* Load error metadata for the page.
|
||||
* This runs during prerendering to generate static HTML.
|
||||
*/
|
||||
export const load: PageServerLoad = ({ params }) => {
|
||||
const code = parseInt(params.code, 10) as keyof typeof ERROR_CODES;
|
||||
|
||||
return {
|
||||
code,
|
||||
...ERROR_CODES[code],
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from "$app/paths";
|
||||
import AppWrapper from "$components/AppWrapper.svelte";
|
||||
import Dots from "$lib/components/Dots.svelte";
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
const title = $derived(
|
||||
`${data.code} - ${data.message.charAt(0).toUpperCase() + data.message.slice(1)}`,
|
||||
);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{title}</title>
|
||||
</svelte:head>
|
||||
|
||||
<AppWrapper>
|
||||
<div class="min-h-screen flex items-center justify-center">
|
||||
<div class="mx-4 max-w-3xl text-center">
|
||||
<h1 class="text-6xl sm:text-9xl font-hanken font-black text-zinc-200">
|
||||
{data.code}
|
||||
</h1>
|
||||
<p class="text-2xl sm:text-3xl text-zinc-400 mb-8 capitalize">
|
||||
{data.message}
|
||||
</p>
|
||||
|
||||
<!-- Only show "Return home" for non-transient errors -->
|
||||
{#if !data.transient}
|
||||
<a
|
||||
href={resolve("/")}
|
||||
class="inline-block py-2 px-4 bg-zinc-900 text-zinc-50 no-underline rounded-sm transition-colors hover:bg-zinc-800"
|
||||
>
|
||||
Return home
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</AppWrapper>
|
||||
Reference in New Issue
Block a user