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,
parsePageId
} from 'notion-utils'
import * as libConfig from '@/lib/config'
import interSemiBoldFont from '@/lib/fonts/inter-semibold'
import { mapImageUrl } from '@/lib/map-image-url'
import { notion } from '@/lib/notion-api'
import { type NotionPageInfo, type PageError } from '@/lib/types'
export const runtime = 'edge'
export default async function OGImage(
req: NextApiRequest,
res: NextApiResponse
) {
const { searchParams } = new URL(req.url!)
const pageId = parsePageId(
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: 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}%`
: undefined
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 : undefined,
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 | undefined | null
): Promise {
if (!url) {
return false
}
try {
await ky.head(url)
return true
} catch {
return false
}
}
async function getCompatibleImageUrl(
url: string | undefined | null,
fallbackUrl: string | undefined | 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 ?? undefined
}