Hot properties
Show the homes people are looking at most. ListingViewTracker counts a view each time someone opens a listing page, Sonor ranks the listings from those views, and getHotListings reads the ranking back for a "Trending this week" section. The same numbers power the Hot Properties view in the Sonor dashboard.
import { getHotListings } from '@sonordev/re-site-kit/server'
import { ListingViewTracker } from '@sonordev/re-site-kit/client'Count views: ListingViewTracker
Render it on the listing detail page, as a sibling of the page content:
<ListingViewTracker slug={listing.slug} listingKey={listing.source_listing_id} />| Prop | Type | Default | What it does |
|---|---|---|---|
slug | string | null | The listing's slug. The preferred identifier. | |
listingKey | string | null | The MLS number (source_listing_id), used when there's no slug. | |
enabled | boolean | true | Set false to stop tracking, for example on a preview route. |
On mount it sends one listing_view event (category real_estate) through site-kit's analytics, with listing_slug and listing_key as properties. That means the same transport, visitor identity, batching and spam handling as every other event on the site, with no extra beacon. Sonor files these events under the Real Estate module rather than your general event log.
A few details:
- It renders nothing (
null) and never wraps content, so it can't affect how the page renders or prerenders. Keep it that way: don't put page content inside it or inside any provider. - It needs site-kit analytics on the page, which
SiteKitLayoutgives you (see site-kit's Analytics docs). If analytics loads late, the event waits for it, so deferred analytics still catch the view. - It doesn't double count. A second mount for the same listing on the same path within 30 seconds (React strict mode, a quick re-render) is skipped.
- With neither
slugnorlistingKey, it sends nothing.
Read the ranking: getHotListings
getHotListings(options?: GetHotListingsOptions): Promise<HotListingEntry[]>| Option | Type | Default | What it does |
|---|---|---|---|
window | string | '7d' | How far back to count: '24h', '7d', '30d', or a number of hours such as '48'. The longest window is 90 days. |
limit | number | 10 | How many listings. At most 50. |
site | string | Multi-site projects: the sub-site host. |
Only active listings are ranked. Each entry:
| Field | Type | What it is |
|---|---|---|
listing | RealEstateListing | The listing. |
views | number | Views in the window. |
unique_viewers | number | Distinct visitors in the window. |
prior_views | number | Views in the window before this one, for comparison. |
trend_pct | number | null | Percent change from the prior window, as a whole number (25 means up 25%). null when there's no prior data. |
last_viewed_at | string | null | When the listing was last viewed (ISO 8601). |
The result is cached for 5 minutes. It returns [] on any failure, and also when nothing has been viewed yet, so always handle the empty case.
A trending section
// components/TrendingHomes.tsx
import { getHotListings } from '@sonordev/re-site-kit/server'
import { ListingGrid } from '@sonordev/re-site-kit'
export async function TrendingHomes() {
const hot = await getHotListings({ window: '7d', limit: 6 })
if (!hot.length) return null
return (
<section>
<h2>Trending this week</h2>
<ListingGrid
listings={hot.map((entry) => entry.listing)}
hrefFor={(listing) => (listing.slug ? `/listings/${listing.slug}` : null)}
/>
</section>
)
}It's a server component, so the section arrives as HTML with no client JavaScript. Drop <TrendingHomes /> on the home page or a listings index.
Showing the numbers
To show view counts or the trend, render the cards yourself:
// components/MostViewed.tsx
import { getHotListings } from '@sonordev/re-site-kit/server'
import { ListingCard } from '@sonordev/re-site-kit'
export async function MostViewed() {
const hot = await getHotListings({ window: '24h', limit: 4 })
if (!hot.length) return null
return (
<ol className="most-viewed">
{hot.map(({ listing, views, trend_pct }) => (
<li key={listing.id}>
<ListingCard listing={listing} href={listing.slug ? `/listings/${listing.slug}` : null} />
<p>
{views} views in the last day
{trend_pct != null ? ` (${trend_pct > 0 ? '+' : ''}${trend_pct}%)` : ''}
</p>
</li>
))}
</ol>
)
}A brand-new site has no views to rank, so a trending section stays hidden until people start opening listing pages. Place it where an empty space won't look odd, or fall back to searchListings({ sort: 'newest', limit: 6 }).