Skip to content

Commit

Permalink
Log when a tweet does not exist or if it's private (#133)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
lfades authored Dec 18, 2023
1 parent 1592c89 commit 261e72d
Show file tree
Hide file tree
Showing 19 changed files with 2,365 additions and 1,322 deletions.
5 changes: 5 additions & 0 deletions .changeset/warm-impalas-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'react-tweet': minor
---

Updated docs on caching tweets and added fetchTweet function.
3 changes: 2 additions & 1 deletion apps/create-react-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"react-tweet": "workspace:*"
},
"devDependencies": {
"@babel/runtime": "7.22.6"
"@babel/runtime": "7.22.6",
"postcss-flexbugs-fixes": "^5.0.2"
},
"browserslist": [
">0.2%",
Expand Down
28 changes: 14 additions & 14 deletions apps/custom-tweet-dub/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,27 @@
"clean": "rm -rf .next && rm -rf .turbo"
},
"dependencies": {
"@next/mdx": "^13.4.6",
"clsx": "^1.2.1",
"next": "13.4.6",
"@next/mdx": "^14.0.4",
"clsx": "^2.0.0",
"next": "14.0.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-parallax-tilt": "^1.7.149",
"react-parallax-tilt": "^1.7.177",
"react-tweet": "workspace:*"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.9",
"@types/node": "20.3.1",
"@types/react": "^18.2.13",
"autoprefixer": "^10.4.14",
"eslint": "^8.43.0",
"eslint-config-next": "^13.4.6",
"postcss": "^8.4.24",
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10",
"@types/node": "20.10.5",
"@types/react": "^18.2.45",
"autoprefixer": "^10.4.16",
"eslint": "^8.56.0",
"eslint-config-next": "^14.0.4",
"postcss": "^8.4.32",
"tailwind-scrollbar-hide": "^1.1.7",
"tailwindcss": "^3.3.2",
"tailwindcss": "^3.3.6",
"tailwindcss-radix": "^2.8.0",
"typescript": "^5.1.3"
"typescript": "^5.3.3"
},
"version": null
}
21 changes: 10 additions & 11 deletions apps/next-app/app/api/tweet/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
import { NextResponse } from 'next/server'
import { getTweet } from 'react-tweet/api'
import cors from 'edge-cors'

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

