From 2d137fea3ef42571454807e18252d8871b0d5493 Mon Sep 17 00:00:00 2001 From: Alejandro Veras Date: Tue, 27 Oct 2026 15:27:13 -0400 Subject: [PATCH] Replace the sitemap page with Yoast-style XML sitemaps: an index, one sitemap per kind of page, styled tables --- public/sitemap-style.xml | 83 ++++++++++++++++++++++++ src/app/[lang]/sitemap/page.tsx | 76 ---------------------- src/app/globals.css | 90 --------------------------- src/app/industry-sitemap.xml/route.ts | 3 + src/app/page-sitemap.xml/route.ts | 3 + src/app/service-sitemap.xml/route.ts | 3 + src/app/sitemap.ts | 31 --------- src/app/sitemap.xml/route.ts | 3 + src/components/site-footer.tsx | 1 - src/i18n/ui/ar.ts | 9 --- src/i18n/ui/en.ts | 9 --- src/i18n/ui/es.ts | 9 --- src/lib/sitemap.ts | 51 +++++++++++++++ 13 files changed, 146 insertions(+), 225 deletions(-) create mode 100644 public/sitemap-style.xml delete mode 100644 src/app/[lang]/sitemap/page.tsx create mode 100644 src/app/industry-sitemap.xml/route.ts create mode 100644 src/app/page-sitemap.xml/route.ts create mode 100644 src/app/service-sitemap.xml/route.ts delete mode 100644 src/app/sitemap.ts create mode 100644 src/app/sitemap.xml/route.ts create mode 100644 src/lib/sitemap.ts diff --git a/public/sitemap-style.xml b/public/sitemap-style.xml new file mode 100644 index 0000000..df28ced --- /dev/null +++ b/public/sitemap-style.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + XML Sitemap + + + +
+

XML Sitemap

+

This is an XML Sitemap, meant for consumption by search engines.

+

You can find more information about XML sitemaps on sitemaps.org.

+ +
+ + +
+ + +

This XML Sitemap Index file contains sitemaps.

+ + + + + + + + + + + +
#SitemapLast Modified
+
+ + +

This XML Sitemap contains URLs. ← Sitemap Index

