From 96595b073dec66f57abe206f10bb69e12d4f1873 Mon Sep 17 00:00:00 2001 From: Xevion Date: Mon, 5 Jan 2026 14:38:52 -0600 Subject: [PATCH] refactor: replace sveltekit-og with native Satori implementation - Remove @ethercorps/sveltekit-og and bits-ui dependencies - Implement direct Satori + Resvg rendering pipeline - Add OgImage.svelte component for template generation - Create /internal/ogp preview page for development - Load fonts from node_modules via fs for production compatibility - Add 2s startup delay before OG image regeneration --- src/og.rs | 12 +- web/bun.lock | 108 ++++------ web/package.json | 6 +- web/src/app.css | 2 +- web/src/lib/components/Dots.svelte | 5 + web/src/lib/components/OgImage.svelte | 52 +++++ web/src/lib/logger.ts | 17 +- web/src/lib/og-fonts.ts | 75 ++++--- web/src/lib/og-template.ts | 116 ----------- web/src/routes/+layout.svelte | 8 +- web/src/routes/internal/ogp/+page.server.ts | 43 ++++ web/src/routes/internal/ogp/+page.svelte | 190 ++++++++++++++++++ .../internal/ogp/{ => generate}/+server.ts | 145 +++++++------ web/svelte.config.js | 6 +- web/vite.config.ts | 3 - 15 files changed, 496 insertions(+), 292 deletions(-) create mode 100644 web/src/lib/components/OgImage.svelte delete mode 100644 web/src/lib/og-template.ts create mode 100644 web/src/routes/internal/ogp/+page.server.ts create mode 100644 web/src/routes/internal/ogp/+page.svelte rename web/src/routes/internal/ogp/{ => generate}/+server.ts (60%) diff --git a/src/og.rs b/src/og.rs index 291ce31..b018855 100644 --- a/src/og.rs +++ b/src/og.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use std::sync::Arc; +use std::{sync::Arc, time::Duration}; use crate::{AppState, r2::R2Client}; @@ -38,9 +38,9 @@ pub async fn generate_og_image(spec: &OGImageSpec, state: Arc) -> Resu // Call Bun's internal endpoint let bun_url = if state.downstream_url.starts_with('/') || state.downstream_url.starts_with("./") { - "http://localhost/internal/ogp".to_string() + "http://localhost/internal/ogp/generate".to_string() } else { - format!("{}/internal/ogp", state.downstream_url) + format!("{}/internal/ogp/generate", state.downstream_url) }; let client = state.unix_client.as_ref().unwrap_or(&state.http_client); @@ -48,7 +48,7 @@ pub async fn generate_og_image(spec: &OGImageSpec, state: Arc) -> Resu let response = client .post(&bun_url) .json(spec) - .timeout(std::time::Duration::from_secs(30)) + .timeout(Duration::from_secs(30)) .send() .await .map_err(|e| format!("Failed to call Bun: {}", e))?; @@ -93,8 +93,10 @@ pub async fn ensure_og_image(spec: &OGImageSpec, state: Arc) -> Result /// Regenerate common OG images (index, projects) on server startup pub async fn regenerate_common_images(state: Arc) { - tracing::info!("Regenerating common OG images"); + // Wait 2 seconds before starting + tokio::time::sleep(Duration::from_secs(2)).await; + tracing::info!("Regenerating common OG images"); let specs = vec![OGImageSpec::Index, OGImageSpec::Projects]; for spec in specs { diff --git a/web/bun.lock b/web/bun.lock index 5664542..4a05075 100644 --- a/web/bun.lock +++ b/web/bun.lock @@ -4,13 +4,15 @@ "workspaces": { "": { "dependencies": { - "@ethercorps/sveltekit-og": "^4.2.1", "@fontsource-variable/inter": "^5.2.8", + "@fontsource-variable/schibsted-grotesk": "^5.2.8", "@fontsource/hanken-grotesk": "^5.1.0", "@fontsource/schibsted-grotesk": "^5.2.8", "@logtape/logtape": "^1.3.5", - "bits-ui": "^2.8.2", + "@resvg/resvg-js": "^2.6.2", + "@xevion/satori-html": "^0.4.1", "clsx": "^2.1.1", + "satori": "^0.18.3", "tailwind-merge": "^3.3.1", }, "devDependencies": { @@ -117,16 +119,10 @@ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], - "@ethercorps/sveltekit-og": ["@ethercorps/sveltekit-og@4.2.1", "", { "dependencies": { "@resvg/resvg-wasm": "^2.6.2", "@takumi-rs/helpers": "^0.55.0", "@takumi-rs/image-response": "^0.55.0", "@takumi-rs/wasm": "^0.55.0", "satori": "^0.10.14", "satori-html": "0.3.2", "std-env": "^3.9.0", "unwasm": "^0.5.0" }, "peerDependencies": { "@sveltejs/kit": ">=2.0.0" } }, "sha512-mMkoKWMMBXL5iAYrMZqklezZDUU7HpHd+sNsz78e4gElXFyxdOnsIFfPPXpqDcUn6orZHs5MGHvtPi5II5xNAA=="], - - "@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="], - - "@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="], - - "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], - "@fontsource-variable/inter": ["@fontsource-variable/inter@5.2.8", "", {}, "sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ=="], + "@fontsource-variable/schibsted-grotesk": ["@fontsource-variable/schibsted-grotesk@5.2.8", "", {}, "sha512-nZAorDrFue4dXZbI613WdvAhu5DPH1UJYNn5fWl7Poa+nl/s9o3VUcQImIiVZpQnYG/jkPVzHsTE7WZbSDvvkw=="], + "@fontsource/hanken-grotesk": ["@fontsource/hanken-grotesk@5.2.8", "", {}, "sha512-J/e6hdfNCbyc4WK5hmZtk0zjaIsFx3pvCdPVxY25iYw2C9v1ZggGz4nfHnRjMhcz4WfaadUuwLNtvj8sQ70tkg=="], "@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="], @@ -147,8 +143,6 @@ "@iconify/utils": ["@iconify/utils@3.1.0", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "mlly": "^1.8.0" } }, "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw=="], - "@internationalized/date": ["@internationalized/date@3.10.1", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-oJrXtQiAXLvT9clCf1K4kxp3eKsQhIaZqxEyowkBcsvZDdZkbWrVmnGknxs5flTD0VGsxrxKgBCZty1EzoiMzA=="], - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -169,7 +163,31 @@ "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], - "@resvg/resvg-wasm": ["@resvg/resvg-wasm@2.6.2", "", {}, "sha512-FqALmHI8D4o6lk/LRWDnhw95z5eO+eAa6ORjVg09YRR7BkcM6oPHU9uyC0gtQG5vpFLvgpeU4+zEAz2H8APHNw=="], + "@resvg/resvg-js": ["@resvg/resvg-js@2.6.2", "", { "optionalDependencies": { "@resvg/resvg-js-android-arm-eabi": "2.6.2", "@resvg/resvg-js-android-arm64": "2.6.2", "@resvg/resvg-js-darwin-arm64": "2.6.2", "@resvg/resvg-js-darwin-x64": "2.6.2", "@resvg/resvg-js-linux-arm-gnueabihf": "2.6.2", "@resvg/resvg-js-linux-arm64-gnu": "2.6.2", "@resvg/resvg-js-linux-arm64-musl": "2.6.2", "@resvg/resvg-js-linux-x64-gnu": "2.6.2", "@resvg/resvg-js-linux-x64-musl": "2.6.2", "@resvg/resvg-js-win32-arm64-msvc": "2.6.2", "@resvg/resvg-js-win32-ia32-msvc": "2.6.2", "@resvg/resvg-js-win32-x64-msvc": "2.6.2" } }, "sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q=="], + + "@resvg/resvg-js-android-arm-eabi": ["@resvg/resvg-js-android-arm-eabi@2.6.2", "", { "os": "android", "cpu": "arm" }, "sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA=="], + + "@resvg/resvg-js-android-arm64": ["@resvg/resvg-js-android-arm64@2.6.2", "", { "os": "android", "cpu": "arm64" }, "sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ=="], + + "@resvg/resvg-js-darwin-arm64": ["@resvg/resvg-js-darwin-arm64@2.6.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A=="], + + "@resvg/resvg-js-darwin-x64": ["@resvg/resvg-js-darwin-x64@2.6.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw=="], + + "@resvg/resvg-js-linux-arm-gnueabihf": ["@resvg/resvg-js-linux-arm-gnueabihf@2.6.2", "", { "os": "linux", "cpu": "arm" }, "sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw=="], + + "@resvg/resvg-js-linux-arm64-gnu": ["@resvg/resvg-js-linux-arm64-gnu@2.6.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg=="], + + "@resvg/resvg-js-linux-arm64-musl": ["@resvg/resvg-js-linux-arm64-musl@2.6.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg=="], + + "@resvg/resvg-js-linux-x64-gnu": ["@resvg/resvg-js-linux-x64-gnu@2.6.2", "", { "os": "linux", "cpu": "x64" }, "sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw=="], + + "@resvg/resvg-js-linux-x64-musl": ["@resvg/resvg-js-linux-x64-musl@2.6.2", "", { "os": "linux", "cpu": "x64" }, "sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ=="], + + "@resvg/resvg-js-win32-arm64-msvc": ["@resvg/resvg-js-win32-arm64-msvc@2.6.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ=="], + + "@resvg/resvg-js-win32-ia32-msvc": ["@resvg/resvg-js-win32-ia32-msvc@2.6.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w=="], + + "@resvg/resvg-js-win32-x64-msvc": ["@resvg/resvg-js-win32-x64-msvc@2.6.2", "", { "os": "win32", "cpu": "x64" }, "sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ=="], "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-beta.9-commit.d91dfb5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Mp0/gqiPdepHjjVm7e0yL1acWvI0rJVVFQEADSezvAjon9sjQ7CEg9JnXICD4B1YrPmN9qV/e7cQZCp87tTV4w=="], @@ -253,8 +271,6 @@ "@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.1", "", { "dependencies": { "debug": "^4.4.1" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA=="], - "@swc/helpers": ["@swc/helpers@0.5.18", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ=="], - "@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="], "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="], @@ -285,30 +301,6 @@ "@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="], - "@takumi-rs/core": ["@takumi-rs/core@0.55.4", "", { "optionalDependencies": { "@takumi-rs/core-darwin-arm64": "0.55.4", "@takumi-rs/core-darwin-x64": "0.55.4", "@takumi-rs/core-linux-arm64-gnu": "0.55.4", "@takumi-rs/core-linux-arm64-musl": "0.55.4", "@takumi-rs/core-linux-x64-gnu": "0.55.4", "@takumi-rs/core-linux-x64-musl": "0.55.4", "@takumi-rs/core-win32-arm64-msvc": "0.55.4", "@takumi-rs/core-win32-x64-msvc": "0.55.4" } }, "sha512-+zB9r5pzRDDMTonwOgywG+SR3Ydsl7jOJef233Wo2pwcakcfjntgI3O+iEZthWuD8OK16Dhj5+JmG8B3mqBh+w=="], - - "@takumi-rs/core-darwin-arm64": ["@takumi-rs/core-darwin-arm64@0.55.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LH/X/ul19DActLGcBpXnxH3OBEq8qOgPD56hNHAJMbnCRxAO6TDaIh2U7WqPVliSkFk3jZfikbD21SIEpZrp8A=="], - - "@takumi-rs/core-darwin-x64": ["@takumi-rs/core-darwin-x64@0.55.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-UW7ovR/D1Qp8n8bJOo6JLqZZUDFWWtGRXEZZUZhzUeMSzJ4k3C6ef/DEc75bUTGeBKqCeypMPcvtkQAjcVwwhw=="], - - "@takumi-rs/core-linux-arm64-gnu": ["@takumi-rs/core-linux-arm64-gnu@0.55.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-y1d5yuPapKlmt77TpE+XrtULj7LZ51leBqWSg6qMNKxhpvRqmjI/SYjHmk5YvshnrTkdKmRQiXJiiN5EzOhbmA=="], - - "@takumi-rs/core-linux-arm64-musl": ["@takumi-rs/core-linux-arm64-musl@0.55.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-VRbQqbMeoPlrMmaqPwn30Sw82LYya+o4ru9dqV/7BKExozWj/pX9ahexlJdHsZ6wqmsr+ZxexZivK1mPum9ang=="], - - "@takumi-rs/core-linux-x64-gnu": ["@takumi-rs/core-linux-x64-gnu@0.55.4", "", { "os": "linux", "cpu": "x64" }, "sha512-ecCUtNgOe6mCWKf+SE7cbJXWd6D6TQoCnKZAJAGrJkJLAdy/gBhCFhOyPz8M7q/4uWHUATentqi35KAp+jxBiQ=="], - - "@takumi-rs/core-linux-x64-musl": ["@takumi-rs/core-linux-x64-musl@0.55.4", "", { "os": "linux", "cpu": "x64" }, "sha512-YBM2zPrGE/1sfHoFZvOsCvCuK9PfaxzePN/GnnlaAvpvgeRHiAU4PJkLGDpjMFfsWUAEdjly/b0HSAjVQ7NL6Q=="], - - "@takumi-rs/core-win32-arm64-msvc": ["@takumi-rs/core-win32-arm64-msvc@0.55.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-VcgLCWnmyWuhwLv0Tpob8Hv5IFPreFVykoHruPGwXDVVoUcCo+lQ8oCO5EYTB8B/tBAXl2S0xUL0nMDbyLzMxQ=="], - - "@takumi-rs/core-win32-x64-msvc": ["@takumi-rs/core-win32-x64-msvc@0.55.4", "", { "os": "win32", "cpu": "x64" }, "sha512-ta9g1gUybS2V4mHaccJHcMeBb+w1P6pgZuqHzLoQzBIEK9a/KncHPfnR48cz4sGfg4atorfSa6UBffa2FqijyQ=="], - - "@takumi-rs/helpers": ["@takumi-rs/helpers@0.55.4", "", {}, "sha512-Q+iol0en/Az377+iox/jocJKUZ5JJK3R7yMtRI7zWgxXaOWkUspdwy66a3YC9pqlDszcM/YB5xMgbFEbn5wlPQ=="], - - "@takumi-rs/image-response": ["@takumi-rs/image-response@0.55.4", "", { "dependencies": { "@takumi-rs/core": "0.55.4", "@takumi-rs/helpers": "0.55.4", "@takumi-rs/wasm": "0.55.4" } }, "sha512-E7IfI4Y01UK4I95Jq1/BkLaIWIoLT5bn5D5yPvcweSxMXZxpPMcukSWWmNFDboH+p9lj9ozjME75cf9kRdn9/w=="], - - "@takumi-rs/wasm": ["@takumi-rs/wasm@0.55.4", "", {}, "sha512-/iOhQW+nJW0hhv2viu6806JehiAKWFvJ4LXux6CW4XBpP1xWdr4H+VBS7OYMbQu/7XaPITyL7B10lSTtRUAHoA=="], - "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], @@ -339,6 +331,8 @@ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.51.0", "", { "dependencies": { "@typescript-eslint/types": "8.51.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-mM/JRQOzhVN1ykejrvwnBRV3+7yTKK8tVANVN3o1O0t0v7o+jqdVu9crPy5Y9dov15TJk/FTIgoUGHrTOVL3Zg=="], + "@xevion/satori-html": ["@xevion/satori-html@0.4.1", "", { "dependencies": { "ultrahtml": "^1.6.0" } }, "sha512-IjHBtcrTwJeqyWRJ1A13DDEpeutJ+5nBVRtOi7x9TNn58sKT8l6WcY+17soyO96omvbd1mxlHmDHtJGZocs1Qg=="], + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], @@ -361,8 +355,6 @@ "base64-js": ["base64-js@0.0.8", "", {}, "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw=="], - "bits-ui": ["bits-ui@2.15.2", "", { "dependencies": { "@floating-ui/core": "^1.7.1", "@floating-ui/dom": "^1.7.1", "esm-env": "^1.1.2", "runed": "^0.35.1", "svelte-toolbelt": "^0.10.6", "tabbable": "^6.2.0" }, "peerDependencies": { "@internationalized/date": "^3.8.1", "svelte": "^5.33.0" } }, "sha512-S8eDbFkZCN17kZ7J9fD3MRXziV9ozjdFt2D3vTr2bvXCl7BtrIqguYt2U/zrFgLdR2erwybvCKv0JXYn8uKLDQ=="], - "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], @@ -397,6 +389,8 @@ "css-color-keywords": ["css-color-keywords@1.0.0", "", {}, "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg=="], + "css-gradient-parser": ["css-gradient-parser@0.0.17", "", {}, "sha512-w2Xy9UMMwlKtou0vlRnXvWglPAceXCTtcmVSo8ZBUvqCV5aXEFP/PC6d+I464810I9FT++UACwTD5511bmGPUg=="], + "css-to-react-native": ["css-to-react-native@3.2.0", "", { "dependencies": { "camelize": "^1.0.0", "css-color-keywords": "^1.0.0", "postcss-value-parser": "^4.0.2" } }, "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ=="], "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], @@ -407,13 +401,13 @@ "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], - "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], - "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "devalue": ["devalue@5.6.1", "", {}, "sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A=="], - "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "emoji-regex-xs": ["emoji-regex-xs@2.0.1", "", {}, "sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g=="], "enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="], @@ -489,8 +483,6 @@ "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], - "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], @@ -515,8 +507,6 @@ "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], - "knitwork": ["knitwork@1.3.0", "", {}, "sha512-4LqMNoONzR43B1W0ek0fhXMsDNW/zxa1NdFAVMY+k28pgZLovR4G3PB5MrpTxCy1QaZCqNoiaKPr5w5qZHfSNw=="], - "known-css-properties": ["known-css-properties@0.37.0", "", {}, "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ=="], "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], @@ -557,8 +547,6 @@ "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], - "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], - "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], @@ -633,15 +621,11 @@ "rollup": ["rollup@4.54.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.54.0", "@rollup/rollup-android-arm64": "4.54.0", "@rollup/rollup-darwin-arm64": "4.54.0", "@rollup/rollup-darwin-x64": "4.54.0", "@rollup/rollup-freebsd-arm64": "4.54.0", "@rollup/rollup-freebsd-x64": "4.54.0", "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", "@rollup/rollup-linux-arm-musleabihf": "4.54.0", "@rollup/rollup-linux-arm64-gnu": "4.54.0", "@rollup/rollup-linux-arm64-musl": "4.54.0", "@rollup/rollup-linux-loong64-gnu": "4.54.0", "@rollup/rollup-linux-ppc64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-musl": "4.54.0", "@rollup/rollup-linux-s390x-gnu": "4.54.0", "@rollup/rollup-linux-x64-gnu": "4.54.0", "@rollup/rollup-linux-x64-musl": "4.54.0", "@rollup/rollup-openharmony-arm64": "4.54.0", "@rollup/rollup-win32-arm64-msvc": "4.54.0", "@rollup/rollup-win32-ia32-msvc": "4.54.0", "@rollup/rollup-win32-x64-gnu": "4.54.0", "@rollup/rollup-win32-x64-msvc": "4.54.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw=="], - "runed": ["runed@0.35.1", "", { "dependencies": { "dequal": "^2.0.3", "esm-env": "^1.0.0", "lz-string": "^1.5.0" }, "peerDependencies": { "@sveltejs/kit": "^2.21.0", "svelte": "^5.7.0" }, "optionalPeers": ["@sveltejs/kit"] }, "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q=="], - "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="], - "satori": ["satori@0.10.14", "", { "dependencies": { "@shuding/opentype.js": "1.4.0-beta.0", "css-background-parser": "^0.1.0", "css-box-shadow": "1.0.0-3", "css-to-react-native": "^3.0.0", "emoji-regex": "^10.2.1", "escape-html": "^1.0.3", "linebreak": "^1.1.0", "parse-css-color": "^0.2.1", "postcss-value-parser": "^4.2.0", "yoga-wasm-web": "^0.3.3" } }, "sha512-abovcqmwl97WKioxpkfuMeZmndB1TuDFY/R+FymrZyiGP+pMYomvgSzVPnbNMWHHESOPosVHGL352oFbdAnJcA=="], - - "satori-html": ["satori-html@0.3.2", "", { "dependencies": { "ultrahtml": "^1.2.0" } }, "sha512-wjTh14iqADFKDK80e51/98MplTGfxz2RmIzh0GqShlf4a67+BooLywF17TvJPD6phO0Hxm7Mf1N5LtRYvdkYRA=="], + "satori": ["satori@0.18.3", "", { "dependencies": { "@shuding/opentype.js": "1.4.0-beta.0", "css-background-parser": "^0.1.0", "css-box-shadow": "1.0.0-3", "css-gradient-parser": "^0.0.17", "css-to-react-native": "^3.0.0", "emoji-regex-xs": "^2.0.1", "escape-html": "^1.0.3", "linebreak": "^1.1.0", "parse-css-color": "^0.2.1", "postcss-value-parser": "^4.2.0", "yoga-layout": "^3.2.1" } }, "sha512-T3DzWNmnrfVmk2gCIlAxLRLbGkfp3K7TyRva+Byyojqu83BNvnMeqVeYRdmUw4TKCsyH4RiQ/KuF/I4yEzgR5A=="], "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], @@ -657,8 +641,6 @@ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], - "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "string.prototype.codepointat": ["string.prototype.codepointat@0.2.1", "", {}, "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg=="], @@ -667,8 +649,6 @@ "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], - "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], - "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], "svelte": ["svelte@5.46.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.5.0", "esm-env": "^1.2.1", "esrap": "^2.2.1", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-ynjfCHD3nP2el70kN5Pmg37sSi0EjOm9FgHYQdC4giWG/hzO3AatzXXJJgP305uIhGQxSufJLuYWtkY8uK/8RA=="], @@ -679,10 +659,6 @@ "svelte-eslint-parser": ["svelte-eslint-parser@1.4.1", "", { "dependencies": { "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.0.0", "espree": "^10.0.0", "postcss": "^8.4.49", "postcss-scss": "^4.0.9", "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-1eqkfQ93goAhjAXxZiu1SaKI9+0/sxp4JIWQwUpsz7ybehRE5L8dNuz7Iry7K22R47p5/+s9EM+38nHV2OlgXA=="], - "svelte-toolbelt": ["svelte-toolbelt@0.10.6", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.35.1", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.30.2" } }, "sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ=="], - - "tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="], - "tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], @@ -721,8 +697,6 @@ "unplugin-icons": ["unplugin-icons@22.5.0", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/utils": "^3.0.2", "debug": "^4.4.3", "local-pkg": "^1.1.2", "unplugin": "^2.3.10" }, "peerDependencies": { "@svgr/core": ">=7.0.0", "@svgx/core": "^1.0.1", "@vue/compiler-sfc": "^3.0.2 || ^2.7.0", "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0", "vue-template-compiler": "^2.6.12", "vue-template-es2015-compiler": "^1.9.0" }, "optionalPeers": ["@svgr/core", "@svgx/core", "@vue/compiler-sfc", "svelte", "vue-template-compiler", "vue-template-es2015-compiler"] }, "sha512-MBlMtT5RuMYZy4TZgqUL2OTtOdTUVsS1Mhj6G1pEzMlFJlEnq6mhUfoIt45gBWxHcsOdXJDWLg3pRZ+YmvAVWQ=="], - "unwasm": ["unwasm@0.5.2", "", { "dependencies": { "exsolve": "^1.0.8", "knitwork": "^1.3.0", "magic-string": "^0.30.21", "mlly": "^1.8.0", "pathe": "^2.0.3", "pkg-types": "^2.3.0" } }, "sha512-uWhB7IXQjMC4530uVAeu0lzvYK6P3qHVnmmdQniBi48YybOLN/DqEzcP9BRGk1YTDG3rRWRD8me55nIYoTHyMg=="], - "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], @@ -749,7 +723,7 @@ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "yoga-wasm-web": ["yoga-wasm-web@0.3.3", "", {}, "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA=="], + "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], "zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="], @@ -779,8 +753,6 @@ "mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], - "string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], diff --git a/web/package.json b/web/package.json index 831d4c9..31185f7 100644 --- a/web/package.json +++ b/web/package.json @@ -13,13 +13,15 @@ "format": "prettier --write ." }, "dependencies": { - "@ethercorps/sveltekit-og": "^4.2.1", "@fontsource-variable/inter": "^5.2.8", + "@fontsource-variable/schibsted-grotesk": "^5.2.8", "@fontsource/hanken-grotesk": "^5.1.0", "@fontsource/schibsted-grotesk": "^5.2.8", "@logtape/logtape": "^1.3.5", - "bits-ui": "^2.8.2", + "@resvg/resvg-js": "^2.6.2", + "@xevion/satori-html": "^0.4.1", "clsx": "^2.1.1", + "satori": "^0.18.3", "tailwind-merge": "^3.3.1" }, "devDependencies": { diff --git a/web/src/app.css b/web/src/app.css index 9221f01..6557128 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -13,7 +13,7 @@ /* Font families */ --font-inter: "Inter Variable", sans-serif; --font-hanken: "Hanken Grotesk", sans-serif; - --font-schibsted: "Schibsted Grotesk", sans-serif; + --font-schibsted: "Schibsted Grotesk Variable", sans-serif; /* Background images */ --background-image-gradient-radial: radial-gradient( diff --git a/web/src/lib/components/Dots.svelte b/web/src/lib/components/Dots.svelte index 78d42b7..8043714 100644 --- a/web/src/lib/components/Dots.svelte +++ b/web/src/lib/components/Dots.svelte @@ -365,6 +365,11 @@ let animationId: number; function render() { + if (document.hidden) { + animationId = requestAnimationFrame(render); + return; + } + const time = ((Date.now() - startTime) / 1000) * timeScale; uniforms.set("u_resolution", [canvas.width, canvas.height]); diff --git a/web/src/lib/components/OgImage.svelte b/web/src/lib/components/OgImage.svelte new file mode 100644 index 0000000..7a6c07f --- /dev/null +++ b/web/src/lib/components/OgImage.svelte @@ -0,0 +1,52 @@ + + +
+
+ +
+

