From ac82825cb51de540a6eea7e842c6cd4646d33e28 Mon Sep 17 00:00:00 2001 From: Alejandro Veras Date: Tue, 27 Oct 2026 15:21:32 -0400 Subject: [PATCH] Add a sitemap page and tidy sitemap.xml: site order, x-default and build-date last-modified --- next.config.ts | 2 + src/app/[lang]/sitemap/page.tsx | 76 ++++++++++++++++++++++++++++ src/app/globals.css | 90 +++++++++++++++++++++++++++++++++ src/app/sitemap.ts | 12 +++-- src/components/site-footer.tsx | 1 + src/i18n/ui/ar.ts | 9 ++++ src/i18n/ui/en.ts | 9 ++++ src/i18n/ui/es.ts | 9 ++++ 8 files changed, 204 insertions(+), 4 deletions(-) create mode 100644 src/app/[lang]/sitemap/page.tsx diff --git a/next.config.ts b/next.config.ts index a0cf0c7..42d7d2c 100644 --- a/next.config.ts +++ b/next.config.ts @@ -3,6 +3,8 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { /* config options here */ cacheComponents: true, + // The sitemap's last-modified date: the day this deploy was built. + env: { BUILD_DATE: new Date().toISOString().slice(0, 10) }, partialPrefetching: true, turbopack: { rules: { diff --git a/src/app/[lang]/sitemap/page.tsx b/src/app/[lang]/sitemap/page.tsx new file mode 100644 index 0000000..397925a --- /dev/null +++ b/src/app/[lang]/sitemap/page.tsx @@ -0,0 +1,76 @@ +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 4722fb1..588f955 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2980,3 +2980,93 @@ 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/sitemap.ts b/src/app/sitemap.ts index da27edd..0763497 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -6,22 +6,26 @@ export default function sitemap(): MetadataRoute.Sitemap { const paths = [ "/", "/about", - "/contact", "/services", ...serviceNav.map((s) => `/services/${s.slug}`), "/industries", ...industryNav.map((s) => `/industries/${s.slug}`), + "/contact", + "/sitemap", ]; const url = (p: string) => SITE_URL + (p === "/" ? "" : p); - const lastModified = new Date(); + // 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.split("/").length === 2 ? 0.8 : 0.6, - alternates: { languages: Object.fromEntries(locales.map((l) => [l, url(localize(p, l))])) }, + 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/components/site-footer.tsx b/src/components/site-footer.tsx index ee6a8ec..ef4b9f2 100644 --- a/src/components/site-footer.tsx +++ b/src/components/site-footer.tsx @@ -50,6 +50,7 @@ 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 25866a0..848d0e8 100644 --- a/src/i18n/ui/ar.ts +++ b/src/i18n/ui/ar.ts @@ -53,6 +53,15 @@ 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 f1dce30..7251ecc 100644 --- a/src/i18n/ui/en.ts +++ b/src/i18n/ui/en.ts @@ -53,6 +53,15 @@ 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 c269448..e288cca 100644 --- a/src/i18n/ui/es.ts +++ b/src/i18n/ui/es.ts @@ -53,6 +53,15 @@ 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" },