7 Commits
Author SHA1 Message Date
Najjar\NajjarV02andClaude Opus 5.5 60d39e857a chore(contact): update office phone to +971 4 289 6095
CI/CD / test-and-build (push) Canceled after 0s
CI/CD / deploy (push) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 12:35:36 +04:00
mohammad 78aec0e08d Merge pull request 'fix(checkout): skip loadStripe when no publishable key is set' (#3) from seo-round-2 into main
CI/CD / test-and-build (push) Canceled after 0s
CI/CD / deploy (push) Canceled after 0s
Reviewed-on: #3
2026-10-19 13:11:41 +00:00
Najjar\NajjarV02andClaude Opus 5.5 81358b0815 fix(checkout): skip loadStripe when no publishable key is set
CI/CD / test-and-build (pull_request) Canceled after 0s
CI/CD / deploy (pull_request) Canceled after 0s
loadStripe(undefined) threw "Expected publishable key to be of type
string" on every page that imports CheckoutOverlay. Production sets the
key, so live behaviour is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:18:46 +04:00
mohammad 1d9d620d67 Merge pull request 'feat(seo): coming-soon industry pages and round-2 title fixes' (#2) from seo-round-2 into main
CI/CD / test-and-build (push) Canceled after 0s
CI/CD / deploy (push) Canceled after 0s
Reviewed-on: #2
2026-10-19 09:52:53 +00:00
Najjar\NajjarV02andClaude Opus 5.5 bcd7bd2c7b feat(seo): coming-soon industry pages and round-2 title fixes
CI/CD / test-and-build (pull_request) Canceled after 0s
CI/CD / deploy (pull_request) Canceled after 0s
- /industries/events, /smart-buildings and /government render a short
  coming-soon page with their own title, description and canonical, and
  are listed in the sitemap and on /industries.
- Homepage title: "Robot Shop and Configurator in the UAE", set as
  absolute so the template does not add the brand twice.
- /configure title: "Configure Your Robot and Request a UAE Quotation".
- Card and hero alt/aria text use displayName() (no "Pudu PUDU").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 13:03:48 +04:00
mohammad 38cc2025de Merge pull request 'feat(seo): fix titles, canonicals, share tags and soft 404s' (#1) from seo-batch into main
CI/CD / test-and-build (push) Canceled after 0s
CI/CD / deploy (push) Canceled after 0s
Reviewed-on: #1
2026-10-14 11:15:30 +00:00
Najjar\NajjarV02andClaude Opus 5.5 8382501598 feat(seo): fix titles, canonicals, share tags and soft 404s
CI/CD / test-and-build (pull_request) Canceled after 0s
CI/CD / deploy (pull_request) Canceled after 0s
- Titles carried the brand twice (page title plus layout template) and Pudu
  items read "Pudu PUDU". Pages now go through pageMetadata() and displayName().
- /configure, /privacy-policy and /terms-of-service reused the homepage title.
- Eight pages had no canonical and inherited the homepage og:url.
- Unknown /robots, /accessories and /industries slugs returned 200; they are
  now a real 404 (dynamicParams = false) with a not-found page.
- Events, Smart Buildings and Government have no page content yet and rendered
  not-found: moved to UPCOMING_INDUSTRIES until it is written.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 16:57:18 +04:00
28 changed files with 490 additions and 99 deletions
+1 -1
View File
@@ -29,6 +29,6 @@ Hospitality, restaurants and cafes, hotels and resorts, shopping malls, healthca
## Contact
- Email: info@yslootahrobotics.com
- Phone: +971 55 948 2728, +971 4 349 9319
- Phone: +971 55 948 2728, +971 4 289 6095
- WhatsApp: https://wa.me/971559482728
- Location: Office 408, City Bay Business Center, Dubai, United Arab Emirates
+5 -3
View File
@@ -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 -11
View File
@@ -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 -4
View File
@@ -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 (
+5 -3
View File
@@ -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 -3
View File
@@ -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'];
+5 -10
View File
@@ -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 = [
{
+14
View File
@@ -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: 'Configure Your Robot and Request a UAE Quotation',
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;
}
+6 -4
View File
@@ -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 (
@@ -27,7 +29,7 @@ export default function ContactPage() {
<div className="card" style={{ padding: 'clamp(1.5rem, 4vw, 2rem)', display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<ContactRow label="Phone" value="+971 55 948 2728" href="tel:+971559482728" />
<ContactRow label="Office" value="+971 4 349 9319" href="tel:+97143499319" />
<ContactRow label="Office" value="+971 4 289 6095" href="tel:+97142896095" />
<ContactRow label="Email" value="info@yslootahrobotics.com" href="mailto:info@yslootahrobotics.com" />
<ContactRow label="WhatsApp" value="+971 55 948 2728" href="https://wa.me/971559482728" external />
<ContactRow label="Address" value="Showroom No 03, Sheikh Zayed Rd Trade Center Second - DIFC - Dubai, UAE" href="https://maps.google.com/?q=Showroom+No+03+Sheikh+Zayed+Rd+Trade+Center+Second+DIFC+Dubai" external />
+180 -13
View File
@@ -24,8 +24,10 @@ import { Navbar } from '@/components/Navbar';
import { FooterAndContact } from '@/components/FooterAndContact';
import { MotionSection } from '@/components/ui/MotionSection';
import { CTAButton } from '@/components/ui/CTAButton';
import { INDUSTRIES, getIndustryBySlug } from '@/data/industries';
import { INDUSTRIES, UPCOMING_INDUSTRIES, getIndustryBySlug, getUpcomingIndustryBySlug, type Industry } from '@/data/industries';
import { ROBOTS } from '@/data/robots';
import { INDUSTRY_PAGES, type IndustrySolution } from '@/data/industry-pages';
import { displayName, pageMetadata } from '@/lib/seo';
type Params = { slug: string };
@@ -44,24 +46,37 @@ 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 }));
return [...INDUSTRIES, ...UPCOMING_INDUSTRIES].map((i) => ({ slug: i.slug }));
}
export async function generateMetadata({ params }: { params: Promise<Params> }): Promise<Metadata> {
const { slug } = await params;
const soon = getUpcomingIndustryBySlug(slug);
if (soon?.upcoming) {
return pageMetadata({
title: soon.upcoming.seoTitle,
description: soon.upcoming.seoDescription,
path: `/industries/${soon.slug}`,
image: soon.image,
imageAlt: soon.imageAlt,
});
}
const industry = getIndustryBySlug(slug);
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 = [
@@ -73,6 +88,8 @@ const TRUST_DEFAULT = [
export default async function IndustryDetailPage({ params }: { params: Promise<Params> }) {
const { slug } = await params;
const upcoming = getUpcomingIndustryBySlug(slug);
if (upcoming) return <ComingSoonIndustry industry={upcoming} />;
const industry = getIndustryBySlug(slug);
if (!industry) notFound();
const content = INDUSTRY_PAGES[industry.id];
@@ -437,7 +454,160 @@ export default async function IndustryDetailPage({ params }: { params: Promise<P
<FooterAndContact />
<style>{`
<style>{IP_STYLES}</style>
</>
);
}
// Short page for a sector whose full content is not written yet. Same layout
// and styles as the full page, built from the card copy and matching robots.
function ComingSoonIndustry({ industry }: { industry: Industry }) {
const accent = industry.accent;
const robots = ROBOTS.filter((r) => r.categories.some((c) => industry.matchCategories.includes(c))).slice(0, 6);
return (
<>
<Navbar />
<main className="ip-main">
<div className="container-wide ip-stack">
<nav aria-label="Breadcrumb" className="ip-breadcrumbs">
<ol>
<li><Link href="/">Home</Link></li>
<li aria-hidden><ChevronRight size={12} /></li>
<li><Link href="/industries/">Industries</Link></li>
<li aria-hidden><ChevronRight size={12} /></li>
<li aria-current="page">{industry.name}</li>
</ol>
</nav>
<MotionSection>
<section className="ip-hero" style={{ ['--acc' as string]: accent }}>
<div className="ip-hero-glow" aria-hidden />
<div className="ip-hero-glow-b" aria-hidden />
<div className="ip-hero-grid" aria-hidden />
<div className="ip-hero-inner">
<div className="ip-hero-copy">
<span className="eyebrow ip-hero-eyebrow">
<span className="ip-dot" />
Industry Solution · Coming soon
</span>
<h1 className="ip-hero-title">
<span className="text-gradient">Robots for {industry.name}</span>
</h1>
<p className="ip-hero-sub">
{industry.solution} A full guide for this sector is coming soon. Our Dubai team can already
advise on robots and arrange a demo.
</p>
<div className="ip-hero-actions">
<CTAButton href="/book-demo/" variant="primary" size="lg" arrow="up-right">
Book a Demo
</CTAButton>
<CTAButton href="/contact/" variant="secondary" size="lg" arrow="right">
Talk to Our Team
</CTAButton>
</div>
</div>
<aside className="ip-hero-visual">
<Image
src={industry.image}
alt={industry.imageAlt}
fill
sizes="(max-width: 920px) 100vw, 520px"
className="ip-hero-visual-img"
style={{ objectFit: 'cover', objectPosition: 'center' }}
priority
/>
<span className="ip-hero-visual-overlay" aria-hidden />
<div className="ip-hero-visual-meta">
<span className="ip-mono ip-live">
<span className="ip-live-dot" />Coming soon · UAE
</span>
<span className="ip-mono">Dubai Showroom · Live Demo</span>
</div>
</aside>
</div>
</section>
</MotionSection>
<MotionSection>
<section className="ip-block">
<header className="ip-block-head">
<span className="eyebrow">At a glance</span>
<h2 className="ip-block-title">What robots change for {industry.name.toLowerCase()}.</h2>
</header>
<ul className="ip-problems" role="list">
{[industry.problem, industry.solution, industry.benefit].map((p, i) => (
<li key={i} className="ip-problem">
<span className="ip-problem-num">{String(i + 1).padStart(2, '0')}</span>
<p>{p}</p>
</li>
))}
</ul>
</section>
</MotionSection>
{robots.length > 0 && (
<MotionSection>
<section className="ip-block" style={{ ['--acc' as string]: accent }}>
<header className="ip-block-head">
<span className="eyebrow">Suggested robots</span>
<h2 className="ip-block-title">Robots we would start with.</h2>
</header>
<div className="ip-robots">
{robots.map((r) => (
<article key={r.slug} className="ip-robot">
<div className="ip-robot-head">
<span className="ip-robot-brand">{r.brandLabel}</span>
<span className="ip-robot-arrow"><ArrowRight size={14} strokeWidth={1.8} /></span>
</div>
<h3>{displayName(r.brandLabel, r.name)}</h3>
<p>{r.tagline}</p>
<Link href={`/robots/${r.slug}/`} className="ip-robot-link">
View robot <ArrowRight size={13} strokeWidth={2} />
</Link>
</article>
))}
</div>
</section>
</MotionSection>
)}
<MotionSection>
<section className="ip-cta">
<div className="ip-cta-glow" aria-hidden />
<div className="ip-cta-grid" aria-hidden />
<div className="ip-cta-inner">
<div className="ip-cta-text">
<span className="eyebrow">Next step</span>
<h2 className="ip-cta-title">Planning robots for {industry.name.toLowerCase()}?</h2>
<p>Tell us about your venue and goals. Our Dubai team will recommend robots and arrange a live demo or quotation.</p>
</div>
<div className="ip-cta-actions">
<CTAButton href="/book-demo/" variant="primary" size="lg" arrow="up-right">
Book a Live Demo
</CTAButton>
<CTAButton href="/contact/" variant="secondary" size="lg" arrow="right">
Request UAE Quotation
</CTAButton>
</div>
</div>
</section>
</MotionSection>
</div>
</main>
<FooterAndContact />
<style>{IP_STYLES}</style>
</>
);
}
const IP_STYLES = `
.ip-main {
padding-top: clamp(5rem, 9vw, 7rem);
padding-bottom: clamp(2rem, 5vw, 4rem);
@@ -1094,7 +1264,4 @@ export default async function IndustryDetailPage({ params }: { params: Promise<P
.ip-cta-actions .cta-btn { width: 100%; justify-content: space-between; }
.ip-hero-actions .cta-btn { width: 100%; justify-content: space-between; }
}
`}</style>
</>
);
}
`;
+6 -4
View File
@@ -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' },
+39
View File
@@ -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 />
</>
);
}
+7 -2
View File
@@ -16,11 +16,17 @@ import { FounderSection } from '@/components/robotics/FounderSection';
import { BuSunaidahSection } from '@/components/robotics/BuSunaidahSection';
import { MotionSection } from '@/components/ui/MotionSection';
import { FEATURED_ROBOTS } from '@/data/robots';
import { pageMetadata } from '@/lib/seo';
// `absolute` keeps the layout template from adding the brand a second time.
export const metadata: Metadata = {
title: 'YS Lootah Robotics UAE Destination for Unitree & Pudu Robotics',
...pageMetadata({
title: 'Robot Shop and Configurator in the UAE',
description:
'YS Lootah Robotics is the UAE sales destination for selected Unitree and Pudu Robotics solutions. Explore, configure, book demos, and deploy advanced robots across Dubai and the UAE.',
path: '/',
}),
title: { absolute: 'Robot Shop and Configurator in the UAE | YS Lootah Robotics' },
keywords: [
'YS Lootah Robotics',
'robotics Dubai',
@@ -35,7 +41,6 @@ export const metadata: Metadata = {
'robotics configuration UAE',
'robot sales and demo UAE',
],
alternates: { canonical: '/' },
};
export default function HomePage() {
+9
View File
@@ -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 (
+17 -14
View File
@@ -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' }}>
+5 -4
View File
@@ -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;
+2 -2
View File
@@ -1,6 +1,6 @@
import { MetadataRoute } from 'next';
import { ROBOTS } from '@/data/robots';
import { INDUSTRIES } from '@/data/industries';
import { INDUSTRIES, UPCOMING_INDUSTRIES } from '@/data/industries';
import { ACCESSORIES } from '@/data/accessories';
import { SITE_URL } from '@/lib/seo';
@@ -30,7 +30,7 @@ export default function sitemap(): MetadataRoute.Sitemap {
priority: 0.7,
}));
const industryRoutes: MetadataRoute.Sitemap = INDUSTRIES.map((i) => ({
const industryRoutes: MetadataRoute.Sitemap = [...INDUSTRIES, ...UPCOMING_INDUSTRIES].map((i) => ({
url: u(`/industries/${i.slug}/`),
lastModified: now,
changeFrequency: 'monthly',
+9
View File
@@ -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 (
+4 -1
View File
@@ -9,7 +9,10 @@ import { PaymentStep } from './checkout/PaymentStep';
import { ReviewStep } from './checkout/ReviewStep';
import { ConfirmationStep } from './checkout/ConfirmationStep';
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);
// Without a publishable key (local builds), loadStripe throws on every page that
// imports this module. Elements accepts null and the payment step stays empty.
const publishableKey = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY;
const stripePromise = publishableKey ? loadStripe(publishableKey) : null;
const STEPS: { id: CheckoutStep; label: string }[] = [
{ id: 'shipping', label: 'Shipping' },
+1 -1
View File
@@ -12,7 +12,7 @@ import { CTAButton } from '@/components/ui/CTAButton';
const CONTACT = {
phonePrimary: '+971 55 948 2728',
phoneSecondary: '+971 4 349 9319',
phoneSecondary: '+971 4 289 6095',
email: 'info@yslootahrobotics.com',
address: 'Showroom No 03, Sheikh Zayed Rd Trade Center Second - DIFC - Dubai, UAE',
whatsapp: 'https://wa.me/971559482728',
@@ -4,6 +4,7 @@ import { useMemo, useState } from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { ACCESSORIES, GROUP_LABELS, type AccessoryGroup, type Accessory } from '@/data/accessories';
import { displayName } from '@/lib/seo';
type Filter = AccessoryGroup | 'all';
@@ -97,7 +98,7 @@ function AccessoryCard({ accessory }: { accessory: Accessory }) {
>
<Link
href={detailHref}
aria-label={`View details for ${accessory.brandLabel} ${accessory.name}`}
aria-label={`View details for ${displayName(accessory.brandLabel, accessory.name)}`}
style={{
position: 'relative',
display: 'block',
@@ -123,7 +124,7 @@ function AccessoryCard({ accessory }: { accessory: Accessory }) {
/>
<Image
src={accessory.image}
alt={`${accessory.brandLabel} ${accessory.name}`}
alt={displayName(accessory.brandLabel, accessory.name)}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
style={{
+2 -1
View File
@@ -6,6 +6,7 @@ import Link from 'next/link';
import { PremiumButton } from '@/components/ui/PremiumButton';
import { CursorSpotlight } from '@/components/ui/CursorSpotlight';
import { FEATURED_ROBOTS } from '@/data/robots';
import { displayName } from '@/lib/seo';
const HERO_STATS = [
{ value: 'UAE', label: 'Access' },
@@ -341,7 +342,7 @@ export function Hero3DRobotics() {
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
<Image
src={r.image}
alt={`${r.brandLabel} ${r.name}`}
alt={displayName(r.brandLabel, r.name)}
fill
sizes="(max-width: 768px) 90vw, 620px"
style={{ objectFit: 'contain', objectPosition: 'center bottom' }}
+3 -3
View File
@@ -15,7 +15,7 @@ import Sparkles from 'lucide-react/dist/esm/icons/sparkles';
import Landmark from 'lucide-react/dist/esm/icons/landmark';
import ArrowRight from 'lucide-react/dist/esm/icons/arrow-right';
import type { LucideIcon } from 'lucide-react';
import { INDUSTRIES, type Industry } from '@/data/industries';
import { INDUSTRIES, UPCOMING_INDUSTRIES, type Industry } from '@/data/industries';
const ease = [0.16, 1, 0.3, 1] as const;
@@ -53,7 +53,7 @@ const STATS = [
] as const;
export function IndustryUseCases({ limit }: { limit?: number }) {
const list = limit ? INDUSTRIES.slice(0, limit) : INDUSTRIES;
const list = limit ? INDUSTRIES.slice(0, limit) : [...INDUSTRIES, ...UPCOMING_INDUSTRIES];
return (
<section className="iu" aria-label="Industries served">
@@ -232,7 +232,7 @@ function IndustryCard({ industry, delay }: { industry: Industry; delay: number }
<h3 className="ic-title">{industry.name}</h3>
<p className="ic-desc">{blurb}</p>
<span className="ic-cta">
<span>Learn more</span>
<span>{industry.upcoming ? 'Coming soon' : 'Learn more'}</span>
<span className="ic-cta-arrow" aria-hidden>
<ArrowRight size={14} strokeWidth={2} />
</span>
+3 -2
View File
@@ -3,6 +3,7 @@
import Link from 'next/link';
import Image from 'next/image';
import { Robot, CATEGORY_LABELS } from '@/data/robots';
import { displayName } from '@/lib/seo';
type Props = {
robot: Robot;
@@ -24,7 +25,7 @@ export function RobotProductCard({ robot, priority = false }: Props) {
>
<Link
href={detailHref}
aria-label={`View details for ${robot.brandLabel} ${robot.name}`}
aria-label={`View details for ${displayName(robot.brandLabel, robot.name)}`}
style={{
position: 'relative',
display: 'block',
@@ -50,7 +51,7 @@ export function RobotProductCard({ robot, priority = false }: Props) {
/>
<Image
src={robot.image}
alt={`${robot.brandLabel} ${robot.name}`}
alt={displayName(robot.brandLabel, robot.name)}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
style={{
+27
View File
@@ -12,6 +12,8 @@ export interface Industry {
icon: 'building' | 'utensils' | 'hotel' | 'shopping-bag' | 'heart-pulse' | 'graduation-cap' | 'shield' | 'warehouse' | 'sparkles' | 'landmark';
image: string;
imageAlt: string;
/** Sector with a short coming-soon page instead of a full INDUSTRY_PAGES entry. */
upcoming?: { seoTitle: string; seoDescription: string };
}
export const INDUSTRIES: Industry[] = [
@@ -119,6 +121,12 @@ export const INDUSTRIES: Industry[] = [
image: '/industries/cards/warehouses-logistics.webp',
imageAlt: 'Industrial robot for UAE warehouses and logistics operations',
},
];
// Sectors with no full detail page content yet (no entry in INDUSTRY_PAGES).
// Each renders a short coming-soon page from its `upcoming` copy. Move an entry
// up into INDUSTRIES once its INDUSTRY_PAGES content is written.
export const UPCOMING_INDUSTRIES: Industry[] = [
{
id: 'events',
slug: 'events',
@@ -131,6 +139,11 @@ export const INDUSTRIES: Industry[] = [
icon: 'sparkles',
image: '/industries/cards/events.webp',
imageAlt: 'Robotics activation for UAE live events and brand experiences',
upcoming: {
seoTitle: 'Robots for Events and Exhibitions in the UAE',
seoDescription:
'Humanoid and quadruped robot showcases for UAE events, exhibitions and brand activations. Book a demo with YS Lootah Robotics in Dubai.',
},
},
{
id: 'smart-buildings',
@@ -144,6 +157,11 @@ export const INDUSTRIES: Industry[] = [
icon: 'building',
image: '/industries/cards/smart-buildings.webp',
imageAlt: 'Cleaning robot in a modern UAE smart office building',
upcoming: {
seoTitle: 'Robots for Smart Buildings in the UAE',
seoDescription:
'Cleaning, delivery and patrol robots working as one fleet in UAE smart buildings. Talk to YS Lootah Robotics about a deployment in Dubai.',
},
},
{
id: 'government',
@@ -157,9 +175,18 @@ export const INDUSTRIES: Industry[] = [
icon: 'landmark',
image: '/industries/unitree/utility-room.webp',
imageAlt: 'Quadruped robots deployed for UAE civic and emergency services',
upcoming: {
seoTitle: 'Robots for Government and Public Sector in the UAE',
seoDescription:
'Humanoid, quadruped and service robots for UAE government and public sector programs, with local deployment and support from YS Lootah Robotics.',
},
},
];
export function getIndustryBySlug(slug: string): Industry | undefined {
return INDUSTRIES.find((i) => i.slug === slug);
}
export function getUpcomingIndustryBySlug(slug: string): Industry | undefined {
return UPCOMING_INDUSTRIES.find((i) => i.slug === slug);
}
+9 -8
View File
@@ -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.',
},
+66
View File
@@ -0,0 +1,66 @@
import { describe, expect, it } from 'vitest';
import { ROBOTS } from '@/data/robots';
import { ACCESSORIES } from '@/data/accessories';
import { INDUSTRIES, UPCOMING_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);
}
});
it('gives every upcoming sector its own coming-soon title', () => {
for (const industry of UPCOMING_INDUSTRIES) {
expect(industry.upcoming?.seoTitle, industry.slug).toBeTruthy();
expect(industry.upcoming?.seoTitle).not.toContain(siteName);
expect(INDUSTRIES.some((i) => i.slug === industry.slug), industry.slug).toBe(false);
}
});
});
+40 -1
View File
@@ -15,10 +15,49 @@ 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',
phones: ['+971559482728', '+97143499319'],
phones: ['+971559482728', '+97142896095'],
email: 'info@yslootahrobotics.com',
whatsapp: 'https://wa.me/971559482728',
street: 'Showroom No 03, Sheikh Zayed Rd, Trade Center Second, DIFC',