Creating a Twitter Feed in Astro

I've been playing around with Astro lately and decided to compare adding a static Twitter feed in Astro vs Nuxt. As such I've written this tutorial to teach you how to generate a Twitter feed and automatically redeploy your site to Netlify using IFTTT every time you tweet.

The Twitter API has changed a lot since I wrote my old Nuxt tutorial, and the npm module I'd used before has been unmaintained for a few years at the time of writing this. As such I decided to go ahead and fetch the data manually, as opposed to relying on a package on npm.

Like last time, a demo repo is available in case you have any issues following along. It can be found here: https://github.com/davidwarrington/astro-tweet.

1. Create a Twitter App

Before writing any code, visit the Twitter Developer Portal and create an app to use.

We'll need the Bearer Token to access the endpoints we need, so copy that from your new app and make a note of it somewhere.

2. Generate an Astro project

If you don't already have an Astro project to use, create one by running npm init astro in the directory you want to create it. You will be presented with some options, none of which will impact how we build this feed, so pick whichever options suit your needs.

Then install the dependencies with the package manager of your choice. For example if you're using yarn you'll run yarn, and if using npm you'll run npm install.

3. Pulling your Twitter feed

Before we worry about creating an Astro component to display our tweets, let's make sure we can actually pull them from the API.

To begin create a file in your project root called index.mjs and install node-fetch.

If you're using yarn, run:

yarn add node-fetch

Or if you're using npm, run:

npm install node-fetch

We'll be deleting index.mjs and uninstalling node-fetch later on as Astro can handle all of it internally.

4. Pull your user data

First let's import our dependencies in index.mjs, set up some config values and add a couple of utility functions:

import fs from 'node:fs/promises';
import fetch from 'node-fetch';

const config = {
  bearerToken: '[BEARER TOKEN]',
  username: '[YOUR USERNAME]',
};

function prettifyJSON(data, space = 2) {
  return JSON.stringify(data, null, space);
}

function buildURL(baseURL, params = {}) {
  const url = new URL(baseURL);
  Object.entries(params).forEach(([key, value]) => {
    url.searchParams.set(key, String(value));
  });

  return url;
}

Later on we'll replace the config with environment variables in a .env file but just for the sake of testing we'll stick them here. Replace [BEARER TOKEN] with the bearer token provided when you created your Twitter app, and replace [YOUR USERNAME] with your Twitter username (excluding the "@").

The prettifyJSON function is just something we'll be using later to make it easier to read the data we've pulled in.

The buildURL function is a nice utility for building a URL without having to worry about small things like whether a parameter needs a ? or & in front of it. We create a URL so that we can use its searchParams.set method to URL encode any values we pass in the params object. By passing an object for our parameters we can simply loop through all the key-value pairs inside.

Next we'll create a base function that all of our Twitter API calls will run. It will handle adding URL parameters and our auth headers, so we never have to worry about those again.

async function fetchFromTwitter(endpoint, params = {}) {
  const url = buildURL(`https://api.twitter.com${endpoint}`, params);

  const headers = {
    authorization: `Bearer ${config.bearerToken}`,
  };

  const response = await fetch(url, { headers });

  return response.json();
}

To start off with we hide the https://api.twitter.com part of the URL in this function because all our requests are going to go there. The endpoint we're calling can then be copied straight from the Twitter API docs.

Finally after composing and making our request, the function converts the response to JSON so we don't need to remember to add that after any other requests.

We can finally create a function to fetch our user data! You can't directly request a users tweets just with their username; instead you need their account ID. That's why we start with the getUser function, which gets the account ID for a given username.

async function getUser(username) {
  const url = '/2/users/by';
  const params = { usernames: username };
  const response = await fetchFromTwitter(url, params);

  return response.data[0];
}

Twitter actually allows us to request the data for multiple users at once here, hence the parameter key is "usernames", not "username", but in this tutorial we're only interested in getting the data for one. Also since it supports fetching multiple users the data is contained in an array, hence we're just returning the first item in the array.

If you add the following to the end of your file:

getUser(config.username).then(console.log)

Then run this script from the root of your project:

node ./index.mjs

You should then see something like this in your terminal:

{
  id: '123456789',
  name: 'Your Name',
  username: 'username'
}

If you don't, check back through this tutorial in case you missed something. If you do, hooray! Now remove the getUser(config.username).then(console.log) line and let's move onto the next step.

5. Fetch your Tweets

Add this getTweets function to your index.mjs.

async function getTweets(username, total = 20) {
  const user = await getUser(username);

  const params = {
    exclude: ['retweets', 'replies'],
    max_results: total,
  };
  const url = `/2/users/${user.id}/tweets`;

  return fetchFromTwitter(url, params);
}

First of all we fetch the user data with the previous function - getUser. After this we pass their account ID to build the API endpoint.

We also specify some options here. I've chosen to exclude retweets and replies from the response because I'm not interested in those. I also want to fetch 20 tweets at most. If you want to fetch a different number of tweets you can either change the default value for total in the function arguments or you can pass the number as an argument when you call this function later on.

You can find more query parameters for this endpoint at get-users-id-tweets in the API docs. If you want to fetch images, video or mentioned users for example, you'll need to add some of these.

Now just add the following to the end of your file:

(async () => {
  const data = await getTweets(config.username);

  fs.writeFile(
    './tweets.json',
    prettifyJSON(data),
    'utf-8',
  );
})();

