feat: agregar rutas dinámicas para videos en la sección de ayuda

This commit is contained in:
2026-09-16 09:31:22 -03:00
parent 65d149a4e7
commit 583ddf1ea9
2 changed files with 297 additions and 1 deletions
@@ -0,0 +1,272 @@
import { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
import type { Video, VideoCategory } from "../../videoData";
import { videoCategories, getAllVideos } from "../../videoData";
// Static generation: Next.js builds a page for every video at build time
export function generateStaticParams() {
return getAllVideos().map((video) => ({ videoId: video.id }));
}
// SEO metadata per video
export async function generateMetadata({
params,
}: {
params: Promise<{ videoId: string }>;
}): Promise<Metadata> {
const { videoId } = await params;
const video = getAllVideos().find((v) => v.id === videoId);
if (!video) {
return {};
}
const category = videoCategories.find((c) => c.videos.some((v) => v.id === videoId));
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "https://turnosxpress.com.ar";
const url = `${baseUrl}/landing/help/video/${videoId}`;
return {
title: `${video.title} | TurnosXpress`,
description: video.description,
alternates: {
canonical: url,
},
openGraph: {
type: "video.other",
title: video.title,
description: video.description,
images: [{ url: video.thumbnail, width: 480, height: 360, alt: video.title }],
siteName: "TurnosXpress",
locale: "es_AR",
},
twitter: {
card: "summary_large_image",
title: video.title,
description: video.description,
images: [video.thumbnail],
},
robots: {
index: true,
follow: true,
},
};
}
const allVideos = getAllVideos();
// Build flat array with category context for each video
interface VideoWithContext {
video: Video;
category: VideoCategory;
index: number;
}
function getVideoWithContext(videoId: string): VideoWithContext | null {
let flatIndex = 0;
for (const cat of videoCategories) {
const idx = cat.videos.findIndex((v) => v.id === videoId);
if (idx !== -1) {
return {
video: cat.videos[idx],
category: cat,
index: flatIndex,
};
}
flatIndex += cat.videos.length;
}
return null;
}
export default async function VideoPage({
params,
}: {
params: Promise<{ videoId: string }>;
}) {
const { videoId } = await params;
const ctx = getVideoWithContext(videoId);
if (!ctx) {
notFound();
}
const { video, category, index } = ctx;
const prevVideo = index > 0 ? allVideos[index - 1] : null;
const nextVideo = index < allVideos.length - 1 ? allVideos[index + 1] : null;
// JSON-LD VideoObject schema for Google rich results
const jsonLd = {
"@context": "https://schema.org",
"@type": "VideoObject",
name: video.title,
description: video.description,
thumbnailUrl: video.thumbnail,
contentUrl: `https://www.youtube.com/watch?v=${video.id}`,
embedUrl: `https://www.youtube.com/embed/${video.id}`,
uploadDate: "2025-01-01",
publisher: {
"@type": "Organization",
name: "TurnosXpress",
logo: {
"@type": "ImageObject",
url: `${process.env.NEXT_PUBLIC_BASE_URL || "https://turnosxpress.com.ar"}/favicon.ico`,
},
},
category: category.name,
};
return (
<>
{/* JSON-LD structured data */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<div style={{ maxWidth: 900, margin: "0 auto", padding: "2rem 1.5rem" }}>
{/* Breadcrumb nav */}
<nav
style={{
marginBottom: "2rem",
fontSize: "0.875rem",
color: "var(--gray-dark)",
}}
>
<Link
href="/landing/help"
style={{ color: "var(--wine-dark)", textDecoration: "none" }}
>
Centro de Ayuda
</Link>
{" / "}
<span style={{ color: "var(--black)" }}>{video.title}</span>
</nav>
{/* Category badge */}
<div
style={{
display: "inline-block",
padding: "0.35rem 1rem",
borderRadius: 50,
background: category.color,
color: "white",
fontSize: "0.8rem",
fontWeight: 700,
marginBottom: "1.25rem",
}}
>
{category.icon} {category.name}
</div>
{/* Title */}
<h1
style={{
fontSize: "2rem",
fontWeight: 800,
color: "var(--wine-superdark)",
marginBottom: "0.75rem",
lineHeight: 1.3,
}}
>
{video.title}
</h1>
{/* Description */}
<p
style={{
fontSize: "1.05rem",
color: "var(--gray-dark)",
lineHeight: 1.6,
marginBottom: "2rem",
}}
>
{video.description}
</p>
{/* YouTube embed */}
<div style={{ position: "relative", paddingBottom: "56.25%", height: 0, overflow: "hidden", borderRadius: 16, boxShadow: "0 8px 30px rgba(0,0,0,0.12)", marginBottom: "2rem" }}>
<iframe
src={`https://www.youtube.com/embed/${video.id}?rel=0&modestbranding=1`}
title={video.title}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
style={{ position: "absolute", top: 0, left: 0, width: "100%", height: "100%", border: 0 }}
/>
</div>
{/* Prev/Next navigation */}
{(prevVideo || nextVideo) && (
<div style={{ display: "flex", gap: "1rem", flexWrap: "wrap" }}>
{prevVideo && (
<Link
href={`/landing/help/video/${prevVideo.id}`}
style={{
flex: 1,
minWidth: 200,
padding: "1rem 1.25rem",
borderRadius: 12,
border: "1px solid var(--white-darkest)",
background: "var(--white)",
textDecoration: "none",
transition: "all 0.2s ease",
}}
>
<div style={{ fontSize: "0.8rem", color: "var(--gray-dark)", marginBottom: "0.25rem" }}>
{"← Anterior"}
</div>
<div style={{ fontSize: "0.95rem", fontWeight: 700, color: "var(--wine-superdark)" }}>
{prevVideo.title}
</div>
</Link>
)}
{nextVideo && (
<Link
href={`/landing/help/video/${nextVideo.id}`}
style={{
flex: 1,
minWidth: 200,
padding: "1rem 1.25rem",
borderRadius: 12,
border: "1px solid var(--white-darkest)",
background: "var(--white)",
textDecoration: "none",
textAlign: "right",
transition: "all 0.2s ease",
}}
>
<div style={{ fontSize: "0.8rem", color: "var(--gray-dark)", marginBottom: "0.25rem" }}>
{"Siguiente →"}
</div>
<div style={{ fontSize: "0.95rem", fontWeight: 700, color: "var(--wine-superdark)" }}>
{nextVideo.title}
</div>
</Link>
)}
</div>
)}
{/* Back to help CTA */}
<div style={{ textAlign: "center", marginTop: "3rem", paddingTop: "2rem", borderTop: "1px solid var(--white-darkest)" }}>
<p style={{ color: "var(--gray-dark)", marginBottom: "1rem" }}>
Querés ver más tutoriales?
</p>
<Link
href="/landing/help"
style={{
display: "inline-block",
padding: "0.75rem 2rem",
background: "linear-gradient(135deg, var(--wine-red), var(--wine-dark))",
color: "white",
borderRadius: 50,
textDecoration: "none",
fontWeight: 700,
fontSize: "1rem",
}}
>
Ver todos los tutoriales
</Link>
</div>
</div>
</>
);
}
+25 -1
View File
@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
import ApiServerService from "@core/Services/Api.Server.Service";
import { PublicServicesResults } from "@models/Service.model";
import { sliderFrames } from "@core/app/Services/SliderFrames";
import { getAllVideos } from "@core/app/landing/help/videoData";
export async function GET() {
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "https://turnosxpress.com.ar";
@@ -42,6 +43,25 @@ export async function GET() {
changeFrequency: "weekly",
priority: 0.8,
},
];
// Dynamic video routes (one per tutorial)
const allVideos = getAllVideos();
const videoRoutes: SitemapRoute[] = allVideos.map((video) => ({
url: `${baseUrl}/landing/help/video/${video.id}`,
changeFrequency: "monthly",
priority: 0.6,
images: [
{
url: video.thumbnail,
title: video.title,
caption: video.description,
},
],
}));
// Login and signup routes (lower priority)
const authRoutes: SitemapRoute[] = [
{
url: `${baseUrl}/landing/login`,
changeFrequency: "monthly",
@@ -52,6 +72,10 @@ export async function GET() {
changeFrequency: "monthly",
priority: 0.5,
},
];
// Marketing pages
const marketingRoutes: SitemapRoute[] = [
{
url: `${baseUrl}/landing/offer-appointments`,
changeFrequency: "weekly",
@@ -107,7 +131,7 @@ export async function GET() {
console.error("Error generating sitemap:", error);
}
const allRoutes = [...staticRoutes, ...serviceRoutes];
const allRoutes = [...staticRoutes, ...videoRoutes, ...authRoutes, ...marketingRoutes, ...serviceRoutes];
const sitemapXml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"