Merge pull request 'feat(seo): fix titles, canonicals, share tags and soft 404s' (#1) from seo-batch into main
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
@@ -7,12 +7,14 @@ import { WhyUs } from '@/components/robotics/WhyUs';
|
||||
import { FloatingTechPanel } from '@/components/robotics/FloatingTechPanel';
|
||||
import { CompanyStory } from '@/components/robotics/CompanyStory';
|
||||
import { FounderSection } from '@/components/robotics/FounderSection';
|
||||
import { pageMetadata } from '@/lib/seo';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'About YS Lootah Robotics UAE Destination for Unitree & Pudu Robotics',
|
||||
export const metadata: Metadata = pageMetadata({
|
||||
title: 'About Us · UAE Destination for Unitree & Pudu Robotics',
|
||||
description:
|
||||
'YS Lootah Robotics is part of the Yousuf Saeed Lootah Investment Group a trusted UAE robotics partner delivering AI, automation, and intelligent robotics across Dubai and the UAE.',
|
||||
};
|
||||
path: '/about/',
|
||||
});
|
||||
|
||||
export default function AboutPage() {
|
||||
return (
|
||||
|
||||
@@ -10,9 +10,14 @@ import { FooterAndContact } from '@/components/FooterAndContact';
|
||||
import { MotionSection } from '@/components/ui/MotionSection';
|
||||
import { CTAButton } from '@/components/ui/CTAButton';
|
||||
import { ACCESSORIES, GROUP_LABELS, getAccessoryBySlug, type Accessory } from '@/data/accessories';
|
||||
import { displayName, pageMetadata } from '@/lib/seo';
|
||||
|
||||
type Params = { slug: string };
|
||||
|
||||
// Only the slugs below exist. Anything else is a real 404 from the router, not a
|
||||
// page that renders "not found" in the browser with status 200.
|
||||
export const dynamicParams = false;
|
||||
|
||||
export function generateStaticParams() {
|
||||
return ACCESSORIES.map((a) => ({ slug: a.slug }));
|
||||
}
|
||||
@@ -20,18 +25,12 @@ export function generateStaticParams() {
|
||||
export async function generateMetadata({ params }: { params: Promise<Params> }): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const a = getAccessoryBySlug(slug);
|
||||
if (!a) return { title: 'Accessory not found · YS Lootah Robotics' };
|
||||
return {
|
||||
title: `${a.brandLabel} ${a.name} · Available in UAE | YS Lootah Robotics`,
|
||||
if (!a) return { title: 'Accessory not found' };
|
||||
return pageMetadata({
|
||||
title: `${displayName(a.brandLabel, a.name)} · Available in UAE`,
|
||||
description: `${a.tagline} ${a.description} Available through YS Lootah Robotics in Dubai.`,
|
||||
alternates: { canonical: `/accessories/${a.slug}/` },
|
||||
openGraph: {
|
||||
title: `${a.brandLabel} ${a.name}`,
|
||||
description: a.tagline,
|
||||
url: `/accessories/${a.slug}/`,
|
||||
type: 'website',
|
||||
},
|
||||
};
|
||||
path: `/accessories/${a.slug}`,
|
||||
});
|
||||
}
|
||||
|
||||
export default async function AccessoryDetailPage({ params }: { params: Promise<Params> }) {
|
||||
|
||||
@@ -5,13 +5,14 @@ import { Navbar } from '@/components/Navbar';
|
||||
import { FooterAndContact } from '@/components/FooterAndContact';
|
||||
import { AccessoriesShowcase } from '@/components/robotics/AccessoriesShowcase';
|
||||
import { ACCESSORIES } from '@/data/accessories';
|
||||
import { pageMetadata } from '@/lib/seo';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Robotics Accessories · Dexterous Hands, Arms, Sensors, IoT | YS Lootah Robotics',
|
||||
export const metadata: Metadata = pageMetadata({
|
||||
title: 'Robotics Accessories · Dexterous Hands, Arms, Sensors, IoT',
|
||||
description:
|
||||
'Browse Unitree dexterous hands, robotic arms, perception sensors, and Pudu dispatch, docking, modular attachments, and building IoT modules — available in the UAE through YS Lootah Robotics.',
|
||||
alternates: { canonical: '/accessories/' },
|
||||
};
|
||||
path: '/accessories/',
|
||||
});
|
||||
|
||||
export default function AccessoriesIndexPage() {
|
||||
return (
|
||||
|
||||
@@ -2,12 +2,14 @@ import type { Metadata } from 'next';
|
||||
import { Navbar } from '@/components/Navbar';
|
||||
import { FooterAndContact } from '@/components/FooterAndContact';
|
||||
import { InquiryForm } from '@/components/robotics/InquiryForm';
|
||||
import { pageMetadata } from '@/lib/seo';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Book a Robotics Demo in Dubai YS Lootah Robotics',
|
||||
export const metadata: Metadata = pageMetadata({
|
||||
title: 'Book a Robotics Demo in Dubai',
|
||||
description:
|
||||
'Book a live robot demo at our Dubai showroom or at your venue. See selected Unitree and Pudu Robotics solutions in action, available in the UAE through YS Lootah Robotics.',
|
||||
};
|
||||
path: '/book-demo/',
|
||||
});
|
||||
|
||||
export default function BookDemoPage() {
|
||||
return (
|
||||
|
||||
@@ -5,12 +5,14 @@ import { BRANDS, ROBOTS, type RobotBrand } from '@/data/robots';
|
||||
import { RobotProductCard } from '@/components/robotics/RobotProductCard';
|
||||
import { MotionSection } from '@/components/ui/MotionSection';
|
||||
import { DemoCTA } from '@/components/robotics/DemoCTA';
|
||||
import { pageMetadata } from '@/lib/seo';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Robotics Brands Unitree & Pudu | UAE Access via YS Lootah Robotics',
|
||||
export const metadata: Metadata = pageMetadata({
|
||||
title: 'Robotics Brands in the UAE · Unitree & Pudu',
|
||||
description:
|
||||
'Selected Unitree and Pudu Robotics solutions available in the UAE through YS Lootah Robotics. Humanoid, quadruped, service, delivery, and cleaning robots.',
|
||||
};
|
||||
path: '/brands/',
|
||||
});
|
||||
|
||||
const ORDER: RobotBrand[] = ['unitree', 'pudu'];
|
||||
|
||||
|
||||
@@ -23,22 +23,17 @@ import {
|
||||
} from '@/data/bu-sunaidah';
|
||||
import { BU_SUNAIDAH_PRESS } from '@/data/bu-sunaidah-press';
|
||||
import { getLatestBuSunaidahPosts } from '@/lib/bu-sunaidah-fetch';
|
||||
import { pageMetadata } from '@/lib/seo';
|
||||
|
||||
// Re-fetch latest Instagram posts every 30 minutes on the server.
|
||||
export const revalidate = 1800;
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Bu Sunaidah · Emirati Robotics Persona | YS Lootah Robotics',
|
||||
export const metadata: Metadata = pageMetadata({
|
||||
title: 'Bu Sunaidah · Emirati Robotics Persona',
|
||||
description:
|
||||
'Meet Bu Sunaidah — the Emirati culture-inspired robotics character from YS Lootah Robotics. Media activations, exhibitions, and community storytelling across the UAE.',
|
||||
alternates: { canonical: '/bu-sunaidah/' },
|
||||
openGraph: {
|
||||
title: 'Bu Sunaidah · Emirati Robotics Persona',
|
||||
description: 'Emirati culture-inspired robotics character — UAE media, exhibitions, community.',
|
||||
url: '/bu-sunaidah/',
|
||||
type: 'website',
|
||||
},
|
||||
};
|
||||
path: '/bu-sunaidah/',
|
||||
});
|
||||
|
||||
const PILLARS = [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { pageMetadata } from '@/lib/seo';
|
||||
|
||||
// The page itself is a client component and cannot export metadata.
|
||||
export const metadata: Metadata = pageMetadata({
|
||||
title: 'Unitree G1 3D Configurator',
|
||||
description:
|
||||
'Build your Unitree G1 in 3D. Choose persona, attire, colours and accessories, preview it live, then request a UAE quotation from YS Lootah Robotics.',
|
||||
path: '/configure/',
|
||||
});
|
||||
|
||||
export default function ConfigureLayout({ children }: { children: React.ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -2,12 +2,14 @@ import type { Metadata } from 'next';
|
||||
import { Navbar } from '@/components/Navbar';
|
||||
import { FooterAndContact } from '@/components/FooterAndContact';
|
||||
import { InquiryForm } from '@/components/robotics/InquiryForm';
|
||||
import { pageMetadata } from '@/lib/seo';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Contact YS Lootah Robotics Dubai Robotics Sales & Demo Inquiries',
|
||||
export const metadata: Metadata = pageMetadata({
|
||||
title: 'Contact · Dubai Robotics Sales & Demo Inquiries',
|
||||
description:
|
||||
'Dubai robotics sales, support, and demo inquiries. Contact YS Lootah Robotics, the UAE destination for selected Unitree and Pudu Robotics solutions, by phone, email, or WhatsApp.',
|
||||
};
|
||||
path: '/contact/',
|
||||
});
|
||||
|
||||
export default function ContactPage() {
|
||||
return (
|
||||
|
||||
@@ -26,6 +26,7 @@ import { MotionSection } from '@/components/ui/MotionSection';
|
||||
import { CTAButton } from '@/components/ui/CTAButton';
|
||||
import { INDUSTRIES, getIndustryBySlug } from '@/data/industries';
|
||||
import { INDUSTRY_PAGES, type IndustrySolution } from '@/data/industry-pages';
|
||||
import { pageMetadata } from '@/lib/seo';
|
||||
|
||||
type Params = { slug: string };
|
||||
|
||||
@@ -44,6 +45,10 @@ const ICONS: Record<IndustrySolution['iconKey'], React.ComponentType<{ size?: nu
|
||||
tray: UtensilsCrossed,
|
||||
};
|
||||
|
||||
// Only the slugs below exist. Anything else is a real 404 from the router, not a
|
||||
// page that renders "not found" in the browser with status 200.
|
||||
export const dynamicParams = false;
|
||||
|
||||
export function generateStaticParams() {
|
||||
return INDUSTRIES.map((i) => ({ slug: i.slug }));
|
||||
}
|
||||
@@ -54,14 +59,13 @@ export async function generateMetadata({ params }: { params: Promise<Params> }):
|
||||
if (!industry) return {};
|
||||
const content = INDUSTRY_PAGES[industry.id];
|
||||
if (!content) return {};
|
||||
const url = `/industries/${industry.slug}/`;
|
||||
return {
|
||||
return pageMetadata({
|
||||
title: content.seoTitle,
|
||||
description: content.seoDescription,
|
||||
alternates: { canonical: url },
|
||||
openGraph: { title: content.seoTitle, description: content.seoDescription, url, type: 'website' },
|
||||
twitter: { card: 'summary_large_image', title: content.seoTitle, description: content.seoDescription },
|
||||
};
|
||||
path: `/industries/${industry.slug}`,
|
||||
image: industry.image,
|
||||
imageAlt: industry.imageAlt,
|
||||
});
|
||||
}
|
||||
|
||||
const TRUST_DEFAULT = [
|
||||
|
||||
@@ -9,12 +9,14 @@ import { FooterAndContact } from '@/components/FooterAndContact';
|
||||
import { IndustryUseCases } from '@/components/robotics/IndustryUseCases';
|
||||
import { DemoCTA } from '@/components/robotics/DemoCTA';
|
||||
import { MotionSection } from '@/components/ui/MotionSection';
|
||||
import { pageMetadata } from '@/lib/seo';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Robotics for UAE Industries YS Lootah Robotics Dubai',
|
||||
export const metadata: Metadata = pageMetadata({
|
||||
title: 'Robotics for UAE Industries',
|
||||
description:
|
||||
'Robotics solutions for hospitality, restaurants, hotels, healthcare, education, security, warehouses, smart buildings, and government across the UAE.',
|
||||
};
|
||||
'Robotics solutions for hospitality, restaurants, hotels, healthcare, education, security, and warehouses across the UAE.',
|
||||
path: '/industries/',
|
||||
});
|
||||
|
||||
const HERO_COLLAGE: { src: string; alt: string }[] = [
|
||||
{ src: '/industries/cards/hospitality.webp', alt: 'Service robot in a luxury hospitality lobby' },
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { Metadata } from 'next';
|
||||
import Link from 'next/link';
|
||||
import { Navbar } from '@/components/Navbar';
|
||||
import { FooterAndContact } from '@/components/FooterAndContact';
|
||||
|
||||
// Next adds the noindex tag to not-found responses itself.
|
||||
export const metadata: Metadata = {
|
||||
title: 'Page not found',
|
||||
};
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<main style={{ paddingTop: 'clamp(8rem, 14vw, 11rem)', paddingBottom: 'clamp(5rem, 10vw, 8rem)' }}>
|
||||
<div
|
||||
className="container-wide"
|
||||
style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '1.5rem', textAlign: 'center' }}
|
||||
>
|
||||
<span className="eyebrow">Error 404</span>
|
||||
<h1 style={{ fontSize: 'clamp(2.2rem, 6vw, 4rem)', fontWeight: 600, letterSpacing: '-0.03em', lineHeight: 1.05, color: '#f5f5f5', maxWidth: '16ch' }}>
|
||||
This page could not be found.
|
||||
</h1>
|
||||
<p style={{ color: '#a8a8a8', maxWidth: 520, lineHeight: 1.7 }}>
|
||||
The address may have changed or the product may no longer be listed. Browse the catalog or
|
||||
get in touch and we will point you to the right robot.
|
||||
</p>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'center', gap: '0.75rem 1.5rem', marginTop: '0.5rem' }}>
|
||||
<Link href="/robots/" style={{ color: '#f5f5f5' }}>Robots</Link>
|
||||
<Link href="/accessories/" style={{ color: '#f5f5f5' }}>Accessories</Link>
|
||||
<Link href="/industries/" style={{ color: '#f5f5f5' }}>Industries</Link>
|
||||
<Link href="/contact/" style={{ color: '#f5f5f5' }}>Contact</Link>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<FooterAndContact />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,15 @@
|
||||
import type { Metadata } from 'next';
|
||||
import React from 'react';
|
||||
import { Navbar } from '@/components/Navbar';
|
||||
import { FooterAndContact } from '@/components/FooterAndContact';
|
||||
import { pageMetadata } from '@/lib/seo';
|
||||
|
||||
export const metadata: Metadata = pageMetadata({
|
||||
title: 'Privacy Policy',
|
||||
description:
|
||||
'How YS Lootah Robotics collects, uses and protects personal information on this website and in enquiries, demo bookings and orders.',
|
||||
path: '/privacy-policy/',
|
||||
});
|
||||
|
||||
export default function PrivacyPolicyPage() {
|
||||
return (
|
||||
|
||||
@@ -12,10 +12,14 @@ import { ProductGallery } from '@/components/robotics/ProductGallery';
|
||||
import { MotionSection } from '@/components/ui/MotionSection';
|
||||
import { ROBOTS, getRobotBySlug, BRANDS, CATEGORY_LABELS } from '@/data/robots';
|
||||
import JsonLd from '@/components/JsonLd';
|
||||
import { canonical, abs, productJsonLd, breadcrumbJsonLd } from '@/lib/seo';
|
||||
import { displayName, pageMetadata, productJsonLd, breadcrumbJsonLd } from '@/lib/seo';
|
||||
|
||||
type Params = { slug: string };
|
||||
|
||||
// Only the slugs below exist. Anything else is a real 404 from the router, not a
|
||||
// page that renders "not found" in the browser with status 200.
|
||||
export const dynamicParams = false;
|
||||
|
||||
export function generateStaticParams() {
|
||||
return ROBOTS.map((r) => ({ slug: r.slug }));
|
||||
}
|
||||
@@ -23,18 +27,17 @@ export function generateStaticParams() {
|
||||
export async function generateMetadata({ params }: { params: Promise<Params> }): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const robot = getRobotBySlug(slug);
|
||||
if (!robot) return { title: 'Robot not found YS Lootah Robotics' };
|
||||
const url = canonical(`/robots/${robot.slug}`);
|
||||
const img = abs(robot.image);
|
||||
const title = `${robot.brandLabel} ${robot.name} Available in Dubai | YS Lootah Robotics`;
|
||||
const description = `${robot.shortDescription} Available through YS Lootah Robotics in Dubai request a price or book a live demo.`;
|
||||
if (!robot) return { title: 'Robot not found' };
|
||||
const name = displayName(robot.brandLabel, robot.name);
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
...pageMetadata({
|
||||
title: `${name} Available in Dubai`,
|
||||
description: `${robot.shortDescription} Available through YS Lootah Robotics in Dubai request a price or book a live demo.`,
|
||||
path: `/robots/${robot.slug}`,
|
||||
image: robot.image,
|
||||
imageAlt: name,
|
||||
}),
|
||||
keywords: [robot.brandLabel, robot.name, 'Dubai', 'UAE', CATEGORY_LABELS[robot.category], 'robotics'],
|
||||
alternates: { canonical: url },
|
||||
openGraph: { type: 'website', url, title, description, images: [{ url: img, alt: `${robot.brandLabel} ${robot.name}` }] },
|
||||
twitter: { card: 'summary_large_image', title, description, images: [img] },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -47,7 +50,7 @@ export default async function RobotDetailPage({ params }: { params: Promise<Para
|
||||
const related = ROBOTS.filter((r) => r.id !== robot.id && (r.brand === robot.brand || r.category === robot.category)).slice(0, 3);
|
||||
|
||||
const productLd = productJsonLd({
|
||||
name: `${robot.brandLabel} ${robot.name}`,
|
||||
name: displayName(robot.brandLabel, robot.name),
|
||||
description: robot.shortDescription,
|
||||
image: robot.image,
|
||||
brandName: brand.name,
|
||||
@@ -85,7 +88,7 @@ export default async function RobotDetailPage({ params }: { params: Promise<Para
|
||||
>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<ProductGallery
|
||||
images={[{ src: robot.image, alt: `${robot.brandLabel} ${robot.name}` }]}
|
||||
images={[{ src: robot.image, alt: displayName(robot.brandLabel, robot.name) }]}
|
||||
accent={robot.accent}
|
||||
/>
|
||||
{robot.imageType === 'placeholder' && (
|
||||
@@ -184,7 +187,7 @@ export default async function RobotDetailPage({ params }: { params: Promise<Para
|
||||
Tell us about your venue, timeline, and use case. We will respond with availability, configuration options, and pricing for the UAE.
|
||||
</p>
|
||||
</div>
|
||||
<InquiryForm defaultRobot={`${robot.brandLabel} ${robot.name}`} defaultCategory={robot.category} intent="quotation" />
|
||||
<InquiryForm defaultRobot={displayName(robot.brandLabel, robot.name)} defaultCategory={robot.category} intent="quotation" />
|
||||
</div>
|
||||
|
||||
<div className="card" style={{ padding: 'clamp(1.5rem, 4vw, 2.25rem)', display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||
|
||||
@@ -9,13 +9,14 @@ import { RoboticsSplineShowcase } from '@/components/sections/robotics-spline-sh
|
||||
import { CTAButton } from '@/components/ui/CTAButton';
|
||||
import { ROBOTS } from '@/data/robots';
|
||||
import { ACCESSORIES } from '@/data/accessories';
|
||||
import { pageMetadata } from '@/lib/seo';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Robots Catalog · Unitree & Pudu in UAE | YS Lootah Robotics',
|
||||
export const metadata: Metadata = pageMetadata({
|
||||
title: 'Robots Catalog · Unitree & Pudu in UAE',
|
||||
description:
|
||||
'Explore selected humanoid, quadruped, industrial, service, delivery, hospitality, and cleaning robots — plus dexterous hands, arms, sensors, and accessories — from Unitree and Pudu available in the UAE through YS Lootah Robotics.',
|
||||
alternates: { canonical: '/robots/' },
|
||||
};
|
||||
path: '/robots/',
|
||||
});
|
||||
|
||||
const unitreeCount = ROBOTS.filter((r) => r.brand === 'unitree').length;
|
||||
const puduCount = ROBOTS.filter((r) => r.brand === 'pudu').length;
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import type { Metadata } from 'next';
|
||||
import React from 'react';
|
||||
import { Navbar } from '@/components/Navbar';
|
||||
import { FooterAndContact } from '@/components/FooterAndContact';
|
||||
import { pageMetadata } from '@/lib/seo';
|
||||
|
||||
export const metadata: Metadata = pageMetadata({
|
||||
title: 'Terms of Service',
|
||||
description:
|
||||
'The terms that apply to using the YS Lootah Robotics website, requesting quotations, booking demos and ordering robots in the UAE.',
|
||||
path: '/terms-of-service/',
|
||||
});
|
||||
|
||||
export default function TermsOfServicePage() {
|
||||
return (
|
||||
|
||||
@@ -119,6 +119,12 @@ export const INDUSTRIES: Industry[] = [
|
||||
image: '/industries/cards/warehouses-logistics.webp',
|
||||
imageAlt: 'Industrial robot for UAE warehouses and logistics operations',
|
||||
},
|
||||
];
|
||||
|
||||
// Sectors with no detail page content yet (no entry in INDUSTRY_PAGES). They are
|
||||
// kept out of INDUSTRIES so no card, sitemap entry or route points at an empty
|
||||
// page. Move an entry back up once its content is written.
|
||||
export const UPCOMING_INDUSTRIES: Industry[] = [
|
||||
{
|
||||
id: 'events',
|
||||
slug: 'events',
|
||||
|
||||
@@ -54,6 +54,7 @@ export type IndustryPageContent = {
|
||||
suggestedRobots: SuggestedRobot[];
|
||||
ctaTitle?: string;
|
||||
ctaBody?: string;
|
||||
/** Without the brand: the layout title template appends it. */
|
||||
seoTitle: string;
|
||||
seoDescription: string;
|
||||
};
|
||||
@@ -141,7 +142,7 @@ export const INDUSTRY_PAGES: Record<string, IndustryPageContent> = {
|
||||
chips: ['Humanoid', 'Concierge', 'Premium'],
|
||||
},
|
||||
],
|
||||
seoTitle: 'Hospitality Robots in UAE | YS Lootah Robotics',
|
||||
seoTitle: 'Hospitality Robots in UAE',
|
||||
seoDescription:
|
||||
'Service, delivery, cleaning, and concierge robots for hotels, lounges, and hospitality venues across the UAE. Pudu and Unitree solutions available in the UAE through YS Lootah Robotics.',
|
||||
},
|
||||
@@ -223,7 +224,7 @@ export const INDUSTRY_PAGES: Record<string, IndustryPageContent> = {
|
||||
chips: ['Compact', 'Auto-doors', 'Multi-zone'],
|
||||
},
|
||||
],
|
||||
seoTitle: 'Restaurant & Café Robots in UAE | YS Lootah Robotics',
|
||||
seoTitle: 'Restaurant & Café Robots in UAE',
|
||||
seoDescription:
|
||||
'Delivery, host, and tray-runner robots for UAE restaurants, cafés, and F&B venues. Faster service, lower staff fatigue, premium positioning.',
|
||||
},
|
||||
@@ -305,7 +306,7 @@ export const INDUSTRY_PAGES: Record<string, IndustryPageContent> = {
|
||||
chips: ['Humanoid', 'Concierge', 'Activation'],
|
||||
},
|
||||
],
|
||||
seoTitle: 'Hotel & Resort Robots in UAE | YS Lootah Robotics',
|
||||
seoTitle: 'Hotel & Resort Robots in UAE',
|
||||
seoDescription:
|
||||
'Multi-floor delivery, cleaning, and humanoid concierge robots for hotels and resorts across the UAE. Local Dubai support and deployment.',
|
||||
},
|
||||
@@ -393,7 +394,7 @@ export const INDUSTRY_PAGES: Record<string, IndustryPageContent> = {
|
||||
chips: ['Humanoid', 'Activation', 'PR'],
|
||||
},
|
||||
],
|
||||
seoTitle: 'Shopping Mall Robots in UAE | YS Lootah Robotics',
|
||||
seoTitle: 'Shopping Mall Robots in UAE',
|
||||
seoDescription:
|
||||
'Cleaning, service, activation, and patrol robots for UAE shopping malls and retail flagships. Pudu and Unitree, deployed and supported locally.',
|
||||
},
|
||||
@@ -475,7 +476,7 @@ export const INDUSTRY_PAGES: Record<string, IndustryPageContent> = {
|
||||
chips: ['Greeter', 'Wayfinding', 'Ad screen'],
|
||||
},
|
||||
],
|
||||
seoTitle: 'Healthcare Robots in UAE | YS Lootah Robotics',
|
||||
seoTitle: 'Healthcare Robots in UAE',
|
||||
seoDescription:
|
||||
'Autonomous delivery, cleaning, and visitor-guidance robots for UAE clinics, hospitals, and healthcare networks. Local support from Dubai.',
|
||||
},
|
||||
@@ -584,7 +585,7 @@ export const INDUSTRY_PAGES: Record<string, IndustryPageContent> = {
|
||||
ctaTitle: 'Build a future-ready robotics program.',
|
||||
ctaBody:
|
||||
'Tell us about your school, university, or innovation lab. Our Dubai team will recommend the right robotics setup and arrange a demo or quotation.',
|
||||
seoTitle: 'Education & STEM Robots in UAE | YS Lootah Robotics',
|
||||
seoTitle: 'Education & STEM Robots in UAE',
|
||||
seoDescription:
|
||||
'Humanoid and quadruped robots for UAE schools, universities, and innovation labs. Hands-on AI and robotics education with local Dubai support.',
|
||||
},
|
||||
@@ -700,7 +701,7 @@ export const INDUSTRY_PAGES: Record<string, IndustryPageContent> = {
|
||||
chips: ['Thermal', 'LiDAR', 'AI vision'],
|
||||
},
|
||||
],
|
||||
seoTitle: 'Security & Surveillance Robots in UAE | YS Lootah Robotics',
|
||||
seoTitle: 'Security & Surveillance Robots in UAE',
|
||||
seoDescription:
|
||||
'Quadruped patrol, inspection, and perimeter robots for UAE facilities, compounds, and critical sites. Unitree platforms with local support.',
|
||||
},
|
||||
@@ -792,7 +793,7 @@ export const INDUSTRY_PAGES: Record<string, IndustryPageContent> = {
|
||||
chips: ['Thermal', 'LiDAR', 'AI'],
|
||||
},
|
||||
],
|
||||
seoTitle: 'Warehouse & Logistics Robots in UAE | YS Lootah Robotics',
|
||||
seoTitle: 'Warehouse & Logistics Robots in UAE',
|
||||
seoDescription:
|
||||
'Quadruped inspection, autonomous transport, and monitoring robots for UAE warehouses, logistics, and industrial sites. Local Dubai deployment.',
|
||||
},
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { ROBOTS } from '@/data/robots';
|
||||
import { ACCESSORIES } from '@/data/accessories';
|
||||
import { INDUSTRIES } from '@/data/industries';
|
||||
import { INDUSTRY_PAGES } from '@/data/industry-pages';
|
||||
import { canonical, displayName, pageMetadata, siteName } from './seo';
|
||||
|
||||
describe('displayName', () => {
|
||||
it('joins brand and name', () => {
|
||||
expect(displayName('Unitree', 'G1 Humanoid')).toBe('Unitree G1 Humanoid');
|
||||
});
|
||||
|
||||
it('does not repeat a brand the name already starts with', () => {
|
||||
expect(displayName('Pudu', 'PUDU CC1')).toBe('PUDU CC1');
|
||||
expect(displayName('Pudu', 'PuduBot 2')).toBe('PuduBot 2');
|
||||
});
|
||||
|
||||
it('never yields a doubled brand for any catalog item', () => {
|
||||
for (const item of [...ROBOTS, ...ACCESSORIES]) {
|
||||
const name = displayName(item.brandLabel, item.name).toLowerCase();
|
||||
const brand = item.brandLabel.toLowerCase();
|
||||
expect(name.startsWith(`${brand} ${brand}`), name).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('pageMetadata', () => {
|
||||
const meta = pageMetadata({ title: 'Robots Catalog', description: 'd', path: '/robots' });
|
||||
|
||||
it('leaves the brand out of the title, because the layout template appends it', () => {
|
||||
expect(meta.title).not.toContain(siteName);
|
||||
});
|
||||
|
||||
it('adds the brand once to share titles, which get no template', () => {
|
||||
expect(meta.openGraph.title).toBe(`Robots Catalog | ${siteName}`);
|
||||
expect(meta.twitter.title).toBe(meta.openGraph.title);
|
||||
});
|
||||
|
||||
it('points canonical and og:url at the page itself, with a trailing slash', () => {
|
||||
expect(meta.alternates.canonical).toBe(canonical('/robots/'));
|
||||
expect(meta.openGraph.url).toBe(meta.alternates.canonical);
|
||||
expect(meta.alternates.canonical.endsWith('/robots/')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('industries', () => {
|
||||
it('lists only sectors that have a detail page', () => {
|
||||
for (const industry of INDUSTRIES) {
|
||||
expect(INDUSTRY_PAGES[industry.id], industry.slug).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps the brand out of industry titles', () => {
|
||||
for (const page of Object.values(INDUSTRY_PAGES)) {
|
||||
expect(page.seoTitle).not.toContain(siteName);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -15,6 +15,45 @@ export const siteDescription =
|
||||
'YS Lootah Robotics is the UAE sales destination for selected Unitree, AgiBot and Pudu robotics. Explore, configure and book a demo of humanoid, quadruped, delivery and cleaning robots across Dubai and the UAE.';
|
||||
export const OG_IMAGE = abs('/og.jpg');
|
||||
|
||||
// "Pudu" + "PUDU CC1" would read "Pudu PUDU CC1": drop the label when the name already starts with it.
|
||||
export const displayName = (brandLabel: string, name: string) =>
|
||||
name.toLowerCase().startsWith(brandLabel.toLowerCase()) ? name : `${brandLabel} ${name}`;
|
||||
|
||||
type PageMeta = {
|
||||
/** Page title without the brand. The root layout template appends " | YS Lootah Robotics". */
|
||||
title: string;
|
||||
description: string;
|
||||
/** Path on this site: '/robots/', '/about/'. */
|
||||
path: string;
|
||||
/** Share image path or URL. Defaults to the site image. */
|
||||
image?: string;
|
||||
imageAlt?: string;
|
||||
};
|
||||
|
||||
// One place for title, canonical and share tags, so no page is left with the
|
||||
// homepage's og:url or without a canonical. The title template does not apply
|
||||
// to openGraph or twitter titles, so the brand is added to those here.
|
||||
export function pageMetadata({ title, description, path, image, imageAlt }: PageMeta) {
|
||||
const url = canonical(path);
|
||||
const fullTitle = `${title} | ${siteName}`;
|
||||
const img = image ? (image.startsWith('http') ? image : abs(image)) : OG_IMAGE;
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
alternates: { canonical: url },
|
||||
openGraph: {
|
||||
type: 'website' as const,
|
||||
url,
|
||||
title: fullTitle,
|
||||
description,
|
||||
siteName,
|
||||
locale: 'en_AE',
|
||||
images: [{ url: img, alt: imageAlt ?? siteName }],
|
||||
},
|
||||
twitter: { card: 'summary_large_image' as const, title: fullTitle, description, images: [img] },
|
||||
};
|
||||
}
|
||||
|
||||
export const business = {
|
||||
legalName: 'YS Lootah Robotics',
|
||||
parent: 'Yousuf Saeed Lootah Investment Group',
|
||||
|
||||
Reference in New Issue
Block a user