Comment the image generator

This commit is contained in:
Jade Ellis
2024-09-15 17:53:30 +01:00
parent 076433b21c
commit 9522e043a1
@@ -9,41 +9,36 @@ import { format } from "@tusbar/cache-control";
const cache = new TTLCache({ max: 10000, ttl: 1000 * 60 * 60 }) const cache = new TTLCache({ max: 10000, ttl: 1000 * 60 * 60 })
import fnv from "fnv-plus" import fnv from "fnv-plus"
// import type { Endpoints } from "@octokit/types";
// let repoRegex = new RegExp("https?://github\.com/(?<repo>[a-zA-Z0-9]+/[a-zA-Z0-9]+)/?")
const fontFile = await fetch('https://og-playground.vercel.app/inter-latin-ext-700-normal.woff'); const fontFile = await fetch('https://og-playground.vercel.app/inter-latin-ext-700-normal.woff');
const fontData: ArrayBuffer = await fontFile.arrayBuffer(); const fontData: ArrayBuffer = await fontFile.arrayBuffer();
// const height = 630;
// const width = 1200;
const defaultWidth = 800; const defaultWidth = 800;
const defaultRatio = 0.5 const defaultRatio = 0.5
// const defaultWidth = 800;
// JSX stub
const h = (type: any, props: any) => { return { type, props } } const h = (type: any, props: any) => { return { type, props } }
type a = RequestHandler; type a = RequestHandler;
/** @type {RequestHandler} */ /** @type {RequestHandler} */
export async function GET({ url, request }) { 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') const slug = url.searchParams.get('slug')
let dateParts = url.searchParams.get('date')?.split(/[\/-]/)?.map((p: string) => parseInt(p, 10)) let dateParts = url.searchParams.get('date')?.split(/[\/-]/)?.map((p: string) => parseInt(p, 10))
if (dateParts && dateParts.length > 3) { if (dateParts && dateParts.length > 3) {
throw error(404, 'Post not found (bad date)') 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 width = Number(url.searchParams.get('width'))
const ratio = Number(url.searchParams.get('ratio')) 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) { if (width > 10000 || ratio > 50) {
throw error(400, 'Image too big') throw error(400, 'Image too big')
} }
let image; let image;
// let start = new Date(dateParts[0] || 1, dateParts[1] || 0, dateParts[2] || 0); // Look up the post in the database
// // @ts-ignore
// let end = new Date(...dateParts);
// console.log(dateParts)
// get post with metadata
const page = pages const page = pages
.filter((post) => slug === post.slug) .filter((post) => slug === post.slug)
.filter((post) => { .filter((post) => {
@@ -61,16 +56,21 @@ export async function GET({ url, request }) {
throw error(404, 'Post not found') 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 cache_key = fnv.hash(page.canonical + "\x00" + page.readingTime.text + "\x00" + width + "\x00" + ratio).str()
let recieved_etag = request.headers.get("if-none-match"); 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
if (recieved_etag == cache_key) { // This means we don't even have to have the image cached in memory
console.log("304") if (received_etag == cache_key) {
return new Response(null, { status: 304 }) return new Response(null, { status: 304 })
} }
// If the image is not cached, generate the image and cache it
if (!cache.has(cache_key)) { if (!cache.has(cache_key)) {
// First, render the HTML / JSX-based template
let template = h("div", { let template = h("div", {
style: { style: {
display: 'flex', 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}` 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, { const svg = await satori(template, {
fonts: [ fonts: [
{ {
@@ -126,6 +127,7 @@ export async function GET({ url, request }) {
width: defaultWidth, width: defaultWidth,
}); });
// Then, convert the SVG to a PNG image using resvg
const resvg = new Resvg(svg, { const resvg = new Resvg(svg, {
fitTo: { fitTo: {
mode: 'width', mode: 'width',
@@ -134,20 +136,25 @@ export async function GET({ url, request }) {
}); });
image = resvg.render().asPng(); image = resvg.render().asPng();
// Finally, save the image to the cache
cache.set(cache_key, image); cache.set(cache_key, image);
} else { } else {
// If the image is cached, return it
image = cache.get(cache_key) as Buffer image = cache.get(cache_key) as Buffer
} }
// Finally, return the image as a response
return new Response(image, { return new Response(image, {
headers: { headers: {
'Content-Type': 'image/png', 'Content-Type': 'image/png',
// Cache the image for 24 hours
'Cache-Control': format({ 'Cache-Control': format({
public: true, public: true,
// immutable: true // immutable: true
maxAge: 60 * 60 * 24 maxAge: 60 * 60 * 24
}), }),
// Set the cache key as the ETag
'ETag': cache_key, 'ETag': cache_key,
// Allow cross-origin requests to serve the image
'Cross-Origin-Resource-Policy': 'cross-origin' 'Cross-Origin-Resource-Policy': 'cross-origin'
} }
}); });