+ {title} +

+ {#if subtitle} +

+ {subtitle} +

+ {/if} +
+ + +
+
xevion.dev
+ {#if type === 'project'} +
+ PROJECT +
+ {/if} +
+
+
diff --git a/web/src/lib/logger.ts b/web/src/lib/logger.ts index d94565a..affcbad 100644 --- a/web/src/lib/logger.ts +++ b/web/src/lib/logger.ts @@ -27,29 +27,28 @@ export async function initLogger() { const useJsonLogs = process.env.LOG_JSON === "true" || process.env.LOG_JSON === "1"; - const sinkName = useJsonLogs ? "json" : "console"; - const sink = useJsonLogs - ? (record: LogRecord) => { - process.stdout.write(railwayFormatter(record)); - } - : getConsoleSink(); + const jsonSink = (record: LogRecord) => { + process.stdout.write(railwayFormatter(record)); + }; + const consoleSink = getConsoleSink(); try { await configure({ sinks: { - [sinkName]: sink, + json: useJsonLogs ? jsonSink : consoleSink, + console: useJsonLogs ? jsonSink : consoleSink, }, filters: {}, loggers: [ { category: ["logtape", "meta"], lowestLevel: "warning", - sinks: [sinkName], + sinks: [useJsonLogs ? "json" : "console"], }, { category: [], lowestLevel: "debug", - sinks: [sinkName], + sinks: [useJsonLogs ? "json" : "console"], }, ], }); diff --git a/web/src/lib/og-fonts.ts b/web/src/lib/og-fonts.ts index 69c4bd5..f6f5481 100644 --- a/web/src/lib/og-fonts.ts +++ b/web/src/lib/og-fonts.ts @@ -1,39 +1,62 @@ -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"; +import { readFile } from "node:fs/promises"; +import { join } from "node:path"; +import { cwd } from "node:process"; +import type { SatoriOptions } from "satori"; /** * Load fonts for OG image generation. - * Fonts are sourced from @fontsource packages and imported directly from node_modules. + * Fonts are loaded directly from node_modules using fs.readFile for production compatibility. * 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", - }, +export async function loadOGFonts(): Promise { + // In production, the server runs from web/build, so node_modules is at ../node_modules + // In dev, we're already in web/ directory + const workingDir = cwd(); + const nodeModulesPath = workingDir.endsWith("/build") + ? join(workingDir, "..", "node_modules") + : join(workingDir, "node_modules"); + + const [hankenGrotesk, schibstedGrotesk, inter] = await Promise.all([ + readFile( + join( + nodeModulesPath, + "@fontsource/hanken-grotesk/files/hanken-grotesk-latin-900-normal.woff", + ), ), - new CustomFont( - "Schibsted Grotesk", - () => read(SchibstedGrotesk400).arrayBuffer(), - { - weight: 400, - style: "normal", - }, + readFile( + join( + nodeModulesPath, + "@fontsource/schibsted-grotesk/files/schibsted-grotesk-latin-400-normal.woff", + ), ), - new CustomFont("Inter", () => read(Inter500).arrayBuffer(), { + readFile( + join( + nodeModulesPath, + "@fontsource/inter/files/inter-latin-500-normal.woff", + ), + ), + ]); + + return [ + { + name: "Hanken Grotesk", + data: hankenGrotesk, + weight: 900, + style: "normal", + }, + { + name: "Schibsted Grotesk", + data: schibstedGrotesk, + weight: 400, + style: "normal", + }, + { + name: "Inter", + data: inter, weight: 500, style: "normal", - }), + }, ]; - - return await resolveFonts(fonts); } diff --git a/web/src/lib/og-template.ts b/web/src/lib/og-template.ts deleted file mode 100644 index c11dad0..0000000 --- a/web/src/lib/og-template.ts +++ /dev/null @@ -1,116 +0,0 @@ -/** - * 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 ` -
-
- -
-

- ${escapeHtml(title)} -

- ${ - subtitle - ? ` -

- ${escapeHtml(subtitle)} -

- ` - : "" - } -
- - -
-
- xevion.dev -
- ${ - type === "project" - ? ` -
- PROJECT -
- ` - : "" - } -
-
-
- `; -} - -function escapeHtml(text: string): string { - return text - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); -} diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte index 2820780..8a284e5 100644 --- a/web/src/routes/+layout.svelte +++ b/web/src/routes/+layout.svelte @@ -2,19 +2,19 @@ import "../app.css"; import "@fontsource-variable/inter"; import "@fontsource/hanken-grotesk/900.css"; - import "@fontsource/schibsted-grotesk/400.css"; - import "@fontsource/schibsted-grotesk/500.css"; - import "@fontsource/schibsted-grotesk/600.css"; + import "@fontsource-variable/schibsted-grotesk"; let { children, data } = $props(); - const metadata = data?.metadata ?? { + const defaultMetadata = { title: "Xevion.dev", description: "The personal website of Xevion, a full-stack software developer.", ogImage: "/api/og/home.png", url: "https://xevion.dev", }; + + const metadata = $derived(data?.metadata ?? defaultMetadata); diff --git a/web/src/routes/internal/ogp/+page.server.ts b/web/src/routes/internal/ogp/+page.server.ts new file mode 100644 index 0000000..7defaa3 --- /dev/null +++ b/web/src/routes/internal/ogp/+page.server.ts @@ -0,0 +1,43 @@ +import type { PageServerLoad } from "./$types"; +import type { OGImageSpec } from "$lib/og-types"; +import { error } from "@sveltejs/kit"; + +export const load: PageServerLoad = async ({ url, parent }) => { + const parentData = await parent(); + const type = url.searchParams.get("type"); + + if (!type) { + throw error(400, 'Missing "type" query parameter'); + } + + let spec: OGImageSpec; + let title: string; + + switch (type) { + case "index": + spec = { type: "index" }; + title = "Index Page"; + break; + case "projects": + spec = { type: "projects" }; + title = "Projects Page"; + break; + case "project": { + const id = url.searchParams.get("id"); + if (!id) { + throw error(400, 'Missing "id" query parameter for project type'); + } + spec = { type: "project", id }; + title = `Project: ${id}`; + break; + } + default: + throw error(400, `Invalid "type" query parameter: ${type}`); + } + + return { + ...parentData, + spec, + title, + }; +}; diff --git a/web/src/routes/internal/ogp/+page.svelte b/web/src/routes/internal/ogp/+page.svelte new file mode 100644 index 0000000..d0981be --- /dev/null +++ b/web/src/routes/internal/ogp/+page.svelte @@ -0,0 +1,190 @@ + + + + OG Image Preview - {data.title} + + +
+
+

OG Image Preview

+
+ +
+
+ +
+

Type: {data.spec.type}

+ {#if data.spec.type === "project"} +

ID: {data.spec.id}

+ {/if} +

+ Image auto-reloads when server updates (HMR) or every 2 seconds +

+
+ +
+ Preview +
+ +
+

Example URLs:

+ +
+
+ + diff --git a/web/src/routes/internal/ogp/+server.ts b/web/src/routes/internal/ogp/generate/+server.ts similarity index 60% rename from web/src/routes/internal/ogp/+server.ts rename to web/src/routes/internal/ogp/generate/+server.ts index c57d0d6..dff132a 100644 --- a/web/src/routes/internal/ogp/+server.ts +++ b/web/src/routes/internal/ogp/generate/+server.ts @@ -1,13 +1,58 @@ -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 { loadOGFonts } from "$lib/og-fonts"; +import { apiFetch } from "$lib/api"; +import type { Project } from "../../../projects/+page.server"; import { getLogger } from "@logtape/logtape"; +import satori from "satori"; +import { Resvg } from "@resvg/resvg-js"; +import { render } from "svelte/server"; +import { html } from "@xevion/satori-html"; +import OgImage from "$lib/components/OgImage.svelte"; -const logger = getLogger(["ssr", "routes", "internal", "ogp"]); +const logger = getLogger(["ssr", "routes", "internal", "ogp", "generate"]); + +/** + * Generate endpoint for OG images. + * Parses query parameters and generates the image. + */ +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); +}; /** * Internal endpoint for OG image generation. @@ -29,75 +74,58 @@ export const POST: RequestHandler = async ({ request }) => { return await generateOGImage(spec); }; -/** - * GET handler for OG image generation using query parameters. - * Supports: ?type=index, ?type=projects, ?type=project&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 { logger.info("Generating OG image", { spec }); - const templateData = await getTemplateData(spec); - try { - const fonts = await loadOGFonts(); - const html = generateOGTemplate(templateData); + const templateData = await getTemplateData(spec); + logger.debug("Template data prepared", { templateData }); - const imageResponse = new ImageResponse(html, { + const fonts = await loadOGFonts(); + logger.debug("Fonts loaded", { fontCount: fonts.length }); + + // Render Svelte component to HTML string + const { html: renderedHtml } = render(OgImage, { + props: { + title: templateData.title, + subtitle: templateData.subtitle, + type: spec.type, + }, + }); + + // Convert HTML to Satori VNode + const vnode = html(renderedHtml); + + // Generate SVG with satori + const svg = await satori(vnode, { width: 1200, height: 630, fonts, }); - const imageBuffer = await imageResponse.arrayBuffer(); + // Convert SVG to PNG with resvg + const resvg = new Resvg(svg, { + fitTo: { + mode: "width", + value: 1200, + }, + }); + const pngData = resvg.render(); + const pngBuffer = pngData.asPng(); logger.info("OG image generated successfully", { spec }); - return new Response(imageBuffer, { - status: 200, - headers: { "Content-Type": "image/png" }, + return new Response(new Uint8Array(pngBuffer), { + headers: { + "Content-Type": "image/png", + "Cache-Control": "no-cache, no-store, must-revalidate", + }, }); } catch (error) { logger.error("OG image generation failed", { spec, error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, }); return new Response("Failed to generate image", { status: 500 }); } @@ -106,6 +134,9 @@ async function generateOGImage(spec: OGImageSpec): Promise { async function getTemplateData(spec: OGImageSpec): Promise<{ title: string; subtitle?: string; + description?: string; + image?: string; + color?: string; type?: "default" | "project"; }> { switch (spec.type) { diff --git a/web/svelte.config.js b/web/svelte.config.js index b23b72d..08441ee 100644 --- a/web/svelte.config.js +++ b/web/svelte.config.js @@ -8,7 +8,11 @@ const config = { kit: { adapter: adapter({ out: "build", - precompress: false, + precompress: { + brotli: true, + gzip: true, + files: ["html", "js", "json", "css", "svg", "xml", "wasm"], + }, serveAssets: false, }), alias: { diff --git a/web/vite.config.ts b/web/vite.config.ts index d00d580..8622375 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -2,7 +2,6 @@ import { sveltekit } from "@sveltejs/kit/vite"; import tailwindcss from "@tailwindcss/vite"; import { defineConfig } from "vite"; import Icons from "unplugin-icons/vite"; -import { sveltekitOG } from "@ethercorps/sveltekit-og/plugin"; import { jsonLogger } from "./vite-plugin-json-logger"; export default defineConfig({ @@ -10,9 +9,7 @@ export default defineConfig({ jsonLogger(), tailwindcss(), sveltekit(), - sveltekitOG(), Icons({ compiler: "svelte" }), ], clearScreen: false, - assetsInclude: ["**/*.wasm"], });