Skip to content

Commit 261e72d

Browse files
authored
Log when a tweet does not exist or if it's private (#133)
* Log when a tweet does not exist or if it's private * Added caching * Updated deps * Updated docs * Updated docs more * Added changeset * Updated deps * Updated link
1 parent 1592c89 commit 261e72d

File tree

19 files changed

+2365
-1322
lines changed

19 files changed

+2365
-1322
lines changed

.changeset/warm-impalas-dance.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'react-tweet': minor
3+
---
4+
5+
Updated docs on caching tweets and added fetchTweet function.

apps/create-react-app/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
"react-tweet": "workspace:*"
1818
},
1919
"devDependencies": {
20-
"@babel/runtime": "7.22.6"
20+
"@babel/runtime": "7.22.6",
21+
"postcss-flexbugs-fixes": "^5.0.2"
2122
},
2223
"browserslist": [
2324
">0.2%",

apps/custom-tweet-dub/package.json

+14-14
Original file line numberDiff line numberDiff line change
@@ -12,27 +12,27 @@
1212
"clean": "rm -rf .next && rm -rf .turbo"
1313
},
1414
"dependencies": {
15-
"@next/mdx": "^13.4.6",
16-
"clsx": "^1.2.1",
17-
"next": "13.4.6",
15+
"@next/mdx": "^14.0.4",
16+
"clsx": "^2.0.0",
17+
"next": "14.0.4",
1818
"react": "^18.2.0",
1919
"react-dom": "^18.2.0",
20-
"react-parallax-tilt": "^1.7.149",
20+
"react-parallax-tilt": "^1.7.177",
2121
"react-tweet": "workspace:*"
2222
},
2323
"devDependencies": {
24-
"@tailwindcss/forms": "^0.5.3",
25-
"@tailwindcss/typography": "^0.5.9",
26-
"@types/node": "20.3.1",
27-
"@types/react": "^18.2.13",
28-
"autoprefixer": "^10.4.14",
29-
"eslint": "^8.43.0",
30-
"eslint-config-next": "^13.4.6",
31-
"postcss": "^8.4.24",
24+
"@tailwindcss/forms": "^0.5.7",
25+
"@tailwindcss/typography": "^0.5.10",
26+
"@types/node": "20.10.5",
27+
"@types/react": "^18.2.45",
28+
"autoprefixer": "^10.4.16",
29+
"eslint": "^8.56.0",
30+
"eslint-config-next": "^14.0.4",
31+
"postcss": "^8.4.32",
3232
"tailwind-scrollbar-hide": "^1.1.7",
33-
"tailwindcss": "^3.3.2",
33+
"tailwindcss": "^3.3.6",
3434
"tailwindcss-radix": "^2.8.0",
35-
"typescript": "^5.1.3"
35+
"typescript": "^5.3.3"
3636
},
3737
"version": null
3838
}
+10-11
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,22 @@
11
import { NextResponse } from 'next/server'
22
import { getTweet } from 'react-tweet/api'
3-
import cors from 'edge-cors'
43

54
type RouteSegment = { params: { id: string } }
65

