Replace the sitemap page with Yoast-style XML sitemaps: an index, one sitemap per kind of page, styled tables
This commit is contained in:
@@ -0,0 +1,83 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Shows the sitemap index and each sitemap as a plain table in the browser. Search engines read the XML and ignore this file. -->
|
||||||
|
<xsl:stylesheet version="1.0"
|
||||||
|
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
|
||||||
|
xmlns:sm="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||||
|
xmlns:xhtml="http://www.w3.org/1999/xhtml"
|
||||||
|
exclude-result-prefixes="sm xhtml">
|
||||||
|
<xsl:output method="html" encoding="UTF-8" indent="yes"/>
|
||||||
|
|
||||||
|
<xsl:template match="/">
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
|
<meta name="robots" content="noindex, follow"/>
|
||||||
|
<title>XML Sitemap</title>
|
||||||
|
<style>
|
||||||
|
body { margin:0; background:#fff; color:#444; font:13px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }
|
||||||
|
#content { max-width:1000px; margin:0 auto; padding:24px 16px 48px; }
|
||||||
|
h1 { margin:0 0 8px; font-size:24px; font-weight:600; color:#222; }
|
||||||
|
p.desc { margin:0 0 4px; color:#555; }
|
||||||
|
p.desc a { color:#0073aa; }
|
||||||
|
p.count { margin:16px 0 12px; color:#555; }
|
||||||
|
table { width:100%; border-collapse:collapse; border:1px solid #e5e5e5; }
|
||||||
|
th { padding:8px 10px; background:#0073aa; color:#fff; text-align:left; font-weight:600; }
|
||||||
|
td { padding:7px 10px; border-bottom:1px solid #eee; vertical-align:top; }
|
||||||
|
tr:nth-child(even) td { background:#f6f8fa; }
|
||||||
|
td a { color:#0073aa; text-decoration:none; word-break:break-all; }
|
||||||
|
td a:hover { text-decoration:underline; }
|
||||||
|
td.n, th.n { width:40px; color:#888; }
|
||||||
|
td.date { width:160px; white-space:nowrap; }
|
||||||
|
td.langs a { margin-right:6px; text-transform:uppercase; font-size:11px; font-weight:600; }
|
||||||
|
@media (max-width:640px) { .hide-sm { display:none; } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="content">
|
||||||
|
<h1>XML Sitemap</h1>
|
||||||
|
<p class="desc">This is an XML Sitemap, meant for consumption by search engines.</p>
|
||||||
|
<p class="desc">You can find more information about XML sitemaps on <a href="https://www.sitemaps.org/" rel="noopener">sitemaps.org</a>.</p>
|
||||||
|
<xsl:apply-templates select="sm:sitemapindex | sm:urlset"/>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<xsl:template match="sm:sitemapindex">
|
||||||
|
<p class="count">This XML Sitemap Index file contains <strong><xsl:value-of select="count(sm:sitemap)"/></strong> sitemaps.</p>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th class="n">#</th><th>Sitemap</th><th>Last Modified</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<xsl:for-each select="sm:sitemap">
|
||||||
|
<tr>
|
||||||
|
<td class="n"><xsl:value-of select="position()"/></td>
|
||||||
|
<td><a href="{sm:loc}"><xsl:value-of select="sm:loc"/></a></td>
|
||||||
|
<td class="date"><xsl:value-of select="sm:lastmod"/></td>
|
||||||
|
</tr>
|
||||||
|
</xsl:for-each>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<xsl:template match="sm:urlset">
|
||||||
|
<p class="count">This XML Sitemap contains <strong><xsl:value-of select="count(sm:url)"/></strong> URLs. <a href="/sitemap.xml">← Sitemap Index</a></p>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th class="n">#</th><th>URL</th><th class="hide-sm">Languages</th><th>Last Mod.</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<xsl:for-each select="sm:url">
|
||||||
|
<tr>
|
||||||
|
<td class="n"><xsl:value-of select="position()"/></td>
|
||||||
|
<td><a href="{sm:loc}"><xsl:value-of select="sm:loc"/></a></td>
|
||||||
|
<td class="langs hide-sm">
|
||||||
|
<xsl:for-each select="xhtml:link[@hreflang != 'x-default'][@href != current()/sm:loc]">
|
||||||
|
<a href="{@href}"><xsl:value-of select="@hreflang"/></a>
|
||||||
|
</xsl:for-each>
|
||||||
|
</td>
|
||||||
|
<td class="date"><xsl:value-of select="sm:lastmod"/></td>
|
||||||
|
</tr>
|
||||||
|
</xsl:for-each>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</xsl:template>
|
||||||
|
</xsl:stylesheet>
|
||||||
@@ -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<Metadata> {
|
|
||||||
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 (
|
|
||||||
<section className="section sitemap" aria-labelledby="page-title">
|
|
||||||
<div className="wrap">
|
|
||||||
<nav className="crumbs" aria-label={t.crumbs.label}>
|
|
||||||
<span>
|
|
||||||
<Link href={localize("/", locale)}>{t.crumbs.home}</Link>
|
|
||||||
<i aria-hidden="true">/</i>
|
|
||||||
</span>
|
|
||||||
<span aria-current="page">{t.sitemap.title}</span>
|
|
||||||
</nav>
|
|
||||||
<p className="eyebrow">{t.sitemap.title}</p>
|
|
||||||
<h1 id="page-title" className="display h2">
|
|
||||||
{t.sitemap.title}
|
|
||||||
</h1>
|
|
||||||
<p className="lead muted sitemap-intro">{t.sitemap.intro}</p>
|
|
||||||
<div className="sitemap-grid">
|
|
||||||
{groups.map((g) => (
|
|
||||||
<div key={g.title} className="sitemap-group">
|
|
||||||
<h2>
|
|
||||||
{g.title} <span>{String(g.links.length).padStart(2, "0")}</span>
|
|
||||||
</h2>
|
|
||||||
<ul>
|
|
||||||
{g.links.map((l) => (
|
|
||||||
<li key={l.href}>
|
|
||||||
<Link href={l.href} aria-current={l.href === localize("/sitemap", locale) ? "page" : undefined}>
|
|
||||||
{l.label}
|
|
||||||
<span className="arrow">→</span>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -2980,93 +2980,3 @@ html[lang="ar"] .nav a {
|
|||||||
align-items: flex-start;
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { sitemap } from "@/lib/sitemap";
|
||||||
|
|
||||||
|
export const GET = () => sitemap("industry");
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { sitemap } from "@/lib/sitemap";
|
||||||
|
|
||||||
|
export const GET = () => sitemap("page");
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { sitemap } from "@/lib/sitemap";
|
||||||
|
|
||||||
|
export const GET = () => sitemap("service");
|
||||||
@@ -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) },
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { sitemapIndex } from "@/lib/sitemap";
|
||||||
|
|
||||||
|
export const GET = () => sitemapIndex();
|
||||||
@@ -50,7 +50,6 @@ export default async function SiteFooter() {
|
|||||||
{ label: t.nav.allServices, href: localize("/services", locale) },
|
{ label: t.nav.allServices, href: localize("/services", locale) },
|
||||||
{ label: t.nav.allIndustries, href: localize("/industries", locale) },
|
{ label: t.nav.allIndustries, href: localize("/industries", locale) },
|
||||||
{ label: t.nav.contact, href: localize("/contact", locale) },
|
{ label: t.nav.contact, href: localize("/contact", locale) },
|
||||||
{ label: t.sitemap.nav, href: localize("/sitemap", locale) },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -53,15 +53,6 @@ export const ar: UI = {
|
|||||||
rights: "© 2026 AI Growth Technologies. جميع الحقوق محفوظة.",
|
rights: "© 2026 AI Growth Technologies. جميع الحقوق محفوظة.",
|
||||||
cities: "دبي · هيوستن · ميامي · نيويورك",
|
cities: "دبي · هيوستن · ميامي · نيويورك",
|
||||||
},
|
},
|
||||||
sitemap: {
|
|
||||||
nav: "خريطة الموقع",
|
|
||||||
title: "خريطة الموقع",
|
|
||||||
metaTitle: "خريطة الموقع | AI Growth Technologies",
|
|
||||||
intro: "جميع صفحات الموقع في مكان واحد، مرتبة حسب القسم.",
|
|
||||||
company: "الشركة",
|
|
||||||
home: "الرئيسية",
|
|
||||||
languages: "اللغات",
|
|
||||||
},
|
|
||||||
crumbs: { label: "مسار التنقل", home: "الرئيسية" },
|
crumbs: { label: "مسار التنقل", home: "الرئيسية" },
|
||||||
faqTitle: "الأسئلة الشائعة",
|
faqTitle: "الأسئلة الشائعة",
|
||||||
office: { phone: "الهاتف", email: "البريد الإلكتروني" },
|
office: { phone: "الهاتف", email: "البريد الإلكتروني" },
|
||||||
|
|||||||
@@ -53,15 +53,6 @@ export const en = {
|
|||||||
rights: "© 2026 AI Growth Technologies. All rights reserved.",
|
rights: "© 2026 AI Growth Technologies. All rights reserved.",
|
||||||
cities: "Dubai · Houston · Miami · New York",
|
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" },
|
crumbs: { label: "Breadcrumb", home: "Home" },
|
||||||
faqTitle: "Frequently Asked Questions",
|
faqTitle: "Frequently Asked Questions",
|
||||||
office: { phone: "Phone", email: "Email" },
|
office: { phone: "Phone", email: "Email" },
|
||||||
|
|||||||
@@ -53,15 +53,6 @@ export const es: UI = {
|
|||||||
rights: "© 2026 AI Growth Technologies. Todos los derechos reservados.",
|
rights: "© 2026 AI Growth Technologies. Todos los derechos reservados.",
|
||||||
cities: "Dubái · Houston · Miami · Nueva York",
|
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" },
|
crumbs: { label: "Ruta de navegación", home: "Inicio" },
|
||||||
faqTitle: "Preguntas frecuentes",
|
faqTitle: "Preguntas frecuentes",
|
||||||
office: { phone: "Teléfono", email: "Correo" },
|
office: { phone: "Teléfono", email: "Correo" },
|
||||||
|
|||||||
@@ -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 = `<?xml version="1.0" encoding="UTF-8"?>\n<?xml-stylesheet type="text/xsl" href="/sitemap-style.xml"?>\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) => ` <sitemap>
|
||||||
|
<loc>${SITE_URL}/${name}-sitemap.xml</loc>
|
||||||
|
<lastmod>${lastmod}</lastmod>
|
||||||
|
</sitemap>`,
|
||||||
|
);
|
||||||
|
return xml(`<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
${entries.join("\n")}
|
||||||
|
</sitemapindex>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 <xhtml:link rel="alternate" hreflang="${l}" href="${url(href)}"/>`)
|
||||||
|
.join("");
|
||||||
|
const entries = locales.flatMap((locale) =>
|
||||||
|
sitemaps[name].map(
|
||||||
|
(p) => ` <url>
|
||||||
|
<loc>${url(localize(p, locale))}</loc>
|
||||||
|
<lastmod>${lastmod}</lastmod>${alternates(p)}
|
||||||
|
</url>`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return xml(`<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">
|
||||||
|
${entries.join("\n")}
|
||||||
|
</urlset>
|
||||||
|
`);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user