From 83825015980895a286d4229a66a5fc1b3c8512db Mon Sep 17 00:00:00 2001 From: "Najjar\\NajjarV02" Date: Tue, 13 Oct 2026 16:57:18 +0400 Subject: [PATCH] feat(seo): fix titles, canonicals, share tags and soft 404s - 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 --- src/app/about/page.tsx | 8 ++-- src/app/accessories/[slug]/page.tsx | 21 +++++------ src/app/accessories/page.tsx | 9 +++-- src/app/book-demo/page.tsx | 8 ++-- src/app/brands/page.tsx | 8 ++-- src/app/bu-sunaidah/page.tsx | 15 +++----- src/app/configure/layout.tsx | 14 +++++++ src/app/contact/page.tsx | 8 ++-- src/app/industries/[slug]/page.tsx | 16 +++++--- src/app/industries/page.tsx | 10 +++-- src/app/not-found.tsx | 39 +++++++++++++++++++ src/app/privacy-policy/page.tsx | 9 +++++ src/app/robots/[slug]/page.tsx | 31 ++++++++------- src/app/robots/page.tsx | 9 +++-- src/app/terms-of-service/page.tsx | 9 +++++ src/data/industries.ts | 6 +++ src/data/industry-pages.ts | 17 +++++---- src/lib/seo.test.ts | 58 +++++++++++++++++++++++++++++ src/lib/seo.ts | 39 +++++++++++++++++++ 19 files changed, 261 insertions(+), 73 deletions(-) create mode 100644 src/app/configure/layout.tsx create mode 100644 src/app/not-found.tsx create mode 100644 src/lib/seo.test.ts diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx index d9574b0..07ac8be 100644 --- a/src/app/about/page.tsx +++ b/src/app/about/page.tsx @@ -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 ( diff --git a/src/app/accessories/[slug]/page.tsx b/src/app/accessories/[slug]/page.tsx index 03de81e..a4474b6 100644 --- a/src/app/accessories/[slug]/page.tsx +++ b/src/app/accessories/[slug]/page.tsx @@ -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 }): Promise { 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 }) { diff --git a/src/app/accessories/page.tsx b/src/app/accessories/page.tsx index e732ff0..486d102 100644 --- a/src/app/accessories/page.tsx +++ b/src/app/accessories/page.tsx @@ -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 ( diff --git a/src/app/book-demo/page.tsx b/src/app/book-demo/page.tsx index 2b104fc..0d64a18 100644 --- a/src/app/book-demo/page.tsx +++ b/src/app/book-demo/page.tsx @@ -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 ( diff --git a/src/app/brands/page.tsx b/src/app/brands/page.tsx index 00314c1..389b205 100644 --- a/src/app/brands/page.tsx +++ b/src/app/brands/page.tsx @@ -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']; diff --git a/src/app/bu-sunaidah/page.tsx b/src/app/bu-sunaidah/page.tsx index 281ddd5..4acecd0 100644 --- a/src/app/bu-sunaidah/page.tsx +++ b/src/app/bu-sunaidah/page.tsx @@ -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 = [ { diff --git a/src/app/configure/layout.tsx b/src/app/configure/layout.tsx new file mode 100644 index 0000000..1a12641 --- /dev/null +++ b/src/app/configure/layout.tsx @@ -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; +} diff --git a/src/app/contact/page.tsx b/src/app/contact/page.tsx index 0656cd3..53aafad 100644 --- a/src/app/contact/page.tsx +++ b/src/app/contact/page.tsx @@ -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 ( diff --git a/src/app/industries/[slug]/page.tsx b/src/app/industries/[slug]/page.tsx index db2192b..f469aa3 100644 --- a/src/app/industries/[slug]/page.tsx +++ b/src/app/industries/[slug]/page.tsx @@ -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 ({ slug: i.slug })); } @@ -54,14 +59,13 @@ export async function generateMetadata({ params }: { params: Promise }): 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 = [ diff --git a/src/app/industries/page.tsx b/src/app/industries/page.tsx index 72cec6e..458fbbc 100644 --- a/src/app/industries/page.tsx +++ b/src/app/industries/page.tsx @@ -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' }, diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx new file mode 100644 index 0000000..a425280 --- /dev/null +++ b/src/app/not-found.tsx @@ -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 ( + <> + +
+
+ Error 404 +

+ This page could not be found. +

+

+ 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. +

+
+ Robots + Accessories + Industries + Contact +
+
+
+ + + ); +} diff --git a/src/app/privacy-policy/page.tsx b/src/app/privacy-policy/page.tsx index cfc2b8e..3582f40 100644 --- a/src/app/privacy-policy/page.tsx +++ b/src/app/privacy-policy/page.tsx @@ -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 ( diff --git a/src/app/robots/[slug]/page.tsx b/src/app/robots/[slug]/page.tsx index 23fbc33..3a7e688 100644 --- a/src/app/robots/[slug]/page.tsx +++ b/src/app/robots/[slug]/page.tsx @@ -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 }): Promise { 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 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
{robot.imageType === 'placeholder' && ( @@ -184,7 +187,7 @@ export default async function RobotDetailPage({ params }: { params: Promise
- +
diff --git a/src/app/robots/page.tsx b/src/app/robots/page.tsx index bdc1388..7058919 100644 --- a/src/app/robots/page.tsx +++ b/src/app/robots/page.tsx @@ -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; diff --git a/src/app/terms-of-service/page.tsx b/src/app/terms-of-service/page.tsx index 25f5b3a..67bc62c 100644 --- a/src/app/terms-of-service/page.tsx +++ b/src/app/terms-of-service/page.tsx @@ -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 ( diff --git a/src/data/industries.ts b/src/data/industries.ts index 1988ea2..12ad08f 100644 --- a/src/data/industries.ts +++ b/src/data/industries.ts @@ -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', diff --git a/src/data/industry-pages.ts b/src/data/industry-pages.ts index 4d91c6e..a439f3f 100644 --- a/src/data/industry-pages.ts +++ b/src/data/industry-pages.ts @@ -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 = { 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 = { 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 = { 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 = { 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 = { 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 = { 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 = { 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 = { 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.', }, diff --git a/src/lib/seo.test.ts b/src/lib/seo.test.ts new file mode 100644 index 0000000..8639271 --- /dev/null +++ b/src/lib/seo.test.ts @@ -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); + } + }); +}); diff --git a/src/lib/seo.ts b/src/lib/seo.ts index bf8f487..32ca2ed 100644 --- a/src/lib/seo.ts +++ b/src/lib/seo.ts @@ -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',