fix: social images

This commit is contained in:
Travis Fischer
2024-11-07 18:01:40 -06:00
parent 163fda453c
commit 224256b31d
8 changed files with 266 additions and 268 deletions

View File

@@ -36,23 +36,23 @@
"@react-icons/all-files": "^4.1.0",
"@vercel/og": "^0.6.3",
"classnames": "^2.5.1",
"date-fns": "^2.30.0",
"date-fns": "^4.1.0",
"expiry-map": "^2.0.0",
"fathom-client": "^3.4.1",
"ky": "^1.7.2",
"lqip-modern": "^2.1.0",
"next": "^15.0.2",
"next": "^15.0.3",
"notion-client": "^7.0.1",
"notion-types": "^7.0.1",
"notion-utils": "^7.0.1",
"p-map": "^7.0.2",
"p-memoize": "^7.1.1",
"posthog-js": "^1.20.2",
"posthog-js": "^1.181.0",
"prismjs": "^1.29.0",
"react": "^18.2.0",
"react-body-classname": "^1.3.1",
"react-dom": "^18.2.0",
"react-notion-x": "^7.0.1",
"react-notion-x": "^7.1.0",
"react-tweet-embed": "^2.0.0",
"react-use": "^17.4.2",
"rss": "^1.2.2"

View File