export async function GET(req: Request, { params }: RouteSegment) {
export const fetchCache = 'only-cache'

export async function GET(_req: Request, { params }: RouteSegment) {
try {
const tweet = await getTweet(params.id)
return cors(
req,
NextResponse.json({ data: tweet ?? null }, { status: tweet ? 200 : 404 })
return NextResponse.json(
{ data: tweet ?? null },
{ status: tweet ? 200 : 404 }
)
} catch (error: any) {
return cors(
req,
NextResponse.json(
{ error: error.message ?? 'Bad request.' },
{ status: 400 }
)
console.error(error)
return NextResponse.json(
{ error: error.message ?? 'Bad request.' },
{ status: 400 }
)
}
}
13 changes: 13 additions & 0 deletions apps/next-app/app/light/cache/[tweet]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Suspense } from 'react'
import { TweetSkeleton } from 'react-tweet'
import TweetPage from './tweet-page'

export const revalidate = 86400

const Page = ({ params }: { params: { tweet: string } }) => (
<Suspense fallback={<TweetSkeleton />}>
<TweetPage id={params.tweet} />
</Suspense>
)

export default Page
21 changes: 21 additions & 0 deletions apps/next-app/app/light/cache/[tweet]/tweet-page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { unstable_cache } from 'next/cache'
import { getTweet as _getTweet } from 'react-tweet/api'
import { EmbeddedTweet, TweetNotFound } from 'react-tweet'

const getTweet = unstable_cache(
async (id: string) => _getTweet(id),
['tweet'],
{ revalidate: 3600 * 24 }
)

const TweetPage = async ({ id }: { id: string }) => {
try {
const tweet = await getTweet(id)
return tweet ? <EmbeddedTweet tweet={tweet} /> : <TweetNotFound />
} catch (error) {
console.error(error)
return <TweetNotFound error={error} />
}
}

export default TweetPage
13 changes: 13 additions & 0 deletions apps/next-app/app/light/vercel-kv/[tweet]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Suspense } from 'react'
import { TweetSkeleton } from 'react-tweet'
import TweetPage from './tweet-page'

export const revalidate = 86400

const Page = ({ params }: { params: { tweet: string } }) => (
<Suspense fallback={<TweetSkeleton />}>
<TweetPage id={params.tweet} />
</Suspense>
)

export default Page
38 changes: 38 additions & 0 deletions apps/next-app/app/light/vercel-kv/[tweet]/tweet-page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { fetchTweet, Tweet } from 'react-tweet/api'
import { EmbeddedTweet, TweetNotFound } from 'react-tweet'
import { kv } from '@vercel/kv'

async function getTweet(
id: string,
fetchOptions?: RequestInit
): Promise<Tweet | undefined> {
try {
const { data, tombstone, notFound } = await fetchTweet(id, fetchOptions)

if (data) {
await kv.set(`tweet:${id}`, data)
return data
} else if (tombstone || notFound) {
// remove the tweet from the cache if it has been made private by the author (tombstone)
// or if it no longer exists.
await kv.del(`tweet:${id}`)
}
} catch (error) {
console.error('fetching the tweet failed with:', error)
}

const cachedTweet = await kv.get<Tweet>(`tweet:${id}`)
return cachedTweet ?? undefined
}

const TweetPage = async ({ id }: { id: string }) => {
try {
const tweet = await getTweet(id)
return tweet ? <EmbeddedTweet tweet={tweet} /> : <TweetNotFound />
} catch (error) {
console.error(error)
return <TweetNotFound error={error} />
}
}

export default TweetPage
18 changes: 9 additions & 9 deletions apps/next-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,20 @@
"clean": "rm -rf .next && rm -rf .turbo"
},
"dependencies": {
"@next/mdx": "^13.4.6",
"clsx": "^1.2.1",
"@next/mdx": "^14.0.4",
"@vercel/kv": "^1.0.1",
"clsx": "^2.0.0",
"date-fns": "^2.30.0",
"edge-cors": "^0.2.1",
"next": "13.4.6",
"next": "14.0.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-tweet": "workspace:*"
},
"devDependencies": {
"@types/node": "20.3.1",
"@types/react": "^18.2.13",
"eslint": "^8.43.0",
"eslint-config-next": "^13.4.6",
"typescript": "^5.1.3"
"@types/node": "20.10.4",
"@types/react": "^18.2.45",
"eslint": "^8.56.0",
"eslint-config-next": "^14.0.4",
"typescript": "^5.3.3"
}
}
3 changes: 3 additions & 0 deletions apps/site/app/api/tweet/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import cors from 'edge-cors'

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

export const fetchCache = 'only-cache'

