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

Whether you're a blogger or a corporation desperate for millennial interaction, there are plenty of reasons you might want to surface real-time social media interactions in your web app. As the internet helplessly drifts into a dystopia of centralized walled-gardens, independent entities such as blogs or apps face an ever-shortening average lifespan. Showcasing dynamically sourced content can be reassuring to users who inherently have to gauge whether or not the our corner of the internet is maintained or currently relevant.

Widgets are a simple way to show audiences you're alive and actively engaged- for instance, a Twitter widget showcasing activity from you or your organization's Twitter account. We've had ways of handling such widgets in the past, but they've frankly been quite shitty in terms of load times and customization.

First we have Twitter's officially supported embed widgets. Twitter has a simple GUI to help you generate embedded tweets. Unfortunately, simplicity is the only upside to this solution. These widgets are generated as iFrames which essentially open a portal to hell wherever they're dropped, forcefully loading assets like fonts and tracking to the pages they fall on to. Anybody with a sense of self respect or regard for site performance should run far away from officially supported widgets.

A promised React-based library dubbed react-twitter-embed 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 Disqus.

A New Tomorrow: Gatsby's Twitter Source Plugin

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 powerful Gatsby plugin which allows us to source Tweets into our app via convenient GraphQL queries. gatsby-source-twitter comes with the full power of Twitter's official API, which enables us to source Tweets in a myriad of different ways. We have the ability to grab tweets from individual users, from lists of users, or even by performing a generic search across Twitter.

Go ahead and install the library. We'll need to grab some auth keys from Twitter before we can do anything:

$ npm i gatsby-source-twitter --save

Get Authorized With Twitter

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. The consumer key and consumer secret are available upon registering a Twitter developer app, which is relatively painless. We then get our bearer token by executing the following API request:

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's Twitter Source

With our credentials in hand, configuring our plugin in gatsby-config.js is straightforward:

...

{
  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: {
      // Your GraphQL queries here
    },
  },
},
    
 ...
gatsby-config.js

When it comes to how we want to source our tweets, we have a myriad of options available, but three are particularly notable: fetching Tweets by username, by list, or by search query.

Tweets from User Timeline

statuses/user_timeline enables us to fetch Tweets from a particular user's timeline. This is particularly useful if you're looking to build a widget of your most recent tweets:

queries: {
  HackersTweets: {
    endpoint: `statuses/user_timeline`,
      params: {
        screen_name: `hackersslackers`,
        include_rts: true,
        exclude_replies: true,
        tweet_mode: `extended`,
        count: 3,
      },
   },
}
Twitter user timeline query.

Tweets From Users in a List

Twitter has a concept of "lists," which is essentially an arbitrary group of users. Hackers and Slackers has one of these "lists," which is comprised of our authors and their Tweets: https://twitter.com/i/lists/1043490256052539392

This is an extremely useful way to source Tweets from multiple users; this is actually how we source the Twitter widgets on each individual author page:

queries: {
  AuthorTwitterProfiles: {
    endpoint: `lists/members`,
    params: {
      list_id: `1043490256052539392`,
        include_rts: true,
        exclude_replies: true,
        tweet_mode: `extended`,
        count: 20,
      },
    },
  },
}
Querying all tweets of users belonging to a list.

search/tweets is way for us to source Tweets in a less explicit way. We can fetch Tweets matching a certain hashtag, username, or any plaintext query:

queries: {
  SearchTweets: {
    endpoint: "search/tweets",
    params: {
      q: "#python",
      tweet_mode: "extended",
    },
  },
},
Sourcing tweets matching a hashtag.

Of course, we could configure Gatsby to source Tweets from all of the above:

{
  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_BEARER_TOKEN,
    },
    queries: {
      HackersTweets: {
        endpoint: `statuses/user_timeline`,
        params: {
          screen_name: `hackersslackers`,
          include_rts: true,
          exclude_replies: true,
          tweet_mode: `extended`,
          count: 3,
        },
      },
      AuthorTwitterProfiles: {
        endpoint: `lists/members`,
        params: {
          list_id: `1043490256052539392`,
          include_rts: true,
          exclude_replies: true,
          tweet_mode: `extended`,
          count: 20,
        },
      },
      SearchTweets: {
        endpoint: "search/tweets",
        params: {
          q: "#python",
          tweet_mode: "extended",
        },
      },
    },
  },
},

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. I'm going to focus on Tweets from a user profile to get us started:

http://localhost:8000/_graphql

My query for @hackersslackers's user timeline gave me two new options in Gatsby's GraphQL playground. Note that the only difference in the query options below is the word "all," which should give you a hint as to what's happening:

allTwitterStatusesUserTimelineHackersTweets

