# The vacation rental card

> One vacation rental card across every Kismet site — the structured toVacationRentalCardView (price kinds, capacity, location, availability), the badge-skipping cover-photo choice, useVacationRentalCard, the prebuilt Kismet.VacationRentalCard with take/reword/restyle/extend, and Kismet.CanonicalLink.




Every Kismet site shows homes as cards, and every site used to hand-roll the
same card: its own summary adapter, its own capacity string, its own dodge
for badge photos, its own way of carrying the canonical link. The SDK owns
the data side of all of it now. The card implements the [`property-card`
contract](/sdk/contracts) — `kismet check` grades your card the same way it
grades ours.

**The division of labor: the SDK reads data, the site speaks.** The SDK
decides *which* price applies, *which* photo is the cover, and *which* link
a card opens — returned as structured values, never display strings. The
site decides every word ("sleeps 4", "4 beds", "4 camas") and every pixel.

## The structured view

```ts
import { toVacationRentalCardView } from '@kismet-tech/sdk';

const cardView = toVacationRentalCardView(summary, datedPrice?);
```

`summary` is structural: the mapping reads only `slug`, `name`, `capacity`,
`location`, `media`, and `pricing`, so every SDK summary shape
(`VacationRentalSummary`, `VacationRentalDetail`) satisfies it as-is — one
adapter, not two.

`datedPrice` carries the guest's selected stay:

| Shape | Meaning |
| --- | --- |
| `{ status: 'priced', totalMinor, currency, nights }` | The dated stay total, before taxes and fees |
| `{ status: 'unavailable' }` | The home cannot serve the selected dates |
| omitted | No dates are set |

The view has no labels — only structured values:

- `price` — **which** price applies and its numbers: `{ kind:
  'dated-total', amountMinor, currency, nights, beforeTax: true }` (dates
  set), `{ kind: 'from-nightly', amountMinor, currency, beforeTax: true }`
  (no dates), `{ kind: 'unavailable' }` (dates set, home unavailable),
  `{ kind: 'none' }` (nothing known). `beforeTax: true` marks the amount
  as excluding taxes and fees; saying so in words is the site's job.
- `capacity` / `location` — the numbers and location parts with unknown
  fields dropped (`null` when nothing is known).
- `availability` — `'available' | 'unavailable'`.
- `coverPhoto`, `canonicalUrl`, `slug`, `name`.

## The optional English formatters

```ts
import {
  formatCapacity,
  formatVacationRentalCardPrice,
  formatLocation,
  formatNightly,
  formatStayTotal,
} from '@kismet-tech/sdk';
```

The English wordings are OPTIONAL defaults — what the prebuilt card shows
when the site passes no wording — not THE formats. `formatCapacity` →
`"2 bd · 2 ba · sleeps 6"` (unknown fields drop; `null` when nothing is
known), `formatLocation` → `"Government Camp, Oregon"`, `formatNightly` →
`"From $240 a night"`, `formatStayTotal` → `"$840 total · 3 nights"`,
`formatVacationRentalCardPrice(price)` → the default wording per structured kind
(`'unavailable'` → `"Not available for your dates"`, `'none'` → `null`).
Each accepts an optional `{ locale }` (default `en-US`). A site in another
language skips them entirely and formats the structured view itself — same
data, different words.

## Cover photo

```ts
import { chooseVacationRentalCoverPhoto } from '@kismet-tech/sdk';

const coverPhoto = chooseVacationRentalCoverPhoto(summary.media);
```

The hero wins; otherwise the first gallery photo whose `isBadge` is not set.
Flag badge photos once at the data layer (`{ url, alt, isBadge: true }`) and
no card ever leads with an award sticker — the retired alternative was every
site hard-coding `gallery[1]` and hoping. No usable photo? The view's
`coverPhoto` is `undefined` and the prebuilt card renders an
`aria-hidden` placeholder.

## The React hook + prebuilt card

```tsx
import { Kismet } from '@kismet-tech/sdk';

<Kismet.VacationRentalCard summary={summary} datedPrice={datedPrice} />
```

`Kismet.VacationRentalCard` renders semantic, style-free markup — an `article` marked
`data-kismet-contract="property-card"`, the canonical link, the `img`, an
`h3` name, the meta line, the price with its disclosure, and a badges
region. The wiring lives in the L2 `useVacationRentalCard` hook:

- `contractAttributes` — the property-card contract marker + slug;
- `canonicalLinkProps` — `href`, `data-kismet-slug`, the slug field binding,
  and the home name as the accessible label (data, not wording — override
  it on your anchor);
- `nameFieldAttributes` / `priceFieldAttributes` — the auditable field
  markers (the price binds under the `pricing.nightlyFrom` field prefix,
  with the derivation named: `.amountMinor` for the indicative nightly,
  `.stayTotal` for the dated total).

The escalation levels, one contract constant:

- **Take** — render `<Kismet.VacationRentalCard>` as-is; `className` carries your
  look, and the default English wordings apply.
- **Reword** — `format={{ capacity, location, price, locale,
  priceDisclosure }}`: functions over the structured view in any language
  plus your locale and disclosure sentence. The structured view data is
  byte-identical to the English rendering's — two sites, one SDK.
- **Restyle** — `slots={{ media, title, meta, price, badges }}`; every slot
  is a render prop over `{ card }` (the `useVacationRentalCard` result), so a replaced
  region keeps the wiring.
- **Extend** — `badges={…}` adds content inside the badges region.

## CanonicalLink

```tsx
<Kismet.CanonicalLink slug={summary.slug}>{children}</Kismet.CanonicalLink>
```

The markup-free seam behind the card's links: one anchor to
`/properties/{slug}` carrying `data-kismet-slug` and
`data-kismet-field="slug"`, no wrapper markup and no styles. With no
children the slug itself is the visible text. Build a fully custom card on
`useVacationRentalCard` + `CanonicalLink` and the property-card MUSTs stay satisfied
by construction.