@@ -1,156 +0,0 @@
import ky from 'ky'
import { type NextApiRequest, type NextApiResponse } from 'next'
import { type PageBlock } from 'notion-types'
import {
getBlockIcon,
getBlockTitle,
getPageProperty,
isUrl,
parsePageId
} 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 } from '@/lib/types'
export default async function notionPageInfo(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).send({ error: 'method not allowed' })
}
const pageId: string = parsePageId(req.body.pageId)
if (!pageId) {
throw new Error('Invalid notion page id')
}
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 res.status(400).send({
error: `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<string>('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<string>('Author', block, recordMap) || libConfig.author
// const socialDescription =
// getPageProperty<string>('Description', block, recordMap) ||
// libConfig.description
// const lastUpdatedTime = getPageProperty<number>(
// 'Last Updated',
// block,
// recordMap
// )
const publishedTime = getPageProperty<number>('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
}
res.setHeader(
'Cache-Control',
'public, s-maxage=3600, max-age=3600, stale-while-revalidate=3600'
)
res.status(200).json(pageInfo)
}
async function isUrlReachable(url: string | null): Promise<boolean> {
if (!url) {
return false
}
try {
await ky.head(url)
return true
} catch {
return false
}
}
async function getCompatibleImageUrl(
url: string | null,
fallbackUrl: string | null
): Promise<string | null> {
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
}

View File

@@ -1,47 +1,43 @@
import { ImageResponse } from '@vercel/og'
import ky from 'ky'
import { type NextRequest } from 'next/server'
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 { api, apiHost, rootNotionPageId } from '@/lib/config'
import { type NotionPageInfo } from '@/lib/types'
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'
const interRegularFontP = ky(
new URL('../../public/fonts/Inter-Regular.ttf', import.meta.url)
).arrayBuffer()
import interRegularFont from '../fonts/inter-regular'
import interSemiBoldFont from '../fonts/inter-semibold'
const interBoldFontP = ky(
new URL('../../public/fonts/Inter-SemiBold.ttf', import.meta.url)
).arrayBuffer()
export const runtime = 'edge'
export const config = {
runtime: 'edge'
}
export default async function OGImage(req: NextRequest) {
export default async function OGImage(
req: NextApiRequest,
res: NextApiResponse
) {
const { searchParams } = new URL(req.url)
const pageId = searchParams.get('id') || rootNotionPageId
const pageId = searchParams.get('id') || libConfig.rootNotionPageId
if (!pageId) {
return new Response('Invalid notion page id', { status: 400 })
}
const pageInfoRes = await fetch(`${apiHost}${api.getNotionPageInfo}`, {
method: 'POST',
body: JSON.stringify({ pageId }),
headers: {
'content-type': 'application/json'
}
})
if (!pageInfoRes.ok) {
return new Response(pageInfoRes.statusText, { status: pageInfoRes.status })
const pageInfoOrError = await getNotionPageInfo({ pageId })
if (pageInfoOrError.type === 'error') {
return res.status(pageInfoOrError.error.statusCode).send({
error: pageInfoOrError.error.message
})
}
const pageInfo: NotionPageInfo = await pageInfoRes.json()
const pageInfo = pageInfoOrError.data
console.log(pageInfo)
const [interRegularFont, interBoldFont] = await Promise.all([
interRegularFontP,
interBoldFontP
])
return new ImageResponse(
(
<div
@@ -168,7 +164,7 @@ export default async function OGImage(req: NextRequest) {
},
{
name: 'Inter',
data: interBoldFont,
data: interSemiBoldFont,
style: 'normal',
weight: 700
}
@@ -176,3 +172,142 @@ export default async function OGImage(req: NextRequest) {
}
)
}
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<string>('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<string>('Author', block, recordMap) || libConfig.author
// const socialDescription =
// getPageProperty<string>('Description', block, recordMap) ||
// libConfig.description
// const lastUpdatedTime = getPageProperty<number>(
// 'Last Updated',
// block,
// recordMap
// )
const publishedTime = getPageProperty<number>('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<boolean> {
if (!url) {
return false
}
try {
await ky.head(url)
return true
} catch {
return false
}
}
async function getCompatibleImageUrl(
url: string | null,
fallbackUrl: string | null
): Promise<string | null> {
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
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

163
pnpm-lock.yaml generated
View File

@@ -27,8 +27,8 @@ importers:
specifier: ^2.5.1
version: 2.5.1
date-fns:
specifier: ^2.30.0
version: 2.30.0
specifier: ^4.1.0
version: 4.1.0
expiry-map:
specifier: ^2.0.0
version: 2.0.0
@@ -42,8 +42,8 @@ importers:
specifier: ^2.1.0
version: 2.1.0
next:
specifier: ^15.0.2
version: 15.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
specifier: ^15.0.3
version: 15.0.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
notion-client:
specifier: ^7.0.1
version: 7.0.1
@@ -60,8 +60,8 @@ importers:
specifier: ^7.1.1
version: 7.1.1
posthog-js:
specifier: ^1.20.2
version: 1.180.0
specifier: ^1.181.0
version: 1.181.0
prismjs:
specifier: ^1.29.0
version: 1.29.0
@@ -75,8 +75,8 @@ importers:
specifier: ^18.2.0
version: 18.3.1(react@18.3.1)
react-notion-x:
specifier: ^7.0.1
version: 7.0.1(@babel/runtime@7.26.0)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
specifier: ^7.1.0
version: 7.1.0(@babel/runtime@7.26.0)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-tweet-embed:
specifier: ^2.0.0
version: 2.0.0(react@18.3.1)
@@ -318,53 +318,53 @@ packages:
'@next/bundle-analyzer@15.0.2':
resolution: {integrity: sha512-bV566k+rDsaqXSUgHBof0iMIDx5DWtLx/98jvYtqb9x85e+WJzv+8cpDvbjtxQMf7nFC/LUkPmpruj1cOKfz4A==}
'@next/env@15.0.2':
resolution: {integrity: sha512-c0Zr0ModK5OX7D4ZV8Jt/wqoXtitLNPwUfG9zElCZztdaZyNVnN40rDXVZ/+FGuR4CcNV5AEfM6N8f+Ener7Dg==}
'@next/env@15.0.3':
resolution: {integrity: sha512-t9Xy32pjNOvVn2AS+Utt6VmyrshbpfUMhIjFO60gI58deSo/KgLOp31XZ4O+kY/Is8WAGYwA5gR7kOb1eORDBA==}
'@next/swc-darwin-arm64@15.0.2':
resolution: {integrity: sha512-GK+8w88z+AFlmt+ondytZo2xpwlfAR8U6CRwXancHImh6EdGfHMIrTSCcx5sOSBei00GyLVL0ioo1JLKTfprgg==}
'@next/swc-darwin-arm64@15.0.3':
resolution: {integrity: sha512-s3Q/NOorCsLYdCKvQlWU+a+GeAd3C8Rb3L1YnetsgwXzhc3UTWrtQpB/3eCjFOdGUj5QmXfRak12uocd1ZiiQw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@next/swc-darwin-x64@15.0.2':
resolution: {integrity: sha512-KUpBVxIbjzFiUZhiLIpJiBoelqzQtVZbdNNsehhUn36e2YzKHphnK8eTUW1s/4aPy5kH/UTid8IuVbaOpedhpw==}
'@next/swc-darwin-x64@15.0.3':
resolution: {integrity: sha512-Zxl/TwyXVZPCFSf0u2BNj5sE0F2uR6iSKxWpq4Wlk/Sv9Ob6YCKByQTkV2y6BCic+fkabp9190hyrDdPA/dNrw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@next/swc-linux-arm64-gnu@15.0.2':
resolution: {integrity: sha512-9J7TPEcHNAZvwxXRzOtiUvwtTD+fmuY0l7RErf8Yyc7kMpE47MIQakl+3jecmkhOoIyi/Rp+ddq7j4wG6JDskQ==}
'@next/swc-linux-arm64-gnu@15.0.3':
resolution: {integrity: sha512-T5+gg2EwpsY3OoaLxUIofmMb7ohAUlcNZW0fPQ6YAutaWJaxt1Z1h+8zdl4FRIOr5ABAAhXtBcpkZNwUcKI2fw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-arm64-musl@15.0.2':
resolution: {integrity: sha512-BjH4ZSzJIoTTZRh6rG+a/Ry4SW0HlizcPorqNBixBWc3wtQtj4Sn9FnRZe22QqrPnzoaW0ctvSz4FaH4eGKMww==}
'@next/swc-linux-arm64-musl@15.0.3':
resolution: {integrity: sha512-WkAk6R60mwDjH4lG/JBpb2xHl2/0Vj0ZRu1TIzWuOYfQ9tt9NFsIinI1Epma77JVgy81F32X/AeD+B2cBu/YQA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-x64-gnu@15.0.2':
resolution: {integrity: sha512-i3U2TcHgo26sIhcwX/Rshz6avM6nizrZPvrDVDY1bXcLH1ndjbO8zuC7RoHp0NSK7wjJMPYzm7NYL1ksSKFreA==}
'@next/swc-linux-x64-gnu@15.0.3':
resolution: {integrity: sha512-gWL/Cta1aPVqIGgDb6nxkqy06DkwJ9gAnKORdHWX1QBbSZZB+biFYPFti8aKIQL7otCE1pjyPaXpFzGeG2OS2w==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-linux-x64-musl@15.0.2':
resolution: {integrity: sha512-AMfZfSVOIR8fa+TXlAooByEF4OB00wqnms1sJ1v+iu8ivwvtPvnkwdzzFMpsK5jA2S9oNeeQ04egIWVb4QWmtQ==}
'@next/swc-linux-x64-musl@15.0.3':
resolution: {integrity: sha512-QQEMwFd8r7C0GxQS62Zcdy6GKx999I/rTO2ubdXEe+MlZk9ZiinsrjwoiBL5/57tfyjikgh6GOU2WRQVUej3UA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-win32-arm64-msvc@15.0.2':
resolution: {integrity: sha512-JkXysDT0/hEY47O+Hvs8PbZAeiCQVxKfGtr4GUpNAhlG2E0Mkjibuo8ryGD29Qb5a3IOnKYNoZlh/MyKd2Nbww==}
'@next/swc-win32-arm64-msvc@15.0.3':
resolution: {integrity: sha512-9TEp47AAd/ms9fPNgtgnT7F3M1Hf7koIYYWCMQ9neOwjbVWJsHZxrFbI3iEDJ8rf1TDGpmHbKxXf2IFpAvheIQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@next/swc-win32-x64-msvc@15.0.2':
resolution: {integrity: sha512-foaUL0NqJY/dX0Pi/UcZm5zsmSk5MtP/gxx3xOPyREkMFN+CTjctPfu3QaqrQHinaKdPnMWPJDKt4VjDfTBe/Q==}
'@next/swc-win32-x64-msvc@15.0.3':
resolution: {integrity: sha512-VNAz+HN4OGgvZs6MOoVfnn41kBzT+M+tB+OK4cww6DNyWS6wKaDpaAm/qLeOUbnMh0oVx1+mg0uoYARF69dJyA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -655,6 +655,9 @@ packages:
caniuse-lite@1.0.30001676:
resolution: {integrity: sha512-Qz6zwGCiPghQXGJvgQAem79esjitvJ+CxSbSQkW9H/UX5hg8XM88d4lp2W+MEQ81j+Hip58Il+jGVdazk1z9cw==}
caniuse-lite@1.0.30001678:
resolution: {integrity: sha512-RR+4U/05gNtps58PEBDZcPWTgEO2MBeoPZ96aQcjmfkBWRIDfN451fW2qyDA9/+HohLLIL5GqiMwA+IB1pWarw==}
canvas@2.11.2:
resolution: {integrity: sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==}
engines: {node: '>=6'}
@@ -781,9 +784,8 @@ packages:
resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==}
engines: {node: '>= 0.4'}
date-fns@2.30.0:
resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==}
engines: {node: '>=0.11'}
date-fns@4.1.0:
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
debounce@1.2.1:
resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==}
@@ -1646,16 +1648,16 @@ packages:
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
next@15.0.2:
resolution: {integrity: sha512-rxIWHcAu4gGSDmwsELXacqAPUk+j8dV/A9cDF5fsiCMpkBDYkO2AEaL1dfD+nNmDiU6QMCFN8Q30VEKapT9UHQ==}
engines: {node: '>=18.18.0'}
next@15.0.3:
resolution: {integrity: sha512-ontCbCRKJUIoivAdGB34yCaOcPgYXr9AAkV/IwqFfWWTXEPUgLYkSkqBhIk9KK7gGmgjc64B+RdoeIDM13Irnw==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
hasBin: true
peerDependencies:
'@opentelemetry/api': ^1.1.0
'@playwright/test': ^1.41.2
babel-plugin-react-compiler: '*'
react: ^18.2.0 || 19.0.0-rc-02c0e824-20241028
react-dom: ^18.2.0 || 19.0.0-rc-02c0e824-20241028
react: ^18.2.0 || 19.0.0-rc-66855b96-20241106
react-dom: ^18.2.0 || 19.0.0-rc-66855b96-20241106
sass: ^1.3.0
peerDependenciesMeta:
'@opentelemetry/api':
@@ -1848,8 +1850,8 @@ packages:
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
engines: {node: '>=8'}
path2d@0.2.1:
resolution: {integrity: sha512-Fl2z/BHvkTNvkuBzYTpTuirHZg6wW9z8+4SND/3mDTEcYbbNKWAy21dz9D3ePNNwrrK8pqZO5vLPZ1hLF6T7XA==}
path2d@0.2.2:
resolution: {integrity: sha512-+vnG6S4dYcYxZd+CZxzXCNKdELYZSKfohrk98yajCo1PtRoDgCTrrwOvK1GT0UoAdVszagDVllQc0U1vaX4NUQ==}
engines: {node: '>=6'}
pdfjs-dist@4.4.168:
@@ -1883,8 +1885,8 @@ packages:
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
engines: {node: ^10 || ^12 || >=14}
posthog-js@1.180.0:
resolution: {integrity: sha512-a+LTbmDUaHuskdIlRlTWWV1YOgQdfhhJJ8sSoW8+sCa+UrE8miD2B4Q2PtCj7mTcyNENu/ZR1VqkbfzIWRNWmQ==}
posthog-js@1.181.0:
resolution: {integrity: sha512-bI+J+f4E8x4JwbGtG6LReQv1Xvss01F6cs7UDlvffHySpVhNq4ptkNjV88B92IVEsrCtNYhy/TjFnGxk6RN0Qw==}
preact@10.24.3:
resolution: {integrity: sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==}
@@ -1923,8 +1925,8 @@ packages:
react-fast-compare@3.2.2:
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
react-hotkeys-hook@4.5.1:
resolution: {integrity: sha512-scAEJOh3Irm0g95NIn6+tQVf/OICCjsQsC9NBHfQws/Vxw4sfq1tDQut5fhTEvPraXhu/sHxRd9lOtxzyYuNAg==}
react-hotkeys-hook@4.6.1:
resolution: {integrity: sha512-XlZpbKUj9tkfgPgT9gA+1p7Ey6vFIZHttUjPqpTdyT5nqQ8mHL7elxvSbaC+dpSiHUSmr21Ya1mDxBZG3aje4Q==}
peerDependencies:
react: '>=16.8.1'
react-dom: '>=16.8.1'
@@ -1960,8 +1962,8 @@ packages:
react: ^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18
react-dom: ^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18
react-notion-x@7.0.1:
resolution: {integrity: sha512-/uvP1AGXXJgpbS7y9As2hk/v9pD0yC27fseLLkyhlbAHvWUb3TstHqI4CoJ8hn3jyuvyH6dKQ6wUX7jyb4S9Lg==}
react-notion-x@7.1.0:
resolution: {integrity: sha512-6O388IfFIyfmPmFO3AV4Q2HFZjQbH1ywVjjgM2NgSakrIhOsQZ7KHi1GuExWnG+hGxwAwT6O31MFM59xgT3qjA==}
engines: {node: '>=18'}
peerDependencies:
react: '>=18'
@@ -2716,30 +2718,30 @@ snapshots:
- bufferutil
- utf-8-validate
'@next/env@15.0.2': {}
'@next/env@15.0.3': {}
'@next/swc-darwin-arm64@15.0.2':
'@next/swc-darwin-arm64@15.0.3':
optional: true
'@next/swc-darwin-x64@15.0.2':
'@next/swc-darwin-x64@15.0.3':
optional: true
'@next/swc-linux-arm64-gnu@15.0.2':
'@next/swc-linux-arm64-gnu@15.0.3':
optional: true
'@next/swc-linux-arm64-musl@15.0.2':
'@next/swc-linux-arm64-musl@15.0.3':
optional: true
'@next/swc-linux-x64-gnu@15.0.2':
'@next/swc-linux-x64-gnu@15.0.3':
optional: true
'@next/swc-linux-x64-musl@15.0.2':
'@next/swc-linux-x64-musl@15.0.3':
optional: true
'@next/swc-win32-arm64-msvc@15.0.2':
'@next/swc-win32-arm64-msvc@15.0.3':
optional: true
'@next/swc-win32-x64-msvc@15.0.2':
'@next/swc-win32-x64-msvc@15.0.3':
optional: true
'@nodelib/fs.scandir@2.1.5':
@@ -3065,6 +3067,8 @@ snapshots:
caniuse-lite@1.0.30001676: {}
caniuse-lite@1.0.30001678: {}
canvas@2.11.2:
dependencies:
'@mapbox/node-pre-gyp': 1.0.11
@@ -3095,7 +3099,8 @@ snapshots:
client-only@0.0.1: {}
clsx@2.1.1: {}
clsx@2.1.1:
optional: true
cluster-key-slot@1.1.2: {}
@@ -3190,9 +3195,7 @@ snapshots:
es-errors: 1.3.0
is-data-view: 1.0.1
date-fns@2.30.0:
dependencies:
'@babel/runtime': 7.26.0
date-fns@4.1.0: {}
debounce@1.2.1: {}
@@ -3228,7 +3231,8 @@ snapshots:
denque@2.1.0: {}
dequal@2.0.3: {}
dequal@2.0.3:
optional: true
detect-libc@2.0.3: {}
@@ -4064,14 +4068,16 @@ snapshots:
p-map: 4.0.0
sharp: 0.33.5
make-cancellable-promise@1.3.2: {}
make-cancellable-promise@1.3.2:
optional: true
make-dir@3.1.0:
dependencies:
semver: 6.3.1
optional: true
make-event-props@1.6.2: {}
make-event-props@1.6.2:
optional: true
map-age-cleaner@0.1.3:
dependencies:
@@ -4093,6 +4099,7 @@ snapshots:
merge-refs@1.3.0(@types/react@18.3.12):
optionalDependencies:
'@types/react': 18.3.12
optional: true
merge2@1.4.1: {}
@@ -4165,26 +4172,26 @@ snapshots:
natural-compare@1.4.0: {}
next@15.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
next@15.0.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@next/env': 15.0.2
'@next/env': 15.0.3
'@swc/counter': 0.1.3
'@swc/helpers': 0.5.13
busboy: 1.6.0
caniuse-lite: 1.0.30001676
caniuse-lite: 1.0.30001678
postcss: 8.4.31
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
styled-jsx: 5.1.6(react@18.3.1)
optionalDependencies:
'@next/swc-darwin-arm64': 15.0.2
'@next/swc-darwin-x64': 15.0.2
'@next/swc-linux-arm64-gnu': 15.0.2
'@next/swc-linux-arm64-musl': 15.0.2
'@next/swc-linux-x64-gnu': 15.0.2
'@next/swc-linux-x64-musl': 15.0.2
'@next/swc-win32-arm64-msvc': 15.0.2
'@next/swc-win32-x64-msvc': 15.0.2
'@next/swc-darwin-arm64': 15.0.3
'@next/swc-darwin-x64': 15.0.3
'@next/swc-linux-arm64-gnu': 15.0.3
'@next/swc-linux-arm64-musl': 15.0.3
'@next/swc-linux-x64-gnu': 15.0.3
'@next/swc-linux-x64-musl': 15.0.3
'@next/swc-win32-arm64-msvc': 15.0.3
'@next/swc-win32-x64-msvc': 15.0.3
sharp: 0.33.5
transitivePeerDependencies:
- '@babel/core'
@@ -4378,16 +4385,17 @@ snapshots:
path-type@4.0.0: {}
path2d@0.2.1:
path2d@0.2.2:
optional: true
pdfjs-dist@4.4.168:
optionalDependencies:
canvas: 2.11.2
path2d: 0.2.1
path2d: 0.2.2
transitivePeerDependencies:
- encoding
- supports-color
optional: true
picocolors@1.1.1: {}
@@ -4407,7 +4415,7 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
posthog-js@1.180.0:
posthog-js@1.181.0:
dependencies:
core-js: 3.39.0
fflate: 0.4.8
@@ -4447,7 +4455,7 @@ snapshots:
react-fast-compare@3.2.2: {}
react-hotkeys-hook@4.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
react-hotkeys-hook@4.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
@@ -4484,7 +4492,7 @@ snapshots:
react-lifecycles-compat: 3.0.4
warning: 4.0.3
react-notion-x@7.0.1(@babel/runtime@7.26.0)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
react-notion-x@7.1.0(@babel/runtime@7.26.0)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@fisch0920/medium-zoom': 1.0.7
'@matejmazur/react-katex': 3.1.3(katex@0.16.11)(react@18.3.1)
@@ -4495,10 +4503,11 @@ snapshots:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-fast-compare: 3.2.2
react-hotkeys-hook: 4.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-hotkeys-hook: 4.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-image: 4.1.0(@babel/runtime@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-lazy-images: 1.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-modal: 3.16.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
optionalDependencies:
react-pdf: 9.1.1(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
transitivePeerDependencies:
- '@babel/runtime'
@@ -4523,6 +4532,7 @@ snapshots:
transitivePeerDependencies:
- encoding
- supports-color
optional: true
react-side-effect@2.1.2(react@18.3.1):
dependencies:
@@ -4930,7 +4940,8 @@ snapshots:
tiny-inflate@1.0.3: {}
tiny-invariant@1.3.3: {}
tiny-invariant@1.3.3:
optional: true
to-regex-range@5.0.1:
dependencies:

Binary file not shown.

Binary file not shown.