+ + + + + + + + + + + + +
#URLLanguagesLast Mod.
+ + + +
+
+
diff --git a/src/app/[lang]/sitemap/page.tsx b/src/app/[lang]/sitemap/page.tsx deleted file mode 100644 index 397925a..0000000 --- a/src/app/[lang]/sitemap/page.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import type { Metadata } from "next"; -import Link from "next/link"; -import { locales, localeNames, localize } from "@/i18n/config"; -import { industryLinks, serviceLinks, type NavLink } from "@/i18n/nav"; -import { getUI } from "@/i18n/server"; -import { localeAlternates } from "@/lib/metadata"; - -export async function generateMetadata(): Promise { - const { locale, t } = await getUI(); - return { title: t.sitemap.metaTitle, description: t.sitemap.intro, alternates: localeAlternates("/sitemap", locale) }; -} - -/** A readable index of every page, for visitors; search engines use /sitemap.xml. */ -export default async function SitemapPage() { - const { locale, t } = await getUI(); - const groups: { title: string; links: NavLink[] }[] = [ - { - title: t.sitemap.company, - links: [ - { label: t.sitemap.home, href: localize("/", locale) }, - { label: t.nav.about, href: localize("/about", locale) }, - { label: t.nav.contact, href: localize("/contact", locale) }, - ], - }, - { - title: t.nav.services, - links: [{ label: t.nav.allServices, href: localize("/services", locale) }, ...serviceLinks(t, locale)], - }, - { - title: t.nav.industries, - links: [{ label: t.nav.allIndustries, href: localize("/industries", locale) }, ...industryLinks(t, locale)], - }, - { - title: t.sitemap.languages, - links: locales.map((l) => ({ label: localeNames[l], href: localize("/sitemap", l) })), - }, - ]; - - return ( -
-
- -

{t.sitemap.title}

-

- {t.sitemap.title} -

-

{t.sitemap.intro}

-
- {groups.map((g) => ( -
-

- {g.title} {String(g.links.length).padStart(2, "0")} -

-
    - {g.links.map((l) => ( -
  • - - {l.label} - → - -
  • - ))} -
-
- ))} -
-
-
- ); -} diff --git a/src/app/globals.css b/src/app/globals.css index 588f955..4722fb1 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2980,93 +2980,3 @@ html[lang="ar"] .nav a { align-items: flex-start; } } - -/* ============================================================ - Sitemap page - ============================================================ */ -.sitemap { - padding-top: 152px; -} - -.sitemap h1 { - margin-top: 24px; -} - -.sitemap-intro { - max-width: 46ch; - margin: 24px 0 0; -} - -.sitemap-grid { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 24px; - margin-top: 64px; -} - -.sitemap-group { - padding-top: 20px; - border-top: 1px solid var(--grafite); -} - -.sitemap-group h2 { - display: flex; - justify-content: space-between; - margin: 0 0 20px; - font-size: 11px; - font-weight: 500; - letter-spacing: 0.22em; - text-transform: uppercase; -} - -.sitemap-group h2 span { - color: var(--rosso); -} - -.sitemap-group ul { - list-style: none; - margin: 0; - padding: 0; -} - -.sitemap-group a { - display: flex; - justify-content: space-between; - gap: 12px; - padding: 12px 0; - border-bottom: 1px solid var(--grafite); - font-size: 15px; - color: var(--fumo); - transition: color 0.3s var(--ease); -} - -.sitemap-group a .arrow { - opacity: 0; - transition: opacity 0.3s var(--ease); -} - -.sitemap-group a:hover, -.sitemap-group a[aria-current] { - color: var(--bianco); -} - -.sitemap-group a:hover .arrow { - opacity: 1; - color: var(--rosso); -} - -[dir="rtl"] .sitemap-group a .arrow { - transform: scaleX(-1); -} - -@media (max-width: 1023px) { - .sitemap-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } -} - -@media (max-width: 599px) { - .sitemap-grid { - grid-template-columns: 1fr; - } -} diff --git a/src/app/industry-sitemap.xml/route.ts b/src/app/industry-sitemap.xml/route.ts new file mode 100644 index 0000000..3e5c759 --- /dev/null +++ b/src/app/industry-sitemap.xml/route.ts @@ -0,0 +1,3 @@ +import { sitemap } from "@/lib/sitemap"; + +export const GET = () => sitemap("industry"); diff --git a/src/app/page-sitemap.xml/route.ts b/src/app/page-sitemap.xml/route.ts new file mode 100644 index 0000000..e5c36f0 --- /dev/null +++ b/src/app/page-sitemap.xml/route.ts @@ -0,0 +1,3 @@ +import { sitemap } from "@/lib/sitemap"; + +export const GET = () => sitemap("page"); diff --git a/src/app/service-sitemap.xml/route.ts b/src/app/service-sitemap.xml/route.ts new file mode 100644 index 0000000..3cb1296 --- /dev/null +++ b/src/app/service-sitemap.xml/route.ts @@ -0,0 +1,3 @@ +import { sitemap } from "@/lib/sitemap"; + +export const GET = () => sitemap("service"); diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts deleted file mode 100644 index 0763497..0000000 --- a/src/app/sitemap.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { MetadataRoute } from "next"; -import { locales, localize } from "@/i18n/config"; -import { SITE_URL, industryNav, serviceNav } from "@/lib/routes"; - -export default function sitemap(): MetadataRoute.Sitemap { - const paths = [ - "/", - "/about", - "/services", - ...serviceNav.map((s) => `/services/${s.slug}`), - "/industries", - ...industryNav.map((s) => `/industries/${s.slug}`), - "/contact", - "/sitemap", - ]; - const url = (p: string) => SITE_URL + (p === "/" ? "" : p); - // The day this deploy was built (set in next.config.ts), so dates only move when the site changes. - const lastModified = process.env.BUILD_DATE; - - return locales.flatMap((locale) => - paths.map((p) => ({ - url: url(localize(p, locale)), - lastModified, - changeFrequency: "monthly" as const, - priority: p === "/" ? 1 : p === "/sitemap" ? 0.3 : p.split("/").length === 2 ? 0.8 : 0.6, - alternates: { - languages: { ...Object.fromEntries(locales.map((l) => [l, url(localize(p, l))])), "x-default": url(p) }, - }, - })), - ); -} diff --git a/src/app/sitemap.xml/route.ts b/src/app/sitemap.xml/route.ts new file mode 100644 index 0000000..d05f9d6 --- /dev/null +++ b/src/app/sitemap.xml/route.ts @@ -0,0 +1,3 @@ +import { sitemapIndex } from "@/lib/sitemap"; + +export const GET = () => sitemapIndex(); diff --git a/src/components/site-footer.tsx b/src/components/site-footer.tsx index ef4b9f2..ee6a8ec 100644 --- a/src/components/site-footer.tsx +++ b/src/components/site-footer.tsx @@ -50,7 +50,6 @@ export default async function SiteFooter() { { label: t.nav.allServices, href: localize("/services", locale) }, { label: t.nav.allIndustries, href: localize("/industries", locale) }, { label: t.nav.contact, href: localize("/contact", locale) }, - { label: t.sitemap.nav, href: localize("/sitemap", locale) }, ], }, ]; diff --git a/src/i18n/ui/ar.ts b/src/i18n/ui/ar.ts index 848d0e8..25866a0 100644 --- a/src/i18n/ui/ar.ts +++ b/src/i18n/ui/ar.ts @@ -53,15 +53,6 @@ export const ar: UI = { rights: "© 2026 AI Growth Technologies. جميع الحقوق محفوظة.", cities: "دبي · هيوستن · ميامي · نيويورك", }, - sitemap: { - nav: "خريطة الموقع", - title: "خريطة الموقع", - metaTitle: "خريطة الموقع | AI Growth Technologies", - intro: "جميع صفحات الموقع في مكان واحد، مرتبة حسب القسم.", - company: "الشركة", - home: "الرئيسية", - languages: "اللغات", - }, crumbs: { label: "مسار التنقل", home: "الرئيسية" }, faqTitle: "الأسئلة الشائعة", office: { phone: "الهاتف", email: "البريد الإلكتروني" }, diff --git a/src/i18n/ui/en.ts b/src/i18n/ui/en.ts index 7251ecc..f1dce30 100644 --- a/src/i18n/ui/en.ts +++ b/src/i18n/ui/en.ts @@ -53,15 +53,6 @@ export const en = { rights: "© 2026 AI Growth Technologies. All rights reserved.", cities: "Dubai · Houston · Miami · New York", }, - sitemap: { - nav: "Sitemap", - title: "Sitemap", - metaTitle: "Sitemap | AI Growth Technologies", - intro: "Every page on the site in one place, organized by section.", - company: "Company", - home: "Home", - languages: "Languages", - }, crumbs: { label: "Breadcrumb", home: "Home" }, faqTitle: "Frequently Asked Questions", office: { phone: "Phone", email: "Email" }, diff --git a/src/i18n/ui/es.ts b/src/i18n/ui/es.ts index e288cca..c269448 100644 --- a/src/i18n/ui/es.ts +++ b/src/i18n/ui/es.ts @@ -53,15 +53,6 @@ export const es: UI = { rights: "© 2026 AI Growth Technologies. Todos los derechos reservados.", cities: "Dubái · Houston · Miami · Nueva York", }, - sitemap: { - nav: "Mapa del sitio", - title: "Mapa del sitio", - metaTitle: "Mapa del sitio | AI Growth Technologies", - intro: "Todas las páginas del sitio en un solo lugar, organizadas por sección.", - company: "Empresa", - home: "Inicio", - languages: "Idiomas", - }, crumbs: { label: "Ruta de navegación", home: "Inicio" }, faqTitle: "Preguntas frecuentes", office: { phone: "Teléfono", email: "Correo" }, diff --git a/src/lib/sitemap.ts b/src/lib/sitemap.ts new file mode 100644 index 0000000..b86e4ea --- /dev/null +++ b/src/lib/sitemap.ts @@ -0,0 +1,51 @@ +import { locales, localize } from "@/i18n/config"; +import { SITE_URL, industryNav, serviceNav } from "./routes"; + +// Yoast-style sitemaps: /sitemap.xml is an index of one sitemap per kind of page. Each lists the +// English pages in site order, then their Spanish and Arabic copies, with every page's translations. +// public/sitemap-style.xml (an XSL stylesheet) shows them as tables in a browser; search engines ignore it. + +export const sitemaps = { + page: ["/", "/about", "/contact"], + service: ["/services", ...serviceNav.map((s) => `/services/${s.slug}`)], + industry: ["/industries", ...industryNav.map((s) => `/industries/${s.slug}`)], +} as const; + +// The day this deploy was built (set in next.config.ts), so dates only move when the site changes. +const lastmod = process.env.BUILD_DATE; +const url = (p: string) => SITE_URL + (p === "/" ? "" : p); +const head = `\n\n`; +const xml = (body: string) => + new Response(head + body, { headers: { "content-type": "application/xml; charset=utf-8" } }); + +export function sitemapIndex() { + const entries = Object.keys(sitemaps).map( + (name) => ` + ${SITE_URL}/${name}-sitemap.xml + ${lastmod} + `, + ); + return xml(` +${entries.join("\n")} + +`); +} + +export function sitemap(name: keyof typeof sitemaps) { + const alternates = (p: string) => + [...locales.map((l) => [l, localize(p, l)]), ["x-default", p]] + .map(([l, href]) => `\n `) + .join(""); + const entries = locales.flatMap((locale) => + sitemaps[name].map( + (p) => ` + ${url(localize(p, locale))} + ${lastmod}${alternates(p)} + `, + ), + ); + return xml(` +${entries.join("\n")} + +`); +}