7-
export async function GET(req: Request, { params }: RouteSegment) {
6+
export const fetchCache = 'only-cache'
7+
8+
export async function GET(_req: Request, { params }: RouteSegment) {
89
try {
910
const tweet = await getTweet(params.id)
10-
return cors(
11-
req,
12-
NextResponse.json({ data: tweet ?? null }, { status: tweet ? 200 : 404 })
11+
return NextResponse.json(
12+
{ data: tweet ?? null },
13+
{ status: tweet ? 200 : 404 }
1314
)
1415
} catch (error: any) {
15-
return cors(
16-
req,
17-
NextResponse.json(
18-
{ error: error.message ?? 'Bad request.' },
19-
{ status: 400 }
20-
)
16+
console.error(error)
17+
return NextResponse.json(
18+
{ error: error.message ?? 'Bad request.' },
19+
{ status: 400 }
2120
)
2221
}
2322
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Suspense } from 'react'
2+
import { TweetSkeleton } from 'react-tweet'
3+
import TweetPage from './tweet-page'
4+
5+
export const revalidate = 86400
6+
7+
const Page = ({ params }: { params: { tweet: string } }) => (
8+
<Suspense fallback={<TweetSkeleton />}>
9+
<TweetPage id={params.tweet} />
10+
</Suspense>
11+
)
12+
13+
export default Page
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { unstable_cache } from 'next/cache'
2+
import { getTweet as _getTweet } from 'react-tweet/api'
3+
import { EmbeddedTweet, TweetNotFound } from 'react-tweet'
4+
5+
const getTweet = unstable_cache(
6+
async (id: string) => _getTweet(id),
7+
['tweet'],
8+
{ revalidate: 3600 * 24 }
9+
)
10+
11+
const TweetPage = async ({ id }: { id: string }) => {
12+
try {
13+
const tweet = await getTweet(id)
14+
return tweet ? <EmbeddedTweet tweet={tweet} /> : <TweetNotFound />
15+
} catch (error) {
16+
console.error(error)
17+
return <TweetNotFound error={error} />
18+
}
19+
}
20+
21+
export default TweetPage
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Suspense } from 'react'
2+
import { TweetSkeleton } from 'react-tweet'
3+
import TweetPage from './tweet-page'
4+
5+
export const revalidate = 86400
6+
7+
const Page = ({ params }: { params: { tweet: string } }) => (
8+
<Suspense fallback={<TweetSkeleton />}>
9+
<TweetPage id={params.tweet} />
10+
</Suspense>
11+
)
12+
13+
export default Page
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { fetchTweet, Tweet } from 'react-tweet/api'
2+
import { EmbeddedTweet, TweetNotFound } from 'react-tweet'
3+
import { kv } from '@vercel/kv'
4+
5+
async function getTweet(
6+
id: string,
7+
fetchOptions?: RequestInit
8+
): Promise<Tweet | undefined> {
9+
try {
10+
const { data, tombstone, notFound } = await fetchTweet(id, fetchOptions)
11+
12+
if (data) {
13+
await kv.set(`tweet:${id}`, data)
14+
return data
15+
} else if (tombstone || notFound) {
16+
// remove the tweet from the cache if it has been made private by the author (tombstone)
17+
// or if it no longer exists.
18+
await kv.del(`tweet:${id}`)
19+
}
20+
} catch (error) {
21+
console.error('fetching the tweet failed with:', error)
22+
}
23+
24+
const cachedTweet = await kv.get<Tweet>(`tweet:${id}`)
25+
return cachedTweet ?? undefined
26+
}
27+
28+
const TweetPage = async ({ id }: { id: string }) => {
29+
try {
30+
const tweet = await getTweet(id)
31+
return tweet ? <EmbeddedTweet tweet={tweet} /> : <TweetNotFound />
32+
} catch (error) {
33+
console.error(error)
34+
return <TweetNotFound error={error} />
35+
}
36+
}
37+
38+
export default TweetPage

apps/next-app/package.json

+9-9
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,20 @@
1212
"clean": "rm -rf .next && rm -rf .turbo"
1313
},
1414
"dependencies": {
15-
"@next/mdx": "^13.4.6",
16-
"clsx": "^1.2.1",
15+
"@next/mdx": "^14.0.4",
16+
"@vercel/kv": "^1.0.1",
17+
"clsx": "^2.0.0",
1718
"date-fns": "^2.30.0",
18-
"edge-cors": "^0.2.1",
19-
"next": "13.4.6",
19+
"next": "14.0.4",
2020
"react": "^18.2.0",
2121
"react-dom": "^18.2.0",
2222
"react-tweet": "workspace:*"
2323
},
2424
"devDependencies": {
25-
"@types/node": "20.3.1",
26-
"@types/react": "^18.2.13",
27-
"eslint": "^8.43.0",
28-
"eslint-config-next": "^13.4.6",
29-
"typescript": "^5.1.3"
25+
"@types/node": "20.10.4",
26+
"@types/react": "^18.2.45",
27+
"eslint": "^8.56.0",
28+
"eslint-config-next": "^14.0.4",
29+
"typescript": "^5.3.3"
3030
}
3131
}

apps/site/app/api/tweet/[id]/route.ts

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import cors from 'edge-cors'
44

55
type RouteSegment = { params: { id: string } }
66

