Listing pages
The pieces of a page per listing: getListing and getListingParams to fetch and pre-render, listingMetadata for the title and social cards, ListingDetail and ListingGallery to show it, and ListingSchema for schema.org markup. The Quickstart has a complete app/listings/[slug]/page.tsx built from them.
import { getListing, getListingParams, listingMetadata } from '@sonordev/re-site-kit/server'
import { ListingDetail, ListingSchema, IdxAttribution } from '@sonordev/re-site-kit'
import { ListingGallery } from '@sonordev/re-site-kit/client'getListing
getListing(
slug: string,
options?: { site?: string },
fetchOptions?: { revalidate?: number },
): Promise<RealEstateListing | null>Fetches one listing by its slug. It returns null when the listing doesn't exist (or has left the listings your project shows), and also on any request failure, so pair it with notFound():
const listing = await getListing(slug)
if (!listing) notFound()options.site: on a multi-site project, the sub-site host the listing belongs to.fetchOptions.revalidate: the fetch cache window in seconds. Default 60.- On a project in live serve mode, the listing can include
open_houses, each withdate,start_time,end_timeandcomments.
It's wrapped in React's cache(), so calling getListing(slug) in generateMetadata and again in the page makes one request.
getListingParams
getListingParams(options?: { site?: string }): Promise<{ slug: string }[]>Slugs for generateStaticParams, so listing pages are built ahead of time:
export async function generateStaticParams() {
return getListingParams()
}It asks for up to 1,000 active listings and keeps the ones with a slug. How many come back depends on Sonor's serve mode:
- db mode: up to 1,000, the most recently updated first.
- live mode: only the first page, at most 25 listings, since that's as many as the MLS returns at once. The kit doesn't page past it on purpose: in live mode every pre-built listing page is its own request to the MLS during your build.
Either way, leave dynamicParams at its default of true. Any listing that wasn't pre-built renders on its first visit and is cached after that, so a live-mode site pre-builds its 25 freshest listings and serves the rest on demand. The list itself is cached for 5 minutes. It returns [] on failure, which just means every page renders on demand.
listingMetadata
listingMetadata(
listing: RealEstateListing,
options?: { titleSuffix?: string; url?: string },
): MetadataBuilds Next's Metadata for a detail page from the listing itself:
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params
const listing = await getListing(slug)
if (!listing) return {}
return listingMetadata(listing, {
titleSuffix: ' | Example Realty',
url: `https://example.com/listings/${slug}`,
})
}| Field | Where it comes from |
|---|---|
title | listing.seo_title when it's set. Otherwise the street and location ("12 Oak St #4B, Springfield, IL 62701") followed by titleSuffix. The suffix isn't added to an seo_title. |
description | listing.seo_description when it's set. Otherwise the price, the specs and the first 155 characters of the description, joined with ". " and capped at 300 characters. |
alternates.canonical | url, when you pass it. |
openGraph | The same title and description, type: 'website', url when given, and the listing's first four photos as images. |
Merge in anything else your site sets (Twitter cards, robots) with a spread: { ...listingMetadata(listing), robots: { index: true } }.
ListingDetail
The full listing view, rendered on the server with no client JavaScript.
<ListingDetail listing={listing} />| Prop | Type | Default | What it does |
|---|---|---|---|
listing | RealEstateListing | The listing to show. Required. | |
showPhotos | boolean | true | Show the photo strip. Turn it off when the page shows the photos another way, such as ListingGallery. |
className | string | Added next to re-listing-detail. |
It renders an <article> with, in order:
- A photo strip of up to six photos in a responsive grid, unless
showPhotosisfalse. The first loads eagerly and the rest lazily. - A header with the street address as the page's
<h1>, the location, the price and the specs ("3 bd · 2 ba · 1,960 sqft"). - Facts as a
<dl>, each shown only when the listing has it: Type, Year built, Lot, Garage, HOA (as "$250/Monthly", or "/mo" when the frequency is missing) and Days on market. - The description.
- An attribution line, "Listed by Pat Agent, Example Realty", with a "View original listing" link to
source_urlwhen there's one.
Because ListingDetail renders the <h1>, don't add another one to the page.
Using it with ListingGallery. The strip and the gallery would show the same photos twice, so turn the strip off when you use the gallery:
<ListingGallery images={listing.images ?? []} alt={listing.address} />
<ListingDetail listing={listing} showPhotos={false} />ListingGallery
A photo gallery for the detail page: the main photo with previous and next buttons, a photo counter, a thumbnail strip, and arrow-key navigation when the gallery has focus. It has no dependencies. It's a client component, so import it from /client.
import { ListingGallery } from '@sonordev/re-site-kit/client'
<ListingGallery images={listing.images ?? []} alt={listing.address} />| Prop | Type | Default | What it does |
|---|---|---|---|
images | string[] | Photo URLs. Required. With none, the gallery renders nothing. | |
alt | string | "Listing photo" | The base of each photo's alt text, usually the street address. Photos read "12 Oak St, photo 3 of 18". |
showThumbnails | boolean | true | Show the thumbnail strip. It only appears when there are two or more photos. |
className | string | Added next to re-gallery. |
The gallery takes keyboard focus so the arrow keys work, and shows the browser's focus ring when it has it (keyboard focus only, not mouse clicks). Restyle the ring with .re-gallery:focus-visible (see Styling and performance), but keep one.
The main photo loads eagerly (it's usually the largest thing on the page) and thumbnails load lazily. Photos are plain <img> tags hotlinked from the MLS's image host. Those URLs change as homes sell, so the kit doesn't copy or re-host them, and there's no image optimization step to configure.
ListingSchema
Adds schema.org RealEstateListing JSON-LD to the page, with no client JavaScript. Render it once per listing page.
<ListingSchema
listing={listing}
url={`https://example.com/listings/${listing.slug}`}
siteName="Example Realty"
/>| Prop | Type | What it does |
|---|---|---|
listing | RealEstateListing | The listing. Required. |
url | string | The page's canonical absolute URL. Used as the listing's url and the offer's url. |
siteName | string | Your brokerage's name, used as the provider when the listing has no listing_office_name. |
The markup is built by buildListingJsonLd and escaped so listing text can't close the <script> tag. Formatting and JSON-LD lists every property it sets.
IdxAttribution
The courtesy line and data-freshness timestamp most MLS agreements require on a listing page. Put it on every detail page. It has its own page: IDX compliance.
Revalidation
A detail page with export const revalidate = 60 is rebuilt at most once a minute, and getListing caches its fetch for the same 60 seconds. On a project in live serve mode that keeps the page within a minute or two of the MLS. If your site has Next's Cache Components turned on, leave the revalidate export out (Next rejects it there) and let the fetch windows do the work.