# Quickstart

This page wires a listings index and a listing detail page end to end: search, filters, paging, a detail view with a photo gallery, schema.org markup, IDX attribution, an inquiry form, and the view tracking that powers hot properties. Every file is complete, so you can paste them in order.

## Before you start

- **site-kit is mounted.** `@sonordev/site-kit` 4.3.0 or later is installed and `SiteKitLayout` is in `app/layout.tsx` (see [site-kit's Layout docs](https://sonor.dev/site-kit/layout)). re-site-kit's fetchers, analytics and forms all run through it.
- **`SONOR_API_KEY` is set** in `.env.local` and in your host's environment. It's the same key site-kit reads, and it never reaches the browser through this kit.
- **Your Sonor project has listings.** The Real Estate module is on and a feed is connected, so the API has something to return.

If the key is missing, every fetcher logs a warning and returns an empty result (or `null` for a single listing) instead of throwing, so the pages still render, just with no listings.

## 1. Install

```bash
pnpm add @sonordev/re-site-kit
```

## 2. Read the search parameters

`ListingFilters` submits a plain GET form, so the filters arrive as URL parameters in snake\_case (`min_price`, `beds_min`). `searchListings` takes camelCase options. This small helper does the translation once, and also flattens the parameters for `ListingPagination`, which carries them from page to page:

```ts
// lib/listing-search.ts
import type { SearchListingsOptions } from '@sonordev/re-site-kit'

export type SearchParams = Record<string, string | string[] | undefined>

const SORTS = ['price_asc', 'price_desc', 'newest', 'updated'] as const

function first(value: string | string[] | undefined): string | undefined {
  const v = Array.isArray(value) ? value[0] : value
  return v?.trim() || undefined
}

function positive(value: string | string[] | undefined): number | undefined {
  const n = Number(first(value))
  return Number.isFinite(n) && n > 0 ? n : undefined
}

/**
 * Turns the page's searchParams into searchListings options, plus the flat
 * query ListingPagination carries from page to page.
 */
export function readListingSearch(params: SearchParams) {
  const sort = first(params.sort)
  const options: SearchListingsOptions = {
    q: first(params.q),
    city: first(params.city),
    minPrice: positive(params.min_price),
    maxPrice: positive(params.max_price),
    bedsMin: positive(params.beds_min),
    bathsMin: positive(params.baths_min),
    sort: SORTS.find((s) => s === sort),
    page: Math.floor(positive(params.page) ?? 1),
    limit: 12,
  }
  const query = Object.fromEntries(
    Object.entries(params).map(([key, value]) => [key, first(value)]),
  )
  return { options, query }
}
```

Because `options` uses the same shape as `SearchListingsOptions`, you can hand it straight to `ListingFilters` as `values` to keep the inputs filled in after a search.

## 3. The listings index

```tsx
// app/listings/page.tsx
import type { Metadata } from 'next'
import { searchListings } from '@sonordev/re-site-kit/server'
import { ListingFilters, ListingGrid, ListingPagination } from '@sonordev/re-site-kit'
import { readListingSearch, type SearchParams } from '@/lib/listing-search'

export const metadata: Metadata = { title: 'Homes for sale | Example Realty' }

export default async function ListingsPage({
  searchParams,
}: {
  searchParams: Promise<SearchParams>
}) {
  const { options, query } = readListingSearch(await searchParams)
  const { listings, pagination } = await searchListings(options)

  return (
    <main>
      <h1>Homes for sale</h1>
      <ListingFilters basePath="/listings" values={options} showKeyword />
      <ListingGrid
        listings={listings}
        hrefFor={(listing) => (listing.slug ? `/listings/${listing.slug}` : null)}
        emptyState={<p>No homes match those filters. Try a wider price range.</p>}
      />
      <ListingPagination
        pagination={pagination}
        listingCount={listings.length}
        basePath="/listings"
        searchParams={query}
      />
    </main>
  )
}
```

The page reads `searchParams`, so Next renders it per request. `listingCount` lets the pagination offer a next page even when the total is unknown, which happens on some live-mode responses. The listing fetch itself is cached for 60 seconds, so most requests never wait on the API. `showKeyword` adds the search box, which takes an address, an MLS number, a city or a neighborhood. [Search and filters](https://sonor.dev/re-site-kit/search) covers every option.

## 4. The listing detail page

```tsx
// app/listings/[slug]/page.tsx
import type { Metadata } from 'next'
import { notFound } from 'next/navigation'
import { getListing, getListingParams, listingMetadata } from '@sonordev/re-site-kit/server'
import { IdxAttribution, ListingDetail, ListingSchema } from '@sonordev/re-site-kit'
import {
  ListingGallery,
  ListingInquiryForm,
  ListingViewTracker,
} from '@sonordev/re-site-kit/client'

const SITE_URL = 'https://example.com'

type Props = { params: Promise<{ slug: string }> }

export const revalidate = 60

export async function generateStaticParams() {
  return getListingParams()
}

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: `${SITE_URL}/listings/${slug}`,
  })
}

export default async function ListingPage({ params }: Props) {
  const { slug } = await params
  const listing = await getListing(slug)
  if (!listing) notFound()

  return (
    <main>
      <ListingSchema
        listing={listing}
        url={`${SITE_URL}/listings/${slug}`}
        siteName="Example Realty"
      />
      <ListingGallery images={listing.images ?? []} alt={listing.address} />
      {/* The gallery shows the photos, so leave them out of the detail view. */}
      <ListingDetail listing={listing} showPhotos={false} />
      <IdxAttribution listing={listing} />
      <ListingInquiryForm
        formId="listing-inquiry"
        listing={{
          slug: listing.slug,
          source_listing_id: listing.source_listing_id,
          address: listing.address,
          price: listing.price,
        }}
      />
      <ListingViewTracker slug={listing.slug} listingKey={listing.source_listing_id} />
    </main>
  )
}
```

A few things worth knowing about this page:

- `getListing(slug)` runs in both `generateMetadata` and the page, but it's wrapped in React's `cache()`, so it's one request.
- `ListingDetail` renders the listing's `<h1>`, so the page doesn't add another.
- `ListingInquiryForm` is a client component. Passing it only the four fields it needs keeps the rest of the listing out of the page's client payload.
- `ListingViewTracker` renders nothing. It's a childless sibling, never a wrapper, so it can't turn the page into a client-rendered one.

If your site has Next's Cache Components turned on, leave out the `revalidate` export: Next rejects route segment config there, and the kit's fetches carry their own revalidate times. [Listing pages](https://sonor.dev/re-site-kit/listing-pages) has the details on each piece.

## 5. Set up the inquiry form in Sonor

`formId="listing-inquiry"` points at a managed form in your Sonor project. Create a `prospect` form with that slug, and add three hidden fields with the slugs `listing_slug`, `listing_key` and `listing_address` so each lead lands in the CRM attached to its listing. [Inquiry forms](https://sonor.dev/re-site-kit/inquiry-forms) walks through it.

## 6. Check it

Build and start the site, then confirm the pages are server-rendered with real content (not just the RSC payload):

```bash
# How many listing cards are in the HTML
curl -s http://localhost:3000/listings | grep -o 'class="re-listing-card"' | wc -l
# The detail page's heading (use a slug from the index)
curl -s http://localhost:3000/listings/<slug> | grep -o '<h1[^>]*>[^<]*'
```

Zero cards on a project with listings usually means `SONOR_API_KEY` isn't set where the server can read it. Check the server log for a `Missing SONOR_API_KEY` warning.

## Next steps

- Put a [trending homes](https://sonor.dev/re-site-kit/hot-listings) section on the home page.
- Condo site? Give each building page its listings with [Buildings and the registry](https://sonor.dev/re-site-kit/buildings).
- Let a buyer's AI assistant search your listings and ask for a showing with the [MCP tools](https://sonor.dev/re-site-kit/mcp).
- Restyle everything with the class hooks in [Styling and performance](https://sonor.dev/re-site-kit/styling).