7+
export const fetchCache = 'only-cache'
8+
79
export async function GET(req: Request, { params }: RouteSegment) {
810
try {
911
const tweet = await getTweet(params.id)
@@ -12,6 +14,7 @@ export async function GET(req: Request, { params }: RouteSegment) {
1214
NextResponse.json({ data: tweet ?? null }, { status: tweet ? 200 : 404 })
1315
)
1416
} catch (error: any) {
17+
console.error(error)
1518
return cors(
1619
req,
1720
NextResponse.json(

apps/site/package.json

+9-9
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,19 @@
1212
},
1313
"dependencies": {
1414
"edge-cors": "^0.2.1",
15-
"next": "^13.4.7",
16-
"nextra": "^2.8.0",
17-
"nextra-theme-docs": "^2.8.0",
15+
"next": "^14.0.4",
16+
"nextra": "^2.13.2",
17+
"nextra-theme-docs": "^2.13.2",
1818
"react": "^18.2.0",
1919
"react-dom": "^18.2.0",
2020
"react-tweet": "workspace:*"
2121
},
2222
"devDependencies": {
23-
"@types/node": "20.3.2",
24-
"@types/react": "^18.2.14",
25-
"autoprefixer": "^10.4.14",
26-
"postcss": "^8.4.24",
27-
"tailwindcss": "^3.3.2",
28-
"typescript": "^5.1.5"
23+
"@types/node": "20.10.5",
24+
"@types/react": "^18.2.45",
25+
"autoprefixer": "^10.4.16",
26+
"postcss": "^8.4.32",
27+
"tailwindcss": "^3.3.6",
28+
"typescript": "^5.3.3"
2929
}
3030
}

apps/site/pages/api-reference.mdx

+19
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,25 @@ Fetches and returns a [`Tweet`](https://github.com/vercel-labs/react-tweet/blob/
2020

2121
If a tweet is not found it returns `undefined`.
2222

23+
## `fetchTweet`
24+
25+
```tsx
26+
function fetchTweet(
27+
id: string,
28+
fetchOptions?: RequestInit
29+
): Promise<{
30+
data?: Tweet | undefined
31+
tombstone?: true | undefined
32+
notFound?: true | undefined
33+
}>
34+
```
35+
36+
Fetches and returns a [`Tweet`](https://github.com/vercel-labs/react-tweet/blob/main/packages/react-tweet/src/api/types/tweet.ts) just like [`getTweet`](#gettweet), but it also returns additional information about the tweet:
37+
38+
- **data** - `Tweet` (Optional): The tweet data.
39+
- **tombstone** - `true` (Optional): Indicates if the tweet has been made private.
40+
- **notFound** - `true` (Optional): Indicates if the tweet was not found.
41+
2342
## `enrichTweet`
2443

2544
```tsx

apps/site/pages/index.mdx

+60
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ Now follow the usage instructions for your framework or builder:
2828
- [Vite](/vite)
2929
- [Create React App](/create-react-app)
3030

31+
> **Important**: Before going to production, we recommend [enabling cache for the Twitter API](#enabling-cache-for-the-twitter-api) as server IPs might get rate limited by Twitter.
32+
3133
## Choosing a theme
3234

3335
The [`prefers-color-scheme`](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme) CSS media feature is used to select the theme of the tweet.
@@ -61,3 +63,61 @@ In CSS Modules, you can use the `:global` selector to update the CSS variables u
6163
```
6264

6365
For Global CSS the usage of `:global` is not necessary.
66+
67+
## Enabling cache for the Twitter API
68+
69+
Rendering tweets requires making a call to Twitter's syndication API. Getting rate limited by that API is very hard but it's possible if you're relying only on the endpoint we provide for SWR (`react-tweet.vercel.app/api/tweet/:id`) as the IPs of the server are making many requests to the syndication API. This also applies to RSC where the API endpoint is not required but the server is still making the request from the same IP.
70+
71+
To prevent this, you can use a db like Redis or [Vercel KV](https://vercel.com/docs/storage/vercel-kv) to cache the tweets. For example using [Vercel KV](https://vercel.com/docs/storage/vercel-kv):
72+
73+
```tsx
74+
import { Suspense } from 'react'
75+
import { TweetSkeleton, EmbeddedTweet, TweetNotFound } from 'react-tweet'
76+
import { fetchTweet, Tweet } from 'react-tweet/api'
77+
import { kv } from '@vercel/kv'
78+
79+
async function getTweet(
80+
id: string,
81+
fetchOptions?: RequestInit
82+
): Promise<Tweet | undefined> {
83+
try {
84+
const { data, tombstone, notFound } = await fetchTweet(id, fetchOptions)
85+
86+
if (data) {
87+
await kv.set(`tweet:${id}`, data)
88+
return data
89+
} else if (tombstone || notFound) {
90+
// remove the tweet from the cache if it has been made private by the author (tombstone)
91+
// or if it no longer exists.
92+
await kv.del(`tweet:${id}`)
93+
}
94+
} catch (error) {
95+
console.error('fetching the tweet failed with:', error)
96+
}
97+
98+
const cachedTweet = await kv.get<Tweet>(`tweet:${id}`)
99+
return cachedTweet ?? undefined
100+
}
101+
102+
const TweetPage = async ({ id }: { id: string }) => {
103+
try {
104+
const tweet = await getTweet(id)
105+
return tweet ? <EmbeddedTweet tweet={tweet} /> : <TweetNotFound />
106+
} catch (error) {
107+
console.error(error)
108+
return <TweetNotFound error={error} />
109+
}
110+
}
111+
112+
const Page = ({ params }: { params: { tweet: string } }) => (
113+
<Suspense fallback={<TweetSkeleton />}>
114+
<TweetPage id={params.tweet} />
115+
</Suspense>
116+
)
117+
118+
export default Page
119+
```
120+
121+
You can see it working at [react-tweet-next.vercel.app/light/vercel-kv/1629307668568633344](https://react-tweet-next.vercel.app/light/vercel-kv/1629307668568633344) ([source](https://github.com/vercel/react-tweet/blob/main/apps/next-app/app/light/vercel-kv/%5Btweet%5D/page.tsx)).
122+
123+
If you're using Next.js then using [`unstable_cache`](/next#enabling-cache) works too.

0 commit comments

Comments
 (0)