feat: agregar rutas dinámicas para videos en la sección de ayuda
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user