import fs from 'node:fs' import path from 'node:path' import { ImageResponse } from '@vercel/og' import sharp from 'sharp' import defaultOGImageBg from '../assets/ogimage-bg.png' import defaultOGImage from '../assets/ogimage.png' import { makeOverallScoreInfo } from '../lib/overallScore' import { urlWithParams } from '../lib/urls' import type { APIContext } from 'astro' import type { Prettify } from 'ts-essentials' ////////////////////////////////////////////////////// // NOTE // // Use this website to create and preview templates // // https://og-playground.vercel.app/ // ////////////////////////////////////////////////////// const defaultOptions = { width: 1200, height: 630, fonts: [ { name: 'Inter', weight: 400, style: 'normal', data: fs.readFileSync( path.resolve( process.cwd(), 'node_modules', '@fontsource', 'inter', 'files', 'inter-latin-400-normal.woff' ) ), }, { name: 'Inter', weight: 700, style: 'normal', data: fs.readFileSync( path.resolve( process.cwd(), 'node_modules', '@fontsource', 'inter', 'files', 'inter-latin-700-normal.woff' ) ), }, { name: 'Space Grotesk', weight: 400, style: 'normal', data: fs.readFileSync( path.resolve( process.cwd(), 'node_modules', '@fontsource', 'space-grotesk', 'files', 'space-grotesk-latin-400-normal.woff' ) ), }, { name: 'Space Grotesk', weight: 700, style: 'normal', data: fs.readFileSync( path.resolve( process.cwd(), 'node_modules', '@fontsource', 'space-grotesk', 'files', 'space-grotesk-latin-700-normal.woff' ) ), }, ], } as const satisfies ConstructorParameters[1] export const ogImageTemplates = { default: (_props: Record = {}, context) => { return new ImageResponse( ( ), defaultOptions ) }, service: async ( { title, description, categories, score, imageUrl, }: { title: string description: string categories: { name: string icon: string }[] score: number imageUrl: string | null }, context ) => { const scoreInfo = makeOverallScoreInfo(score, 10) const scoreColors = { 'bg-score-1': '#e26136', 'bg-score-2': '#eba370', 'bg-score-3': '#eddb82', 'bg-score-4': '#8de2d7', 'bg-score-5': '#3cdd71', } as const satisfies Record const scoreColor = Object.entries(scoreColors).find(([className]) => scoreInfo.classNameBg?.includes(className))?.[1] ?? 'white' const PADING = 80 return new ImageResponse( (
{!!imageUrl && ( )}
{title}
{description}
{await Promise.all( categories.map(async (category) => ( {category.name} )) )}
{score}
), defaultOptions ) }, generic: async ( { title, description, icon, }: { title: string description?: string | null icon?: string | null }, context ) => { const PADING = 80 return new ImageResponse( (
{title}
{description} {!!icon && ( )}
), defaultOptions ) }, } as const satisfies Record< string, // eslint-disable-next-line @typescript-eslint/no-explicit-any (props: any, context: APIContext) => ImageResponse | Promise | null > type OgImageTemplate = keyof typeof ogImageTemplates type OgImageProps = Parameters<(typeof ogImageTemplates)[T]>[0] export type OgImageAllTemplatesWithProps = Prettify< { // eslint-disable-next-line @typescript-eslint/sort-type-constituents [K in OgImageTemplate]: { template: K } & Omit, 'template'> }[OgImageTemplate] > export function makeOgImageUrl( ogImage: OgImageAllTemplatesWithProps | string | undefined, baseUrl: URL | string ) { return typeof ogImage === 'string' ? new URL(ogImage, baseUrl).href : urlWithParams(new URL('/ogimage.png', baseUrl), { data: JSON.stringify(ogImage ?? {}) }) } // Utilities ------------------------------------------------------------ function absoluteUrl(url: string, context: Pick) { return new URL(url, context.url.origin).href } async function svgUrlToBase64Png(svgUrl: string, width?: number, height?: number): Promise { // 1. Fetch the SVG file const response = await fetch(svgUrl) if (!response.ok) { throw new Error(`Failed to fetch SVG: ${response.statusText}`) } const svgBuffer = await response.arrayBuffer() // 2. Convert SVG to PNG using sharp let image = sharp(svgBuffer).png().negate({ alpha: false }) if (width || height) { image = image.resize(width, height, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 }, }) } const pngBuffer = await image.toBuffer() // 3. Convert to base64 string const base64 = pngBuffer.toString('base64') return `data:image/png;base64,${base64}` } async function iconUrl(icon: string, size = 30) { const [, prefix, name] = /^([^:]+):(.*)$/.exec(icon) ?? [] if (!prefix || !name) return undefined const url = `https://api.iconify.design/${prefix}/${name}.svg` const result = await svgUrlToBase64Png(url, size, size) return result }