NodeBB Twitter / X embeds
-
@DownPW I see the issue. The URL has underscores present

This basically means that the composer sees this as Markdown and is attempting to render it as such. It’s not a bug in the X embed code. I may be able to work around it though.
-
@DownPW I see the issue. The URL has underscores present

This basically means that the composer sees this as Markdown and is attempting to render it as such. It’s not a bug in the X embed code. I may be able to work around it though.
@phenomlab said in NodeBB Twitter / X embeds:
@DownPW I see the issue. The URL has underscores present

This basically means that the composer sees this as Markdown and is attempting to render it as such. It’s not a bug in the X embed code. I may be able to work around it though.
yep I tell you that here :
@DownPW said in NodeBB Twitter / X embeds:
URL:
Itt seems on my forum the “_” character was delete in the URL (Don’t know why) :
-
@phenomlab said in NodeBB Twitter / X embeds:
@DownPW I see the issue. The URL has underscores present

This basically means that the composer sees this as Markdown and is attempting to render it as such. It’s not a bug in the X embed code. I may be able to work around it though.
yep I tell you that here :
@DownPW said in NodeBB Twitter / X embeds:
URL:
Itt seems on my forum the “_” character was delete in the URL (Don’t know why) :
@DownPW Yes. Can you grant me admin temporarily for your site?
-
@DownPW this should work now with the below commit
https://github.com/phenomlab/nodebb-twitter-embeds/blob/main/embeds.js
The reason is that markdown processes the URL and replaces
_with<em>meaning the tweet can no longer be rendered. The quick “fix” would be to escape the underscore characters, so_SaxX_becomes\_SaxX\_but that really is ugly, and places the responsibility on the OP, or an admin to go in and makes those changes afterwards.This new code effectively changes

into
https://x.com/\_SaxX\_/status/1850458923481825567?mx=20It then removes the
<em></em>tags, and implodes the URL so the Tweet Parser can read it. In a nutshell, you land up with this -
@DownPW this should work now with the below commit
https://github.com/phenomlab/nodebb-twitter-embeds/blob/main/embeds.js
The reason is that markdown processes the URL and replaces
_with<em>meaning the tweet can no longer be rendered. The quick “fix” would be to escape the underscore characters, so_SaxX_becomes\_SaxX\_but that really is ugly, and places the responsibility on the OP, or an admin to go in and makes those changes afterwards.This new code effectively changes

into
https://x.com/\_SaxX\_/status/1850458923481825567?mx=20It then removes the
<em></em>tags, and implodes the URL so the Tweet Parser can read it. In a nutshell, you land up with this@phenomlab said in NodeBB Twitter / X embeds:
@DownPW this should work now with the below commit
https://github.com/phenomlab/nodebb-twitter-embeds/blob/main/embeds.js
The reason is that markdown processes the URL and replaces
_with<em>meaning the tweet can no longer be rendered. The quick “fix” would be to escape the underscore characters, so_SaxX_becomes\_SaxX\_but that really is ugly, and places the responsibility on the OP, or an admin to go in and makes those changes afterwards.This new code effectively changes

into
https://x.com/\_SaxX\_/status/1850458923481825567?mx=20It then removes the
<em></em>tags, and implodes the URL so the Tweet Parser can read it. In a nutshell, you land up with thisThat’s Fix works great

