feat(web): add SEO optimization — JSON-LD, dynamic sitemap, meta tags for listings

Add comprehensive SEO support for property listing pages to improve
organic search visibility and social sharing.

Changes:
- Convert listing detail page from client-only to server component wrapper
  with generateMetadata() for per-listing title, description, OG tags,
  canonical URLs, and hreflang alternates
- Add JSON-LD structured data (Schema.org RealEstateListing) with price,
  location, property specs, and breadcrumb markup
- Add Website JSON-LD with SearchAction to root layout
- Upgrade sitemap.xml to dynamically include all active listings across
  both locales (vi, en) with ISR revalidation
- Improve robots.txt with pagination/sort exclusions and GPTBot block
- Create server-side fetch utility (listings-server.ts) for SSR data
- Extract client UI into ListingDetailClient component

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-10 20:38:28 +07:00
parent 05abbc5250
commit 50c5168529
7 changed files with 756 additions and 359 deletions

View File

@@ -5,6 +5,8 @@ import { getMessages, getTranslations } from 'next-intl/server';
import { AuthProvider } from '@/components/providers/auth-provider';
import { QueryProvider } from '@/components/providers/query-provider';
import { ThemeProvider } from '@/components/providers/theme-provider';
import { WebVitals } from '@/components/providers/web-vitals';
import { JsonLd, generateWebsiteJsonLd } from '@/components/seo/json-ld';
import type { Locale } from '@/i18n/config';
import { routing } from '@/i18n/routing';
import '../globals.css';
@@ -99,6 +101,7 @@ export default async function LocaleLayout({
return (
<html lang={locale} suppressHydrationWarning>
<body>
<JsonLd data={generateWebsiteJsonLd(siteUrl)} />
<a
href="#main-content"
className="fixed left-2 top-2 z-[100] -translate-y-16 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow-lg transition-transform focus:translate-y-0"
@@ -108,7 +111,10 @@ export default async function LocaleLayout({
<NextIntlClientProvider messages={messages}>
<ThemeProvider>
<QueryProvider>
<AuthProvider>{children}</AuthProvider>
<AuthProvider>
<WebVitals />
{children}
</AuthProvider>
</QueryProvider>
</ThemeProvider>
</NextIntlClientProvider>