While migrating my blog to Next.js, I took the opportunity to address the big performance pitfalls that were degrading the reader's experience in the previous version. With Core Web Vitals becoming one of the biggest factors in search ranking in 2021, I needed to get my act together and finally find workarounds to these issues before they impact my overall traffic.
One of those issues was embed tweets. I often find myself in need to quote or reference a tweet in my MDX blog posts. However, using the classic Twitter embed iframe is not the best solution for that: they are slow to load and triggers a lot of Content Layout Shift (CLS) which hurts the performance of my blog.
Thankfully, by leveraging some of Next.js' key features, a bit of hacking, and also the awesome work from Vercel's Head of DevRel Lee Robinson, we can get around this problem and have tweets in MDX based pages that do not require an iframe and load instantly 🚀 like this one:
📨 just sent the latest issue of my newsletter! Topics for this one include - looking back at one year of learning in public⭐️ - my writing process ✍️ - what's coming up next on my blog! Curious but not yet subscribed? You can read it right here 👇 https://t.co/xQRm1wrNQw
Curious how it works? Let's take a look at the solution I managed to put together to solve this problem and some MDX/Next.js magic ✨.
Coming up with a plan
The original inspiration for this solution comes from @leerob himself: a few months ago he came up with a video titled Rebuilding the Twitter Embed Widget! which covers the following:
- what are the issues with the classic embed tweets?
- how to leverage the Twitter API to fetch the content of tweets
- how to build a
<Tweet />
component to display the content of a tweet with the output of the Twitter API - how to put these pieces together to display a predefined list of tweets in a Next.js page.
However, after watching this video, one could indeed follow this method to get a predefined list of tweets to render on a dedicated route/page in a Next.js project, but this still doesn't quite solve the problem for tweets in MDX-based pages 🤔. Thus I came up with the following plan to address this gap:
The core of this plan happens at build time when every page/article of the blog gets generated:
- When processing a given path, we get its corresponding MDX document content by reading a static .mdx file.
- Each MDX file can use/import React components. When it comes to handling tweets, I planned on using the following interface/component:
<StaticTweet id="abcdef123"/>
where the id prop contains the id of the tweet I want to render. - Then, by using some regex magic (I'll detail the code later in this article) we can extract each
StaticTweet
component from the content of the MDX document, and finally get a list of tweet ids where each id represents a tweet we want to eventually render. - This list of tweet ids is then returned in
getStaticProps
and used to fetch each tweet from the Twitter API and eventually get a map of tweet ids to tweet content (see first code snippet below). This map will help us find the content associated with each static tweet. - Finally, the most "hacky" part of this implementation: rendering each tweet declared in the MDX document with the proper content (you'll see why it's "hacky" in the next part 😄).
Sample map of tweet ids to tweet content
1const tweets = {2'1392141438528458758': {3created_at: '2021-05-11T15:35:58.000Z',4text:5"📨 just sent the latest issue of my newsletter!\n\nTopics for this one include\n- looking back at one year of learning in public⭐️\n- my writing process ✍️\n- what's coming up next on my blog!\n\nCurious but not yet subscribed? You can read it right here 👇\nhttps://t.co/xQRm1wrNQw",6id: '1392141438528458758',7public_metrics: {8retweet_count: 1,9reply_count: 0,10like_count: 6,11quote_count: 0,12},13author_id: '116762918',14media: [],15referenced_tweets: [],16author: {17profile_image_url:18'https://pbs.twimg.com/profile_images/813646702553010176/rOM8J8DC_normal.jpg',19verified: false,20id: '116762918',21url: 'https://t.co/CePDMvig2q',22name: 'Maxime',23protected: false,24username: 'MaximeHeckel',25},26},27'1386013361809281024': {28attachments: {29media_keys: ['3_1386013216527077377'],30},31created_at: '2021-04-24T17:45:10.000Z',32text:33"24h dans le volume d'une Fiat 500 avec trois amis et pourtant on se sent comme chez soi... à 400 km d'altitude ! Superbe performance technique et opérationelle de toutes les équipes qui nous ont entrainés et encadrés pour ce voyage 👏 https://t.co/kreeGnnLUM",34id: '1386013361809281024',35public_metrics: {36retweet_count: 8578,37reply_count: 959,38like_count: 101950,39quote_count: 627,40},41author_id: '437520768',42media: [43{44type: 'photo',45url: 'https://pbs.twimg.com/media/EzwbrVEX0AEdSDO.jpg',46width: 4096,47media_key: '3_1386013216527077377',48height: 2731,49},50],51referenced_tweets: [],52author: {53profile_image_url:54'https://pbs.twimg.com/profile_images/1377261846827270149/iUn8fDU6_normal.jpg',55verified: true,56id: '437520768',57url: 'https://t.co/6gdcdKt160',58name: 'Thomas Pesquet',59protected: false,60username: 'Thom_astro',61},62},63};
The implementation: a mix of regex, static site generation, and a hack
Now that we went through the plan, it's time to take a look at the implementation. There are 3 major pieces to implement:
- Using regex to find all the occurrences of
StaticTweet
and eventually get a list of tweet ids from the MDX document. - In
getStaticProps
, i.e. during static site generation, use that list of tweet ids to fetch their corresponding tweets with the Twitter API and return the map of tweets to id so the Next.js page can use it as a prop. - Define the StaticTweet component.
Extracting static tweets from an MDX document
Our first step consists of getting the list of ids of tweets we want to later fetch during the "static site generation" step.
For that, I took the easy path: **using regex to find each occurrence of ** StaticTweet
when reading the content of my MDX file.
Most MDX + Next.js setups, including this blog, have a function dedicated to reading and parsing the content of MDX files/documents. One example of such function can be found in Vercel's own tutorial to build an MDX-based blog with Next.JS: getDocBySlug
. It's in this function that we'll extract each StaticTweet
and build the list of ids:
Extraction of each occurrence of StaticTweet
1import matter from 'gray-matter';2import { serialize } from 'next-mdx-remote/serialize';34// Regex to find all the custom static tweets in a MDX file5const TWEET_RE = /<StaticTweet\sid="[0-9]+"\s\/>/g;67const docsDirectory = join(process.cwd(), 'docs')89export function getDocBySlug(slug) {10const realSlug = slug.replace(/\.md$/, '')11const fullPath = join(docsDirectory, `${realSlug}.md`)12const fileContents = fs.readFileSync(fullPath, 'utf8')13const { data, content } = matter(fileContents)1415/**16* Find all occurrence of <StaticTweet id="NUMERIC_TWEET_ID"/>17* in the content of the MDX blog post18*/19const tweetMatch = content.match(TWEET_RE);2021/**22* For all occurrences / matches, extract the id portion of the23* string, i.e. anything matching the regex /[0-9]+/g24*25* tweetIDs then becomes an array of string where each string is26* the id of a tweet.27* These IDs are then passed to the getTweets function to be fetched from28* the Twitter API.29*/30const tweetIDs = tweetMatch?.map((mdxTweet) => {31const id = mdxTweet.match(/[0-9]+/g)![0];32return id;33});3435const mdxSource = await serialize(source)3637return {38slug: realSlug,39frontMatter: data,40mdxSource,41tweetIDs: tweetIDs || []42}43}
Here, we execute the following tasks:
- extract each occurrence of
StaticTweet
- extract the value of the
id
prop - return the array of ids along with the content of the article
Build a map of tweet ids to tweet content
This step will be a bit easier since it mostly relies on @leerob's code to fetch tweets that he detailed in his video. You can find his implementation on his blog's repository. My implementation is the same as his but with Typescript type definitions.
At this stage, however, we still need to do some little edits in our getStaticProps
function and Next.js page:
- Get the tweet ids out of the
getDocBySlug
- Fetch the content associated with each tweet id
- Return the map of tweet ids to tweet content
- Read the map of ids tweet ids to tweet content in the Next.js page code.
Fetch the list of tweets and inject the content in the page
1import Image from 'next/image';2import { MDXRemote } from 'next-mdx-remote';3import { Heading, Text, Pre, Code } from '../components';45const components = {6img: Image,7h1: Heading.H1,8h2: Heading.H2,9p: Text,10code: Pre,11inlineCode: Code,12};1314export default function Post({ mdxSource, tweets }) {15console.log(tweets); // prints the map of tweet id to tweet content1617return <MDXRemote {...mdxSource} components={components} />;18}1920export async function getStaticProps({ params }) {21const { mdxSource, frontMatter, slug, tweetIDs } = getDocBySlug(params.slug);2223// Fetch the tweet content of each tweet id24const tweets = tweetIDs.length > 0 ? await getTweets(tweetIDs) : {};2526return {27props: {28frontMatter,29mdxSource,30slug,31tweets,32},33};34}
Define the StaticTweet component
This is where the core of this implementation resides, and also where things get a bit hacky 😬.
We can now, at build time, for a given path, get the content of all the tweets present in a corresponding MDX document. But now the main problem is: how can we render that content?
It's at this stage that I kind of hit a wall, and had to resolve to use, what I'd call, "unconventional patterns" and here are the reasons why:
- we can't override the interface of my MDX component. MDX makes us use the same interface between the definition of the component and how it's used in the MDX documents, i.e. in our case it takes one
id
prop, so it can only be defined with anid
prop. Thus we can't simply define an MDX component forStaticTweet
and call it a day. - our map of tweet ids to tweet content is only available at the "page" level, and thus can't be extracted out of that scope.
One way to fix this is to define the StaticTweet
component inline, i.e. inside the Next.js page, and use the map returned by getStaticProps
in the definition of the component:
Definition of the StaticTweet component used in MDX documents
1import Image from 'next/image';2import { MDXRemote } from 'next-mdx-remote';3import { Heading, Text, Pre, Code, Tweet } from '../components';45const components = {6img: Image,7h1: Heading.H1,8h2: Heading.H2,9p: Text,10code: Pre,11inlineCode: Code,12};1314export default function Post({ mdxSource, tweets }) {15const StaticTweet = ({ id }) => {16// Use the tweets map that is present in the outer scope to get the content associated with the id passed as prop17return <Tweet tweet={tweets[id]} />;18};1920return (21<MDXRemote22{...mdxSource}23components={{24// Append the newly defined StaticTweet component to the list of predefined MDX components25...components,26StaticTweet,27}}28/>29);30}
Usually, I'd not define a React component this way and even less with external dependencies that are not passed as props, however in this case:
- it's only to render static data, thus that map will never change after the static site generation
- it's still a valid Javascript pattern: our
StaticTweet
component definition is inherently a Javascript function and thus has access to variables outside of its inner scope.
So, it may sound a bit weird but it's not a red flag I promise 😄.
The result
We now have everything in place to render Static Tweets in our Next.js + MDX setup so let's take a look at a couple of examples to show what this implementation is capable of.
In the MDX document powering this same blog post, I added the following StaticTweets
:
1<StaticTweet id="1397739827706183686" />23<StaticTweet id="1386013361809281024" />45<StaticTweet id="1384267021991309314" />
The first one renders a standard tweet:
The following one renders a tweet with images:
24h dans le volume d'une Fiat 500 avec trois amis et pourtant on se sent comme chez soi... à 400 km d'altitude ! Superbe performance technique et opérationelle de toutes les équipes qui nous ont entrainés et encadrés pour ce voyage 👏 https://t.co/kreeGnnLUM
Finally, the last one renders a "quote tweet":
Just updated some of my projects to fix the missing headers, thank you @leeerob for sharing https://t.co/njBo8GLohm 🔒 and some of your tips! Just a note for Netlify users: you will have to add the headers either in your netlify.toml or a header file https://t.co/RN65w73I4r
Learned about https://t.co/RAxyJCKWjZ today 🔒 Here's how to take your Next.js site to an A. https://t.co/APq7nxngVw
And the best thing about this implementation: the resulting will remain as fast no matter how many tweets you add in your MDX document!
Pretty sweet right? ✨
Conclusion
First of all, thank you @leerob for the original inspiration for this implementation 🙌! This was yet another moment where I saw how Next.js and static site generation can shine.
I hope you all liked this little extension of Lee's static tweets tutorial. Adding support for MDX-based pages while keeping the interface clean was no easy feat as you can see but the result is definitely worth the effort and hours of tinkering put into this.
I'm still looking to improve the <Tweet />
component as I'm writing these words. There are yet a few elements that remain to be tackled in my current implementation, such as:
- figuring out a clean/secure way to parse links, right now they just render as text
- providing a better way to render a grid of images, as of now some images might see their aspect ratio altered
- parsing numbers, i.e. displaying
118k
instead of118000
when it comes to likes, retweets, or replies
It's not perfect but for now, it will do! I revisited previous blog posts that referenced tweets and replaced them with this new component to guarantee the best reading experience. If you have any suggestions or ideas on how I could further improve how tweets are rendered on my blog, as always, don't hesitate to reach out! I love hearing your feedback!
Liked this article? Share it with a friend on Bluesky or Twitter or support me to take on more ambitious projects to write about. Have a question, feedback or simply wish to contact me privately? Shoot me a DM and I'll do my best to get back to you.
Have a wonderful day.
– Maxime
A solution to remove sluggish Twitter embed iframes and load the tweets in your blog posts at the speed of light.