diff --git a/txclient/src/app/robots.ts b/txclient/src/app/robots.ts index b555154..a674ff2 100644 --- a/txclient/src/app/robots.ts +++ b/txclient/src/app/robots.ts @@ -8,12 +8,17 @@ export default function robots(): MetadataRoute.Robots { allow: "/", disallow: [ "/admin/", - "/dashboard/", - "/my-appointments/", - "/offer-appointments/", + "/user/", + "/messenger/", + "/landing/dashboard/", + "/landing/my-appointments/", + "/landing/current-plan/", + "/landing/upgrade-plan/", + "/landing/pending-ratings/", + "/landing/subscription/", "/landing/recover-account/", ], }, sitemap: `${baseUrl}/sitemap.xml`, }; -} +} \ No newline at end of file diff --git a/txclient/src/app/sitemap.xml/route.ts b/txclient/src/app/sitemap.xml/route.ts index bf62939..1a296d9 100644 --- a/txclient/src/app/sitemap.xml/route.ts +++ b/txclient/src/app/sitemap.xml/route.ts @@ -4,6 +4,17 @@ import { PublicServicesResults } from "@models/Service.model"; import { sliderFrames } from "@core/app/Services/SliderFrames"; 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, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + export async function GET() { 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[] = []; try { @@ -99,19 +129,12 @@ export async function GET() { if (servicesResult && servicesResult.data) { 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) => ({ url: `${baseUrl}/landing/service/${service.id}`, changeFrequency: "weekly", 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]; @@ -126,7 +149,7 @@ export async function GET() { console.error("Error generating sitemap:", error); } - const allRoutes = [...staticRoutes, ...videoRoutes, ...authRoutes, ...marketingRoutes, ...serviceRoutes]; + const allRoutes = [...staticRoutes, ...videoRoutes, ...authRoutes, ...marketingRoutes, ...legalRoutes, ...serviceRoutes]; const sitemapXml = ` { return ` - ${route.url} - ${route.lastModified ? `${route.lastModified}` : ""} - ${route.changeFrequency} + ${escapeXml(route.url)} + ${route.lastModified ? `${escapeXml(route.lastModified)}` : ""} + ${escapeXml(route.changeFrequency)} ${route.priority} ${route.images ? route.images .map( (img) => ` - ${img.url} - - + ${escapeXml(img.url)} + ${escapeXml(img.title)} + ${escapeXml(img.caption)} ` ) .join("") @@ -159,7 +182,10 @@ export async function GET() { return new NextResponse(sitemapXml, { 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", }, }); }