Files
goodgo-platform/apps/web/lib/listings-server.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

52 lines
1.5 KiB
TypeScript

/**
* Server-side listing data fetching for Next.js Server Components.
*
* This module uses `fetch` directly (no browser-only helpers) so it can run
* inside `generateMetadata`, `generateStaticParams`, `sitemap()`, etc.
*/
import type { ListingDetail, PaginatedResult } from './listings-api';
const API_BASE_URL = process.env['NEXT_PUBLIC_API_URL'] || 'http://localhost:3001/api/v1';
/**
* Fetch a single listing by ID — server-only.
* Returns `null` when the listing is not found (404) so callers can `notFound()`.
*/
export async function fetchListingById(id: string): Promise<ListingDetail | null> {
try {
const res = await fetch(`${API_BASE_URL}/listings/${id}`, {
next: { revalidate: 300 }, // ISR: re-validate every 5 min
});
if (!res.ok) return null;
return (await res.json()) as ListingDetail;
} catch {
return null;
}
}
/**
* Fetch active listings — server-only, used by the dynamic sitemap.
*/
export async function fetchActiveListings(params: {
page?: number;
limit?: number;
}): Promise<PaginatedResult<ListingDetail>> {
const query = new URLSearchParams({
status: 'ACTIVE',
page: String(params.page ?? 1),
limit: String(params.limit ?? 100),
});
const res = await fetch(`${API_BASE_URL}/listings?${query}`, {
next: { revalidate: 3600 }, // re-validate every hour for sitemap
});
if (!res.ok) {
return { data: [], total: 0, page: 1, limit: 100, totalPages: 0 };
}
return (await res.json()) as PaginatedResult<ListingDetail>;
}