-
Have you experienced that some posts will not show up after they have been posted, or that the post will look greyed out? This goes away if you refresh the page.
@phenomlab Still have this issue where certain posts will show up blank until you refresh the page. Any clue what to do? Using your latest code.
-
@phenomlab Still have this issue where certain posts will show up blank until you refresh the page. Any clue what to do? Using your latest code.
@OT I honestly am not able to replicate this. Can you PM me a link to a post on your forum with the specific issue so I can have a look?
-
I have problems with this code and nodebb-plugin-poll and nodebb-recent-card :
see ehere : https://community.nodebb.org/post/106781
Hi @baris @phenomlab ,
Following up on my previous post, I managed to identify and fix the issue.
Here’s a full breakdown of what was happening and how I resolved it.Root cause
The custom X/Twitter embed script was using
.html(content)to rewrite the inner HTML of the[component="topic"]container. While this worked for embedding tweets, it was destroying the DOM of other plugins, specificallynodebb-plugin-pollandnodebb-plugin-recent-cards, because it wiped out everything they had injected into the same container.What the new script does
The script is meant to replace NodeBB’s default link preview for X/Twitter URLs (the broken blockquote card with the warning triangle) with a proper embedded tweet widget via the Twitter widgets API.
It handles two cases:
- Raw X link pasted in a post → NodeBB generates a
<blockquote class="twitter-tweet">preview that shows “undefined / This website did not return any description”. The script detects this blockquote, extracts the tweet ID from thehref, and replaces it with the real widget. - Link posted via the
<em>username format → The script reconstructs the proper X URL and embeds the tweet the same way.
The fix
Instead of rewriting the entire container HTML, the script now:
- Targets only the specific
blockquote.twitter-tweetor<p>elements that contain tweet URLs - Replaces only those elements using
.replaceWith(), leaving everything else in the DOM untouched - Uses
:not([data-processed="true"])to avoid re-processing already-embedded tweets when NodeBB events fire multiple times - Cleans up the placeholder on failure instead of leaving an empty
<div>in the DOM - Covers multiple containers via a
TWEET_CONTAINERSconstant ([component="post/content"]and.teaser-contentfor the recent cards plugin)
Full updated code
// ------------------------------------------------------------------ // X/Twitter Embed // Automatically replaces X/Twitter links with embedded tweet widgets // ------------------------------------------------------------------ (function() { // Matches both x.com and twitter.com status URLs const TWEET_URL_REGEX = /https?:\/\/(?:x|twitter)\.com\/(?:#!\/)?(\w+)\/status(?:es)?\/(\d+)/; // Containers where tweet embeds should be processed // Covers: post content, recent cards plugin teaser const TWEET_CONTAINERS = [ '[component="post/content"]', '.recent-cards-plugin .teaser-content' ].join(', '); // Dynamically loads the Twitter widgets script if not already present function loadTwitterScript(callback) { if (typeof twttr !== 'undefined' && twttr.widgets) { callback(); return; } const script = document.createElement('script'); script.src = 'https://platform.twitter.com/widgets.js'; script.async = true; script.onload = callback; script.onerror = function() { console.error('[Twitter Embed] Failed to load Twitter script.'); }; document.head.appendChild(script); } // Creates a temporary placeholder div that will be replaced by the tweet widget function createPlaceholder(tweetId) { return $('<div>', { 'class': 'tweet-placeholder', 'data-tweet-id': tweetId, 'data-processed': 'false' }); } // Replaces a given DOM element with an embedded tweet widget // On failure, removes the placeholder cleanly instead of leaving an empty div function embedTweet($element, tweetId) { const $placeholder = createPlaceholder(tweetId); $element.replaceWith($placeholder); twttr.widgets.createTweet(tweetId, $placeholder[0], { conversation: 'none' // Hide conversation thread }).then(function() { console.log('[Twitter Embed] Tweet ' + tweetId + ' embedded successfully'); $placeholder.attr('data-processed', 'true'); }).catch(function(error) { console.error('[Twitter Embed] Failed to embed tweet ' + tweetId, error); $placeholder.remove(); }); } // Scans all known containers for tweet URLs and triggers embedding // Handles two cases: // 1. A blockquote generated by NodeBB when a raw X link is pasted // 2. A plain <p> containing an <a> tag pointing to a tweet function embedTweets() { // Case 1: NodeBB-generated blockquote preview (raw X link pasted in post) // Uses :not([data-processed="true"]) to skip already-embedded tweets $(TWEET_CONTAINERS).find('blockquote.twitter-tweet:not([data-processed="true"])').each(function() { const href = $(this).find('a').first().attr('href') || ''; const match = href.match(TWEET_URL_REGEX); if (match) embedTweet($(this), match[2]); }); // Case 2: Plain link inside a <p> tag (e.g. posted via the <em> username format) $(TWEET_CONTAINERS).find('p').each(function() { const $link = $(this).find('a').first(); if (!$link.length) return; const match = ($link.attr('href') || '').match(TWEET_URL_REGEX); if (match) embedTweet($(this), match[2]); }); } // Reconstructs a proper X URL from posts where the username is wrapped in <em> // and the tweet ID appears as plain text, then cleans up the surrounding markup function updateTwitterLinks() { $('li[component="post"] p').each(function() { const $p = $(this); const $link = $p.find('a'); const $em = $p.find('em'); if (!$link.length || !$em.length) return; // Extract tweet ID from the text content of the paragraph const tweetIdMatch = $p.text().trim().match(/status\/(\d+)/); if (!tweetIdMatch) return; const tweetId = tweetIdMatch[1]; const username = $em.text().replace(/_/g, '\\_'); // Escape underscores in username const newUrl = `https://x.com/${username}/status/${tweetId}`; // Update the link href and visible text, replace <em> with plain text $link.attr('href', newUrl).text(newUrl); $em.replaceWith(`_${username}_`); // Remove leftover plain text nodes around the reconstructed link $p.contents().filter(function() { return this.nodeType === Node.TEXT_NODE; }).remove(); }); } // Trigger on all relevant NodeBB navigation and post loading events $(window).on('action:ajaxify.end action:posts.loaded action:posts.edited action:chat.loaded', function() { updateTwitterLinks(); loadTwitterScript(embedTweets); }); }()); - Raw X link pasted in a post → NodeBB generates a
-
I have problems with this code and nodebb-plugin-poll and nodebb-recent-card :
see ehere : https://community.nodebb.org/post/106781
Hi @baris @phenomlab ,
Following up on my previous post, I managed to identify and fix the issue.
Here’s a full breakdown of what was happening and how I resolved it.Root cause
The custom X/Twitter embed script was using
.html(content)to rewrite the inner HTML of the[component="topic"]container. While this worked for embedding tweets, it was destroying the DOM of other plugins, specificallynodebb-plugin-pollandnodebb-plugin-recent-cards, because it wiped out everything they had injected into the same container.What the new script does
The script is meant to replace NodeBB’s default link preview for X/Twitter URLs (the broken blockquote card with the warning triangle) with a proper embedded tweet widget via the Twitter widgets API.
It handles two cases:
- Raw X link pasted in a post → NodeBB generates a
<blockquote class="twitter-tweet">preview that shows “undefined / This website did not return any description”. The script detects this blockquote, extracts the tweet ID from thehref, and replaces it with the real widget. - Link posted via the
<em>username format → The script reconstructs the proper X URL and embeds the tweet the same way.
The fix
Instead of rewriting the entire container HTML, the script now:
- Targets only the specific
blockquote.twitter-tweetor<p>elements that contain tweet URLs - Replaces only those elements using
.replaceWith(), leaving everything else in the DOM untouched - Uses
:not([data-processed="true"])to avoid re-processing already-embedded tweets when NodeBB events fire multiple times - Cleans up the placeholder on failure instead of leaving an empty
<div>in the DOM - Covers multiple containers via a
TWEET_CONTAINERSconstant ([component="post/content"]and.teaser-contentfor the recent cards plugin)
Full updated code
// ------------------------------------------------------------------ // X/Twitter Embed // Automatically replaces X/Twitter links with embedded tweet widgets // ------------------------------------------------------------------ (function() { // Matches both x.com and twitter.com status URLs const TWEET_URL_REGEX = /https?:\/\/(?:x|twitter)\.com\/(?:#!\/)?(\w+)\/status(?:es)?\/(\d+)/; // Containers where tweet embeds should be processed // Covers: post content, recent cards plugin teaser const TWEET_CONTAINERS = [ '[component="post/content"]', '.recent-cards-plugin .teaser-content' ].join(', '); // Dynamically loads the Twitter widgets script if not already present function loadTwitterScript(callback) { if (typeof twttr !== 'undefined' && twttr.widgets) { callback(); return; } const script = document.createElement('script'); script.src = 'https://platform.twitter.com/widgets.js'; script.async = true; script.onload = callback; script.onerror = function() { console.error('[Twitter Embed] Failed to load Twitter script.'); }; document.head.appendChild(script); } // Creates a temporary placeholder div that will be replaced by the tweet widget function createPlaceholder(tweetId) { return $('<div>', { 'class': 'tweet-placeholder', 'data-tweet-id': tweetId, 'data-processed': 'false' }); } // Replaces a given DOM element with an embedded tweet widget // On failure, removes the placeholder cleanly instead of leaving an empty div function embedTweet($element, tweetId) { const $placeholder = createPlaceholder(tweetId); $element.replaceWith($placeholder); twttr.widgets.createTweet(tweetId, $placeholder[0], { conversation: 'none' // Hide conversation thread }).then(function() { console.log('[Twitter Embed] Tweet ' + tweetId + ' embedded successfully'); $placeholder.attr('data-processed', 'true'); }).catch(function(error) { console.error('[Twitter Embed] Failed to embed tweet ' + tweetId, error); $placeholder.remove(); }); } // Scans all known containers for tweet URLs and triggers embedding // Handles two cases: // 1. A blockquote generated by NodeBB when a raw X link is pasted // 2. A plain <p> containing an <a> tag pointing to a tweet function embedTweets() { // Case 1: NodeBB-generated blockquote preview (raw X link pasted in post) // Uses :not([data-processed="true"]) to skip already-embedded tweets $(TWEET_CONTAINERS).find('blockquote.twitter-tweet:not([data-processed="true"])').each(function() { const href = $(this).find('a').first().attr('href') || ''; const match = href.match(TWEET_URL_REGEX); if (match) embedTweet($(this), match[2]); }); // Case 2: Plain link inside a <p> tag (e.g. posted via the <em> username format) $(TWEET_CONTAINERS).find('p').each(function() { const $link = $(this).find('a').first(); if (!$link.length) return; const match = ($link.attr('href') || '').match(TWEET_URL_REGEX); if (match) embedTweet($(this), match[2]); }); } // Reconstructs a proper X URL from posts where the username is wrapped in <em> // and the tweet ID appears as plain text, then cleans up the surrounding markup function updateTwitterLinks() { $('li[component="post"] p').each(function() { const $p = $(this); const $link = $p.find('a'); const $em = $p.find('em'); if (!$link.length || !$em.length) return; // Extract tweet ID from the text content of the paragraph const tweetIdMatch = $p.text().trim().match(/status\/(\d+)/); if (!tweetIdMatch) return; const tweetId = tweetIdMatch[1]; const username = $em.text().replace(/_/g, '\\_'); // Escape underscores in username const newUrl = `https://x.com/${username}/status/${tweetId}`; // Update the link href and visible text, replace <em> with plain text $link.attr('href', newUrl).text(newUrl); $em.replaceWith(`_${username}_`); // Remove leftover plain text nodes around the reconstructed link $p.contents().filter(function() { return this.nodeType === Node.TEXT_NODE; }).remove(); }); } // Trigger on all relevant NodeBB navigation and post loading events $(window).on('action:ajaxify.end action:posts.loaded action:posts.edited action:chat.loaded', function() { updateTwitterLinks(); loadTwitterScript(embedTweets); }); }()); - Raw X link pasted in a post → NodeBB generates a
-
Hello! It looks like you're interested in this conversation, but you don't have an account yet.
Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.
With your input, this post could be even better 💗
Register Login