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>
88 lines
2.7 KiB
TypeScript
88 lines
2.7 KiB
TypeScript
import type { MetadataRoute } from 'next';
|
|
import { locales } from '@/i18n/config';
|
|
import { fetchActiveListings } from '@/lib/listings-server';
|
|
|
|
const siteUrl = process.env['NEXT_PUBLIC_SITE_URL'] || 'https://goodgo.vn';
|
|
|
|
/**
|
|
* Dynamic sitemap that includes:
|
|
* - Static pages (home, search, login, register) per locale
|
|
* - All active property listings per locale
|
|
*
|
|
* ISR: the server-side fetch in fetchActiveListings uses `next: { revalidate: 3600 }`
|
|
* so the sitemap is regenerated roughly every hour.
|
|
*/
|
|
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|
// ------------------------------------------------------------------
|
|
// 1. Static pages
|
|
// ------------------------------------------------------------------
|
|
const staticRoutes: MetadataRoute.Sitemap = [];
|
|
|
|
for (const locale of locales) {
|
|
staticRoutes.push(
|
|
{
|
|
url: `${siteUrl}/${locale}`,
|
|
lastModified: new Date(),
|
|
changeFrequency: 'daily',
|
|
priority: 1,
|
|
},
|
|
{
|
|
url: `${siteUrl}/${locale}/search`,
|
|
lastModified: new Date(),
|
|
changeFrequency: 'daily',
|
|
priority: 0.9,
|
|
},
|
|
{
|
|
url: `${siteUrl}/${locale}/login`,
|
|
lastModified: new Date(),
|
|
changeFrequency: 'monthly',
|
|
priority: 0.3,
|
|
},
|
|
{
|
|
url: `${siteUrl}/${locale}/register`,
|
|
lastModified: new Date(),
|
|
changeFrequency: 'monthly',
|
|
priority: 0.3,
|
|
},
|
|
);
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// 2. Dynamic listing pages
|
|
// ------------------------------------------------------------------
|
|
const listingRoutes: MetadataRoute.Sitemap = [];
|
|
|
|
try {
|
|
// Fetch up to 1000 listings per page, paginate if needed.
|
|
// For very large catalogs this could be split into multiple sitemap
|
|
// indices, but for the current scale (<50k) a single file is fine.
|
|
let page = 1;
|
|
let totalPages = 1;
|
|
|
|
do {
|
|
const result = await fetchActiveListings({ page, limit: 1000 });
|
|
totalPages = result.totalPages;
|
|
|
|
for (const listing of result.data) {
|
|
for (const locale of locales) {
|
|
listingRoutes.push({
|
|
url: `${siteUrl}/${locale}/listings/${listing.id}`,
|
|
lastModified: listing.publishedAt
|
|
? new Date(listing.publishedAt)
|
|
: new Date(listing.createdAt),
|
|
changeFrequency: 'weekly',
|
|
priority: 0.8,
|
|
});
|
|
}
|
|
}
|
|
|
|
page++;
|
|
} while (page <= totalPages);
|
|
} catch {
|
|
// If the API is unreachable, return static pages only.
|
|
// The sitemap will be retried on the next request (ISR).
|
|
}
|
|
|
|
return [...staticRoutes, ...listingRoutes];
|
|
}
|