Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 62 additions & 50 deletions src/server/og/assets.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<ImageAsset, Buffer>()

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 }
}
39 changes: 26 additions & 13 deletions src/server/og/generate.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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<ImageResponseOptions['images']>
},
init?: ResponseInit,
): Promise<ImageResponse> {
const assets = await loadNodeOgAssets(requestUrl)

const options: ImageResponseOptions = {
width: size.width,
height: size.height,
Expand All @@ -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,
}

Expand All @@ -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),
Expand All @@ -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,
)
}
Expand All @@ -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()
Expand All @@ -137,22 +146,26 @@ 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,
})

return renderOgImage(
tree,
{ width: 1800, height: 450 },
input.requestUrl,
{
interRegular: assets.interRegular,
bricolageBold: assets.bricolageBold,
images: [{ src: emblemSrc, data: assets.imageData }],
},
init,
)
}
Loading