Files
goodgo-platform/apps/web/app/[locale]/layout.tsx
Ho Ngoc Hai 50c5168529 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>
2026-04-10 20:38:28 +07:00

125 lines
3.3 KiB
TypeScript

import type { Metadata, Viewport } from 'next';
import { notFound } from 'next/navigation';
import { NextIntlClientProvider } from 'next-intl';
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';
const siteUrl = process.env['NEXT_PUBLIC_SITE_URL'] || 'https://goodgo.vn';
export const viewport: Viewport = {
width: 'device-width',
initialScale: 1,
themeColor: '#15803d',
};
export async function generateMetadata({
params: { locale },
}: {
params: { locale: string };
}): Promise<Metadata> {
const t = await getTranslations({ locale, namespace: 'metadata' });
return {
metadataBase: new URL(siteUrl),
title: {
default: t('title'),
template: '%s | GoodGo',
},
description: t('description'),
keywords: [
'bất động sản',
'mua bán nhà đất',
'cho thuê nhà',
'goodgo',
'nhà đất việt nam',
'real estate vietnam',
],
authors: [{ name: 'GoodGo' }],
creator: 'GoodGo',
openGraph: {
type: 'website',
locale: locale === 'vi' ? 'vi_VN' : 'en_US',
url: siteUrl,
siteName: 'GoodGo',
title: t('ogTitle'),
description: t('ogDescription'),
images: [
{
url: '/og-image.png',
width: 1200,
height: 630,
alt: t('ogTitle'),
},
],
},
twitter: {
card: 'summary_large_image',
title: t('ogTitle'),
description: t('ogDescription'),
images: ['/og-image.png'],
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-video-preview': -1,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
};
}
export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }));
}
export default async function LocaleLayout({
children,
params: { locale },
}: {
children: React.ReactNode;
params: { locale: string };
}) {
// Validate locale
if (!routing.locales.includes(locale as Locale)) {
notFound();
}
const messages = await getMessages();
const t = await getTranslations({ locale, namespace: 'common' });
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"
>
{t('skipToContent')}
</a>
<NextIntlClientProvider messages={messages}>
<ThemeProvider>
<QueryProvider>
<AuthProvider>
<WebVitals />
{children}
</AuthProvider>
</QueryProvider>
</ThemeProvider>
</NextIntlClientProvider>
</body>
</html>
);
}