diff --git a/packages/website/src/routes/blog/image/+server.ts b/packages/website/src/routes/blog/image/+server.ts index 7ee36f71..dec7833d 100644 --- a/packages/website/src/routes/blog/image/+server.ts +++ b/packages/website/src/routes/blog/image/+server.ts @@ -9,41 +9,36 @@ import { format } from "@tusbar/cache-control"; const cache = new TTLCache({ max: 10000, ttl: 1000 * 60 * 60 }) import fnv from "fnv-plus" -// import type { Endpoints } from "@octokit/types"; - -// let repoRegex = new RegExp("https?://github\.com/(?[a-zA-Z0-9]+/[a-zA-Z0-9]+)/?") - const fontFile = await fetch('https://og-playground.vercel.app/inter-latin-ext-700-normal.woff'); const fontData: ArrayBuffer = await fontFile.arrayBuffer(); -// const height = 630; -// const width = 1200; const defaultWidth = 800; const defaultRatio = 0.5 -// const defaultWidth = 800; +// JSX stub const h = (type: any, props: any) => { return { type, props } } + type a = RequestHandler; /** @type {RequestHandler} */ export async function GET({ url, request }) { + // First, get the information about the post + // We have the slug and date of the post, which we can use to look up the post const slug = url.searchParams.get('slug') let dateParts = url.searchParams.get('date')?.split(/[\/-]/)?.map((p: string) => parseInt(p, 10)) if (dateParts && dateParts.length > 3) { throw error(404, 'Post not found (bad date)') } + // Next, get the width and ratio of the image + // to determine the size of the image const width = Number(url.searchParams.get('width')) const ratio = Number(url.searchParams.get('ratio')) + // If the width or ratio is too big, don't render the image to prevent DoS attacks if (width > 10000 || ratio > 50) { throw error(400, 'Image too big') } let image; - // let start = new Date(dateParts[0] || 1, dateParts[1] || 0, dateParts[2] || 0); - // // @ts-ignore - // let end = new Date(...dateParts); - // console.log(dateParts) - - // get post with metadata + // Look up the post in the database const page = pages .filter((post) => slug === post.slug) .filter((post) => { @@ -61,16 +56,21 @@ export async function GET({ url, request }) { throw error(404, 'Post not found') } + // Generate a cache key based on the post's canonical URL, reading time, width, and ratio + // Caching the image based on this key ensures that the image is not regenerated every time + // The cache key is also used for browser caching let cache_key = fnv.hash(page.canonical + "\x00" + page.readingTime.text + "\x00" + width + "\x00" + ratio).str() - let recieved_etag = request.headers.get("if-none-match"); - - if (recieved_etag == cache_key) { - console.log("304") + let received_etag = request.headers.get("if-none-match"); + // If the client has a cached version of the image, return a 304 Not Modified response, indicating that the image has not changed + // This means we don't even have to have the image cached in memory + if (received_etag == cache_key) { return new Response(null, { status: 304 }) } + // If the image is not cached, generate the image and cache it if (!cache.has(cache_key)) { + // First, render the HTML / JSX-based template let template = h("div", { style: { display: 'flex', @@ -114,6 +114,7 @@ export async function GET({ url, request }) { children: `Published on ${new Date(page.date).toLocaleDateString()} by Jade Ellis ยท ${page.readingTime.text}` })] }); + // Then, convert the vdom to SVG using satori const svg = await satori(template, { fonts: [ { @@ -126,6 +127,7 @@ export async function GET({ url, request }) { width: defaultWidth, }); + // Then, convert the SVG to a PNG image using resvg const resvg = new Resvg(svg, { fitTo: { mode: 'width', @@ -134,20 +136,25 @@ export async function GET({ url, request }) { }); image = resvg.render().asPng(); + // Finally, save the image to the cache cache.set(cache_key, image); } else { + // If the image is cached, return it image = cache.get(cache_key) as Buffer } - + // Finally, return the image as a response return new Response(image, { headers: { 'Content-Type': 'image/png', + // Cache the image for 24 hours 'Cache-Control': format({ public: true, // immutable: true maxAge: 60 * 60 * 24 }), + // Set the cache key as the ETag 'ETag': cache_key, + // Allow cross-origin requests to serve the image 'Cross-Origin-Resource-Policy': 'cross-origin' } });