Wire astro-seo-meta for OG, Twitter Card, and canonical tags on all pages. Add Satori + resvg dynamic OG image endpoints at /og/[id].png with branded dark-theme cards. Replace inline SVGs with zero-JS astro-icon rendering. SSR fetches use 5s AbortController timeout and shared ID validation across all dynamic routes.
32 lines
1.1 KiB
TypeScript
32 lines
1.1 KiB
TypeScript
import type { APIRoute } from 'astro';
|
|
import { renderOgImage } from '../../lib/og-renderer';
|
|
import { fetchNotebookMeta } from '../../lib/server-api';
|
|
import { NOTEBOOK_ID_RE, buildNotebookDescription } from '../../lib/notebook-meta';
|
|
|
|
export const GET: APIRoute = async ({ params }) => {
|
|
const { id } = params;
|
|
|
|
if (!id || !NOTEBOOK_ID_RE.test(id)) {
|
|
return new Response('Invalid notebook ID', { status: 400 });
|
|
}
|
|
|
|
const notebook = await fetchNotebookMeta(id);
|
|
const title = notebook?.metadata?.title || id;
|
|
const engine = notebook?.metadata?.engine || 'ngspice';
|
|
const tags: string[] = notebook?.metadata?.tags || [];
|
|
const description = buildNotebookDescription(title, engine, tags);
|
|
|
|
try {
|
|
const png = await renderOgImage({ title, description, engine });
|
|
return new Response(png.buffer as ArrayBuffer, {
|
|
headers: {
|
|
'Content-Type': 'image/png',
|
|
'Cache-Control': 'public, max-age=3600, s-maxage=86400',
|
|
},
|
|
});
|
|
} catch (err) {
|
|
console.error(`[og/${id}] render failed:`, err);
|
|
return new Response('OG image generation failed', { status: 500 });
|
|
}
|
|
};
|