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;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
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.allIndustries, href: localize("/industries", 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. جميع الحقوق محفوظة.",
|
||||
cities: "دبي · هيوستن · ميامي · نيويورك",
|
||||
},
|
||||
sitemap: {
|
||||
nav: "خريطة الموقع",
|
||||
title: "خريطة الموقع",
|
||||
metaTitle: "خريطة الموقع | AI Growth Technologies",
|
||||
intro: "جميع صفحات الموقع في مكان واحد، مرتبة حسب القسم.",
|
||||
company: "الشركة",
|
||||
home: "الرئيسية",
|
||||
languages: "اللغات",
|
||||
},
|
||||
crumbs: { label: "مسار التنقل", home: "الرئيسية" },
|
||||
faqTitle: "الأسئلة الشائعة",
|
||||
office: { phone: "الهاتف", email: "البريد الإلكتروني" },
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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