Static tweets — the better way to embed Twitter

by Kartik Chaturvedi

September 3, 2022
Static tweets — the better way to embed Twitter image

Twitter is a great way to share thoughts and content. But let's face it — despite the platform's ease of use, its embed feature is extremely cumbersome. Embedding a tweet means:

  1. The page loads. Tweets are nowhere to be seen.
  2. The tweet widget JS loads after pageload.
  3. An iframe is initialized, kicking off more JS. The page content shifts around to make room.
  4. A whole webview is rendered inside the iframe. More page layout shift.

This is a sub-par experience, since embedded tweets often load many seconds after the page has finished loading. It's quite ironic that for something so static as a tweet (still waiting for an edit button), the embed process is so dynamic. Why not have a static embed for static tweets?

One could suggest simply embedding images of tweets instead - however images are also slow to load, and tweet statistics such as retweets or likes would be stale in the image.

Well, with a few lines of code, we can make a React component that loads instantly, every single time, with the latest statistics, like this:

Let's see how to do this in just a few simple steps, using NextJS, Tailwind CSS, and Twitter's API.

Designing a Tweet

Since we are avoiding Twitter's embeded tool, we will also need to design a tweet ourselves. Tailwind CSS makes it extremely easy to do this with its utility-first approach.

Starting with a basic rounded rectangle, we can add styles to HTML elements, using a random tweet as a reference. Later we will make this component dynamic to work with any tweet.

You can experiment with an interactive version of this component on Tailwind Play.

components/Tweet.jsx
<div class="relative flex min-h-screen flex-col justify-center">
  <div class='bg-gray-100 border border-slate-300 rounded-2xl duration-300 my-8 p-5 max-w-xl mx-auto'>
      <div class='flex justify-between'>
        <a class='flex items-center gap-3 group' href='https://twitter.com/naval/status/1516188493541785606'>
          <img
            class='rounded-full h-12 w-12'
            src='https://pbs.twimg.com/profile_images/1256841238298292232/ycqwaMI2_400x400.jpg'
          />
          <div class='flex flex-col leading-snug'>
            <span class='text-sm font-semibold flex gap-2'>
              Naval
              <span class='text-sm font-normal opacity-70 group-hover:opacity-100 duration-300'>@naval</span>
            </span>
            <span class='text-sm opacity-80 group-hover:opacity-100 duration-300'>April 18, 2022</span>
          </div>
        </a>
      </div>
      <div class='text-lg my-3 leading-normal'>
        Your success in life depends on your ability to make good decisions.Your happiness depends on your ability to not care about the outcomes.
      </div>
      <div class='flex mt-2 gap-6 text-sm font-medium tracking-wider'>
        <span>20 Replies</span>
        <span>6190 Retweets</span>
        <span>32.9K Likes</span>
      </div>
    </div>
</div>

Fetching Tweets from Twitter's API

To make the tweet component above dynamic and reusable, we'll need to consume Twitter's API to fetch any tweet by ID, parse the response, and pass the tweet information to the component.

For this, you will need a Twitter account to register at developer.twitter.com. Once registered, you will create a project and obtain a Bearer token to set in your application. For this example, assume the Bearer token is set as an environment variable and fetched using process.env.TWITTER_TOKEN.

Referencing Twitter's API documentation, we will pass tweet IDs to the /2/tweets endpoint as query parameters and receive an array of tweet data in response. We can parse this data for just the content we need to display our tweets on the page. We'll have this function take in an array of tweet IDs as strings, which we will implement in the next step.

lib/fetchTweets.js
export const fetchTweets = async (tweetIds = []) => {
  if (tweetIds.length === 0) return []

  const options = '&expansions=author_id&tweet.fields=public_metrics,created_at&user.fields=profile_image_url'

  const response = await fetch(
    `https://api.twitter.com/2/tweets/?ids=${tweetIds.join(',')}${options}`,
    { headers: { Authorization: `Bearer ${process.env.TWITTER_TOKEN}` } }
  )
  const body = await response.json()
  const tweets = body.data.map((t) => {
    const author = body.includes.users.find((a) => a.id === t.author_id)
    return {
      id: t.id,
      text: t.text,
      createdAt: new Date(t.created_at).toLocaleDateString('en', {
        year: 'numeric',
        month: 'long',
        day: 'numeric',
        timeZone: 'UTC',
      }),
      metrics: {
        replies: formatMetric(t.public_metrics?.reply_count ?? 0),
        likes: formatMetric(t.public_metrics?.like_count ?? 0),
        retweets: formatMetric(t.public_metrics?.retweet_count ?? 0),
      },
      author: {
        name: author.name,
        username: author.username,
        profileImageUrl: author.profile_image_url,
      },
      url: `https://twitter.com/${author.username}/status/${t.id}`,
    }
  })

  return tweets
}

export const formatMetric = (number) => {
  if (number < 1000) {
    return number
  }
  if (number < 1000000) {
    return `${(number / 1000).toFixed(1)}K`
  }
  return `${(number / 1000000).toFixed(1)}M`
}

Server-side Rendering with NextJS

Lastly, we need to consume our API function on the page displaying the tweets.

The secret to instant page loads is SSR - server-side rendering. For webpages displaying static content, we can cache the data powering the webpage and deliver it with the HTML/CSS. This way, the data does not need to be fetched by the browser after pageload. Instead, it is loaded alongside the webpage itself, and simply consumed within our components.

So to replace the Twitter embed iframe, we can fetch and cache the tweet data and deliver it as static content. This also reduce calls to Twitter's API, since the tweet data will only be fetched at buildtime.

NextJS makes this entire process easy with getStaticProps.

Say we have our page component created in NextJS. Adding this function lets us define data that will be fetched at buildtime, and served as static props to the page component. We simply need to pass this function an array of tweet IDs:

pages/index.jsx
export default function Index({ tweets }) {
  // {...}
}

export async function getStaticProps() {
  const tweets = await getTweets(['1516188493541785606'])
  return {
    props: {
      tweets,
    },
  }
}

And finally, we can consume the resulting tweets prop in our page component. Let's create a StaticTweet function to help match each referenced tweet with its data in the tweets prop, and then pass that data to the Tweet UI component we created earlier.

pages/index.jsx
import Head from 'next/head'
import Tweet from 'components/Tweet'
import { fetchTweets } from 'lib/fetchTweets'

export default function Home({ tweets }) {
  const StaticTweet = ({ id }) => {
    const tweet = tweets.find((tweet) => tweet.id === id)
    return <Tweet tweet={tweet} />
  }

  return (
    <div>
      <Head>
        <title>Static Tweets</title>
      </Head>

      <main className='container max-w-4xl py-8'>
        <h1 className='font-bold text-4xl text-center'>Static Tweets</h1>
        <StaticTweet id='1516188493541785606' />
      </main>
    </div>
  )
}

export async function getStaticProps() {
  const tweets = await fetchTweets(['1516188493541785606'])
  return {
    props: {
      tweets,
    },
  }
}

And we are done! Statically rendered, server-side generated tweets, loaded instantly with the rest of the webpage content.

A working demo of this code is available on GitHub.

Kartik's Newsletter

Subscribe to get science and tech news, new posts, and the latest updates from me.

Subscribe on Substack