feat: improve tweet embedding using react-tweet

This commit is contained in:
Travis Fischer
2024-11-10 16:07:25 +07:00
parent 172bd84f83
commit e2017fdd3e
7 changed files with 165 additions and 87 deletions

View File

@@ -7,8 +7,12 @@ import { type PageBlock } from 'notion-types'
import { formatDate, getBlockTitle, getPageProperty } from 'notion-utils'
import * as React from 'react'
import BodyClassName from 'react-body-classname'
import { type NotionComponents, NotionRenderer } from 'react-notion-x'
import TweetEmbed from 'react-tweet-embed'
import {
type NotionComponents,
NotionRenderer,
useNotionContext
} from 'react-notion-x'
import { EmbeddedTweet, TweetNotFound, TweetSkeleton } from 'react-tweet'
import { useSearchParam } from 'react-use'
import type * as types from '@/lib/types'
@@ -97,7 +101,14 @@ const Modal = dynamic(
)
function Tweet({ id }: { id: string }) {
return <TweetEmbed tweetId={id} />
const { recordMap } = useNotionContext()
const tweet = (recordMap as types.ExtendedTweetRecordMap)?.tweets?.[id]
return (
<React.Suspense fallback={<TweetSkeleton />}>
{tweet ? <EmbeddedTweet tweet={tweet} /> : <TweetNotFound />}
</React.Suspense>
)
}
const propertyLastEditedTimeValue = (

62
lib/get-tweets.ts Normal file
View File

@@ -0,0 +1,62 @@
import { type ExtendedRecordMap } from 'notion-types'
import { getPageTweetIds } from 'notion-utils'
import pMap from 'p-map'
import pMemoize from 'p-memoize'
import { getTweet as getTweetData } from 'react-tweet/api'
import type { ExtendedTweetRecordMap } from './types'
import { db } from './db'
export async function getTweetsMap(
recordMap: ExtendedRecordMap
): Promise<void> {
const tweetIds = getPageTweetIds(recordMap)
const tweetsMap = Object.fromEntries(
await pMap(
tweetIds,
async (tweetId: string) => {
return [tweetId, await getTweet(tweetId)]
},
{
concurrency: 8
}
)
)
;(recordMap as ExtendedTweetRecordMap).tweets = tweetsMap
}
async function getTweetImpl(tweetId: string): Promise<any> {
if (!tweetId) return null
const cacheKey = `tweet:${tweetId}`
try {
try {
const cachedTweet = await db.get(cacheKey)
if (cachedTweet) {
return cachedTweet
}
} catch (err) {
// ignore redis errors
console.warn(`redis error get "${cacheKey}"`, err.message)
}
const tweetData = await getTweetData(tweetId)
try {
await db.set(cacheKey, tweetData)
} catch (err) {
// ignore redis errors
console.warn(`redis error set "${cacheKey}"`, err.message)
}
return tweetData
} catch (err: any) {
console.warn('failed to get tweet', tweetId, err.message)
return null
}
}
export const getTweet = pMemoize(getTweetImpl)

View File

@@ -12,6 +12,7 @@ import {
navigationLinks,
navigationStyle
} from './config'
import { getTweetsMap } from './get-tweets'
import { notion } from './notion-api'
import { getPreviewImageMap } from './preview-images'
@@ -64,6 +65,8 @@ export async function getPage(pageId: string): Promise<ExtendedRecordMap> {
;(recordMap as any).preview_images = previewImageMap
}
await getTweetsMap(recordMap)
return recordMap
}

View File

@@ -18,6 +18,10 @@ export interface PageProps {
error?: PageError
}
export interface ExtendedTweetRecordMap extends ExtendedRecordMap {
tweets: Record<string, any>
}
export interface Params extends ParsedUrlQuery {
pageId: string
}

View File

@@ -10,41 +10,12 @@ export default withBundleAnalyzer({
staticPageGenerationTimeout: 300,
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'www.notion.so',
pathname: '**'
},
{
protocol: 'https',
hostname: 'notion.so',
pathname: '**'
},
{
protocol: 'https',
hostname: 'images.unsplash.com',
pathname: '**'
},
{
protocol: 'https',
hostname: 'pbs.twimg.com',
pathname: '**'
},
{
protocol: 'https',
hostname: 'abs.twimg.com',
pathname: '**'
},
{
protocol: 'https',
hostname: 's3.us-west-2.amazonaws.com',
pathname: '**'
},
{
protocol: 'https',
hostname: 'transitivebullsh.it',
pathname: '**'
}
{ protocol: 'https', hostname: 'www.notion.so' },
{ protocol: 'https', hostname: 'notion.so' },
{ protocol: 'https', hostname: 'images.unsplash.com' },
{ protocol: 'https', hostname: 'abs.twimg.com' },
{ protocol: 'https', hostname: 'pbs.twimg.com' },
{ protocol: 'https', hostname: 's3.us-west-2.amazonaws.com' }
],
formats: ['image/avif', 'image/webp'],
dangerouslyAllowSVG: true,
@@ -62,5 +33,8 @@ export default withBundleAnalyzer({
'node_modules/react-dom'
)
return config
}
},
// See https://react-tweet.vercel.app/next#troubleshooting
transpilePackages: ['react-tweet']
})