This will fetch your tweets and print them in a "tweets.json" file. We're wrapping it in an immediately invoked function expression (IIFE) just so we can use async/await.

Run the script again with:

node ./index.mjs

If you followed all of these steps correctly, you should find a "tweets.json" file in your project root. Open it to have a look at the data you've just fetched from Twitter.

6. Stash your secrets in an env file

If you're happy it's worked, let's start by moving our Bearer Token somewhere safe. Astro runs Vite under the hood, and that imports variables from .env files by default, so we don't need to install any extra dependencies here. Create a .env file in your project root with the following, and add the appropriate values to each variable.

PUBLIC_TWITTER_BEARER_TOKEN=
PUBLIC_TWITTER_USERNAME=

We're moving them into a .env file so we won't commit them to the repo. You shouldn't share your Bearer Token publicly since other people could use it, and Astro projects have .env in the .gitignore by default to prevent this being committed.

Note: As of the time of writing, there's an issue where environment variables declared on Netlify are only available to Astro if prefixed with PUBLIC_. This may not be the case when you're reading this. It's worth checking because environment variables prefixed with PUBLIC_ can be made available client-side. You can learn more in the Astro docs.

There's a good chance you don't need to keep your username secret, however it's still a config option so I prefer not to keep it hardcoded.

It's completely optional but I'd also suggest creating a .env.example file with the same variables as your .env but no values. This way you can commit it to your repo and if you clone it from elsewhere in future you know which keys to add for the project to work.

7. Create the Tweets component

Now we can finally create our Astro component. Create Tweets.astro in src/components/ and start by adding the following code:

---
const bearerToken = import.meta.env.PUBLIC_TWITTER_BEARER_TOKEN;
const username = import.meta.env.PUBLIC_TWITTER_USERNAME;

const tweets = await getTweets(username);
---

<section>
  <ul>
    {tweets.data.map(tweet => (
      <li>
        {tweet.text}
      </li>
    ))}
  </ul>
</section>

The component will now use the bearerToken and username variables in our .env file when it's built. Vite attaches these values to import.meta.env.

The final line of JS here, beginning const tweets will provide us with a tweets variable to access when rendering our component. We don't need to wrap this in an IIFE like we had in the index.mjs file because Astro supports top-level await.

The HTML in this file will just loop through our tweets and print the text in a list. You'll likely want to do some more stuff like add styles, but I'll let you decide what you want to do there.

Next copy the buildURL, fetchFromTwitter, getUser and getTweets functions from the index.mjs file and paste them in the space above the const tweets line in your Astro component file. The only adjustment we need to make is to replace config.bearerToken with bearerToken in the fetchFromTwitter function.

Now import your component wherever you want to use it. For this demo I just imported it into the index.astro page file:

import Tweets from '../components/Tweets.astro';

And then rendered the component on that page:

<Tweets />

Now try running your project to check that it works. If you're using yarn you can use yarn dev, or with npm it should be npm run dev.

If it's worked, fantastic! If not please compare your code with the demo repo to see if there's a difference.

8. Clean up

If you're satisfied that it's working, before we go deploying anything to Netlify let's just do a little cleanup. The index.mjs file is no longer needed since we're no longer just verifying that we can access the Twitter API, so you can delete that. You can also uninstall node-fetch. If you're using yarn run:

yarn remove node-fetch

Or with npm run:

npm uninstall node-fetch

9. Deploying to Netlify

If you run yarn build and check the output you'll notice we've not output any JS in our final build. This is great, but you might be wondering how the feed will stay up to date. After all, there's not much point in a Twitter feed that's out of date, right?

In order to make it update automatically you can deploy to a platform like Netlify and use a service like IFTTT to listen for any tweets you post, rebuild and redeploy the site each time.

To deploy to Netlify, log in and choose create a new site. Choose your Git provider, then select the repo you've pushed this project to. Set your build command to yarn build and the publish directory to dist (these might already be the defaults).

Under "Advanced build settings" you'll also need to add your environment variables since they're not committed in the repo.

Adding environment variables in Netlify

10. Automating deployments with IFTTT

Now we've set up Netlify to deploy our project, we'll use a service called IFTTT to rebuild it every time we tweet.

To begin with in Netlify visit the "Continuous Deployment" section of your site settings, and under "Build Hooks" create one. I've called mine "IFTT - Build on tweet" so I'll know what it is in the future.

Netlify Build hooks section

After you've saved your new build hook Netlify will create a url. Copy that for later.

Now visit IFTTT, create an account and then create your own Applet. The Applet we'll be creating is fairly simple, and can be described as follows: "If I tweet, send a request to the build hook URL".

Your "If This" trigger will be "New tweet by you" in the "Twitter" service, and the only "Then That" action you need is "Make a web request" under the "Webhooks" service. Paste the build hook URL from Netlify into your webhook URL setting, and set the request method to "POST". The "Content Type" field should be set to application/x-www-form-urlencoded and set the body to {}. Once this is done save your IFTTT applet and you should be good to go! Try tweeting and you should see a new deployment trigger on Netlify!

Note: A big thanks goes out to @adamj_design for helping me keep this post up to date. He shared updated steps from the 11ty docs. If the steps I've described above don't work you can try their steps at the bottom of Quick Tip #008.