How I automate sharing old blog posts on X (Twitter)
By Flavio Copes
I have lots of evergreen content on my blog. This tutorial explains how I automated the repurpose of such content on X (Twitter)
I regularly post evergreen content on my blog. Evergreen means that it’s not about news or recent events, it’s content that’s valid today, but will also be valid 1 year from now, and maybe even 5 years from now, if I put in the time to keep it up to date in case the information gets obsolete.
Every time I write a new blog post, I share it on X (formerly Twitter), and if one of my few amazing followers (👋) does not see it there, they will probably never see it again, and I think that’s a disservice to me (as I put a lot of time in writing that blog post) and also a disservice to people that might learn something new with that post.
I sometimes share an old post once in a while, but it’s a manual process. I must remember to do so, and also I have to keep track of which content I already shared, to avoid reposting the same thing.
So I want to create a little automation around this problem, to post 2 old posts every day while I’m sleeping.
I know there are services that let you do this, but - hey - we’re developers, and developers build their own tools 😄
- The stack
- Host the data on Airtable
- Create the Node.js app
- Dependencies
- Initialize Airtable
- Paginate the records of the table
- Process all the posts
- Enter Express
- It’s all Working!
- Automate publishing using IFTTT
The stack
I want to make an app that looks up the posts I want to repurpose from a list, and when I call a particular URL, it shares a random post, always picking one that was not recently shared.
The application is based on Node.js and Express. I originally hosted it on Glitch, which shut down project hosting in July 2025. Today you can run the same Express app on Railway (that’s my referral link: sign up through it and you get $20 in Railway credit, and I get a 15% commission on what you pay in your first year), on Fly.io, or on any host that runs a Node process.
Any host that gives you an HTTPS URL is fine. IFTTT will hit that URL on a schedule.
The only piece of the puzzle left is deciding where to host the data. I want a service that’s easy to access, does not require a complicated permission and authorization setup, so I opted for Airtable.
Host the data on Airtable
Airtable is a sort of mix between a spreadsheet and a database. You can add data very easily, and also export it very easily through the API.
I made a simple base that hosts the posts table:
It has these columns:
- Title: the post title
- Link: the post link
- Shared: if the post has recently been shared already
- Disabled: if checked, the post will not be shared. Useful to “pause” content
- Description: an optional text that will be shared on X after the title, before the link
I filled the table with some posts.
I’ll use this table as the data source, and access it using the Node.js API. Airtable.js is the official JavaScript library.
One very cool thing about Airtable is that every base has its own API documentation, personalized with the IDs of your base and tables, so you can directly put the code in the app. It also goes one step further and provides the exact fields you use, with live content from your records:

This is kind of amazing on its own, I wish more services did this nice thing in their API docs.
Now, off to the Node app!
Create the Node.js app
The Node.js app has to communicate with Airtable and X.
Keep keys and parameters secret with a .env file (or your host’s secrets UI).
I set those keys for Airtable access:
- AIRTABLE_API_KEY
- AIRTABLE_BASE_ID
- AIRTABLE_BASE_NAME
- AIRTABLE_VIEW_NAME
and those for X:
- TWITTER_CONSUMER_KEY (API Key)
- TWITTER_CONSUMER_SECRET (API Key Secret)
- TWITTER_ACCESS_TOKEN
- TWITTER_ACCESS_TOKEN_SECRET
Those parameters come from the X Developer Console. Sign in, create an app, and the console generates the API Key and Secret and the Access Token and Secret for your account. They are shown once, so copy them into the .env file right away.
One thing changed since 2018: the API is not free anymore. As of September 2026 X bills every call from prepaid credits. Creating a post costs $0.015, and $0.20 if the post contains a URL, which ours always does. Two posts a day is about $12 a month. Check the current pricing before you build this.
In the Node.js code, those .env values are accessed through process.env.
Dependencies
The app uses
The original version of this app used Twit, which only speaks the old v1.1 API and hasn’t been updated since 2018. The code below uses twitter-api-v2 instead.
Install them with npm.
Initialize Airtable
Let’s get our base:
const base = require('airtable').base(process.env.AIRTABLE_BASE_ID)
By calling the select() method on a base, Airtable returns a query object.
The query object can be used to fetch the records. It returns the contents in chunks of 100 rows at a time.
We need to paginate through those and store all the posts, in case our list will grow over 100 items.
Paginate the records of the table
We do so by using the eachPage method, which accepts two functions. One that is executed on every iteration, and one that’s executed when the content fetching has been completed, and we have all the records:
const repurposeOldPost = () => {
posts = []
base(process.env.AIRTABLE_BASE_NAME)
.select({
view: process.env.AIRTABLE_VIEW_NAME,
})
.eachPage(processPage, processPosts)
}
processPage() processes a single “page” of results, which are up to 100 records. It first filters the list to exclude items without a value in the Title (to skip empty rows) and items with the Disabled column set to true.
Then it maps the values to a new array, which contains just the properties we need (id, title, link, shared, description) and assigns the result to partialPosts.
It merges the array with the current content of the posts variable, which holds all the items, and calls fetchNextPage() to get the next 100 items to process.
const processPage = (records, fetchNextPage) => {
const partialPosts = records
.filter((record) => {
if (record.get('Title') && record.get('Disabled') !== true) return true
})
.map((record) => {
return {
id: record.id,
title: record.get('Title'),
link: record.get('Link'),
shared: record.get('Shared') || false,
description: record.get('Description'),
}
})
posts = [...posts, ...partialPosts]
fetchNextPage()
}
When this process is done, eachPage() calls processPosts().
Process all the posts
This is where the full list of posts is analyzed, and the post is processed:
const processPosts = (err) => {
if (err) {
console.error(err)
return
}
let post
const notShared = posts.filter((post) => post.shared === false)
if (notShared.length === 0) {
//all posts shared already, clear them and just get the first
post = posts[0]
clearSharedStateForAllPosts(posts)
} else {
post = notShared[0]
}
if (canTweet(lastTweetTimestamp)) {
updateLastTweetTimestamp()
tweet(post)
setAsSharedOnAirtable(post)
} else {
console.error('Tweeting too much')
}
}
First, I find the list of posts that have not been shared recently. I want to pick from this list, so that before one post is shared again, all the other posts must be shared. This is important, because I want to do a proper rotation of the material.
I pick the first in this list. If all posts have been shared already, I clear the Shared column, and I just pick the first post in the list of all the posts.
Once I have a post, I check if I can actually tweet - I set a 60 minutes check between posts in the canTweet() function, to prevent an accidental flood in case you call the URL multiple times without realizing.
let lastTweetTimestamp = 0
const canTweet = (lastTweetTimestamp) => {
if (lastTweetTimestamp === 0) {
return true
}
const currentTimestamp = Math.floor(Date.now() / 1000)
if (currentTimestamp - lastTweetTimestamp > 60 * 60) {
return true
}
return false
}
If I can tweet, I update the global timestamp value, so the next check is performed against this last tweet time.
The posting happens in tweet(post). Here is the twitter-api-v2 version:
const { TwitterApi } = require('twitter-api-v2')
const twitter = new TwitterApi({
appKey: process.env.TWITTER_CONSUMER_KEY,
appSecret: process.env.TWITTER_CONSUMER_SECRET,
accessToken: process.env.TWITTER_ACCESS_TOKEN,
accessSecret: process.env.TWITTER_ACCESS_TOKEN_SECRET,
})
const rwClient = twitter.readWrite
const tweet = async (post) => {
let status = `${post.title} ${post.link}`
if (post.description) {
status = `${post.title} ${post.description} ${post.link}`
}
try {
await rwClient.v2.tweet(status)
} catch (err) {
console.error(err)
}
}
The post content is ${post.title} ${post.link} unless there is something in the Description field, in which case it’s ${post.title} ${post.description} ${post.link}.
That field is a multiline field, so I can go funny with emojis and formatting.
Enter Express
We need a way to expose all this functionality now! Express is perfect for this. I set it to listen for GET requests on /repurpose-old-post.
GET is more convenient for testing purposes. To prevent anyone with the URL to be able to send posts, I set an URL query parameter called secret, which is set in the .env file, so that’s just visible to me:
SECRET_URL_PARAMETER=something
const express = require('express')
const app = express()
app.get('/repurpose-old-post', (request, response) => {
if (request.query.secret === process.env.SECRET_URL_PARAMETER) {
repurposeOldPost()
response.send('Tweet sent')
} else {
console.log('Wrong secret parameter')
}
})
app.listen(process.env.PORT || 3000)
It’s all Working!
Deploy the app, open the public URL, and call /repurpose-old-post?secret=... with the secret you set up.
If you correctly configured the X and Airtable credentials, and you have put content in the Airtable base, a post with the title and the link shows up on your X profile.
Automate publishing using IFTTT
IFTTT, namely IF This Then That, is an amazing tool to help you automate tasks.
I set it up to call my URL at specific times during the day, one applet per time slot.
Creating one is quick. Click Create, then Add next to “If This”. Pick the Date & Time service and the “Every day at” trigger, and choose the time of day.
Then click Then That, pick the Webhooks service and the “Make a web request” action. Paste your app URL, including the secret query parameter, and set the method to GET.
That’s it! We can now sit back and watch the account go on autopilot 🤖 but don’t forget that X is not just for pushing out your “thing”. It’s meant for conversations, so make sure you don’t fall into the trap of just posting your content, but engage and balance posts with your content if you want to grow your following.
Want me to talk about your product? You can sponsor this site.