Our "all" query encompasses all the data we pulled from our configuration's query. The intended usage is to fetch multiple Tweets using Gatsby's standard edges { node { syntax, where each "node" represents a tweet. We're building a widget to display multiple Tweets from a user, so this query suits our needs quite well. Check out the fields we're grabbing:

  • full_text: The plain text content of a Tweet.
  • favorite_count: Number of times a Tweet has been favorited.
  • retweet_count: Number of times a Tweet has been retweeted.
  • created_at: Date the Tweet was published.
  • user: An object containing metadata about the author of the Tweet such as username, profile picture, etc. It's important to distinguish that retweeted Tweets will return the user details of the original author.
  • entities/urls: An array of all URLs contained in the Tweet.
  • entities/hashtags: An array of hashtags contained in the original tweet.

twitterStatusesUserTimelineHackersTweets

Unlike the above, this query is intended to return a single result of all the results we pulled - instead of an array of Tweets, this query type is typically used to grab one of those Tweets.

A neat trick is we can actually pull zero Tweets, but still pull the metadata of the underlying user timeline. This is a handy way to grab information about our Twitter profile without pulling it once per every Tweet! Here's the actual query we're using to power our Twitter widget on hackersandslackers.com:

query TwitterQuery {
  tweets: allTwitterStatusesUserTimelineHackersTweets {
    edges {
      node {
        full_text
        favorite_count
        retweet_count
        created_at
        id
        user {
          name
          url
          profile_image_url_https
          screen_name
        }
        entities{
          urls {
            display_url
            expanded_url
          }
          hashtags {
            text
          }
        }
        retweeted_status {
          user {
            profile_image_url_https
            url
            screen_name
            name
          }
          full_text
        }
      }
    }
  }
  twitterProfile: twitterStatusesUserTimelineHackersTweets {
    user {
      url
      screen_name
      profile_image_url_https
      name
      followers_count
    }
  }
}

Now let's get building.

Building a Twitter React Component in GatsbyJS

Let's set our eyes on the prize before we start slinging code to visualize the end goal:

GatsbyJS Twitter widget example.

Our widget's header includes basic information of the profile we're pulling tweets from (name, Twitter handle, avatar). We're pulling this information via the twitterProfile subquery we made earlier.

Next we have our user timeline's list of Tweets (nice and customized to our liking, I might add)! As an added bonus we're rendering retweets as well, which are of course handled differently from our user's own tweets. We'll be building a Gatsby React component to handle both of these bases, but we'll focus on pulling our user's own Tweets first.

import React from 'react'
import PropTypes from 'prop-types'
import { StaticQuery, graphql } from 'gatsby'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'

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

  return (
    <>
      <div className="widget twitter">
        <div className="tweets">
          <div className="twitter-header">
            <FontAwesomeIcon icon={[`fab`, `twitter`]} size="s" className="twitter-logo" />
            <div className="profile-details">
              <div className="profile-details">
                <a href={twitterProfile.url} className="twitter-name" target="_blank" rel="noopener noreferrer">{`@${twitterProfile.screen_name}`}</a>
                <div className="twitter-followers"><FontAwesomeIcon icon={[`fad`, `users`]} size="xs" /> <span>{twitterProfile.followers_count} Followers</span></div>
              </div>
            </div>
          </div>
          {tweets.map(({ node }) => (
            <div className="tweet" key={node.id}>
              <p className="tweet-content">{node.full_text.split(`#`)[0].split(`https`)[0]}</p>
              {/* Tweet hashtags. */}
              { node.entities.hashtags.length > 0 ?
                <div className="tweet-hastags">
                  {node.entities.hashtags.map(({ text }) => (
                    <a href={`https://twitter.com/hashtag/${text}`} key={`${node.id}-${text}`} className="hashtag" rel="nofollow noreferrer">#{text}</a>
                  ))}
                </div> : null }
              {/* URLs included in each Tweet. */}
              { node.entities.urls.length > 0 ?
                node.entities.urls.map(({ display_url, expanded_url }) => (
                  <a href={expanded_url} className="tweet-link" key={`${node.id}-link`} rel="nofollow noreferrer">{ display_url }</a>
                )) : null }
              {/* Tweet metadata (favorites, retweets, etc). */}
              <div className="tweet-footer">
                <div className="retweets meta-item"><FontAwesomeIcon icon={[`fad`, `retweet`]} size="xs" swapOpacity /> <span>{node.retweet_count}</span></div>
                <div className="favorites meta-item"><FontAwesomeIcon icon={[`fad`, `heartbeat`]} size="xs" swapOpacity/> <span>{node.favorite_count}</span></div>
                <div className="date meta-item"><FontAwesomeIcon icon={[`fad`, `calendar`]} size="xs" /> {node.created_at.split(` `, 3).join(` `)}</div>
              </div>
            </div>
          ))}
        </div>
      </div>
    </>
  )
}
TwitterWidget.js

Handling Retweets

Our Twitter plugin allows us to query a field called retweeted_status, which is actually an object containing metadata specific to retweets. Querying retweeted_status will return null for regular Tweets, so we can rely on this field as an indicator for which "type" of Tweet we should display. We can adjust our Tweet loop to look as such:

...
{tweets.map(({ node }) => (
  {node.retweeted_status ?
    {/* Retweet snippet */} 
  :
    {/* Regular Tweet snippet */}
  }
))}
...

For those of you here to copy+paste, here's how I handle retweets:

...

<div className="retweeted-tweet">
  <div className="retweeted-header">
    <FontAwesomeIcon icon={[`fad`, `retweet`]} size="xs" swapOpacity />
    <span>{`${node.user.name} retweeted`}</span>
  </div>
  <div className="retweeted-body">
    <div className="tweet-header">
      <div className="twitter-avatar">
        <img className="lazyload" data-src={node.retweeted_status.user.profile_image_url_https} alt="twitter-avatar" />
      </div>
      <a href={node.retweeted_status.user.url} className="twitter-name" rel="nofollow noreferrer">@{node.retweeted_status.user.screen_name}</a>
    </div>
    <p className="tweet-content">{node.retweeted_status.full_text.split(`http`)[0]}</p>
    {node.entities.urls &&
      node.entities.urls.map(({ display_url, expanded_url }) => (
        <a href={expanded_url} className="tweet-link" key={`${node.id}-link`} rel="nofollow noreferrer">{ display_url }</a>
      ))
    }
  </div>
</div>
                
...

The Results are In

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?

Find the source on Github here:

Twittter widget for GatsbyJS displaying tweets from a user’s profile. Based on the tutorial: https://hackersandslackers.com/custom-twitter-widget-in-gatsbyjs/
Twittter widget for GatsbyJS displaying tweets from a user’s profile. Based on the tutorial: https://hackersandslackers.com/custom-twitter-widget-in-gatsbyjs/ - TwitterWidget.js