- Set SameSite=lax for auth & CSRF cookies in development (cross-port) - Set refresh_token cookie path to / (was /auth, preventing cross-port send) - Await params in Next.js 15 async server components (layout, listings, agents) - Add CSRF token to web-vitals POST requests - Fix: 401 Unauthorized on all authenticated API calls from web app - Fix: CSRF token missing on POST requests from different port - Fix: params.locale sync access warning in generateMetadata Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
115 lines
3.6 KiB
TypeScript
115 lines
3.6 KiB
TypeScript
import type { Metadata } from 'next';
|
|
import { notFound } from 'next/navigation';
|
|
import { AgentProfileClient } from '@/components/agents/agent-profile-client';
|
|
import {
|
|
JsonLd,
|
|
generateAgentJsonLd,
|
|
generateBreadcrumbJsonLd,
|
|
} from '@/components/seo/json-ld';
|
|
import { fetchAgentProfile, fetchAgentReviews } from '@/lib/agents-server';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Constants
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const siteUrl = process.env['NEXT_PUBLIC_SITE_URL'] || 'https://goodgo.vn';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Metadata (runs server-side, provides <title>, <meta>, OG, canonical)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
interface PageProps {
|
|
params: Promise<{ locale: string; id: string }>;
|
|
}
|
|
|
|
export async function generateMetadata({ params: paramsPromise }: PageProps): Promise<Metadata> {
|
|
const params = await paramsPromise;
|
|
const agent = await fetchAgentProfile(params.id);
|
|
if (!agent) {
|
|
return { title: 'Không tìm thấy môi giới' };
|
|
}
|
|
|
|
const title = agent.isVerified
|
|
? `${agent.fullName} — Môi giới xác minh | GoodGo`
|
|
: `${agent.fullName} — Môi giới BĐS | GoodGo`;
|
|
|
|
const description = [
|
|
agent.agency ? `Công ty: ${agent.agency}` : null,
|
|
agent.serviceAreas.length > 0
|
|
? `Khu vực: ${agent.serviceAreas.join(', ')}`
|
|
: null,
|
|
agent.totalReviews > 0
|
|
? `Đánh giá: ${agent.avgReviewRating}/5 (${agent.totalReviews} lượt)`
|
|
: null,
|
|
`${agent.activeListings.length} tin đăng đang hoạt động`,
|
|
`${agent.totalDeals} giao dịch thành công`,
|
|
]
|
|
.filter(Boolean)
|
|
.join(' | ');
|
|
|
|
const canonicalUrl = `${siteUrl}/${params.locale}/agents/${params.id}`;
|
|
|
|
return {
|
|
title,
|
|
description,
|
|
alternates: {
|
|
canonical: canonicalUrl,
|
|
languages: {
|
|
vi: `${siteUrl}/vi/agents/${params.id}`,
|
|
en: `${siteUrl}/en/agents/${params.id}`,
|
|
},
|
|
},
|
|
openGraph: {
|
|
type: 'profile',
|
|
locale: params.locale === 'vi' ? 'vi_VN' : 'en_US',
|
|
url: canonicalUrl,
|
|
title,
|
|
description,
|
|
siteName: 'GoodGo',
|
|
images: agent.avatarUrl
|
|
? [{ url: agent.avatarUrl, width: 400, height: 400, alt: agent.fullName }]
|
|
: [{ url: '/og-image.png', width: 1200, height: 630, alt: 'GoodGo' }],
|
|
},
|
|
twitter: {
|
|
card: 'summary',
|
|
title,
|
|
description,
|
|
images: agent.avatarUrl ? [agent.avatarUrl] : ['/og-image.png'],
|
|
},
|
|
};
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Page (Server Component)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export default async function AgentProfilePage({ params: paramsPromise }: PageProps) {
|
|
const params = await paramsPromise;
|
|
const [agent, reviewsResult] = await Promise.all([
|
|
fetchAgentProfile(params.id),
|
|
fetchAgentReviews(params.id, 1, 10),
|
|
]);
|
|
|
|
if (!agent) {
|
|
notFound();
|
|
}
|
|
|
|
// Build JSON-LD structured data
|
|
const agentJsonLd = generateAgentJsonLd(agent, siteUrl);
|
|
const breadcrumbJsonLd = generateBreadcrumbJsonLd([
|
|
{ name: 'Trang chủ', url: siteUrl },
|
|
{ name: agent.fullName, url: `${siteUrl}/${params.locale}/agents/${params.id}` },
|
|
]);
|
|
|
|
return (
|
|
<>
|
|
{/* Structured data for search engines */}
|
|
<JsonLd data={agentJsonLd} />
|
|
<JsonLd data={breadcrumbJsonLd} />
|
|
|
|
{/* Interactive client component */}
|
|
<AgentProfileClient agent={agent} reviews={reviewsResult.data} />
|
|
</>
|
|
);
|
|
}
|