design: extract video from post content to display at top as hero media replacing static featured image

This commit is contained in:
Carlos (Almeira) 2026-08-20 09:55:59 -05:00
parent 3dff30ab73
commit 2b308e6d2a

View File

@ -25,6 +25,101 @@ function formatTextToHtml(text: string) {
.join('');
}
function extractMedia(content: string, videoUrlField?: string) {
let videoHtml: string | null = null;
let cleanContent = content;
// 1. Check if there's a video_url field (YouTube/Vimeo)
if (videoUrlField) {
if (videoUrlField.includes('youtube.com') || videoUrlField.includes('youtu.be')) {
let videoId = '';
const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/;
const match = videoUrlField.match(regExp);
if (match && match[2].length === 11) {
videoId = match[2];
}
if (videoId) {
videoHtml = `<iframe src="https://www.youtube.com/embed/${videoId}" class="w-full h-full absolute inset-0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>`;
}
} else if (videoUrlField.includes('vimeo.com')) {
const match = videoUrlField.match(/vimeo\.com\/(\d+)/);
if (match) {
const videoId = match[1];
videoHtml = `<iframe src="https://player.vimeo.com/video/${videoId}" class="w-full h-full absolute inset-0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen></iframe>`;
}
} else {
videoHtml = `<video src="${videoUrlField}" controls preload="metadata" class="w-full h-full object-cover"></video>`;
}
}
// 2. Look for video tag inside content (first occurrence)
if (!videoHtml && content) {
const videoRegex = /<video[^>]*>([\s\S]*?)<\/video>/i;
const match = videoRegex.exec(content);
if (match) {
videoHtml = match[0];
cleanContent = content.replace(videoRegex, '');
}
}
// 3. Look for youtube iframe/div inside content (first occurrence)
if (!videoHtml && content) {
const ytRegex = /<div[^>]*data-youtube-video[^>]*>([\s\S]*?)<\/div>/i;
const ytMatch = ytRegex.exec(content);
if (ytMatch) {
videoHtml = ytMatch[0];
cleanContent = content.replace(ytRegex, '');
} else {
const iframeRegex = /<iframe[^>]*src=["'][^"']*(?:youtube\.com|youtu\.be|vimeo\.com)[^"']*["'][^>]*>([\s\S]*?)<\/iframe>/i;
const iframeMatch = iframeRegex.exec(content);
if (iframeMatch) {
videoHtml = iframeMatch[0];
cleanContent = content.replace(iframeRegex, '');
}
}
}
return { videoHtml, cleanContent };
}
function styleHeroMedia(html: string, posterUrl?: string): string {
if (!html) return '';
if (html.includes('<video')) {
let styled = html
.replace(/class=["'][^"']*["']/gi, '')
.replace(/width=["'][^"']*["']/gi, '')
.replace(/height=["'][^"']*["']/gi, '');
let attributes = 'class="w-full h-full object-cover rounded-3xl"';
if (posterUrl && !styled.includes('poster=')) {
attributes += ` poster="${posterUrl}"`;
}
styled = styled.replace('<video', `<video ${attributes}`);
return styled;
}
if (html.includes('<iframe')) {
let styled = html
.replace(/class=["'][^"']*["']/gi, '')
.replace(/width=["'][^"']*["']/gi, '')
.replace(/height=["'][^"']*["']/gi, '');
styled = styled.replace('<iframe', '<iframe class="w-full h-full absolute inset-0 rounded-3xl"');
return styled;
}
if (html.includes('data-youtube-video')) {
const iframeMatch = html.match(/<iframe[^>]*>[\s\S]*?<\/iframe>/i);
if (iframeMatch) {
return styleHeroMedia(iframeMatch[0]);
}
}
return html;
}
async function fetchPostData(slugOrId: string) {
if (!slugOrId || typeof slugOrId !== 'string' || slugOrId.includes('.')) return null;
@ -141,6 +236,11 @@ export default async function NoticiaDetail({ params }: { params: Promise<{ slug
const fechaObj = new Date(post.fecha_publicacion || post.$createdAt);
const fechaFormateada = fechaObj.toLocaleDateString('es-ES', { day: '2-digit', month: 'long', year: 'numeric' });
// Extract video media if present
const rawContent = post.contenido || '';
const { videoHtml, cleanContent } = extractMedia(rawContent, post.video_url);
const styledHeroMedia = videoHtml ? styleHeroMedia(videoHtml, imagenUrl || undefined) : null;
return (
<main className="animate-fade-in relative min-h-screen pt-24 pb-20">
<div className="container mx-auto px-4 lg:px-8">
@ -173,19 +273,28 @@ export default async function NoticiaDetail({ params }: { params: Promise<{ slug
)}
</header>
{imagenUrl && (
{styledHeroMedia ? (
<div className="relative aspect-video w-full rounded-3xl overflow-hidden mb-12 border border-white/10 shadow-2xl bg-black">
<div className="w-full h-full" dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(styledHeroMedia, {
ADD_TAGS: ['iframe', 'video', 'source'],
ADD_ATTR: ['allow', 'allowfullscreen', 'frameborder', 'scrolling', 'style', 'class', 'controls', 'src', 'type', 'muted', 'loop', 'autoplay', 'preload', 'poster']
})
}} />
</div>
) : imagenUrl ? (
<div className="relative aspect-[21/9] w-full rounded-3xl overflow-hidden mb-12 border border-white/10 shadow-2xl">
<img src={imagenUrl} alt={post.titulo} className="w-full h-full object-cover" />
<div className="absolute inset-0 bg-gradient-to-t from-slate-950/50 to-transparent"></div>
</div>
)}
) : null}
<div
className="prose prose-invert prose-cyan max-w-none text-slate-300 leading-relaxed text-lg whitespace-pre-wrap break-words
prose-headings:font-outfit prose-headings:font-black prose-headings:text-white
prose-strong:text-cyan-400 prose-p:empty:before:content-['\\00a0']"
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(formatTextToHtml(post.contenido), {
__html: DOMPurify.sanitize(formatTextToHtml(cleanContent), {
ADD_TAGS: ['iframe', 'video', 'source'],
ADD_ATTR: ['allow', 'allowfullscreen', 'frameborder', 'scrolling', 'style', 'class', 'controls', 'src', 'type', 'muted', 'loop', 'autoplay', 'preload']
})