Embeds de Twitter (X)
Los embeds oficiales de Twitter son pesados y bloquean el hilo principal. Nuestra implementación retrasa la carga del script de Twitter hasta que el navegador está inactivo, y sincroniza dinámicamente el tema de los tweets con el de la web.
El problema con los tweets oficiales
<blockquote class="twitter-tweet"> clásico junto a widgets.js en el HTML inicial bloquea el renderizado (render-blocking), retrasa el LCP y carga recursos pesados incluso si el tweet está muy abajo en el artículo.Cómo se guarda en la base de datos
El editor Tiptap guarda los embeds de Twitter como enlaces con data-embed="true":
<!-- Ambos formatos funcionan -->
<a href="https://x.com/astrodotbuild/status/1760000000000" data-embed="true">[Tweet Astro]</a>
<a href="https://twitter.com/astrodotbuild/status/1760000000000" data-embed="true">[Tweet Astro]</a>Soporta tanto URLs de twitter.com como de x.com.
Instalación de Dependencias
Zero Dependencies
A diferencia de YouTube, esta implementación es 100% nativa (Vanilla JS). No necesitas instalar paquetes de npm ni wrappers. Solo copiar la lógica SSR e inyectar el script oficial dinámicamente.
Pipeline SSR: del enlace al placeholder
1. Regex de detección
const twitterEmbedRegex = /(?:<p>)?\s*<a\s+(?:[^>]*?\s+)?href=["'](https?:\/\/(?:twitter\.com|x\.com)\/[^/]+\/status\/\d+)[^"']*["']\s+data-embed=["']true["'][^>]*>\[([^\]]+)\]<\/a>\s*(?:<\/p>)?/gi;2. El bug de x.com
x.com. Para solucionarlo, el SSR fuerza el cambio a twitter.com antes de generar el HTML.// CRÍTICO: forzar twitter.com antes de insertar el placeholder
let embedUrl = url;
if (embedUrl.includes('x.com')) {
embedUrl = embedUrl.replace('x.com', 'twitter.com');
}3. El placeholder SSR
El SSR reemplaza el <a> por un contenedor vacío que Twitter hidratará después:
<div class="twitter-embed my-8 flex justify-center w-full">
<blockquote class="twitter-tweet" data-dnt="true" data-theme="dark">
<a href="URL_DEL_TWEET"></a>
</blockquote>
</div>Pipeline Client-side: Hidratación Inteligente
Sincronización inicial del tema
Para evitar un "flashazo" blanco si el usuario está en modo oscuro, leemos el tema actual antes de cargar Twitter:
// Aplicar tema ANTES de cargar widgets.js
const theme = document.documentElement.getAttribute('data-theme') === 'light' ? 'light' : 'dark';
document.querySelectorAll('.twitter-tweet').forEach(el => {
el.setAttribute('data-theme', theme);
});Observador de mutaciones (Live Theme Sync)
Si el usuario cambia el tema después de que el tweet haya cargado, modificamos la URL del iframe ya inyectado:
new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.attributeName === 'data-theme') {
const newTheme = document.documentElement.getAttribute('data-theme') === 'light' ? 'light' : 'dark';
// Buscar iframes inyectados por Twitter
document.querySelectorAll('.twitter-tweet-rendered iframe').forEach(iframe => {
let src = iframe.getAttribute('src');
if (src && src.includes('theme=')) {
iframe.setAttribute('src', src.replace(/theme=[^&]+/, 'theme=' + newTheme));
}
});
}
});
}).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });Carga diferida con requestIdleCallback
El script de Twitter (widgets.js) solo se descarga e inyecta cuando el navegador no tiene otras tareas pendientes:
if ('requestIdleCallback' in window) {
requestIdleCallback(() => loadTwitterScript());
} else {
setTimeout(loadTwitterScript, 1000);
}Compatibilidad con View Transitions
Al cambiar de página sin recargar completa (View Transitions), Twitter no sabe que debe escanear el nuevo DOM. Se lo ordenamos manualmente:
document.addEventListener('astro:page-load', function() {
if (window.twttr && window.twttr.widgets) {
window.twttr.widgets.load();
}
});Diagnóstico de Errores Comunes
El blockquote se ve como texto plano
Causa: La URL usa x.com en lugar de twitter.com en el href del blockquote.
Tweets no cargan al navegar entre posts
Causa: El evento astro:page-load de View Transitions no disparó twttr.widgets.load().
window.twttr existe en el scope global y el observer sigue activo.