Files
goodgo-platform/apps/web/app/sitemap.ts
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

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];
}