If you’ve been anywhere near the internet lately, you’ve surely noticed the epidemic overtaking the developer community known as Gatsby fever. I wish I could say I’m not the type of person who contributes to obnoxiously loud hype trains, but hey. Have some empathy for a guy with a fever (or perhaps an illness).

Like most others in the GatsbyJS cult, I sometimes find myself mindless clicking through my own site. With a sharp eye turned to Chrome’s developer tools, I stare idly as each page refreshes, admiring the load times of each resource as they appear. This mindless satisfaction is the vice that gets me through afternoons, weeks, and so forth. It may seem like we've got life figured out, but as the 80's taught us, no party lasts forever.

It wasn't long before my mindless clickthroughs came to a halt. One of the pages I had come across in my daily self-admiration binges didn't seem right. Handfuls of JS scripts I didn't recognize were being loaded from different sources. Images I've never seen began popping up in Chrome's network monitor. Worst of all, these things were slow. What was the meaning of this? Have I been hacked? Did Matt spam videos of Lynxes all over the site again? It was worse. It was Twitter.

Embedded Twitter Widgets

If you're a blogger (or a corporation desperate for millennial interaction), you may be familiar with Twitter's officially supported embed widgets. They allow lonely bloggers like myself to embed Tweets from sources such as user profiles, as though to say "please follow me."

The widgets are surprisingly not entirely horrendous, but they're horrendous enough to look for a better solution. There are those of us who've built careers by occasionally appearing on the second page of Google search results. It's hard to justify injecting JavaScript and iFrames into such polished sites, as the value is low where the downsides are significant. Previous solutions to Twitter embeds included react-twitter-embed, which appears to be a step up at first glance, yet only somewhat veils the truth that these libraries are simply wrappers for what we're trying to escape. Once rendered, these components are the same iFrames that slow our sites down, and send our precious data back to the Twitter mothership. If I wanted my sites to experience horrible inoperable tumors, I would implement Disquis.

Gatsby's Twitter Source Plugins

If you followed along with our first Gatsby tutorial, you already know how cool sourcing content into Gatsby is. While non-static sites populate certain data on page load (like tweets), we have the luxury of fetching and loading this content each time Gatsby builds. Not only is this faster for end-users, but it lets us customize content any way we want.

Our savior here is gatsby-source-twitter, a Gatsby plugin for populating tweets from searches, profiles, or anything else. Configuring this plugin lets us query against Tweets in our GraphQL queries.

Get Authorized

To use this plugin, we need to get our hands on some Twitter auth keys. It's not exciting, but it isn't particularly difficult either. We need three keys here: a consumer key, consumer secret, and bearer token.

Obtaining the first two is easy. It involves registering a Twitter developer app, which is relatively painless. The consumer keys will be available immediately afterward.

We get our bearer token by constructing an API request from the keys we just received. This curl request should give you what you need:

curl -u '[API_KEY]:[API_SECRET_KEY]' \
  --data 'grant_type=client_credentials' \
  'https://api.twitter.com/oauth2/token'

In case you run into trouble, the official Twitter documentation might come in handy.

Configuring Gatsby

Let's install our plugin:

$ npm i gatsby-source-twitter --save

So, where do we get started? We actually have a number of ways we can populate tweets. The Gatsby source plugin allows us the follow methods for grabbing tweets:

  • search/tweets: Aggregates Tweets that fit a number of criteria (albeit vague criteria). Some examples include limiting tweets to geolocation, language, and time published, but not much else.
  • statuses/show: Get a single tweet by ID.
  • statuses/lookup: Get multiple tweets by ID.
  • statuses/user_timeline: Get tweets from a specific user's timeline (this is probably the most useful use case).
  • favorites/list: Get liked tweets from a specific user.

Let's break into gatsby-config.js to see how we set up pulling Tweets from a user's profile:

...
{
  resolve: `gatsby-source-twitter`,
  options: {
    credentials: {
      consumer_key: process.env.TWITTER_CONSUMER_KEY,
      consumer_secret: process.env.TWITTER_CONSUMER_SECRET,
      bearer_token: process.env.TWITTER_BREARER_TOKEN,
    },
    queries: {
      HackersQuery: {
        endpoint: "statuses/user_timeline",
        params: {
          screen_name: "hackersslackers",
          include_rts: false,
          exclude_replies: true,
          tweet_mode: "extended",
          count: 5,
        },
      },
    },
  },
},
 ...
gatsby-config.js

credentials contains the 3 keys we acquired earlier. So far, so good.

As implied the terminology "queries" implies, we can actually configure Gatsby to pull from multiple Twitter sources. One is good enough for me, which I name HackersTweets here. The parameters we pass are self-explanatory; we're pulling tweets from the hackersslackers Twitter account, ignoring retweets, and maxing out at 5 tweets.

Querying for Tweets via GraphQL

Let's see how this affects the data available to us via GraphQL. Crack open your GraphQL playground at http://localhost:8000/_graphql and take a look:

http://localhost:8000/_graphql

We'll notice two new types of queries available in our explorer:

  • allTwitterStatusesUserTimelineHackersTweets: This allows us to query individual tweets from the source our plugin is configured for (a Twitter profile, in our case). Each tweet we query for actually contains a respectable number of useful fields. In addition to the raw tweet content, we can also receive each tweet's individual hashtags, embedded tweets, user details, and so forth.
  • twitterStatusesUserTimelineTweetQuery: This query is centered around high-level metadata regarding a user's profile. This is a great way to get a snapshot of a Twitter user's profile, and can be used to display a user's number of followers, total tweets, profile photo, location, etc.

Since we're building our own widget, why not use aspects of both? Here's the GraphQL query I'm using:

