diff --git a/src/server/og/assets.server.ts b/src/server/og/assets.server.ts index bc1e6975b..4492166dd 100644 --- a/src/server/og/assets.server.ts +++ b/src/server/og/assets.server.ts @@ -5,9 +5,22 @@ import { fetchStaticAsset } from '~/server/runtime/host.server' const interRegularUrl = '/fonts/Inter-Regular.ttf' const bricolageBoldUrl = '/fonts/BricolageGrotesque-Bold.ttf' -const brandLogoPngUrl = '/images/brand/tanstack-landscape-black-640.png' -const brandEmblemPngUrl = '/images/brand/tanstack-emblem-charcoal-256.png' -const brandEmblemCreamPngUrl = '/images/brand/tanstack-emblem-cream-256.png' +const imageAssets = { + logo: { + path: 'public/images/brand/tanstack-landscape-black-640.png', + url: '/images/brand/tanstack-landscape-black-640.png', + }, + emblem: { + path: 'public/images/brand/tanstack-emblem-charcoal-256.png', + url: '/images/brand/tanstack-emblem-charcoal-256.png', + }, + 'emblem-cream': { + path: 'public/images/brand/tanstack-emblem-cream-256.png', + url: '/images/brand/tanstack-emblem-cream-256.png', + }, +} + +type ImageAsset = keyof typeof imageAssets function tryReadBinary(relPath: string): Buffer | null { // Resolve from the project root for local dev and tests. Workers normally @@ -29,58 +42,57 @@ async function readAssetUrl(assetUrl: string, requestUrl: string) { return Buffer.from(await response.arrayBuffer()) } -let cached: { +type FontAssets = { interRegular: Buffer bricolageBold: Buffer - brandLogoPng: Buffer - brandEmblemPng: Buffer - brandEmblemCreamPng: Buffer -} | null = null - -export async function loadOgAssets(requestUrl?: string) { - if (cached) return cached - - const interRegular = tryReadBinary('public/fonts/Inter-Regular.ttf') - const bricolageBold = tryReadBinary( - 'public/fonts/BricolageGrotesque-Bold.ttf', - ) - const brandLogoPng = tryReadBinary( - 'public/images/brand/tanstack-landscape-black-640.png', - ) - const brandEmblemPng = tryReadBinary( - 'public/images/brand/tanstack-emblem-charcoal-256.png', - ) - const brandEmblemCreamPng = tryReadBinary( - 'public/images/brand/tanstack-emblem-cream-256.png', - ) - - if ( - interRegular && - bricolageBold && - brandLogoPng && - brandEmblemPng && - brandEmblemCreamPng - ) { - cached = { - interRegular, - bricolageBold, - brandLogoPng, - brandEmblemPng, - brandEmblemCreamPng, - } - return cached - } +} + +let cachedFonts: FontAssets | null = null +const cachedImages = new Map() + +async function loadAsset( + relPath: string, + assetUrl: string, + requestUrl: string | undefined, +) { + const localAsset = tryReadBinary(relPath) + if (localAsset) return localAsset if (!requestUrl) { throw new Error('OG asset URL fallback requires a request URL') } - cached = { - interRegular: await readAssetUrl(interRegularUrl, requestUrl), - bricolageBold: await readAssetUrl(bricolageBoldUrl, requestUrl), - brandLogoPng: await readAssetUrl(brandLogoPngUrl, requestUrl), - brandEmblemPng: await readAssetUrl(brandEmblemPngUrl, requestUrl), - brandEmblemCreamPng: await readAssetUrl(brandEmblemCreamPngUrl, requestUrl), - } - return cached + return readAssetUrl(assetUrl, requestUrl) +} + +async function loadFonts(requestUrl?: string) { + if (cachedFonts) return cachedFonts + + const [interRegular, bricolageBold] = await Promise.all([ + loadAsset('public/fonts/Inter-Regular.ttf', interRegularUrl, requestUrl), + loadAsset( + 'public/fonts/BricolageGrotesque-Bold.ttf', + bricolageBoldUrl, + requestUrl, + ), + ]) + + cachedFonts = { interRegular, bricolageBold } + return cachedFonts +} + +export async function loadOgAssets( + imageAsset: ImageAsset, + requestUrl?: string, +) { + const image = imageAssets[imageAsset] + const [fonts, imageData] = await Promise.all([ + loadFonts(requestUrl), + cachedImages.get(imageAsset) ?? + loadAsset(image.path, image.url, requestUrl), + ]) + + cachedImages.set(imageAsset, imageData) + + return { ...fonts, imageData } } diff --git a/src/server/og/generate.server.ts b/src/server/og/generate.server.ts index 83a74541e..6757bf598 100644 --- a/src/server/og/generate.server.ts +++ b/src/server/og/generate.server.ts @@ -3,7 +3,7 @@ import type { ReactElement } from 'react' import { findLibrary } from '~/libraries' import type { LibraryId } from '~/libraries' import type { Framework } from '~/libraries/types' -import { loadOgAssets as loadNodeOgAssets } from './assets.server' +import { loadOgAssets } from './assets.server' import { getAccentColor, getThemeSurface, type OgTheme } from './colors' import { buildOgTree } from './template' import { buildReadmeHeaderTree } from './readme-template' @@ -43,11 +43,13 @@ export type OgLibraryNotFoundError = { async function renderOgImage( tree: ReactElement, size: { width: number; height: number }, - requestUrl: string | undefined, + assets: { + interRegular: Buffer + bricolageBold: Buffer + images: NonNullable + }, init?: ResponseInit, ): Promise { - const assets = await loadNodeOgAssets(requestUrl) - const options: ImageResponseOptions = { width: size.width, height: size.height, @@ -66,11 +68,7 @@ async function renderOgImage( style: 'normal', }, ], - images: [ - { src: BRAND_LOGO_KEY, data: assets.brandLogoPng }, - { src: BRAND_EMBLEM_KEY, data: assets.brandEmblemPng }, - { src: BRAND_EMBLEM_CREAM_KEY, data: assets.brandEmblemCreamPng }, - ], + images: assets.images, ...init, } @@ -86,6 +84,7 @@ export async function generateOgImageResponse( return { kind: 'library-not-found', libraryId: input.libraryId } } + const assets = await loadOgAssets('logo', input.requestUrl) const tree = buildOgTree({ libraryName: library.name, accentColor: getAccentColor(library.id), @@ -102,7 +101,11 @@ export async function generateOgImageResponse( return renderOgImage( tree, { width: 1200, height: 630 }, - input.requestUrl, + { + interRegular: assets.interRegular, + bricolageBold: assets.bricolageBold, + images: [{ src: BRAND_LOGO_KEY, data: assets.imageData }], + }, init, ) } @@ -128,6 +131,12 @@ export async function generateReadmeHeaderResponse( return { kind: 'library-not-found', libraryId: input.libraryId } } + const theme = input.theme ?? 'light' + const assets = await loadOgAssets( + theme === 'dark' ? 'emblem-cream' : 'emblem', + input.requestUrl, + ) + // An explicit title replaces the whole name, so the framework label is not // applied on top of it. const name = input.title?.trim() @@ -137,14 +146,14 @@ export async function generateReadmeHeaderResponse( : library.name const tagline = input.subtitle?.trim() ? input.subtitle : library.tagline - const theme = input.theme ?? 'light' const surface = getThemeSurface(theme) + const emblemSrc = theme === 'dark' ? BRAND_EMBLEM_CREAM_KEY : BRAND_EMBLEM_KEY const tree = buildReadmeHeaderTree({ name, tagline: clampOgText(tagline ?? '', MAX_OG_DESCRIPTION_LENGTH), accentColor: getAccentColor(library.id, theme), - emblemSrc: theme === 'dark' ? BRAND_EMBLEM_CREAM_KEY : BRAND_EMBLEM_KEY, + emblemSrc, background: surface.background, secondaryText: surface.secondaryText, }) @@ -152,7 +161,11 @@ export async function generateReadmeHeaderResponse( return renderOgImage( tree, { width: 1800, height: 450 }, - input.requestUrl, + { + interRegular: assets.interRegular, + bricolageBold: assets.bricolageBold, + images: [{ src: emblemSrc, data: assets.imageData }], + }, init, ) }