import type { AgentPublicProfile } from '@/lib/agents-api'; import type { ListingDetail } from '@/lib/listings-api'; import { PROPERTY_TYPES, TRANSACTION_TYPES } from '@/lib/validations/listings'; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function getLabel(list: readonly { value: string; label: string }[], value: string | null) { if (!value) return null; return list.find((item) => item.value === value)?.label ?? value; } function formatPriceNumber(priceVND: string): number { return Number(priceVND); } // --------------------------------------------------------------------------- // JSON-LD generator for a single listing // --------------------------------------------------------------------------- export function generateListingJsonLd(listing: ListingDetail, siteUrl: string) { const { property } = listing; const propertyTypeLabel = getLabel(PROPERTY_TYPES, property.propertyType) ?? property.propertyType; const transactionLabel = getLabel(TRANSACTION_TYPES, listing.transactionType) ?? listing.transactionType; const fullAddress = [property.address, property.ward, property.district, property.city] .filter(Boolean) .join(', '); const images = property.media .filter((m) => m.type === 'image') .map((m) => m.url); const priceNum = formatPriceNumber(listing.priceVND); // Schema.org RealEstateListing // https://schema.org/RealEstateListing const jsonLd: Record = { '@context': 'https://schema.org', '@type': 'RealEstateListing', name: property.title, description: property.description?.slice(0, 500), url: `${siteUrl}/listings/${listing.id}`, datePosted: listing.publishedAt ?? listing.createdAt, ...(images.length > 0 && { image: images }), // Offer offers: { '@type': 'Offer', price: priceNum, priceCurrency: 'VND', availability: listing.status === 'ACTIVE' ? 'https://schema.org/InStock' : 'https://schema.org/SoldOut', ...(listing.transactionType === 'RENT' && listing.rentPriceMonthly && { priceSpecification: { '@type': 'UnitPriceSpecification', price: Number(listing.rentPriceMonthly), priceCurrency: 'VND', unitText: 'MONTH', }, }), }, // Location contentLocation: { '@type': 'Place', name: fullAddress, address: { '@type': 'PostalAddress', streetAddress: property.address, addressLocality: property.district, addressRegion: property.city, addressCountry: 'VN', }, ...(property.latitude != null && property.longitude != null && { geo: { '@type': 'GeoCoordinates', latitude: property.latitude, longitude: property.longitude, }, }), }, // Additional property details via additionalProperty additionalProperty: [ { '@type': 'PropertyValue', name: 'Loại BDS', value: propertyTypeLabel, }, { '@type': 'PropertyValue', name: 'Loại giao dich', value: transactionLabel, }, { '@type': 'PropertyValue', name: 'Dien tich', value: `${property.areaM2} m²`, unitCode: 'MTK', }, ...(property.bedrooms != null ? [{ '@type': 'PropertyValue' as const, name: 'Phong ngu', value: property.bedrooms, }] : []), ...(property.bathrooms != null ? [{ '@type': 'PropertyValue' as const, name: 'Phong tam', value: property.bathrooms, }] : []), ...(property.floors != null ? [{ '@type': 'PropertyValue' as const, name: 'So tang', value: property.floors, }] : []), ], }; return jsonLd; } // --------------------------------------------------------------------------- // BreadcrumbList JSON-LD // --------------------------------------------------------------------------- export function generateBreadcrumbJsonLd( items: { name: string; url: string }[], ) { return { '@context': 'https://schema.org', '@type': 'BreadcrumbList', itemListElement: items.map((item, index) => ({ '@type': 'ListItem', position: index + 1, name: item.name, item: item.url, })), }; } // --------------------------------------------------------------------------- // Website (Organization) JSON-LD — for the root layout // --------------------------------------------------------------------------- export function generateWebsiteJsonLd(siteUrl: string) { return { '@context': 'https://schema.org', '@type': 'WebSite', name: 'GoodGo', url: siteUrl, description: 'Nen tang bat dong san thong minh tai Viet Nam', potentialAction: { '@type': 'SearchAction', target: { '@type': 'EntryPoint', urlTemplate: `${siteUrl}/search?q={search_term_string}`, }, 'query-input': 'required name=search_term_string', }, inLanguage: ['vi', 'en'], }; } // --------------------------------------------------------------------------- // Agent profile JSON-LD (RealEstateAgent) // --------------------------------------------------------------------------- export function generateAgentJsonLd(agent: AgentPublicProfile, siteUrl: string) { const jsonLd: Record = { '@context': 'https://schema.org', '@type': 'RealEstateAgent', name: agent.fullName, url: `${siteUrl}/agents/${agent.id}`, description: agent.bio ?? `Môi giới bất động sản tại GoodGo`, ...(agent.avatarUrl && { image: agent.avatarUrl }), telephone: agent.phone, ...(agent.email && { email: agent.email }), ...(agent.agency && { worksFor: { '@type': 'RealEstateAgent', name: agent.agency, }, }), ...(agent.serviceAreas.length > 0 && { areaServed: agent.serviceAreas.map((area) => ({ '@type': 'AdministrativeArea', name: area, })), }), ...(agent.totalReviews > 0 && { aggregateRating: { '@type': 'AggregateRating', ratingValue: agent.avgReviewRating, reviewCount: agent.totalReviews, bestRating: 5, worstRating: 1, }, }), }; return jsonLd; } // --------------------------------------------------------------------------- // React component that renders