Add Spanish and Arabic versions, a contact form captcha and a multilingual sitemap

- English, Spanish (/es) and Arabic (/ar, right to left) versions of every page, with a language switch
- Self-hosted "I'm not a robot" check on the contact form; the message field is optional
- Google Tag Manager, GA4 and Search Console verification in the shared layout
- Sitemap, robots.txt and canonical links point at aigrowthtechnologies.com
This commit is contained in:
Alejandro Veras
2026-10-13 11:53:18 -04:00
parent e6d06d6fd0
commit 828a81f3a5
105 changed files with 21097 additions and 788 deletions
+51 -54
View File
@@ -1,28 +1,11 @@
import about from "@/content/pages/about.json";
import contact from "@/content/pages/contact.json";
import services from "@/content/pages/services.json";
import industries from "@/content/pages/industries.json";
import paidMedia from "@/content/pages/services_paid-media.json";
import seo from "@/content/pages/services_seo.json";
import llmo from "@/content/pages/services_llmo.json";
import webDevelopment from "@/content/pages/services_web-development.json";
import socialMedia from "@/content/pages/services_social-media.json";
import brand from "@/content/pages/services_brand.json";
import consulting from "@/content/pages/services_consulting.json";
import cybersecurity from "@/content/pages/services_cybersecurity.json";
import hospitality from "@/content/pages/industries_hospitality.json";
import medicalDental from "@/content/pages/industries_medical-dental.json";
import automotive from "@/content/pages/industries_automotive.json";
import ecommerce from "@/content/pages/industries_e-commerce.json";
import homeServices from "@/content/pages/industries_home-services.json";
import b2b from "@/content/pages/industries_b2b.json";
import creators from "@/content/pages/industries_creators.json";
import { industryNav, serviceNav } from "./routes";
import { content } from "@/content/pages";
import { localize, type Locale } from "@/i18n/config";
import { hrefFor, industryNav, serviceNav } from "./routes";
export type SectionKind = "text" | "cards" | "steps" | "faq" | "links" | "stats" | "offices" | "showcase";
export type Item = {
/** A page as stored in the JSON files: CTAs are plain labels from the copy deck. */
type RawItem = {
kicker: string | null;
title: string;
subtitle: string | null;
@@ -32,55 +15,69 @@ export type Item = {
num?: string;
};
export type Section = {
type RawSection = {
eyebrow: string | null;
title: string | null;
intro: string[];
items: Item[];
items: RawItem[];
outro: string[];
ctas: string[];
form: boolean;
kind: SectionKind | string;
};
export type Page = {
type RawPage = {
slug: string;
path: string;
meta: { title: string | null; description: string | null };
hero: RawSection;
sections: RawSection[];
};
/** A page ready to render: every CTA carries its localized label and destination. */
export type Cta = { label: string; href: string };
export type Item = Omit<RawItem, "cta"> & { cta: Cta | null };
export type Section = Omit<RawSection, "ctas" | "items"> & { ctas: Cta[]; items: Item[] };
export type Page = Omit<RawPage, "hero" | "sections"> & {
locale: Locale;
hero: Section;
sections: Section[];
};
const serviceContent: Record<string, unknown> = {
"paid-media": paidMedia,
seo,
llmo,
"web-development": webDevelopment,
"social-media": socialMedia,
brand,
consulting,
cybersecurity,
};
type Key = keyof (typeof content)["en"];
const industryContent: Record<string, unknown> = {
hospitality,
"medical-dental": medicalDental,
automotive,
"e-commerce": ecommerce,
"home-services": homeServices,
b2b,
creators,
};
export const pages = { about, contact, services, industries } as unknown as Record<
"about" | "contact" | "services" | "industries",
Page
>;
export function getService(slug: string): Page | undefined {
return serviceNav.some((s) => s.slug === slug) ? (serviceContent[slug] as Page) : undefined;
// Destinations come from the English labels, which the route map understands; translated pages
// share the English structure, so the same positions line up.
function resolveSection(s: RawSection, en: RawSection, path: string, locale: Locale): Section {
const to = (label: string) => localize(hrefFor(label, path), locale);
return {
...s,
ctas: s.ctas.map((label, i) => ({ label, href: to(en.ctas[i] ?? label) })),
items: s.items.map((it, i) => ({
...it,
cta: it.cta ? { label: it.cta, href: to(en.items[i]?.cta ?? it.cta) } : null,
})),
};
}
export function getIndustry(slug: string): Page | undefined {
return industryNav.some((s) => s.slug === slug) ? (industryContent[slug] as Page) : undefined;
function getPage(locale: Locale, key: Key): Page {
const raw = content[locale][key] as RawPage;
const en = content.en[key] as RawPage;
return {
...raw,
locale,
hero: resolveSection(raw.hero, en.hero, en.path, locale),
sections: raw.sections.map((s, i) => resolveSection(s, en.sections[i], en.path, locale)),
};
}
export const getStaticPage = (locale: Locale, key: "about" | "contact" | "services" | "industries") =>
getPage(locale, key);
export function getService(locale: Locale, slug: string): Page | undefined {
return serviceNav.some((s) => s.slug === slug) ? getPage(locale, `services_${slug}` as Key) : undefined;
}
export function getIndustry(locale: Locale, slug: string): Page | undefined {
return industryNav.some((s) => s.slug === slug) ? getPage(locale, `industries_${slug}` as Key) : undefined;
}