import ky from 'ky' import { type NextApiRequest, type NextApiResponse } from 'next' import { ImageResponse } from 'next/og' import { type PageBlock } from 'notion-types' import { getBlockIcon, getBlockTitle, getPageProperty, isUrl } from 'notion-utils' import * as libConfig from '@/lib/config' import { mapImageUrl } from '@/lib/map-image-url' import { notion } from '@/lib/notion-api' import { type NotionPageInfo, type PageError } from '@/lib/types' import interRegularFont from '../fonts/inter-regular' import interSemiBoldFont from '../fonts/inter-semibold' export const runtime = 'edge' export default async function OGImage( req: NextApiRequest, res: NextApiResponse ) { const { searchParams } = new URL(req.url) const pageId = searchParams.get('id') || libConfig.rootNotionPageId if (!pageId) { return new Response('Invalid notion page id', { status: 400 }) } const pageInfoOrError = await getNotionPageInfo({ pageId }) if (pageInfoOrError.type === 'error') { return res.status(pageInfoOrError.error.statusCode).send({ error: pageInfoOrError.error.message }) } const pageInfo = pageInfoOrError.data console.log(pageInfo) return new ImageResponse( (
{pageInfo.image && ( )}
{pageInfo.detail && (
{pageInfo.detail}
)}
{pageInfo.title}
{pageInfo.detail && (
{pageInfo.detail}
)}
{pageInfo.authorImage && (
)}
), { width: 1200, height: 630, fonts: [ { name: 'Inter', data: interRegularFont, style: 'normal', weight: 400 }, { name: 'Inter', data: interSemiBoldFont, style: 'normal', weight: 700 } ] } ) } export async function getNotionPageInfo({ pageId }: { pageId: string }): Promise< | { type: 'success'; data: NotionPageInfo } | { type: 'error'; error: PageError } > { const recordMap = await notion.getPage(pageId) const keys = Object.keys(recordMap?.block || {}) const block = recordMap?.block?.[keys[0]]?.value if (!block) { throw new Error('Invalid recordMap for page') } const blockSpaceId = block.space_id if ( blockSpaceId && libConfig.rootNotionSpaceId && blockSpaceId !== libConfig.rootNotionSpaceId ) { return { type: 'error', error: { statusCode: 400, message: `Notion page "${pageId}" belongs to a different workspace.` } } } const isBlogPost = block.type === 'page' && block.parent_table === 'collection' const title = getBlockTitle(block, recordMap) || libConfig.name const imageCoverPosition = (block as PageBlock).format?.page_cover_position ?? libConfig.defaultPageCoverPosition const imageObjectPosition = imageCoverPosition ? `center ${(1 - imageCoverPosition) * 100}%` : null const imageBlockUrl = mapImageUrl( getPageProperty('Social Image', block, recordMap) || (block as PageBlock).format?.page_cover, block ) const imageFallbackUrl = mapImageUrl(libConfig.defaultPageCover, block) const blockIcon = getBlockIcon(block, recordMap) const authorImageBlockUrl = mapImageUrl( blockIcon && isUrl(blockIcon) ? blockIcon : null, block ) const authorImageFallbackUrl = mapImageUrl(libConfig.defaultPageIcon, block) const [authorImage, image] = await Promise.all([ getCompatibleImageUrl(authorImageBlockUrl, authorImageFallbackUrl), getCompatibleImageUrl(imageBlockUrl, imageFallbackUrl) ]) const author = getPageProperty('Author', block, recordMap) || libConfig.author // const socialDescription = // getPageProperty('Description', block, recordMap) || // libConfig.description // const lastUpdatedTime = getPageProperty( // 'Last Updated', // block, // recordMap // ) const publishedTime = getPageProperty('Published', block, recordMap) const datePublished = publishedTime ? new Date(publishedTime) : undefined // const dateUpdated = lastUpdatedTime // ? new Date(lastUpdatedTime) // : publishedTime // ? new Date(publishedTime) // : undefined const date = isBlogPost && datePublished ? `${datePublished.toLocaleString('en-US', { month: 'long' })} ${datePublished.getFullYear()}` : undefined const detail = date || author || libConfig.domain const pageInfo: NotionPageInfo = { pageId, title, image, imageObjectPosition, author, authorImage, detail } return { type: 'success', data: pageInfo } } async function isUrlReachable(url: string | null): Promise { if (!url) { return false } try { await ky.head(url) return true } catch { return false } } async function getCompatibleImageUrl( url: string | null, fallbackUrl: string | null ): Promise { const image = (await isUrlReachable(url)) ? url : fallbackUrl if (image) { const imageUrl = new URL(image) if (imageUrl.host === 'images.unsplash.com') { if (!imageUrl.searchParams.has('w')) { imageUrl.searchParams.set('w', '1200') imageUrl.searchParams.set('fit', 'max') return imageUrl.toString() } } } return image }