How To Build a Self-Updating Twitter Banner With Dynamic Content
Display your latest blog articles or your most recent followers with Lambda and EventBridge
The first impression of somebody clicking on your Twitter profile will be your header image. Why not display some dynamic content? There’s no rocket science involved to achieve this, and the possibilities are limitless.
In this article, I want to show you how to easily build this with a simple Node.js script, Twitter's and Medium's APIs, and AWS Lambda.
An overview:
getting credentials so we can use the Twitter API.
creating a Node.js script for retrieving details of our latest followers.
retrieving our latest blog article via Medium’s RSS feed.
updating a static header image with the images of our followers and our blog article title.
putting everything into a Lambda function on AWS which will be executed every 60 seconds.
💡 Even though we’ll be creating infrastructure at AWS, this solution won’t result in any costs due to AWS’ free tier for Lambda.
Disclaimer and Credits: I’ve seen this first on Tibo’s timeline, so I’m definitely not the first one to provide a solution on this and you can also find a lot of other tutorials that helped me a lot when I was stuck at some point taking this journey myself.
Getting started
Let’s set up a new project either via npm or yarn and add the dependencies we need:
axios
— a simple but powerful HTTP clientjimp
— image manipulationsharp
— converting response buffers into imagestwitter-api-client
— communicating with the Twitter APIrss-to-json
— for translating XML (Medium Feeds) to JSONserverless
— provisioning our AWS infrastructure via Serverless Framework. Install this globally!
We’ll only create a single scripting file at src/handler.js
containing all of our needed operations.
Accessing the Twitter API
The next thing we need is access to the Twitter API. So we need to create a new client application at Twitter’s developer portal.
After creating your app, make sure to copy the API key and the API Secret Key. After getting back to the overview screen, select your new app and click on edit on the top right. Make sure to set full permissions (Read and Write and Direct Messages
) so we can later access all of our needed information and update our banner.
The last step is to create our Access Token and Secret. You can do this by selecting your app and switching to the tab Keys and token
right beneath the headline.
Most likely, Twitter will also display your API Key and API Secret Key again together with your new Access Token and Access Token Secret. Note down everything. You can’t display those details again!
Initializing our Twitter client
Now that we’ve got our credentials, we can set up our client so we can communicate with Twitter’s API. At first, let’s put all of our keys and secrets and our Twitter & Medium handle into creds.json
on the root level.
{
"TWITTER_API_KEY": "5Hn9Z28...BW4lEzD",
"TWITTER_API_SECRET_KEY": "yndh2s5sXLuh...33sbWsbbREZYp4",
"TWITTER_API_ACCESS_TOKEN": "42399689...jQIBa3Ew4RDi5ed",
"TWITTER_API_ACCESS_SECRET": "QgYTQoIueX5S...JHk44vVuty9ewwo",
"TWITTER_HANDLE": "tpschmidt_",
"MEDIUM_HANDLE": "tpschmidt"
}
Now we can create the client with the following code:
const { TwitterClient } = require('twitter-api-client')
function getVariable(name) {
if (fs.existsSync('../creds.json')) {
return require('../creds.json')[name]
}
return process.env[name]
}
const twitterClient = new TwitterClient({
apiKey: getVariable('TWITTER_API_KEY'),
apiSecret: getVariable('TWITTER_API_SECRET_KEY'),
accessToken: getVariable('TWITTER_API_ACCESS_TOKEN'),
accessTokenSecret: getVariable('TWITTER_API_ACCESS_SECRET'),
});
For local testing, we’ll directly fall back to our creds.json
file and when deploying to AWS, we’ll use our environment variables which we’ll set up later on.
Retrieving Our Latest Followers
Let’s use the Twitter API client to get our latest followers. The response consists of a list of users that each contain profile_image_url_https
.
We can download the avatars of the users and then resize the image via sharp and save it to a temporary file.
const axios = require('axios')
const sharp = require('sharp')
const numberOfFollowers = 3
const widthHeightFollowerImage = 90
async function saveAvatar(user, path) {
console.log(`Retrieving avatar...`)
const response = await axios({
url: user.profile_image_url_https,
responseType: 'arraybuffer'
})
await sharp(response.data)
.resize(widthHeightFollowerImage, widthHeightFollowerImage)
.toFile(path)
}
async function getImagesOfLatestFollowers() {
console.log(`Retrieving followers...`)
const data = await twitterClient
.accountsAndUsers
.followersList({
screen_name: getVariable('TWITTER_HANDLE'),
count: numberOfFollowers
})
await Promise.all(data.users
.map((user, index) => saveAvatar(user, `/tmp/${index}.png`)))
}
Working With Medium’s Personal RSS Feeds
Next, we want to get the latest blog article title from our own Medium feed. If you’re running a blog on Medium, all of your timelines metadata is available at [https://medium.com/feed/@username](https://medium.com/feed/@username.)
.
As we’re working with JavaScript, we want to covert the XML response into JSON with rss-to-json
. Afterward, we can easily extract our title. Adding a little bit of padding left and right depending on the size of the title assures us that our title is always aligned centrally for a specific point on our header.
const Feed = require('rss-to-json')
async function getLatestArticleHeadline() {
console.log(`Retrieving headline...`)
const rss = await Feed.load(`https://medium.com/feed/@${getVariable('MEDIUM_HANDLE')}`)
const title = rss.items[0].title
console.log(`Retrieved headline: ${title}`)
// add padding left & right to align it properly
const padding = ' '.repeat(Math.ceil((60 - title.length) / 2))
return `${padding}${title}${padding}`;
}
Surely, you’re not limited to Medium. You can fetch anything at this point, even a plain HTML page, and extract the things you want to show.
Updating Our Banner Image
The tricky part is already done now. We’ve retrieved our dynamic content! Now we only need to add a base image for our header into our project into a new assets
folder. Here’s an example of mine:
As you see, I’ve left out space where I want to fill in the blog title as well as the pictures of the latest followers. Depending on your image, you have to later adjust the coordinates where we add the overlays for our dynamic content.
Additionally, I’m using another image to mask our follower’s images to have rounded borders. It’s just a white-filled circle with a black background. Make sure to also have it in the same size as our follower’s images (in the example we’re using 90x90 pixels). If you’re preferring squares, just remove the mask
calls.
const Jimp = require('jimp')
async function createBanner(headline) {
const banner = await Jimp.read(`${__dirname}/../assets/${bannerFileName}`)
const mask = await Jimp.read(`${__dirname}/../assets/${maskFileName}`)
const font = await Jimp.loadFont(Jimp.FONT_SANS_32_WHITE)
// build banner
console.log(`Adding followers...`)
await Promise.all([...Array(numberOfFollowers)].map((_, i) => {
return new Promise(async resolve => {
const image = await Jimp.read(`/tmp/${i}.png`)
const x = 600 + i * (widthHeightFollowerImage + 10);
console.log(`Appending image ${i} with x=${x}`)
banner.composite(image.mask(mask, 0, 0), x, 350);
resolve()
})
}))
console.log(`Adding headline...`)
banner.print(font, 380, 65, headline);
await banner.writeAsync('/tmp/1500x500_final.png');
}
There’s not a lot of magic involved:
we’re loading our assets and our pre-saved follower’s images
we’re setting font, color, and size for our text
we add our images and our headline to the banner
The result is a new file at 1500x500_final.png
file at /tmp
.
Finally, we just need to update our new header on Twitter!
async function uploadBanner() {
console.log(`Uploading to twitter...`)
const base64 = await fs.readFileSync('/tmp/1500x500_final.png', { encoding: 'base64' });
await twitterClient.accountsAndUsers
.accountUpdateProfileBanner({ banner: base64 })
}
Automating Everything With Lambda and Eventbridge Rules
Now we’ve got some local scripts to update our banner. But we want to run this continuously and automatically as often as the Twitter API allows it.
We can easily do this by leveraging AWS Lambda. If you haven’t started with AWS yet, just register yourself (it’s free and you’ll start with additional free limits in the first year, even though there’s a lot of free base contingent for several services like Lambda already).
Afterward, there are just a few things you need to do beforehand:
visit your Security Credentials to generate a new key and corresponding secret.
install the AWS CLI (e.g., via homebrew).
run
aws configure
to set your credentials from the first steps.
That’s it! You’re good to go.
We already installed Serverless Framework globally so we can initialize a new template file by running sls
or serverless
. We’ll end up with a new serverless.yml
template file that defines all of our infrastructures.
What we want to have at the end:
a Lambda function that runs our script.
a Lambda Layer that contains our dependencies.
an EventBridge rule that triggers our function every 60 seconds.
With Serverless Framework, that’s super simple to build and only a few lines of code.
service: twitter-banner
frameworkVersion: '2'
custom:
appName: twitter-banner
provider:
lambdaHashingVersion: 20201221
name: aws
runtime: nodejs12.x
region: eu-central-1
logRetentionInDays: 7
layers:
common:
package:
artifact: deploy/layer-common.zip
name: ${self:custom.appName}-common-layer
description: Common dependencies for Lambdas
compatibleRuntimes:
- nodejs12.x
retain: false
package:
individually: true
functions:
twitterBanner:
handler: src/handler.handler
package:
artifact: deploy/lambda.zip
name: ${self:custom.appName}
description: Function to regularly update the Twitter banner
reservedConcurrency: 1
memorySize: 2048
timeout: 10
layers:
- { Ref: CommonLambdaLayer }
events:
- schedule: rate(1 minute)
environment:
TWITTER_API_KEY: ${file(creds.json):TWITTER_API_KEY}
TWITTER_API_SECRET_KEY: ${file(creds.json):TWITTER_API_SECRET_KEY}
TWITTER_API_ACCESS_TOKEN: ${file(creds.json):TWITTER_API_ACCESS_TOKEN}
TWITTER_API_ACCESS_SECRET: ${file(creds.json):TWITTER_API_ACCESS_SECRET}
TWITTER_HANDLE: ${file(creds.json):TWITTER_HANDLE}
MEDIUM_HANDLE: ${file(creds.json):MEDIUM_HANDLE}
Let’s have a look at the three major parts:
provider
— defines that we want to use AWS and sets a few basics like that we want our logs to expire after seven days so we don’t introduce any costs in the future.layers
— defines our layer that contains all of our node_modules dependencies.functions
— that's where we create our Lambda function.
With the events
field, we define that EventBridge will invoke our function regularly.
Before we get to the packaging of our files, we need to add a proxy function to our script which will be later invoked by Lambda.
module.exports.handler = async () => {
await getImagesOfLatestFollowers()
const title = await getLatestArticleHeadline()
await createBanner(title)
await uploadBanner()
}
If you have a second look, you’ll see that we referenced src/handler.handler
in the serverless.yml
. In our case, the source can be found at src/handler.js
. This tells Lambda where to start the execution of our node script.
Final part: packaging our function and our layer.
#!/usr/bin/env bash
rm -rf deploy
mkdir deploy
# packaging lambda
zip -rq deploy/lambda.zip src/* assets/*
# packaging layer
mkdir -p deploy
rm -rf tmp/common
mkdir -p tmp/common/nodejs
cp package.json tmp/common/nodejs
pushd tmp/common/nodejs 2>&1>/dev/null
docker run -v "$PWD":/var/task lambci/lambda:build-nodejs12.x npm install --no-optional --only=prod
popd 2>&1>/dev/null
pushd tmp/common 2>&1>/dev/null
rm nodejs/package.json
zip -r ../../deploy/layer-common.zip . 2>&1>/dev/null
popd 2>&1>/dev/null
if [[ ! -f deploy/layer-common.zip ]];then
echo "Packaging failed! Distribution package ZIP file could not be found."
exit 1
fi
That’s it! We can now just deploy our whole stack via sls deploy
and we’re good to go. If somethings not working out, have a look at the corresponding CloudWatch log streams to find the underlying issue. If everything runs smooth, you’ll see our debug logs:
INFO Retrieving followers...
INFO Retrieving avatar...
INFO Retrieving avatar...
INFO Retrieving avatar...
INFO Retrieving headline...
INFO Retrieved headline: Distributed Tracing Matters
INFO Adding followers...
INFO Appending image 0 with x=600
INFO Appending image 1 with x=700
INFO Appending image 2 with x=800
INFO Adding headline...
INFO Uploading to twitter...
If you made it here and everything worked out: nice work! 🎉
💡 Don’t be scared that your code will be invoked every 60 seconds because as mentioned before, there’s a free tier for Lambda on AWS which includes 1m executions and computation seconds per month. As our function only takes a few seconds to finish, we’ll never exceed this in any way.
You can find all of the code on GitHub. If there are any issues or you’re stuck with the descriptions, don’t hesitate on messaging me on a contact channel of your choice.
Wrap up
Even though it’s not a unique thing anymore, having some fun with dynamic content on your Twitter timeline can have benefits in building your audience. Also, you maybe learned something new and enjoyed the process of building this.
Besides that, there are basically no limits on what you can do and add. You can also automate updates to your bio or your avatar.
Be creative, and show it to the world.
Related Reads
If you found this article helpful, you might also enjoy these related posts: