feat: add health checks, OG image generation, and R2 integration

- Implement health check system with caching and singleflight pattern
- Add OG image generation via Satori with R2 storage backend
- Configure Railway deployment with health check endpoint
- Add connection pooling and Unix socket support for Bun SSR
- Block external access to internal routes (/internal/*)
This commit is contained in:
2026-01-05 03:16:55 -06:00
parent 9de3c84f00
commit 81d9541b44
27 changed files with 2183 additions and 127 deletions
+12 -29
View File
@@ -27,46 +27,29 @@ export async function initLogger() {
const useJsonLogs =
process.env.LOG_JSON === "true" || process.env.LOG_JSON === "1";
try {
if (!useJsonLogs) {
await configure({
sinks: {
console: getConsoleSink(),
},
filters: {},
loggers: [
{
category: ["logtape", "meta"],
lowestLevel: "warning",
sinks: ["console"],
},
{
category: [],
lowestLevel: "debug",
sinks: ["console"],
},
],
});
return;
}
const sinkName = useJsonLogs ? "json" : "console";
const sink = useJsonLogs
? (record: LogRecord) => {
process.stdout.write(railwayFormatter(record));
}
: getConsoleSink();
try {
await configure({
sinks: {
json: (record: LogRecord) => {
process.stdout.write(railwayFormatter(record));
},
[sinkName]: sink,
},
filters: {},
loggers: [
{
category: ["logtape", "meta"],
lowestLevel: "warning",
sinks: ["json"],
sinks: [sinkName],
},
{
category: ["ssr"],
lowestLevel: "info",
sinks: ["json"],
category: [],
lowestLevel: "debug",
sinks: [sinkName],
},
],
});
+39
View File
@@ -0,0 +1,39 @@
import { read } from "$app/server";
import { CustomFont, resolveFonts } from "@ethercorps/sveltekit-og/fonts";
import HankenGrotesk900 from "@fontsource/hanken-grotesk/files/hanken-grotesk-latin-900-normal.woff?url";
import SchibstedGrotesk400 from "@fontsource/schibsted-grotesk/files/schibsted-grotesk-latin-400-normal.woff?url";
import Inter500 from "@fontsource/inter/files/inter-latin-500-normal.woff?url";
/**
* Load fonts for OG image generation.
* Fonts are sourced from @fontsource packages and imported directly from node_modules.
* Must be called on each request (fonts can't be cached globally in server context).
*
* Note: Only WOFF/TTF/OTF formats are supported by Satori (not WOFF2).
*/
export async function loadOGFonts() {
const fonts = [
new CustomFont(
"Hanken Grotesk",
() => read(HankenGrotesk900).arrayBuffer(),
{
weight: 900,
style: "normal",
},
),
new CustomFont(
"Schibsted Grotesk",
() => read(SchibstedGrotesk400).arrayBuffer(),
{
weight: 400,
style: "normal",
},
),
new CustomFont("Inter", () => read(Inter500).arrayBuffer(), {
weight: 500,
style: "normal",
}),
];
return await resolveFonts(fonts);
}
+116
View File
@@ -0,0 +1,116 @@
/**
* Generate OG image HTML template matching xevion.dev dark aesthetic.
* Satori only supports flex layouts and subset of CSS.
*/
export function generateOGTemplate({
title,
subtitle,
type = "default",
}: {
title: string;
subtitle?: string;
type?: "default" | "project";
}): string {
return `
<div
style="
display: flex;
width: 1200px;
height: 630px;
background-color: #000000;
color: #fafafa;
font-family: 'Schibsted Grotesk', sans-serif;
padding: 60px 80px;
"
>
<div
style="
display: flex;
flex-direction: column;
justify-content: space-between;
width: 100%;
height: 100%;
"
>
<!-- Main Content -->
<div style="display: flex; flex-direction: column; flex: 1; justify-content: center;">
<h1
style="
font-family: 'Hanken Grotesk', sans-serif;
font-weight: 900;
font-size: ${type === "project" ? "72px" : "96px"};
line-height: 1.1;
margin: 0;
color: #ffffff;
"
>
${escapeHtml(title)}
</h1>
${
subtitle
? `
<p
style="
font-family: 'Schibsted Grotesk', sans-serif;
font-size: 36px;
margin: 32px 0 0 0;
color: #a1a1aa;
line-height: 1.4;
"
>
${escapeHtml(subtitle)}
</p>
`
: ""
}
</div>
<!-- Footer -->
<div
style="
display: flex;
justify-content: space-between;
align-items: flex-end;
border-top: 2px solid #27272a;
padding-top: 24px;
"
>
<div
style="
font-size: 28px;
color: #71717a;
font-weight: 500;
"
>
xevion.dev
</div>
${
type === "project"
? `
<div
style="
font-size: 24px;
color: #52525b;
text-transform: uppercase;
letter-spacing: 0.05em;
"
>
PROJECT
</div>
`
: ""
}
</div>
</div>
</div>
`;
}
function escapeHtml(text: string): string {
return text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
+33
View File
@@ -0,0 +1,33 @@
/**
* Discriminated union of all OG image types.
*
* IMPORTANT: Keep in sync with Rust's OGImageSpec in src/og.rs
*/
export type OGImageSpec =
| { type: "index" }
| { type: "projects" }
| { type: "project"; id: string };
/**
* Generate the R2 public URL for an OG image.
* Called at ISR/build time when generating page metadata.
*
* @param spec - The OG image specification
* @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;
if (!R2_BASE) {
throw new Error("VITE_OG_R2_BASE_URL environment variable is not set");
}
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`;
}
}
+14
View File
@@ -0,0 +1,14 @@
import type { LayoutServerLoad } from "./$types";
import { getOGImageUrl } from "$lib/og-types";
export const load: LayoutServerLoad = async ({ url }) => {
return {
metadata: {
title: "Xevion.dev",
description:
"The personal website of Xevion, a full-stack software developer.",
ogImage: getOGImageUrl({ type: "index" }),
url: url.toString(),
},
};
};
+28 -6
View File
@@ -6,16 +6,38 @@
import "@fontsource/schibsted-grotesk/500.css";
import "@fontsource/schibsted-grotesk/600.css";
let { children } = $props();
let { children, data } = $props();
const metadata = data?.metadata ?? {
title: "Xevion.dev",
description:
"The personal website of Xevion, a full-stack software developer.",
ogImage: "/api/og/home.png",
url: "https://xevion.dev",
};
</script>
<svelte:head>
<link rel="icon" href="/favicon.ico" />
<title>Xevion.dev</title>
<meta
name="description"
content="The personal website of Xevion, a full-stack software developer."
/>
<!-- Primary Meta Tags -->
<title>{metadata.title}</title>
<meta name="description" content={metadata.description} />
<!-- Open Graph Meta Tags -->
<meta property="og:type" content="website" />
<meta property="og:url" content={metadata.url} />
<meta property="og:title" content={metadata.title} />
<meta property="og:description" content={metadata.description} />
<meta property="og:image" content={metadata.ogImage} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<!-- Twitter Card Meta Tags -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={metadata.title} />
<meta name="twitter:description" content={metadata.description} />
<meta name="twitter:image" content={metadata.ogImage} />
</svelte:head>
{@render children()}
+41
View File
@@ -0,0 +1,41 @@
import type { RequestHandler } from "./$types";
import { apiFetch } from "$lib/api";
import { getLogger } from "@logtape/logtape";
const logger = getLogger(["ssr", "routes", "internal", "health"]);
/**
* Internal health check endpoint.
* Called by Rust server to validate full round-trip connectivity.
*
* IMPORTANT: This endpoint should never be accessible externally.
* It's blocked by the Rust ISR handler's /internal/* check.
*/
export const GET: RequestHandler = async () => {
try {
// Test connectivity to Rust API by fetching projects
// Use a 5 second timeout for this health check
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
const projects = await apiFetch("/api/projects", {
signal: controller.signal,
});
clearTimeout(timeoutId);
// Validate response shape
if (!Array.isArray(projects)) {
logger.error("Health check failed: /api/projects returned non-array");
return new Response("Internal health check failed", { status: 503 });
}
logger.debug("Health check passed", { projectCount: projects.length });
return new Response("OK", { status: 200 });
} catch (error) {
logger.error("Health check failed", {
error: error instanceof Error ? error.message : String(error),
});
return new Response("Internal health check failed", { status: 503 });
}
};
+144
View File
@@ -0,0 +1,144 @@
import { ImageResponse } from "@ethercorps/sveltekit-og";
import type { RequestHandler } from "./$types";
import { loadOGFonts } from "$lib/og-fonts";
import { generateOGTemplate } from "$lib/og-template";
import { apiFetch } from "$lib/api";
import type { Project } from "../../projects/+page.server";
import type { OGImageSpec } from "$lib/og-types";
import { getLogger } from "@logtape/logtape";
const logger = getLogger(["ssr", "routes", "internal", "ogp"]);
/**
* Internal endpoint for OG image generation.
* Called by Rust server via POST with OGImageSpec JSON body.
*
* IMPORTANT: This endpoint should never be accessible externally.
* It's blocked by the Rust ISR handler's /internal/* check.
*/
export const POST: RequestHandler = async ({ request }) => {
let spec: OGImageSpec;
try {
spec = await request.json();
} catch {
logger.warn("Invalid JSON body received");
return new Response("Invalid JSON body", { status: 400 });
}
return await generateOGImage(spec);
};
/**
* GET handler for OG image generation using query parameters.
* Supports: ?type=index, ?type=projects, ?type=project&id=<id>
*/
export const GET: RequestHandler = async ({ url }) => {
const type = url.searchParams.get("type");
if (!type) {
logger.warn("Missing 'type' query parameter");
return new Response("Missing 'type' query parameter", { status: 400 });
}
let spec: OGImageSpec;
switch (type) {
case "index":
spec = { type: "index" };
break;
case "projects":
spec = { type: "projects" };
break;
case "project": {
const id = url.searchParams.get("id");
if (!id) {
logger.warn("Missing 'id' query parameter for project type");
return new Response("Missing 'id' query parameter for project type", {
status: 400,
});
}
spec = { type: "project", id };
break;
}
default:
logger.warn("Invalid 'type' query parameter", { type });
return new Response(`Invalid 'type' query parameter: ${type}`, {
status: 400,
});
}
return await generateOGImage(spec);
};
async function generateOGImage(spec: OGImageSpec): Promise<Response> {
logger.info("Generating OG image", { spec });
const templateData = await getTemplateData(spec);
try {
const fonts = await loadOGFonts();
const html = generateOGTemplate(templateData);
const imageResponse = new ImageResponse(html, {
width: 1200,
height: 630,
fonts,
});
const imageBuffer = await imageResponse.arrayBuffer();
logger.info("OG image generated successfully", { spec });
return new Response(imageBuffer, {
status: 200,
headers: { "Content-Type": "image/png" },
});
} catch (error) {
logger.error("OG image generation failed", {
spec,
error: error instanceof Error ? error.message : String(error),
});
return new Response("Failed to generate image", { status: 500 });
}
}
async function getTemplateData(spec: OGImageSpec): Promise<{
title: string;
subtitle?: string;
type?: "default" | "project";
}> {
switch (spec.type) {
case "index":
return {
title: "Ryan Walters",
subtitle: "Full-Stack Software Engineer",
type: "default",
};
case "projects":
return {
title: "Projects",
subtitle: "created, maintained, or contributed to by me...",
type: "default",
};
case "project":
try {
const projects = await apiFetch<Project[]>("/api/projects");
const project = projects.find((p) => p.id === spec.id);
if (project) {
return {
title: project.name,
subtitle: project.shortDescription,
type: "project",
};
}
} catch (error) {
logger.error("Failed to fetch project", { id: spec.id, error });
}
return {
title: "Project",
subtitle: "View on xevion.dev",
type: "project",
};
}
}
+8 -1
View File
@@ -1,5 +1,6 @@
import type { PageServerLoad } from "./$types";
import { apiFetch } from "$lib/api";
import { getOGImageUrl } from "$lib/og-types";
interface ProjectLink {
url: string;
@@ -14,9 +15,15 @@ export interface Project {
links: ProjectLink[];
}
export const load: PageServerLoad = async () => {
export const load: PageServerLoad = async ({ url }) => {
const projects = await apiFetch<Project[]>("/api/projects");
return {
projects,
metadata: {
title: "Projects | Xevion.dev",
description: "...",
ogImage: getOGImageUrl({ type: "projects" }),
url: url.toString(),
},
};
};