View File

@@ -40,9 +40,9 @@
"ky": "^1.7.2",
"lqip-modern": "^2.2.1",
"next": "^15.0.3",
"notion-client": "^7.1.1",
"notion-types": "^7.1.1",
"notion-utils": "^7.1.1",
"notion-client": "^7.1.3",
"notion-types": "^7.1.3",
"notion-utils": "^7.1.3",
"p-map": "^7.0.2",
"p-memoize": "^7.1.1",
"posthog-js": "^1.181.0",
@@ -50,8 +50,8 @@
"react": "^18.2.0",
"react-body-classname": "^1.3.1",
"react-dom": "^18.2.0",
"react-notion-x": "^7.2.1",
"react-tweet-embed": "^2.0.0",
"react-notion-x": "^7.2.3",
"react-tweet": "^3.2.1",
"react-use": "^17.4.2",
"rss": "^1.2.2"
},

110
pnpm-lock.yaml generated
View File

@@ -39,14 +39,14 @@ importers:
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.1.1
version: 7.1.1
specifier: ^7.1.3
version: 7.1.3
notion-types:
specifier: ^7.1.1
version: 7.1.1
specifier: ^7.1.3
version: 7.1.3
notion-utils:
specifier: ^7.1.1
version: 7.1.1
specifier: ^7.1.3
version: 7.1.3
p-map:
specifier: ^7.0.2
version: 7.0.2
@@ -69,11 +69,11 @@ importers:
specifier: ^18.2.0
version: 18.3.1(react@18.3.1)
react-notion-x:
specifier: ^7.2.1
version: 7.2.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-tweet-embed:
specifier: ^2.0.0
version: 2.0.0(react@18.3.1)
specifier: ^7.2.3
version: 7.2.3(@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:
specifier: ^3.2.1
version: 3.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-use:
specifier: ^17.4.2
version: 17.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -1627,16 +1627,16 @@ packages:
resolution: {integrity: sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==}
engines: {node: '>=14.16'}
notion-client@7.1.1:
resolution: {integrity: sha512-mT/yEOIlbQzIAMsuIoWMX3XQrkoN07NMtSilDYIQQShxYJDdXMYfLuSoOgalEwFmwwsDnETHJqfrdkfe6sF9Rw==}
notion-client@7.1.3:
resolution: {integrity: sha512-84K4h/pD8fSIth5cKF0qUcHTqGdzzQ6x6hMVErZzbIFcXlOmJvHROd+no5epcMnw1juuIdeDbodN/wT8+U53OA==}
engines: {node: '>=18'}
notion-types@7.1.1:
resolution: {integrity: sha512-wsj/mwTi0hZjldvfVoKgtrXrOaBRgEsXic2n1kh6S1Aj7snx9Xo8sA8KtHqmXqKhJ8BFEsavh0Dv3TlyMv8aWg==}
notion-types@7.1.3:
resolution: {integrity: sha512-kUcMa5SXpzNxmE9PdrSjiP1lmba8ue7+KdMjcmq8QO/wOmdmXCKhr1FHXNqP5Wdxmm2i9sDq87x0S1pA/vJ4kg==}
engines: {node: '>=18'}
notion-utils@7.1.1:
resolution: {integrity: sha512-Hks/sipBA7aDZ3TS90CRKuKvCsvs/cW+MXzZPFh/Q1uTjhEt9GynSUl8o11k+Nm4No3+deQJY/hsuv90gyCFjg==}
notion-utils@7.1.3:
resolution: {integrity: sha512-52dReJMdEBt6O4f/Y9FZqhRNBoDY73p0i1XSqpunaTzxIQW61St6a+8bhZ0HiFPpXvxPUw7HCBfNAqn0CrmF2A==}
engines: {node: '>=18'}
npm-normalize-package-bin@4.0.0:
@@ -1883,8 +1883,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.2.1:
resolution: {integrity: sha512-wy/MOp/+pOL/qKUcY3azMvTn/y6AkKqutFboOKn0OUouPe2OaOjUk/VIGFV081UL7+nwCKbWecHgRZ9MuwwHng==}
react-notion-x@7.2.3:
resolution: {integrity: sha512-eTRkypql15+Uej00MvkG8cWhbt5PDFmj4Zfvyr7582GPblefHFDmtTZOxMiVJhSU7Kv8LZEuGXsYQtFE1cQPqQ==}
engines: {node: '>=18'}
peerDependencies:
react: '>=18'
@@ -1905,10 +1905,11 @@ packages:
peerDependencies:
react: ^16.3.0 || ^17.0.0 || ^18.0.0
react-tweet-embed@2.0.0:
resolution: {integrity: sha512-g2kfPjSRTOKeJtaQF5EMuSTmp/q8I0qdDs/pZ2qLXZjCWExDT/JgjxSlyM65NyNzsz8072PDpvlO/sIXwwVpdQ==}
react-tweet@3.2.1:
resolution: {integrity: sha512-dktP3RMuwRB4pnSDocKpSsW5Hq1IXRW6fONkHhxT5EBIXsKZzdQuI70qtub1XN2dtZdkJWWxfBm/Q+kN+vRYFA==}
peerDependencies:
react: '>=17'
react: '>= 18.0.0'
react-dom: '>= 18.0.0'
react-universal-interface@0.6.2:
resolution: {integrity: sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==}
@@ -2209,6 +2210,11 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
swr@2.2.5:
resolution: {integrity: sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==}
peerDependencies:
react: ^16.11.0 || ^17.0.0 || ^18.0.0
tapable@2.2.1:
resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
engines: {node: '>=6'}
@@ -2320,6 +2326,11 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17.0.0
use-sync-external-store@1.2.2:
resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@@ -2465,8 +2476,8 @@ snapshots:
'@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.6.3)
'@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.6.3)
eslint-config-prettier: 9.1.0(eslint@8.57.1)
eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-plugin-import@2.31.0)(eslint@8.57.1)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1)
eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
eslint-plugin-jest: 28.8.3(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3)
eslint-plugin-jest-dom: 5.4.0(eslint@8.57.1)
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
@@ -2980,8 +2991,7 @@ snapshots:
client-only@0.0.1: {}
clsx@2.1.1:
optional: true
clsx@2.1.1: {}
cluster-key-slot@1.1.2: {}
@@ -3247,37 +3257,37 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-plugin-import@2.31.0)(eslint@8.57.1):
eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.3.7
enhanced-resolve: 5.17.1
eslint: 8.57.1
eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1)
eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
fast-glob: 3.3.2
get-tsconfig: 4.8.1
is-bun-module: 1.2.1
is-glob: 4.0.3
optionalDependencies:
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
transitivePeerDependencies:
- '@typescript-eslint/parser'
- eslint-import-resolver-node
- eslint-import-resolver-webpack
- supports-color
eslint-module-utils@2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1):
eslint-module-utils@2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.6.3)
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-plugin-import@2.31.0)(eslint@8.57.1)
eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1)
transitivePeerDependencies:
- supports-color
eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1):
eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.8
@@ -3288,7 +3298,7 @@ snapshots:
doctrine: 2.1.0
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1)
eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
hasown: 2.0.2
is-core-module: 2.15.1
is-glob: 4.0.3
@@ -4072,21 +4082,21 @@ snapshots:
normalize-url@8.0.1: {}
notion-client@7.1.1:
notion-client@7.1.3:
dependencies:
ky: 1.7.2
notion-types: 7.1.1
notion-utils: 7.1.1
notion-types: 7.1.3
notion-utils: 7.1.3
p-map: 7.0.2
notion-types@7.1.1: {}
notion-types@7.1.3: {}
notion-utils@7.1.1:
notion-utils@7.1.3:
dependencies:
is-url-superb: 6.1.0
mem: 10.0.0
normalize-url: 8.0.1
notion-types: 7.1.1
notion-types: 7.1.3
p-queue: 8.0.1
npm-normalize-package-bin@4.0.0: {}
@@ -4333,13 +4343,13 @@ snapshots:
react-lifecycles-compat: 3.0.4
warning: 4.0.3
react-notion-x@7.2.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.2.3(@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)
katex: 0.16.11
notion-types: 7.1.1
notion-utils: 7.1.1
notion-types: 7.1.3
notion-utils: 7.1.3
prismjs: 1.29.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
@@ -4379,9 +4389,13 @@ snapshots:
dependencies:
react: 18.3.1
react-tweet-embed@2.0.0(react@18.3.1):
react-tweet@3.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@swc/helpers': 0.5.13
clsx: 2.1.1
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
swr: 2.2.5(react@18.3.1)
react-universal-interface@0.6.2(react@18.3.1)(tslib@2.8.1):
dependencies:
@@ -4748,6 +4762,12 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
swr@2.2.5(react@18.3.1):
dependencies:
client-only: 0.0.1
react: 18.3.1
use-sync-external-store: 1.2.2(react@18.3.1)
tapable@2.2.1: {}
tar@6.2.1:
@@ -4865,6 +4885,10 @@ snapshots:
'@use-it/event-listener': 0.1.7(react@18.3.1)
react: 18.3.1
use-sync-external-store@1.2.2(react@18.3.1):
dependencies:
react: 18.3.1
util-deprecate@1.0.2:
optional: true