query TweetQuery {
  allTwitterStatusesUserTimelineTweetQuery {
    edges {
      node {
        full_text
        favorite_count
        retweet_count
        created_at
        user {
          name
          url
          profile_image_url
          screen_name
        }
        entities {
          urls {
            display_url
          }
          hashtags {
            text
          }
        }
      }
    }
  }
  twitterStatusesUserTimelineTweetQuery {
    user {
      profile_image_url_https
      name
      url
      screen_name
    }
  }
}

Now let's get building!

Building a Twitter React Component in GatsbyJS

Before we start slinging code, let's recognize what we're going to build. This is what our end result at Hackers and Slackers looks like:

GatsbyJS Twitter Widget

In our case, we made a widget that contains some simple information. Our widget's header includes basic information of the profile we're pulling tweets from (name, Twitter handle, avatar). That's the first aspect of our query.

Then we have the individual tweets. For each tweet, we're pulling:

  • The raw text content of the tweet.
  • Each tweet's hashtags.
  • Any links included in the tweet, if any.
  • Publish date.

It's important to note two things about the fields we're pulling per tweet. Thanks to GraphQL, each tweet we receive is deconstructed into multiple fields, which we can style and display accordingly. To put it lightly, this is a huge step up from the shackles of a Twitter embed iFrame.

Equally important is the information we've chosen to leave out. Notice how we do not include things like profile pictures, retweet buttons, and other worthless nonsense to muddy the final result.

Here's how we turned out:

import React from 'react'
import PropTypes from 'prop-types'
import { StaticQuery, graphql } from 'gatsby'

const TwitterWidget = ({ data }) => {
    const tweets = data.tweets.edges
    const user = data.twitteruser.user

    return (
          <>
            <div className="widget twitter">
                <div className="twitter-header">
                    <img src={user.profile_image_url_https} className="twitter-avatar"/>
                    <div>
                        <a href={user.url} className="twitter-name">{user.name}</a>
                        <div className="twitter-user">@{user.screen_name}</div>
                    </div>
                </div>
                {tweets.map(({ node }) => (
                    <div className="tweet" key={node.favorite_count}>
                        <p className="tweet-content">{node.full_text.split(`#`)[0].split(`http`)[0]}</p>
                        {node.entities.hashtags ? <div className="tweet-hastags">{node.entities.hashtags.map(({ text }) => (
                            <a href={`https://twitter.com/hashtag/${text}`} key={text} className="hashtag">#{text}</a>
                        ))}</div> : 0 }
                        <div className="tweet-head">
                            {node.entities.urls.map(({ display_url }) => (
                                <a href={display_url} className="tweet-link" key="1">{ display_url }</a>
                            ))}
                            <span className="date">{node.created_at.split(` `, 3).join(` `)}</span>
                        </div>
                    </div>
                ))}
            </div>
          </>
    )
}
TwitterWidget.js

No matter how much we try to customize out-of-the-box widgets, we'll always be polishing a turd when compared to having full control of what our site looks like. We can even get as granular as choosing the format of URLs we want to display (shortened, full, pretty, etc). We can also loop through each hashtag extracted from our tweets, list them out separately, and link them to their appropriate places on Twitter. We aren't even creative here, but we're already doing better than any out-of-the-box alternatives.

Feel free to take the above example as inspiration for your own widget. If you can make it through defining the PropTypes without going insane, you've earned it:

TwitterWidget.propTypes = {
    data: PropTypes.shape({
        tweets: PropTypes.shape({
            full_text: PropTypes.string,
            favorite_count: PropTypes.number,
            retweet_count: PropTypes.number,
            created_at: PropTypes.string,
            user: PropTypes.shape({
                name: PropTypes.string.isRequired,
                url: PropTypes.string.isRequired,
                profile_image_url: PropTypes.string.isRequired,
                screen_name: PropTypes.string.isRequired,
            }).isRequired,
            entities: PropTypes.shape({
                urls: PropTypes.arrayOf(
                    PropTypes.shape({
                        url: PropTypes.string,
                    }),
                ),
                hashtags: PropTypes.arrayOf(
                    PropTypes.shape({
                        text: PropTypes.string,
                    }),
                ),
            }),
        }).isRequired,
        twitteruser: PropTypes.shape({
            user: PropTypes.shape({
                profile_image_url_https: PropTypes.string,
                name: PropTypes.string.isRequired,
                display_url: PropTypes.string.isRequired,
                screen_name: PropTypes.string.isRequired,
            }).isRequired,
        }),
    }),
}
TwitterWidget.js

But Wait! What About the Results?

Our journey started by looking to increase our page speed by avoiding third-party resources. Before making this change, the page in question scored a pathetic 50% page speed on Lighthouse. That's absolutely horrendous for a Gatsby site.

This change alone raised the Lighthouse rating of hackersandslackers.com from 50% to 70%. That's an obscene hit to speed difference at the hands of a single widget, especially one as unimportant as a Twitter feed. Yes, 70% still sucks, but the reason why isn't irrelevant. The biggest offenders to our page score are actually other third-party scripts! While the only other third parties we use on hackersandslackers.com are analytics platforms, the overhead of these scripts raises a lot of questions. What exactly is being collected, and why does a simple dashboard need this information? It's hard to imagine the value any of these services add is worth the SEO hit we take when pages load slowly.

Gatsby gives us the luxury of not being dependent on many third-party services, and we should take advantage of that whenever possible. In cases where can't avoid them, remember how much time we wasted on this widget. Ask yourself: do I want to offset the marginal benefit I received from suffering through that Gatsby tutorial with heavy scripts?