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>
125 lines
3.3 KiB
TypeScript
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>
|
|
);
|
|
}
|