# 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.

```tsx
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:

```tsx
<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 `SiteKitLayout` gives you (see [site-kit's Analytics docs](https://sonor.dev/site-kit/analytics)). 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 `slug` nor `listingKey`**, it sends nothing.

## Read the ranking: getHotListings

```ts
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

```tsx
// 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:

```tsx
// 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 })`.