export async function GET(req: Request, { params }: RouteSegment) {
try {
const tweet = await getTweet(params.id)
Expand All @@ -12,6 +14,7 @@ export async function GET(req: Request, { params }: RouteSegment) {
NextResponse.json({ data: tweet ?? null }, { status: tweet ? 200 : 404 })
)
} catch (error: any) {
console.error(error)
return cors(
req,
NextResponse.json(
Expand Down
18 changes: 9 additions & 9 deletions apps/site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,19 @@
},
"dependencies": {
"edge-cors": "^0.2.1",
"next": "^13.4.7",
"nextra": "^2.8.0",
"nextra-theme-docs": "^2.8.0",
"next": "^14.0.4",
"nextra": "^2.13.2",
"nextra-theme-docs": "^2.13.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-tweet": "workspace:*"
},
"devDependencies": {
"@types/node": "20.3.2",
"@types/react": "^18.2.14",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.24",
"tailwindcss": "^3.3.2",
"typescript": "^5.1.5"
"@types/node": "20.10.5",
"@types/react": "^18.2.45",
"autoprefixer": "^10.4.16",
"postcss": "^8.4.32",
"tailwindcss": "^3.3.6",
"typescript": "^5.3.3"
}
}
19 changes: 19 additions & 0 deletions apps/site/pages/api-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,25 @@ Fetches and returns a [`Tweet`](https://github.com/vercel-labs/react-tweet/blob/

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

## `fetchTweet`

```tsx
function fetchTweet(
id: string,
fetchOptions?: RequestInit
): Promise<{
data?: Tweet | undefined
tombstone?: true | undefined
notFound?: true | undefined
}>
```

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:

- **data** - `Tweet` (Optional): The tweet data.
- **tombstone** - `true` (Optional): Indicates if the tweet has been made private.
- **notFound** - `true` (Optional): Indicates if the tweet was not found.

## `enrichTweet`

```tsx
Expand Down
60 changes: 60 additions & 0 deletions apps/site/pages/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ Now follow the usage instructions for your framework or builder:
- [Vite](/vite)
- [Create React App](/create-react-app)

> **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.
## Choosing a theme

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.
Expand Down Expand Up @@ -61,3 +63,61 @@ In CSS Modules, you can use the `:global` selector to update the CSS variables u
```

For Global CSS the usage of `:global` is not necessary.

## Enabling cache for the Twitter API

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.

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):

```tsx
import { Suspense } from 'react'
import { TweetSkeleton, EmbeddedTweet, TweetNotFound } from 'react-tweet'
import { fetchTweet, Tweet } from 'react-tweet/api'
import { kv } from '@vercel/kv'

async function getTweet(
id: string,
fetchOptions?: RequestInit
): Promise<Tweet | undefined> {
try {
const { data, tombstone, notFound } = await fetchTweet(id, fetchOptions)

if (data) {
await kv.set(`tweet:${id}`, data)
return data
} else if (tombstone || notFound) {
// remove the tweet from the cache if it has been made private by the author (tombstone)
// or if it no longer exists.
await kv.del(`tweet:${id}`)
}
} catch (error) {
console.error('fetching the tweet failed with:', error)
}

const cachedTweet = await kv.get<Tweet>(`tweet:${id}`)
return cachedTweet ?? undefined
}

const TweetPage = async ({ id }: { id: string }) => {
try {
const tweet = await getTweet(id)
return tweet ? <EmbeddedTweet tweet={tweet} /> : <TweetNotFound />
} catch (error) {
console.error(error)
return <TweetNotFound error={error} />
}
}

const Page = ({ params }: { params: { tweet: string } }) => (
<Suspense fallback={<TweetSkeleton />}>
<TweetPage id={params.tweet} />
</Suspense>
)

export default Page
```

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)).

If you're using Next.js then using [`unstable_cache`](/next#enabling-cache) works too.
Loading

4 comments on commit 261e72d

@vercel
Copy link

@vercel vercel bot commented on 261e72d Dec 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

react-tweet-create-react-app – ./apps/create-react-app

create-react-app-gray-seven.vercel.app
react-tweet-create-react-app.vercel.rocks
react-tweet-create-react-app-git-main.vercel.rocks

@vercel
Copy link

@vercel vercel bot commented on 261e72d Dec 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

react-tweet-custom-tweet-dub – ./apps/custom-tweet-dub

react-tweet-dub.vercel.app
react-tweet-custom-tweet-dub.vercel.rocks
react-tweet-custom-tweet-dub-git-main.vercel.rocks

@vercel
Copy link

@vercel vercel bot commented on 261e72d Dec 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on 261e72d Dec 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

react-tweet-vite-app – ./apps/vite-app

react-tweet-vite-app.vercel.app
react-tweet-vite-app.vercel.rocks
react-tweet-vite-app-git-main.vercel.rocks

Please sign in to comment.