feat: mejorar el sitemap añadiendo rutas legales y asegurando la correcta codificación XML

This commit is contained in:
2026-09-16 10:04:18 -03:00
parent ae63d0dea0
commit 8c1089df41
2 changed files with 52 additions and 21 deletions
+9 -4
View File
@@ -8,12 +8,17 @@ export default function robots(): MetadataRoute.Robots {
allow: "/", allow: "/",
disallow: [ disallow: [
"/admin/", "/admin/",
"/dashboard/", "/user/",
"/my-appointments/", "/messenger/",
"/offer-appointments/", "/landing/dashboard/",
"/landing/my-appointments/",
"/landing/current-plan/",
"/landing/upgrade-plan/",
"/landing/pending-ratings/",
"/landing/subscription/",
"/landing/recover-account/", "/landing/recover-account/",
], ],
}, },
sitemap: `${baseUrl}/sitemap.xml`, sitemap: `${baseUrl}/sitemap.xml`,
}; };
} }
+43 -17
View File
@@ -4,6 +4,17 @@ import { PublicServicesResults } from "@models/Service.model";
import { sliderFrames } from "@core/app/Services/SliderFrames"; import { sliderFrames } from "@core/app/Services/SliderFrames";
import { getAllVideos } from "@core/app/landing/help/videoData"; import { getAllVideos } from "@core/app/landing/help/videoData";
// Escape text content so the sitemap is always well-formed XML
// (URLs with query strings, service names/descriptions, etc.).
function escapeXml(value: string): string {
return value
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;");
}
export async function GET() { export async function GET() {
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "https://turnosxpress.com.ar"; const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "https://turnosxpress.com.ar";
@@ -83,6 +94,25 @@ export async function GET() {
}, },
]; ];
// Legal pages (rarely change, low priority)
const legalRoutes: SitemapRoute[] = [
{
url: `${baseUrl}/landing/terms`,
changeFrequency: "yearly",
priority: 0.3,
},
{
url: `${baseUrl}/landing/privacy`,
changeFrequency: "yearly",
priority: 0.3,
},
{
url: `${baseUrl}/landing/cookies`,
changeFrequency: "yearly",
priority: 0.3,
},
];
let serviceRoutes: SitemapRoute[] = []; let serviceRoutes: SitemapRoute[] = [];
try { try {
@@ -99,19 +129,12 @@ export async function GET() {
if (servicesResult && servicesResult.data) { if (servicesResult && servicesResult.data) {
totalPages = servicesResult.pages || 1; totalPages = servicesResult.pages || 1;
// Image sitemap entries are omitted: service images may be hosted
// on arbitrary external domains, which the image extension can't verify.
const newRoutes = servicesResult.data.map((service) => ({ const newRoutes = servicesResult.data.map((service) => ({
url: `${baseUrl}/landing/service/${service.id}`, url: `${baseUrl}/landing/service/${service.id}`,
changeFrequency: "weekly", changeFrequency: "weekly",
priority: 0.8, priority: 0.8,
images: [
{
url: service.image.startsWith("http")
? service.image
: `${process.env.NEXT_PUBLIC_API_URL}${service.image}`,
title: service.name,
caption: service.description,
},
],
})); }));
serviceRoutes = [...serviceRoutes, ...newRoutes]; serviceRoutes = [...serviceRoutes, ...newRoutes];
@@ -126,7 +149,7 @@ export async function GET() {
console.error("Error generating sitemap:", error); console.error("Error generating sitemap:", error);
} }
const allRoutes = [...staticRoutes, ...videoRoutes, ...authRoutes, ...marketingRoutes, ...serviceRoutes]; const allRoutes = [...staticRoutes, ...videoRoutes, ...authRoutes, ...marketingRoutes, ...legalRoutes, ...serviceRoutes];
const sitemapXml = `<?xml version="1.0" encoding="UTF-8"?> const sitemapXml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
@@ -135,18 +158,18 @@ export async function GET() {
.map((route) => { .map((route) => {
return ` return `
<url> <url>
<loc>${route.url}</loc> <loc>${escapeXml(route.url)}</loc>
${route.lastModified ? `<lastmod>${route.lastModified}</lastmod>` : ""} ${route.lastModified ? `<lastmod>${escapeXml(route.lastModified)}</lastmod>` : ""}
<changefreq>${route.changeFrequency}</changefreq> <changefreq>${escapeXml(route.changeFrequency)}</changefreq>
<priority>${route.priority}</priority> <priority>${route.priority}</priority>
${route.images ${route.images
? route.images ? route.images
.map( .map(
(img) => ` (img) => `
<image:image> <image:image>
<image:loc>${img.url}</image:loc> <image:loc>${escapeXml(img.url)}</image:loc>
<image:title><![CDATA[${img.title}]]></image:title> <image:title>${escapeXml(img.title)}</image:title>
<image:caption><![CDATA[${img.caption}]]></image:caption> <image:caption>${escapeXml(img.caption)}</image:caption>
</image:image>` </image:image>`
) )
.join("") .join("")
@@ -159,7 +182,10 @@ export async function GET() {
return new NextResponse(sitemapXml, { return new NextResponse(sitemapXml, {
headers: { headers: {
"Content-Type": "application/xml", "Content-Type": "application/xml; charset=utf-8",
// Regenerated on every request (fetch no-store): cache at the edge/CDN
// so crawlers and browsers don't re-hit the services API each time.
"Cache-Control": "public, max-age=3600, s-maxage=86400, stale-while-revalidate=86400",
}, },
}); });
} }