mirror of
https://github.com/Xevion/xevion.dev.git
synced 2026-01-31 20:26:28 -06:00
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
This commit is contained in:
@@ -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]);
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<script lang="ts">
|
||||
import type { OGImageSpec } from '$lib/og-types';
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
type: OGImageSpec['type'];
|
||||
};
|
||||
|
||||
let { title, subtitle, type }: Props = $props();
|
||||
|
||||
// Calculate font size based on title length (matching original logic)
|
||||
const fontSize = $derived(title.length > 40 ? '60px' : '72px');
|
||||
</script>
|
||||
|
||||
<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%;"
|
||||
>
|
||||
<!-- Content section -->
|
||||
<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: {fontSize}; line-height: 1.1; margin: 0; color: #ffffff;"
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
{#if subtitle}
|
||||
<p
|
||||
style="font-family: 'Schibsted Grotesk', sans-serif; font-size: 36px; margin: 32px 0 0 0; color: #a1a1aa; line-height: 1.4;"
|
||||
>
|
||||
{subtitle}
|
||||
</p>
|
||||
{/if}
|
||||
</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>
|
||||
{#if type === 'project'}
|
||||
<div
|
||||
style="font-size: 24px; color: #52525b; text-transform: uppercase; letter-spacing: 0.05em;"
|
||||
>
|
||||
PROJECT
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
+49
-26
@@ -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<SatoriOptions["fonts"]> {
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -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 `
|
||||
<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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
Reference in New Issue
Block a user