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>
This commit is contained in:
@@ -1,32 +1,87 @@
|
||||
import type { MetadataRoute } from 'next';
|
||||
import { locales } from '@/i18n/config';
|
||||
import { fetchActiveListings } from '@/lib/listings-server';
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
const siteUrl = process.env['NEXT_PUBLIC_SITE_URL'] || 'https://goodgo.vn';
|
||||
const siteUrl = process.env['NEXT_PUBLIC_SITE_URL'] || 'https://goodgo.vn';
|
||||
|
||||
return [
|
||||
{
|
||||
url: siteUrl,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'daily',
|
||||
priority: 1,
|
||||
},
|
||||
{
|
||||
url: `${siteUrl}/search`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'daily',
|
||||
priority: 0.9,
|
||||
},
|
||||
{
|
||||
url: `${siteUrl}/login`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'monthly',
|
||||
priority: 0.3,
|
||||
},
|
||||
{
|
||||
url: `${siteUrl}/register`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'monthly',
|
||||
priority: 0.3,
|
||||
},
|
||||
];
|
||||
/**
|
||||
* 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];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user