docs
    re-site-kit: Search and filters
    v0.6.0.md

    Search and filters

    Everything a listings index needs: searchListings fetches a filtered, sorted page of listings, ListingFilters is the search form, and ListingPagination links between pages. All three render on the server, and the form works with JavaScript off.

    import { searchListings } from '@sonordev/re-site-kit/server'
    import { ListingFilters, ListingGrid, ListingPagination } from '@sonordev/re-site-kit'

    The Quickstart shows them wired together on one page.

    searchListings

    searchListings(
      options?: SearchListingsOptions,
      fetchOptions?: { revalidate?: number },
    ): Promise<ListingSearchResult>

    It calls Sonor's public listings endpoint through site-kit's server fetcher, with your SONOR_API_KEY. Sonor works out the project from the key, so you never pass a project id.

    const { listings, pagination } = await searchListings({
      city: 'Springfield',
      minPrice: 250000,
      bedsMin: 3,
      sort: 'price_asc',
      page: 1,
      limit: 12,
    })

    It returns { listings, pagination }:

    FieldTypeWhat it is
    listingsRealEstateListing[]The page of listings. See Types for every field.
    pagination.pagenumberThe page you got.
    pagination.page_sizenumberListings per page.
    pagination.totalnumber | nullTotal matches, when the backend can count them.
    pagination.total_pagesnumber | nullTotal pages, when the total is known.

    It never throws. If the request fails, or SONOR_API_KEY is missing, you get an empty result (listings: [], page: 1, page_size: 0, total: 0, total_pages: 0) and a warning in the server log, so a render never crashes. If the API answers without pagination, the kit fills it in with your page (or 1), the number of listings returned, and null totals.

    Options

    SearchListingsOptions uses camelCase names. The kit maps each one onto the API's query parameter (the snake_case name you'll see in the URL when ListingFilters submits). Empty strings, null and undefined are left out of the request.

    OptionQuery parameterTypeWhat it does
    qqstringThe search box. See The search box.
    citycitystringCity name. Exact match, case-insensitive.
    zipzipstringZIP code.
    neighborhoodneighborhoodstringNeighborhood name.
    minPricemin_pricenumberLowest list price.
    maxPricemax_pricenumberHighest list price.
    bedsMinbeds_minnumberMinimum bedrooms.
    bathsMinbaths_minnumberMinimum bathrooms.
    propertyTypeproperty_typePropertyTypesingle_family, condo, townhouse, multi_family, land, commercial or other.
    statusstatusListingStatusactive, pending, sold, expired or withdrawn. Leave it out for active listings.
    typetypeListingTypesale, rent or lease.
    buildingbuildingstringA building's registry slug: only listings Sonor matched to it. See Buildings.
    sortsortsee SortResult order.
    pagepagenumberPage number, from 1.
    limitlimitnumberListings per page. See Page size.
    sitesitestringMulti-site projects only: the sub-site host whose listings you want.

    q searches the way people type. It works in both of Sonor's serve modes:

    • An MLS number on its own (123456, MLS 123456 or #123456) finds that listing. A bare five-digit number is also tried as a ZIP.
    • Anything else is words, and every word has to appear somewhere in the address, city, ZIP or neighborhood. "12 Oak St, Springfield" finds "12 Oak Street", because Sonor matches common street abbreviations either way, and filler like "homes for sale in" is ignored.
    • In db serve mode, descriptions are searched too.

    Since every word must match, a search for "3 bedroom pool" usually finds nothing. Point people at the bedroom filter for that and keep the box for places.

    Sort

    ValueOrder
    updatedMost recently updated first. This is what you get when sort is left out.
    newestNewest listings first.
    price_ascPrice, low to high.
    price_descPrice, high to low.

    Page size

    Leave limit out and you get 12 listings per page. In live serve mode a page holds at most 25, whatever you ask for. pagination.page_size always tells you what you actually got.

    Caching

    searchListings is wrapped in React's cache() and its fetch is cached by Next for 60 seconds. Pass a different window as the second argument:

    const result = await searchListings({ city: 'Springfield' }, { revalidate: 300 })

    See Caching and freshness for every fetcher's default.

    ListingFilters

    A plain <form method="get"> whose field names are the API's query parameters. Submitting it reloads the page with the filters in the URL, your page reads searchParams, and the values go straight back into searchListings. No client JavaScript is involved.

    <ListingFilters
      basePath="/listings"
      values={options}
      cities={['Springfield', 'Riverside', 'Fairview']}
      showKeyword
    />
    PropTypeDefaultWhat it does
    basePathstringthe current URLThe form's action, usually your listings page.
    valuesPartial<SearchListingsOptions>{}Current values, so the inputs stay filled in. Uses the camelCase option names (minPrice, not min_price).
    citiesstring[]noneWhen set, City is a <select> of these, with "All cities" first. Otherwise it's a text input.
    showKeywordbooleanfalseShow the search box (q).
    keywordLabelstring"Search"Label for the search box.
    keywordPlaceholderstring"Address, MLS #, city or neighborhood"Placeholder for the search box.
    submitLabelstring"Search"The submit button's text.
    classNamestringnoneAdded to the form next to re-listing-filters.

    The form renders these fields, in this order:

    LabelField nameInput
    Search (with showKeyword)qSearch input
    CitycityText input, or a select when cities is set
    Min pricemin_priceNumber input, steps of 1,000
    Max pricemax_priceNumber input, steps of 1,000
    Bedsbeds_minAny, 1+ to 5+
    Bathsbaths_minAny, 1+ to 3+
    SortsortRecently updated (selected when values.sort is empty), Newest, Price: low to high, Price: high to low

    The sort select always submits a value, so after the first search the URL carries sort=updated even if nobody touched it. That's the same order as leaving sort out.

    Filters it doesn't have

    ZIP, neighborhood, property type, status and building aren't in ListingFilters. If you need them, write your own GET form with the same field names from the options table; searchListings doesn't care which form sent the parameters. Reuse the re-listing-filters classes if you want it to match.

    ListingPagination

    Previous and next links that keep the current filters and swap only the page parameter. They're ordinary <a> tags with rel="prev" and rel="next", so crawlers can follow them.

    <ListingPagination
      pagination={pagination}
      listingCount={listings.length}
      basePath="/listings"
      searchParams={query}
    />
    PropTypeWhat it does
    paginationListingPaginationInfoThe pagination object searchListings returned. Required.
    basePathstringThe page the links point at, e.g. /listings. Required.
    searchParamsRecord<string, string | number | undefined | null>The current query to carry through. Empty values and any existing page are dropped.
    listingCountnumberHow many listings this page holds, listings.length. Used only when the total is unknown; see below.
    classNamestringAdded next to re-listing-pagination.

    Page 1 links without a page parameter, so the first page has one URL. The status line reads "Page 2 of 9 (104 listings)", leaving out whichever numbers the API didn't send. Links that can't go anywhere render as a <span aria-disabled="true"> with the re-listing-pagination__disabled class.

    Next.js gives you searchParams values as string | string[] | undefined, and this prop takes single values, so flatten them first (the Quickstart's readListingSearch returns a flat query for exactly this).

    When the total is unknown. Some live-mode responses can't count matches, so total_pages is null. Pass listingCount and the component goes by the page itself: a full page (as many listings as pagination.page_size) gets a Next link, and a shorter one is treated as the last page. That works on page 1 too. Without listingCount, an unknown total shows a Next link from page 2 on and nothing at all on page 1, so pass it.

    getListings

    The simple list from before search existed. It's still exported for older code; use searchListings for anything with filters or paging.

    getListings(options?: GetListingsOptions): Promise<RealEstateListing[]>
    OptionTypeWhat it does
    sitestringMulti-site projects: the sub-site host.
    statusListingStatusDefaults to active on the server.
    typeListingTypesale, rent or lease.
    limitnumberHow many listings.
    offsetnumberHow many to skip.

    It returns a bare array (empty on any failure) and is cached for 60 seconds; there's no revalidate override.

    Caching and freshness

    Every fetcher in @sonordev/re-site-kit/server is wrapped in React's cache() (one request per render for the same arguments) and sets a Next fetch cache window:

    FetcherDefault windowOverride
    searchListings, getBuildingListings60 secondsfetchOptions.revalidate
    getListing60 secondsfetchOptions.revalidate
    getListings60 secondsnone
    getHotListings5 minutesnone
    getBuildingSummaries5 minutesfetchOptions.revalidate
    getListingParams5 minutesnone

    Your site code is the same in both of Sonor's serve modes. In db mode Sonor serves listings it has synced from the feed. In live mode Sonor answers straight from the MLS through a short cache, so a 60-second window keeps pages within a minute or two of the MLS while most visitors still get a cached page. Either way, the MLS credential stays in Sonor; the site only ever holds SONOR_API_KEY.