# Formatting and JSON-LD

The helpers the components use to turn a listing into display text and schema.org markup. They're exported so your own layouts can say things the same way. All of them are pure functions from the package root, safe in server and client components alike.

```ts
import {
  formatPrice,
  formatSpecs,
  formatLocation,
  formatStreet,
  buildListingJsonLd,
  safeJsonLdString,
} from '@sonordev/re-site-kit'
```

## Display text

Each takes a `RealEstateListing` and returns a string, empty when there's nothing to show.

| Helper                    | Example output               | How it's built                                                                                                                                                                                              |
| ------------------------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `formatPrice(listing)`    | `$349,900`                   | The feed's own `price_display` when it has one (used as is, e.g. "From the low $300s"). Otherwise `price` as currency in `listing.currency` (USD when missing), with no cents. Empty when there's no price. |
| `formatSpecs(listing)`    | `3 bd · 2.5 ba · 1,850 sqft` | Bedrooms, bathrooms and square feet, joined with " · ". Each part is skipped when it's missing or zero, so a studio shows no bedroom count.                                                                 |
| `formatLocation(listing)` | `Springfield, IL 62701`      | City and state joined with ", ", then the ZIP, skipping whatever's missing.                                                                                                                                 |
| `formatStreet(listing)`   | `12 Oak St #4B`              | The street address, plus ` #` and the unit number when there's one.                                                                                                                                         |

```tsx
<p className="listing-line">
  {formatPrice(listing)} · {formatSpecs(listing)}
  <br />
  {formatStreet(listing)}, {formatLocation(listing)}
</p>
```

Price uses US formatting (`en-US`). Square footage uses the runtime's default number formatting, which on a typical server gives "1,850".

## buildListingJsonLd

```ts
buildListingJsonLd(listing: RealEstateListing, options?: ListingJsonLdOptions): Record<string, unknown>
```

Builds the schema.org `RealEstateListing` object that `ListingSchema` renders. Use it directly when you want to add to the markup or combine it with your own graph.

| Option     | Type     | What it does                                                                                 |
| ---------- | -------- | -------------------------------------------------------------------------------------------- |
| `url`      | `string` | The listing page's canonical absolute URL.                                                   |
| `siteName` | `string` | Your brokerage's name, used as the `provider` when the listing has no `listing_office_name`. |

What it sets, each only when the listing has the value:

| Property      | From                                                                                                                                                                            |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `@type`       | Always `RealEstateListing`.                                                                                                                                                     |
| `name`        | The street address (`formatStreet`).                                                                                                                                            |
| `url`         | `options.url`.                                                                                                                                                                  |
| `datePosted`  | `listing_date`.                                                                                                                                                                 |
| `description` | The first 500 characters of `description`.                                                                                                                                      |
| `image`       | Up to eight photos.                                                                                                                                                             |
| `offers`      | An `Offer` with `price` and `priceCurrency` (USD when missing), `availability: https://schema.org/InStock` for active listings only, and `url`. Left out when there's no price. |
| `provider`    | A `RealEstateAgent` named after `listing_office_name`, or `options.siteName`.                                                                                                   |
| `about`       | The home itself: see below.                                                                                                                                                     |

`about` describes the property, with its `name` (the street address), a `PostalAddress` (street, city, state, ZIP, and country, which defaults to `US`), and `geo` coordinates when the listing has both latitude and longitude. Its `@type` comes from the property type, and the type decides which of the home's facts it carries, since schema.org only defines `numberOfBedrooms`, `numberOfFullBathrooms`, `yearBuilt` and `floorSize` (in square feet) on some types:

| `property_type`  | `about['@type']`        | Facts it carries        |
| ---------------- | ----------------------- | ----------------------- |
| `single_family`  | `SingleFamilyResidence` | All four                |
| `condo`          | `Apartment`             | All four                |
| `townhouse`      | `House`                 | All four                |
| `multi_family`   | `ApartmentComplex`      | `numberOfBedrooms` only |
| `land`           | `Place`                 | None                    |
| `commercial`     | `Place`                 | None                    |
| `other`, or none | `Accommodation`         | All four                |

A few of these are judgment calls, because schema.org doesn't have a type for everything an MLS does:

- **Land is a `Place`.** schema.org has no type for a lot, and a lot isn't an `Accommodation`, the type homes and apartments belong to.
- **A townhouse is a `House`.** There's no townhouse type. `SingleFamilyResidence` is a kind of `House` too, but MLS feeds list townhouses separately from single-family homes, so the markup doesn't claim the narrower type.
- **A listing with no type is an `Accommodation`**, the type that `House` and `Apartment` both come from, so its bedrooms and square footage still make it into the markup.

## safeJsonLdString

```ts
safeJsonLdString(value: Record<string, unknown>): string
```

Serializes JSON-LD for a `<script>` tag, escaping every `<` so text inside a listing can never close the script element. Use it whenever you render JSON-LD yourself:

```tsx
import { buildListingJsonLd, safeJsonLdString, type RealEstateListing } from '@sonordev/re-site-kit'

export function MyListingSchema({ listing, url }: { listing: RealEstateListing; url: string }) {
  const jsonLd = buildListingJsonLd(listing, { url, siteName: 'Example Realty' })
  jsonLd.mainEntityOfPage = url
  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: safeJsonLdString(jsonLd) }}
    />
  )
}
```

## Metadata and stats

Two more helpers live with the features they belong to: `listingMetadata` (Next.js metadata for a listing page) in [Listing pages](https://sonor.dev/re-site-kit/listing-pages#listingmetadata), and `summarizeListings` and `monthlyHoa` in [Market snapshot](https://sonor.dev/re-site-kit/market-snapshot).
