@stack-mode/site-manager-kit 1.44.0 → 1.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +618 -606
- package/audit/ts-hooks.mjs +68 -68
- package/audit/wiring-audit.d.mts +37 -37
- package/audit/wiring-audit.mjs +604 -604
- package/bin/site-manager-kit.mjs +10 -10
- package/package.json +73 -72
- package/src/announcements.ts +13 -13
- package/src/attribution-fields.ts +52 -52
- package/src/attribution.tsx +47 -47
- package/src/blog.ts +122 -122
- package/src/chat-route.ts +136 -136
- package/src/chat-types.ts +76 -76
- package/src/client.ts +116 -116
- package/src/config.ts +78 -78
- package/src/copy-keys-route.ts +21 -21
- package/src/copy-keys.ts +77 -77
- package/src/data.ts +189 -189
- package/src/downloads.ts +7 -7
- package/src/faq.ts +21 -21
- package/src/floor-plans.ts +45 -45
- package/src/form-guard.tsx +37 -37
- package/src/form-types.ts +13 -13
- package/src/forms.ts +76 -76
- package/src/glossary.ts +19 -19
- package/src/health-route.ts +34 -34
- package/src/index.ts +36 -36
- package/src/indexnow.ts +40 -40
- package/src/listings.ts +38 -38
- package/src/llms-route.ts +45 -0
- package/src/metadata.ts +55 -55
- package/src/not-found-reporter.tsx +16 -16
- package/src/not-found.ts +53 -53
- package/src/payment.ts +70 -70
- package/src/phone-input.tsx +26 -26
- package/src/phone.ts +37 -37
- package/src/preview-route.ts +48 -48
- package/src/preview.ts +94 -94
- package/src/residents.ts +23 -23
- package/src/revalidate-route.ts +34 -34
- package/src/robots.ts +5 -1
- package/src/rss-route.ts +26 -26
- package/src/services.ts +52 -52
- package/src/site-chat-panel.tsx +537 -537
- package/src/site-chat.tsx +26 -26
- package/src/sitemap.ts +26 -26
- package/src/structured-data.tsx +207 -207
- package/src/types.ts +581 -580
- package/src/version.ts +1 -1
- package/src/visual-accents.ts +126 -126
- package/src/visual-bridge-client.tsx +1070 -1070
- package/src/visual-bridge.tsx +30 -30
- package/src/visual-clicks.ts +13 -13
- package/src/visual-lists.ts +37 -37
- package/src/visual-match.ts +77 -77
- package/src/visual-messages.ts +160 -160
- package/src/visual-mode.ts +18 -18
- package/src/visual-preview.ts +69 -69
- package/src/visual-tags.ts +90 -90
- package/src/visual.ts +59 -59
- package/src/waitlist-form.ts +39 -39
- package/src/waitlist.ts +48 -48
package/src/types.ts
CHANGED
|
@@ -1,580 +1,581 @@
|
|
|
1
|
-
// Site Manager API v1 response shapes. Money is always a decimal string like "1450.00".
|
|
2
|
-
|
|
3
|
-
export type Money = string;
|
|
4
|
-
export type Hours = Record<"mon" | "tue" | "wed" | "thu" | "fri" | "sat" | "sun", { closed: true } | { closed: false; open: string; close: string } | { closed: false; byAppointment: true }>;
|
|
5
|
-
export type Image = { url: string; width: number | null; height: number | null; alt: string };
|
|
6
|
-
export type Address = { line1: string; line2: string; city: string; region: string; postalCode: string; country: string };
|
|
7
|
-
|
|
8
|
-
export type Site = {
|
|
9
|
-
name: string;
|
|
10
|
-
kind: "apartment" | "business" | "real_estate";
|
|
11
|
-
websiteUrl: string | null;
|
|
12
|
-
timezone: string;
|
|
13
|
-
modules: string[];
|
|
14
|
-
business: {
|
|
15
|
-
displayName: string;
|
|
16
|
-
legalName: string | null;
|
|
17
|
-
phone: string | null;
|
|
18
|
-
email: string | null;
|
|
19
|
-
address: Address;
|
|
20
|
-
geo: { lat: number; lng: number } | null;
|
|
21
|
-
hours: Hours | null;
|
|
22
|
-
hoursNote: string | null;
|
|
23
|
-
social: { network: string; url: string }[];
|
|
24
|
-
showEqualHousing: boolean | null;
|
|
25
|
-
// Search and maps facts (kit 1.42.0). Optional, so a site still builds against an older Site Manager; null or
|
|
26
|
-
// empty means not set.
|
|
27
|
-
/** A schema.org LocalBusiness type like "Plumber" or "ApartmentComplex". Null means LocalBusiness. */
|
|
28
|
-
schemaType?: string | null;
|
|
29
|
-
/** The Google Business Profile's main category, as typed there. */
|
|
30
|
-
primaryCategory?: string | null;
|
|
31
|
-
secondaryCategories?: string[];
|
|
32
|
-
/** Cities, towns or neighborhoods the business works in. */
|
|
33
|
-
serviceAreas?: string[];
|
|
34
|
-
priceRange?: string | null;
|
|
35
|
-
foundedYear?: number | null;
|
|
36
|
-
/** The Google Business Profile Place ID. */
|
|
37
|
-
googlePlaceId?: string | null;
|
|
38
|
-
};
|
|
39
|
-
/** Visitor tracking, set by the agency in Site Manager. Missing from Site Manager versions before it existed. */
|
|
40
|
-
tracking?: { provider: "stackos"; siteKey: string } | null;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
/** A link from Site Manager's Pages. `href` is null when the record it points to is hidden or gone: skip the link. */
|
|
44
|
-
export type CopyLink = { href: string | null; label: string };
|
|
45
|
-
|
|
46
|
-
/** `pagePath` is the page the value belongs to in Site Manager's Pages, or null when it's shared across pages. */
|
|
47
|
-
export type CopyValue = (
|
|
48
|
-
// accents: a one-field heading's gold words as [start, end] ranges of text; headingRuns() turns them into runs.
|
|
49
|
-
// strong: words marked important (Bold), rendered <strong>; em: words emphasized (Italic), rendered <em>;
|
|
50
|
-
// textRuns() turns every mark into runs.
|
|
51
|
-
| { kind: "text"; text: string | null; accents?: [number, number][]; strong?: [number, number][]; em?: [number, number][] }
|
|
52
|
-
| { kind: "rich"; html: string | null }
|
|
53
|
-
| { kind: "image"; image: Image | null }
|
|
54
|
-
| { kind: "link"; link: CopyLink | null }
|
|
55
|
-
| { kind: "records"; module: string | null; items: unknown[] }
|
|
56
|
-
) & { pagePath?: string | null };
|
|
57
|
-
|
|
58
|
-
/** A review platform from Reviews > Platforms. `url` is its review link, or null when the site has none. */
|
|
59
|
-
export type ReviewPlatform = { key: string; label: string; url: string | null; logo: Image | null };
|
|
60
|
-
|
|
61
|
-
export type Review = {
|
|
62
|
-
id: string;
|
|
63
|
-
authorName: string;
|
|
64
|
-
/** Where the reviewer is from, such as a city. */
|
|
65
|
-
authorLocation: string | null;
|
|
66
|
-
rating: number | null;
|
|
67
|
-
body: string;
|
|
68
|
-
/** A short version for cards, in the reviewer's own words. */
|
|
69
|
-
excerpt: string | null;
|
|
70
|
-
reviewerType: "client" | "colleague";
|
|
71
|
-
source: string;
|
|
72
|
-
/** The review's platform with its logo, or null for one entered by hand or not in the site's list. */
|
|
73
|
-
platform: ReviewPlatform | null;
|
|
74
|
-
sourceUrl: string | null;
|
|
75
|
-
reviewedOn: string | null;
|
|
76
|
-
response: string | null;
|
|
77
|
-
isFeatured: boolean;
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
export type FloorPlan = {
|
|
81
|
-
slug: string;
|
|
82
|
-
name: string;
|
|
83
|
-
beds: number;
|
|
84
|
-
baths: string;
|
|
85
|
-
sqftMin: number | null;
|
|
86
|
-
sqftMax: number | null;
|
|
87
|
-
startingRent: Money | null;
|
|
88
|
-
maxRent: Money | null;
|
|
89
|
-
deposit: Money | null;
|
|
90
|
-
availableCount: number | null;
|
|
91
|
-
availabilityUpdatedAt: string | null;
|
|
92
|
-
descriptionHtml: string;
|
|
93
|
-
tourUrl: string | null;
|
|
94
|
-
images: (Image & { kind: "plan" | "photo" })[];
|
|
95
|
-
source: "manual" | "appfolio" | "yardi" | "rent_roll";
|
|
96
|
-
// The fields below are missing from Site Manager versions before they existed, so they're optional.
|
|
97
|
-
/** What to show about availability. `count` is null unless the site chose to show exact numbers. Prefer this over availableCount. */
|
|
98
|
-
availability?: Availability;
|
|
99
|
-
/** Short feature labels for comparing plans, for example "Washer and dryer". */
|
|
100
|
-
features?: string[];
|
|
101
|
-
layout?: "flat" | "townhome" | "loft" | "penthouse" | null;
|
|
102
|
-
/** Floors inside the home. */
|
|
103
|
-
levels?: number | null;
|
|
104
|
-
/** The running special that gives this plan its lowest effective monthly rent. */
|
|
105
|
-
concession?: FloorPlanConcession | null;
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
export type Availability = { status: "available" | "none" | "unknown"; count: number | null };
|
|
109
|
-
|
|
110
|
-
export type FloorPlanConcession = { specialId: string; title: string; effectiveRent: Money; baseRent: Money; leaseMonths: number | null };
|
|
111
|
-
|
|
112
|
-
export type Concession = { kind: "weeks_free" | "months_free" | "amount_off" | "monthly_discount"; value: string; leaseMonths: number | null };
|
|
113
|
-
|
|
114
|
-
export type Unit = { unitNumber: string; building: string | null; floor: number | null; sqft: number | null; rent: Money | null; availableOn: string | null; status: "available" | "on_notice" };
|
|
115
|
-
|
|
116
|
-
export type Amenity = { id: string; name: string; category: "community" | "apartment"; description: string | null; icon: string | null; image: Image | null };
|
|
117
|
-
|
|
118
|
-
export type Fee = { id: string; name: string; category: string; frequency: "one_time" | "monthly" | "per_occurrence"; amount: Money | null; amountNote: string | null; isRequired: boolean; notes: string | null };
|
|
119
|
-
|
|
120
|
-
export type Special = { id: string; title: string; detailsHtml: string; startsOn: string | null; endsOn: string | null; floorPlans: string[]; concession?: Concession | null };
|
|
121
|
-
|
|
122
|
-
export type PetRule = {
|
|
123
|
-
id: string;
|
|
124
|
-
petType: "dog" | "cat" | "bird" | "fish" | "small_animal" | "reptile" | "other";
|
|
125
|
-
allowed: boolean;
|
|
126
|
-
maxCount: number | null;
|
|
127
|
-
maxWeightLbs: number | null;
|
|
128
|
-
breedRules: string | null;
|
|
129
|
-
deposit: Money | null;
|
|
130
|
-
fee: Money | null;
|
|
131
|
-
monthlyRent: Money | null;
|
|
132
|
-
notes: string | null;
|
|
133
|
-
};
|
|
134
|
-
export type PetPolicy = { maxPets: number | null; assistanceNote: string | null; notes: string | null; pets: PetRule[] };
|
|
135
|
-
|
|
136
|
-
export type ParkingOption = {
|
|
137
|
-
id: string;
|
|
138
|
-
name: string;
|
|
139
|
-
kind: "garage" | "covered" | "carport" | "surface" | "street" | "tandem" | "other";
|
|
140
|
-
price: Money | null;
|
|
141
|
-
frequency: "monthly" | "one_time" | "included";
|
|
142
|
-
/** Null means the site hasn't said, never "no". */
|
|
143
|
-
assigned: boolean | null;
|
|
144
|
-
evCharging: boolean | null;
|
|
145
|
-
notes: string | null;
|
|
146
|
-
};
|
|
147
|
-
export type Parking = { guestNote: string | null; notes: string | null; options: ParkingOption[] };
|
|
148
|
-
|
|
149
|
-
export type Weekday = "mon" | "tue" | "wed" | "thu" | "fri" | "sat" | "sun";
|
|
150
|
-
|
|
151
|
-
export type UtilityProvider = {
|
|
152
|
-
utility: "electric" | "gas" | "water" | "sewer" | "trash" | "internet" | "cable" | "other";
|
|
153
|
-
provider: string;
|
|
154
|
-
phone: string | null;
|
|
155
|
-
url: string | null;
|
|
156
|
-
/** Who arranges it. Null means the site hasn't said. */
|
|
157
|
-
setupBy: "resident" | "property" | null;
|
|
158
|
-
/** When to set it up, in the site's words. */
|
|
159
|
-
when: string | null;
|
|
160
|
-
notes: string | null;
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
export type Pickup = {
|
|
164
|
-
stream: "trash" | "recycling" | "compost" | "bulk" | "other";
|
|
165
|
-
/** In week order. */
|
|
166
|
-
days: Weekday[];
|
|
167
|
-
/** 24-hour "HH:MM", or null when there's no set time. */
|
|
168
|
-
time: string | null;
|
|
169
|
-
notes: string | null;
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
export type ResidentDocument = { id: string; name: string; url: string; mimeType: string; sizeBytes: number };
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* One section of the residents page. Only visible sections with something in them are sent. The detail matching
|
|
176
|
-
* the section's kind is set; the others are null.
|
|
177
|
-
*/
|
|
178
|
-
export type ResidentSection = {
|
|
179
|
-
id: string;
|
|
180
|
-
kind: "move_in" | "rules" | "utilities" | "trash" | "maintenance" | "rent_payment" | "documents" | "custom";
|
|
181
|
-
title: string;
|
|
182
|
-
html: string;
|
|
183
|
-
utilities: UtilityProvider[] | null;
|
|
184
|
-
trash: Pickup[] | null;
|
|
185
|
-
maintenance: { requestUrl: string | null; phone: string | null; email: string | null; emergencyPhone: string | null; emergencyNote: string | null } | null;
|
|
186
|
-
rentPayment: { portalUrl: string | null; portalLabel: string | null } | null;
|
|
187
|
-
documents: ResidentDocument[];
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
/** Leasing policies. Every fact is null (or an empty list) until the site enters it. */
|
|
191
|
-
export type Policies = {
|
|
192
|
-
application: {
|
|
193
|
-
criteriaHtml: string;
|
|
194
|
-
/** Monthly income as a multiple of the rent, like "3.00". */
|
|
195
|
-
incomeMultiple: string | null;
|
|
196
|
-
incomeNote: string | null;
|
|
197
|
-
creditNote: string | null;
|
|
198
|
-
backgroundNote: string | null;
|
|
199
|
-
fee: Money | null;
|
|
200
|
-
feeNote: string | null;
|
|
201
|
-
holdingFee: Money | null;
|
|
202
|
-
holdingFeeNote: string | null;
|
|
203
|
-
};
|
|
204
|
-
/** One amount when `max` is null, otherwise a range. */
|
|
205
|
-
deposit: { min: Money | null; max: Money | null; note: string | null };
|
|
206
|
-
lease: { termMonths: number[]; note: string | null };
|
|
207
|
-
smoking: { policy: "not_allowed" | "designated_areas" | "allowed" | null; note: string | null };
|
|
208
|
-
guestPolicy: string | null;
|
|
209
|
-
rentersInsurance: { requirement: "required" | "recommended" | "not_required" | null; minLiability: Money | null; note: string | null };
|
|
210
|
-
accessibilityStatement: string | null;
|
|
211
|
-
fairHousingStatement: string | null;
|
|
212
|
-
extra: { id: string; title: string; html: string }[];
|
|
213
|
-
/** Whether pet and parking rules have their own sections on this site, to link to instead of repeating. */
|
|
214
|
-
related: { petPolicy: boolean; parking: boolean };
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
export type Event = {
|
|
218
|
-
slug: string;
|
|
219
|
-
title: string;
|
|
220
|
-
startsAt: string;
|
|
221
|
-
endsAt: string | null;
|
|
222
|
-
startsAtLocal: string;
|
|
223
|
-
endsAtLocal: string | null;
|
|
224
|
-
timezone: string;
|
|
225
|
-
allDay: boolean;
|
|
226
|
-
locationText: string | null;
|
|
227
|
-
descriptionHtml: string;
|
|
228
|
-
image: Image | null;
|
|
229
|
-
linkUrl: string | null;
|
|
230
|
-
linkLabel: string | null;
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
export type ServicePriceKind = "none" | "fixed" | "range" | "starting_at" | "call";
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* A service a business offers. `price.from` and `price.to` are decimal strings ("150.00"), set only for the kinds
|
|
237
|
-
* that use them; `price.label` is ready to show ("Starting at $150", "Call for pricing") and null for "none".
|
|
238
|
-
*/
|
|
239
|
-
export type Service = {
|
|
240
|
-
slug: string;
|
|
241
|
-
name: string;
|
|
242
|
-
summary: string | null;
|
|
243
|
-
descriptionHtml: string;
|
|
244
|
-
category: string | null;
|
|
245
|
-
price: { kind: ServicePriceKind; from: string | null; to: string | null; note: string | null; label: string | null };
|
|
246
|
-
durationText: string | null;
|
|
247
|
-
/** In the order set in Site Manager; the first is the main photo. */
|
|
248
|
-
photos: Image[];
|
|
249
|
-
updatedAt: string;
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
export type Location = { slug: string; name: string; phone: string | null; email: string | null; address: Address; geo: { lat: number; lng: number } | null; hours: Hours | null; isPrimary: boolean };
|
|
253
|
-
|
|
254
|
-
export type NearbyPlace = {
|
|
255
|
-
id: string;
|
|
256
|
-
name: string;
|
|
257
|
-
category: string;
|
|
258
|
-
address: string | null;
|
|
259
|
-
distanceText: string | null;
|
|
260
|
-
url: string | null;
|
|
261
|
-
geo: { lat: number; lng: number } | null;
|
|
262
|
-
description: string | null;
|
|
263
|
-
walkMinutes?: number | null;
|
|
264
|
-
driveMinutes?: number | null;
|
|
265
|
-
/** Bus or train, door to door. Mostly set for places in the `commute` category (employers, campuses, the airport). */
|
|
266
|
-
transitMinutes?: number | null;
|
|
267
|
-
school?: { level: "preschool" | "elementary" | "middle" | "high" | "k12" | "college" | null; grades: string | null; district: string | null; type: "public" | "private" | "charter" | null } | null;
|
|
268
|
-
};
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* A drawing of the property (a site plan or a building) with its open units placed on it. `x` and `y` are percentages
|
|
272
|
-
* of the image's width and height from its top left, so pins stay put at any size. Leased units are never included.
|
|
273
|
-
*/
|
|
274
|
-
export type UnitMap = {
|
|
275
|
-
id: string;
|
|
276
|
-
name: string;
|
|
277
|
-
image: Image;
|
|
278
|
-
units: { unitNumber: string; building: string | null; floorPlan: string; status: "available" | "on_notice"; rent: Money | null; availableOn: string | null; x: number; y: number }[];
|
|
279
|
-
};
|
|
280
|
-
|
|
281
|
-
export type FormField = { name: string; label: string; type: "text" | "email" | "phone" | "textarea" | "select" | "radio" | "checkbox" | "date" | "number" | "hidden"; required: boolean; options?: string[]; maxLength: number; /** A short line to show under the field. */ help?: string };
|
|
282
|
-
export type Form = {
|
|
283
|
-
key: string;
|
|
284
|
-
/** From Site Manager 2026-11 on. A booking form also needs one of getBookingSlots' times, sent as the bp_slot field. */
|
|
285
|
-
kind?: "standard" | "booking";
|
|
286
|
-
name: string;
|
|
287
|
-
description: string | null;
|
|
288
|
-
fields: FormField[];
|
|
289
|
-
successMessage: string;
|
|
290
|
-
booking?: { slotMinutes: number; timezone: string };
|
|
291
|
-
};
|
|
292
|
-
export type BookingSlot = { start: string; end: string; label: string };
|
|
293
|
-
export type BookingDays = { timezone: string; slotMinutes: number; days: { date: string; slots: BookingSlot[] }[] };
|
|
294
|
-
|
|
295
|
-
export type LegalSummary = { kind: string; title: string; slug: string; effectiveDate: string | null };
|
|
296
|
-
export type LegalDoc = LegalSummary & { html: string };
|
|
297
|
-
|
|
298
|
-
export type PageMeta = {
|
|
299
|
-
path: string;
|
|
300
|
-
title: string;
|
|
301
|
-
description: string | null;
|
|
302
|
-
canonical: string | null;
|
|
303
|
-
noindex: boolean;
|
|
304
|
-
ogImage: Image | null;
|
|
305
|
-
siteName: string;
|
|
306
|
-
titleSuffix: string | null;
|
|
307
|
-
// From Site Manager's share and robots settings (kit 1.43.0). Optional, so a site still builds against an older
|
|
308
|
-
// Site Manager. Null share text uses the title and description.
|
|
309
|
-
ogTitle?: string | null;
|
|
310
|
-
ogDescription?: string | null;
|
|
311
|
-
nofollow?: boolean;
|
|
312
|
-
maxImagePreview?: "large" | "standard" | "none";
|
|
313
|
-
};
|
|
314
|
-
export type SitemapEntry = { url: string; lastModified: string };
|
|
315
|
-
|
|
316
|
-
export type
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
export type
|
|
324
|
-
export type
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
badge
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
//
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
export type
|
|
397
|
-
export type
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
area
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
export type
|
|
454
|
-
export type
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
export type
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
*
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
};
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
export type
|
|
507
|
-
export type
|
|
508
|
-
export type
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
};
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
| { ok:
|
|
543
|
-
| { ok: false; error: "
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
1
|
+
// Site Manager API v1 response shapes. Money is always a decimal string like "1450.00".
|
|
2
|
+
|
|
3
|
+
export type Money = string;
|
|
4
|
+
export type Hours = Record<"mon" | "tue" | "wed" | "thu" | "fri" | "sat" | "sun", { closed: true } | { closed: false; open: string; close: string } | { closed: false; byAppointment: true }>;
|
|
5
|
+
export type Image = { url: string; width: number | null; height: number | null; alt: string };
|
|
6
|
+
export type Address = { line1: string; line2: string; city: string; region: string; postalCode: string; country: string };
|
|
7
|
+
|
|
8
|
+
export type Site = {
|
|
9
|
+
name: string;
|
|
10
|
+
kind: "apartment" | "business" | "real_estate";
|
|
11
|
+
websiteUrl: string | null;
|
|
12
|
+
timezone: string;
|
|
13
|
+
modules: string[];
|
|
14
|
+
business: {
|
|
15
|
+
displayName: string;
|
|
16
|
+
legalName: string | null;
|
|
17
|
+
phone: string | null;
|
|
18
|
+
email: string | null;
|
|
19
|
+
address: Address;
|
|
20
|
+
geo: { lat: number; lng: number } | null;
|
|
21
|
+
hours: Hours | null;
|
|
22
|
+
hoursNote: string | null;
|
|
23
|
+
social: { network: string; url: string }[];
|
|
24
|
+
showEqualHousing: boolean | null;
|
|
25
|
+
// Search and maps facts (kit 1.42.0). Optional, so a site still builds against an older Site Manager; null or
|
|
26
|
+
// empty means not set.
|
|
27
|
+
/** A schema.org LocalBusiness type like "Plumber" or "ApartmentComplex". Null means LocalBusiness. */
|
|
28
|
+
schemaType?: string | null;
|
|
29
|
+
/** The Google Business Profile's main category, as typed there. */
|
|
30
|
+
primaryCategory?: string | null;
|
|
31
|
+
secondaryCategories?: string[];
|
|
32
|
+
/** Cities, towns or neighborhoods the business works in. */
|
|
33
|
+
serviceAreas?: string[];
|
|
34
|
+
priceRange?: string | null;
|
|
35
|
+
foundedYear?: number | null;
|
|
36
|
+
/** The Google Business Profile Place ID. */
|
|
37
|
+
googlePlaceId?: string | null;
|
|
38
|
+
};
|
|
39
|
+
/** Visitor tracking, set by the agency in Site Manager. Missing from Site Manager versions before it existed. */
|
|
40
|
+
tracking?: { provider: "stackos"; siteKey: string } | null;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** A link from Site Manager's Pages. `href` is null when the record it points to is hidden or gone: skip the link. */
|
|
44
|
+
export type CopyLink = { href: string | null; label: string };
|
|
45
|
+
|
|
46
|
+
/** `pagePath` is the page the value belongs to in Site Manager's Pages, or null when it's shared across pages. */
|
|
47
|
+
export type CopyValue = (
|
|
48
|
+
// accents: a one-field heading's gold words as [start, end] ranges of text; headingRuns() turns them into runs.
|
|
49
|
+
// strong: words marked important (Bold), rendered <strong>; em: words emphasized (Italic), rendered <em>;
|
|
50
|
+
// textRuns() turns every mark into runs.
|
|
51
|
+
| { kind: "text"; text: string | null; accents?: [number, number][]; strong?: [number, number][]; em?: [number, number][] }
|
|
52
|
+
| { kind: "rich"; html: string | null }
|
|
53
|
+
| { kind: "image"; image: Image | null }
|
|
54
|
+
| { kind: "link"; link: CopyLink | null }
|
|
55
|
+
| { kind: "records"; module: string | null; items: unknown[] }
|
|
56
|
+
) & { pagePath?: string | null };
|
|
57
|
+
|
|
58
|
+
/** A review platform from Reviews > Platforms. `url` is its review link, or null when the site has none. */
|
|
59
|
+
export type ReviewPlatform = { key: string; label: string; url: string | null; logo: Image | null };
|
|
60
|
+
|
|
61
|
+
export type Review = {
|
|
62
|
+
id: string;
|
|
63
|
+
authorName: string;
|
|
64
|
+
/** Where the reviewer is from, such as a city. */
|
|
65
|
+
authorLocation: string | null;
|
|
66
|
+
rating: number | null;
|
|
67
|
+
body: string;
|
|
68
|
+
/** A short version for cards, in the reviewer's own words. */
|
|
69
|
+
excerpt: string | null;
|
|
70
|
+
reviewerType: "client" | "colleague";
|
|
71
|
+
source: string;
|
|
72
|
+
/** The review's platform with its logo, or null for one entered by hand or not in the site's list. */
|
|
73
|
+
platform: ReviewPlatform | null;
|
|
74
|
+
sourceUrl: string | null;
|
|
75
|
+
reviewedOn: string | null;
|
|
76
|
+
response: string | null;
|
|
77
|
+
isFeatured: boolean;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export type FloorPlan = {
|
|
81
|
+
slug: string;
|
|
82
|
+
name: string;
|
|
83
|
+
beds: number;
|
|
84
|
+
baths: string;
|
|
85
|
+
sqftMin: number | null;
|
|
86
|
+
sqftMax: number | null;
|
|
87
|
+
startingRent: Money | null;
|
|
88
|
+
maxRent: Money | null;
|
|
89
|
+
deposit: Money | null;
|
|
90
|
+
availableCount: number | null;
|
|
91
|
+
availabilityUpdatedAt: string | null;
|
|
92
|
+
descriptionHtml: string;
|
|
93
|
+
tourUrl: string | null;
|
|
94
|
+
images: (Image & { kind: "plan" | "photo" })[];
|
|
95
|
+
source: "manual" | "appfolio" | "yardi" | "rent_roll";
|
|
96
|
+
// The fields below are missing from Site Manager versions before they existed, so they're optional.
|
|
97
|
+
/** What to show about availability. `count` is null unless the site chose to show exact numbers. Prefer this over availableCount. */
|
|
98
|
+
availability?: Availability;
|
|
99
|
+
/** Short feature labels for comparing plans, for example "Washer and dryer". */
|
|
100
|
+
features?: string[];
|
|
101
|
+
layout?: "flat" | "townhome" | "loft" | "penthouse" | null;
|
|
102
|
+
/** Floors inside the home. */
|
|
103
|
+
levels?: number | null;
|
|
104
|
+
/** The running special that gives this plan its lowest effective monthly rent. */
|
|
105
|
+
concession?: FloorPlanConcession | null;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export type Availability = { status: "available" | "none" | "unknown"; count: number | null };
|
|
109
|
+
|
|
110
|
+
export type FloorPlanConcession = { specialId: string; title: string; effectiveRent: Money; baseRent: Money; leaseMonths: number | null };
|
|
111
|
+
|
|
112
|
+
export type Concession = { kind: "weeks_free" | "months_free" | "amount_off" | "monthly_discount"; value: string; leaseMonths: number | null };
|
|
113
|
+
|
|
114
|
+
export type Unit = { unitNumber: string; building: string | null; floor: number | null; sqft: number | null; rent: Money | null; availableOn: string | null; status: "available" | "on_notice" };
|
|
115
|
+
|
|
116
|
+
export type Amenity = { id: string; name: string; category: "community" | "apartment"; description: string | null; icon: string | null; image: Image | null };
|
|
117
|
+
|
|
118
|
+
export type Fee = { id: string; name: string; category: string; frequency: "one_time" | "monthly" | "per_occurrence"; amount: Money | null; amountNote: string | null; isRequired: boolean; notes: string | null };
|
|
119
|
+
|
|
120
|
+
export type Special = { id: string; title: string; detailsHtml: string; startsOn: string | null; endsOn: string | null; floorPlans: string[]; concession?: Concession | null };
|
|
121
|
+
|
|
122
|
+
export type PetRule = {
|
|
123
|
+
id: string;
|
|
124
|
+
petType: "dog" | "cat" | "bird" | "fish" | "small_animal" | "reptile" | "other";
|
|
125
|
+
allowed: boolean;
|
|
126
|
+
maxCount: number | null;
|
|
127
|
+
maxWeightLbs: number | null;
|
|
128
|
+
breedRules: string | null;
|
|
129
|
+
deposit: Money | null;
|
|
130
|
+
fee: Money | null;
|
|
131
|
+
monthlyRent: Money | null;
|
|
132
|
+
notes: string | null;
|
|
133
|
+
};
|
|
134
|
+
export type PetPolicy = { maxPets: number | null; assistanceNote: string | null; notes: string | null; pets: PetRule[] };
|
|
135
|
+
|
|
136
|
+
export type ParkingOption = {
|
|
137
|
+
id: string;
|
|
138
|
+
name: string;
|
|
139
|
+
kind: "garage" | "covered" | "carport" | "surface" | "street" | "tandem" | "other";
|
|
140
|
+
price: Money | null;
|
|
141
|
+
frequency: "monthly" | "one_time" | "included";
|
|
142
|
+
/** Null means the site hasn't said, never "no". */
|
|
143
|
+
assigned: boolean | null;
|
|
144
|
+
evCharging: boolean | null;
|
|
145
|
+
notes: string | null;
|
|
146
|
+
};
|
|
147
|
+
export type Parking = { guestNote: string | null; notes: string | null; options: ParkingOption[] };
|
|
148
|
+
|
|
149
|
+
export type Weekday = "mon" | "tue" | "wed" | "thu" | "fri" | "sat" | "sun";
|
|
150
|
+
|
|
151
|
+
export type UtilityProvider = {
|
|
152
|
+
utility: "electric" | "gas" | "water" | "sewer" | "trash" | "internet" | "cable" | "other";
|
|
153
|
+
provider: string;
|
|
154
|
+
phone: string | null;
|
|
155
|
+
url: string | null;
|
|
156
|
+
/** Who arranges it. Null means the site hasn't said. */
|
|
157
|
+
setupBy: "resident" | "property" | null;
|
|
158
|
+
/** When to set it up, in the site's words. */
|
|
159
|
+
when: string | null;
|
|
160
|
+
notes: string | null;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export type Pickup = {
|
|
164
|
+
stream: "trash" | "recycling" | "compost" | "bulk" | "other";
|
|
165
|
+
/** In week order. */
|
|
166
|
+
days: Weekday[];
|
|
167
|
+
/** 24-hour "HH:MM", or null when there's no set time. */
|
|
168
|
+
time: string | null;
|
|
169
|
+
notes: string | null;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export type ResidentDocument = { id: string; name: string; url: string; mimeType: string; sizeBytes: number };
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* One section of the residents page. Only visible sections with something in them are sent. The detail matching
|
|
176
|
+
* the section's kind is set; the others are null.
|
|
177
|
+
*/
|
|
178
|
+
export type ResidentSection = {
|
|
179
|
+
id: string;
|
|
180
|
+
kind: "move_in" | "rules" | "utilities" | "trash" | "maintenance" | "rent_payment" | "documents" | "custom";
|
|
181
|
+
title: string;
|
|
182
|
+
html: string;
|
|
183
|
+
utilities: UtilityProvider[] | null;
|
|
184
|
+
trash: Pickup[] | null;
|
|
185
|
+
maintenance: { requestUrl: string | null; phone: string | null; email: string | null; emergencyPhone: string | null; emergencyNote: string | null } | null;
|
|
186
|
+
rentPayment: { portalUrl: string | null; portalLabel: string | null } | null;
|
|
187
|
+
documents: ResidentDocument[];
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/** Leasing policies. Every fact is null (or an empty list) until the site enters it. */
|
|
191
|
+
export type Policies = {
|
|
192
|
+
application: {
|
|
193
|
+
criteriaHtml: string;
|
|
194
|
+
/** Monthly income as a multiple of the rent, like "3.00". */
|
|
195
|
+
incomeMultiple: string | null;
|
|
196
|
+
incomeNote: string | null;
|
|
197
|
+
creditNote: string | null;
|
|
198
|
+
backgroundNote: string | null;
|
|
199
|
+
fee: Money | null;
|
|
200
|
+
feeNote: string | null;
|
|
201
|
+
holdingFee: Money | null;
|
|
202
|
+
holdingFeeNote: string | null;
|
|
203
|
+
};
|
|
204
|
+
/** One amount when `max` is null, otherwise a range. */
|
|
205
|
+
deposit: { min: Money | null; max: Money | null; note: string | null };
|
|
206
|
+
lease: { termMonths: number[]; note: string | null };
|
|
207
|
+
smoking: { policy: "not_allowed" | "designated_areas" | "allowed" | null; note: string | null };
|
|
208
|
+
guestPolicy: string | null;
|
|
209
|
+
rentersInsurance: { requirement: "required" | "recommended" | "not_required" | null; minLiability: Money | null; note: string | null };
|
|
210
|
+
accessibilityStatement: string | null;
|
|
211
|
+
fairHousingStatement: string | null;
|
|
212
|
+
extra: { id: string; title: string; html: string }[];
|
|
213
|
+
/** Whether pet and parking rules have their own sections on this site, to link to instead of repeating. */
|
|
214
|
+
related: { petPolicy: boolean; parking: boolean };
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export type Event = {
|
|
218
|
+
slug: string;
|
|
219
|
+
title: string;
|
|
220
|
+
startsAt: string;
|
|
221
|
+
endsAt: string | null;
|
|
222
|
+
startsAtLocal: string;
|
|
223
|
+
endsAtLocal: string | null;
|
|
224
|
+
timezone: string;
|
|
225
|
+
allDay: boolean;
|
|
226
|
+
locationText: string | null;
|
|
227
|
+
descriptionHtml: string;
|
|
228
|
+
image: Image | null;
|
|
229
|
+
linkUrl: string | null;
|
|
230
|
+
linkLabel: string | null;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
export type ServicePriceKind = "none" | "fixed" | "range" | "starting_at" | "call";
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* A service a business offers. `price.from` and `price.to` are decimal strings ("150.00"), set only for the kinds
|
|
237
|
+
* that use them; `price.label` is ready to show ("Starting at $150", "Call for pricing") and null for "none".
|
|
238
|
+
*/
|
|
239
|
+
export type Service = {
|
|
240
|
+
slug: string;
|
|
241
|
+
name: string;
|
|
242
|
+
summary: string | null;
|
|
243
|
+
descriptionHtml: string;
|
|
244
|
+
category: string | null;
|
|
245
|
+
price: { kind: ServicePriceKind; from: string | null; to: string | null; note: string | null; label: string | null };
|
|
246
|
+
durationText: string | null;
|
|
247
|
+
/** In the order set in Site Manager; the first is the main photo. */
|
|
248
|
+
photos: Image[];
|
|
249
|
+
updatedAt: string;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
export type Location = { slug: string; name: string; phone: string | null; email: string | null; address: Address; geo: { lat: number; lng: number } | null; hours: Hours | null; isPrimary: boolean };
|
|
253
|
+
|
|
254
|
+
export type NearbyPlace = {
|
|
255
|
+
id: string;
|
|
256
|
+
name: string;
|
|
257
|
+
category: string;
|
|
258
|
+
address: string | null;
|
|
259
|
+
distanceText: string | null;
|
|
260
|
+
url: string | null;
|
|
261
|
+
geo: { lat: number; lng: number } | null;
|
|
262
|
+
description: string | null;
|
|
263
|
+
walkMinutes?: number | null;
|
|
264
|
+
driveMinutes?: number | null;
|
|
265
|
+
/** Bus or train, door to door. Mostly set for places in the `commute` category (employers, campuses, the airport). */
|
|
266
|
+
transitMinutes?: number | null;
|
|
267
|
+
school?: { level: "preschool" | "elementary" | "middle" | "high" | "k12" | "college" | null; grades: string | null; district: string | null; type: "public" | "private" | "charter" | null } | null;
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* A drawing of the property (a site plan or a building) with its open units placed on it. `x` and `y` are percentages
|
|
272
|
+
* of the image's width and height from its top left, so pins stay put at any size. Leased units are never included.
|
|
273
|
+
*/
|
|
274
|
+
export type UnitMap = {
|
|
275
|
+
id: string;
|
|
276
|
+
name: string;
|
|
277
|
+
image: Image;
|
|
278
|
+
units: { unitNumber: string; building: string | null; floorPlan: string; status: "available" | "on_notice"; rent: Money | null; availableOn: string | null; x: number; y: number }[];
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
export type FormField = { name: string; label: string; type: "text" | "email" | "phone" | "textarea" | "select" | "radio" | "checkbox" | "date" | "number" | "hidden"; required: boolean; options?: string[]; maxLength: number; /** A short line to show under the field. */ help?: string };
|
|
282
|
+
export type Form = {
|
|
283
|
+
key: string;
|
|
284
|
+
/** From Site Manager 2026-11 on. A booking form also needs one of getBookingSlots' times, sent as the bp_slot field. */
|
|
285
|
+
kind?: "standard" | "booking";
|
|
286
|
+
name: string;
|
|
287
|
+
description: string | null;
|
|
288
|
+
fields: FormField[];
|
|
289
|
+
successMessage: string;
|
|
290
|
+
booking?: { slotMinutes: number; timezone: string };
|
|
291
|
+
};
|
|
292
|
+
export type BookingSlot = { start: string; end: string; label: string };
|
|
293
|
+
export type BookingDays = { timezone: string; slotMinutes: number; days: { date: string; slots: BookingSlot[] }[] };
|
|
294
|
+
|
|
295
|
+
export type LegalSummary = { kind: string; title: string; slug: string; effectiveDate: string | null };
|
|
296
|
+
export type LegalDoc = LegalSummary & { html: string };
|
|
297
|
+
|
|
298
|
+
export type PageMeta = {
|
|
299
|
+
path: string;
|
|
300
|
+
title: string;
|
|
301
|
+
description: string | null;
|
|
302
|
+
canonical: string | null;
|
|
303
|
+
noindex: boolean;
|
|
304
|
+
ogImage: Image | null;
|
|
305
|
+
siteName: string;
|
|
306
|
+
titleSuffix: string | null;
|
|
307
|
+
// From Site Manager's share and robots settings (kit 1.43.0). Optional, so a site still builds against an older
|
|
308
|
+
// Site Manager. Null share text uses the title and description.
|
|
309
|
+
ogTitle?: string | null;
|
|
310
|
+
ogDescription?: string | null;
|
|
311
|
+
nofollow?: boolean;
|
|
312
|
+
maxImagePreview?: "large" | "standard" | "none";
|
|
313
|
+
};
|
|
314
|
+
export type SitemapEntry = { url: string; lastModified: string };
|
|
315
|
+
/** blockedBots: AI crawlers the site keeps out, by user agent (Site Manager 2026-11, optional for older servers). */
|
|
316
|
+
export type Robots = { indexable: boolean; disallow: string[]; blockedBots?: string[] };
|
|
317
|
+
export type Redirect = { from: string; to: string; status: 301 | 302 | 308 };
|
|
318
|
+
|
|
319
|
+
// Real estate (Site Manager's Listings and Team sections).
|
|
320
|
+
|
|
321
|
+
export type TeamMember = { slug: string; name: string; title: string | null; licenseNumber: string | null; phone: string | null; email: string | null; bioHtml: string; photo: Image | null };
|
|
322
|
+
|
|
323
|
+
export type ListingStatus = "coming_soon" | "active" | "pending" | "sold";
|
|
324
|
+
export type PropertyType = "single_family" | "condo" | "townhouse" | "multi_family" | "manufactured" | "land" | "farm_ranch" | "commercial" | "other";
|
|
325
|
+
export type HoaDues = { amount: Money; frequency: "monthly" | "quarterly" | "yearly" };
|
|
326
|
+
|
|
327
|
+
export type Listing = {
|
|
328
|
+
slug: string;
|
|
329
|
+
status: ListingStatus;
|
|
330
|
+
statusLabel: string;
|
|
331
|
+
/** A manual badge, or "Just Listed" while an active listing is new. Null when there's nothing to show. */
|
|
332
|
+
badge: string | null;
|
|
333
|
+
headline: string | null;
|
|
334
|
+
address: { line1: string; line2: string | null; city: string | null; region: string | null; postalCode: string | null; neighborhood: string | null };
|
|
335
|
+
geo: { lat: number; lng: number } | null;
|
|
336
|
+
price: Money | null;
|
|
337
|
+
/** From the sold price once sold. */
|
|
338
|
+
pricePerSqft: Money | null;
|
|
339
|
+
soldPrice: Money | null;
|
|
340
|
+
soldOn: string | null;
|
|
341
|
+
listedOn: string | null;
|
|
342
|
+
beds: number | null;
|
|
343
|
+
/** A decimal string like "2.5". `bathsText` drops a trailing ".0". */
|
|
344
|
+
baths: string | null;
|
|
345
|
+
sqft: number | null;
|
|
346
|
+
lot: { size: string; unit: "acres" | "sqft" } | null;
|
|
347
|
+
propertyType: PropertyType | null;
|
|
348
|
+
propertyTypeLabel: string | null;
|
|
349
|
+
yearBuilt: number | null;
|
|
350
|
+
stories: number | null;
|
|
351
|
+
garageSpaces: number | null;
|
|
352
|
+
hoa: HoaDues | null;
|
|
353
|
+
mls: { number: string; source: string | null } | null;
|
|
354
|
+
tourUrl: string | null;
|
|
355
|
+
isFeatured: boolean;
|
|
356
|
+
featuredImage: Image | null;
|
|
357
|
+
imageCount: number;
|
|
358
|
+
agent: TeamMember | null;
|
|
359
|
+
source: "manual";
|
|
360
|
+
updatedAt: string;
|
|
361
|
+
// From Site Manager's listing details (kit 1.8.0). Optional, so a site still builds against an older Site Manager.
|
|
362
|
+
// Empty lists and nulls mean not known.
|
|
363
|
+
propertySubtype?: string | null;
|
|
364
|
+
originalPrice?: Money | null;
|
|
365
|
+
/** The day it went under contract. */
|
|
366
|
+
pendingOn?: string | null;
|
|
367
|
+
/** From the list date to the pending date (or today while active). Null for coming soon. */
|
|
368
|
+
daysOnMarket?: number | null;
|
|
369
|
+
interior?: {
|
|
370
|
+
bathsFull: number | null;
|
|
371
|
+
bathsHalf: number | null;
|
|
372
|
+
rooms: number | null;
|
|
373
|
+
basement: "none" | "finished" | "partially_finished" | "unfinished" | null;
|
|
374
|
+
fireplaces: number | null;
|
|
375
|
+
appliances: string[];
|
|
376
|
+
flooring: string[];
|
|
377
|
+
heating: string[];
|
|
378
|
+
cooling: string[];
|
|
379
|
+
laundry: string[];
|
|
380
|
+
features: string[];
|
|
381
|
+
};
|
|
382
|
+
exterior?: { construction: string[]; roof: string[]; pool: "none" | "private" | "community" | "private_and_community" | null; views: string[]; features: string[] };
|
|
383
|
+
lotDetails?: { dimensions: string | null; zoning: string | null; fencing: string[]; water: string[]; sewer: string[]; parcelNumber: string | null; features: string[] };
|
|
384
|
+
parking?: { garageSpaces: number | null; garageType: "attached" | "detached" | "carport" | "none" | null; spaces: number | null; features: string[] };
|
|
385
|
+
utilities?: string[];
|
|
386
|
+
hoaName?: string | null;
|
|
387
|
+
hoaIncludes?: string[];
|
|
388
|
+
taxes?: { annual: Money; year: number | null } | null;
|
|
389
|
+
/** Upcoming open houses, soonest first (kit 1.10.0). Past ones are never sent. */
|
|
390
|
+
openHouses?: ListingOpenHouse[];
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
/** An open house: UTC instants, the same times on the site's wall clock, and a ready label like "Open House Sat 1 to 3 pm". */
|
|
394
|
+
export type ListingOpenHouse = { startsAt: string; endsAt: string; startsAtLocal: string; endsAtLocal: string; label: string; note: string | null };
|
|
395
|
+
|
|
396
|
+
export type ListingFeature = { name: string; value: string };
|
|
397
|
+
export type ListingSchool = { level: "elementary" | "middle" | "high" | "other"; name: string; district: string | null };
|
|
398
|
+
export type ListingHistoryEntry = { date: string; kind: "listed" | "price_change" | "status_change" | "sold" | "note"; label: string; price: Money | null; status: string | null; note: string | null };
|
|
399
|
+
|
|
400
|
+
/** A past sale of the home. Buyer and seller are null unless the agent chose to show names. */
|
|
401
|
+
export type ListingTransaction = { date: string; amount: Money | null; seller: string | null; buyer: string | null };
|
|
402
|
+
|
|
403
|
+
export type ListingDetail = Listing & {
|
|
404
|
+
descriptionHtml: string;
|
|
405
|
+
images: Image[];
|
|
406
|
+
features: ListingFeature[];
|
|
407
|
+
schools: ListingSchool[];
|
|
408
|
+
history: ListingHistoryEntry[];
|
|
409
|
+
/** Past sales, newest first (kit 1.8.0). */
|
|
410
|
+
transactions?: ListingTransaction[];
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
export type ListingFilters = {
|
|
414
|
+
status?: ListingStatus | ListingStatus[];
|
|
415
|
+
minPrice?: string | number;
|
|
416
|
+
maxPrice?: string | number;
|
|
417
|
+
beds?: number;
|
|
418
|
+
type?: PropertyType;
|
|
419
|
+
featured?: boolean;
|
|
420
|
+
limit?: number;
|
|
421
|
+
/** A team member's short name: only the listings they represent (kit 1.10.0). */
|
|
422
|
+
agent?: string;
|
|
423
|
+
/** An area guide's short name: only listings whose neighborhood matches the area (kit 1.17.0). */
|
|
424
|
+
area?: string;
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
/** An area guide (kit 1.17.0). A listing is in the area when its neighborhood matches `name` or one of `otherNames`, ignoring case. */
|
|
428
|
+
export type Area = {
|
|
429
|
+
slug: string;
|
|
430
|
+
name: string;
|
|
431
|
+
summary: string | null;
|
|
432
|
+
descriptionHtml: string;
|
|
433
|
+
/** In the order set in Site Manager; the first is the main photo. */
|
|
434
|
+
photos: Image[];
|
|
435
|
+
/** Short lines like "Walk to the river trail". */
|
|
436
|
+
highlights: string[];
|
|
437
|
+
schools: ListingSchool[];
|
|
438
|
+
otherNames: string[];
|
|
439
|
+
/** Homes on the website in the area now (coming soon, active, pending). Null when the site doesn't use Listings. */
|
|
440
|
+
listingCount: number | null;
|
|
441
|
+
updatedAt: string;
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
/** Each value is null until the site sets it; without a rate, down payment and term there is no estimate. */
|
|
445
|
+
export type PaymentDefaults = { ratePercent: string | null; downPaymentPercent: string | null; termYears: number | null; taxRatePercent: string | null; insuranceYearly: Money | null };
|
|
446
|
+
|
|
447
|
+
export type PaymentEstimate = { downPayment: Money; loanAmount: Money; principalAndInterest: Money; tax: Money | null; insurance: Money | null; hoa: Money | null; total: Money };
|
|
448
|
+
|
|
449
|
+
// Every-site sections (kit 1.14.0): FAQ, Announcements and Downloads.
|
|
450
|
+
|
|
451
|
+
export type FaqCategory = { slug: string; name: string };
|
|
452
|
+
/** `answerHtml` is sanitized HTML for the page and for JSON-LD; `answerText` is the same answer as plain text. `category` is a slug. */
|
|
453
|
+
export type FaqQuestion = { id: string; question: string; answerHtml: string; answerText: string; category: string | null; /** Plain text shown under the answer. */ note: string | null };
|
|
454
|
+
export type Faq = { categories: FaqCategory[]; questions: FaqQuestion[] };
|
|
455
|
+
export type FaqSet = { key: string; name: string; questions: FaqQuestion[] };
|
|
456
|
+
|
|
457
|
+
/** A site-wide notice that is live now. Instants are UTC ISO; the local fields repeat them in the site's time zone. */
|
|
458
|
+
export type Announcement = {
|
|
459
|
+
id: string;
|
|
460
|
+
message: string;
|
|
461
|
+
linkUrl: string | null;
|
|
462
|
+
linkLabel: string | null;
|
|
463
|
+
tone: "info" | "urgent";
|
|
464
|
+
startsAt: string | null;
|
|
465
|
+
endsAt: string | null;
|
|
466
|
+
startsAtLocal: string | null;
|
|
467
|
+
endsAtLocal: string | null;
|
|
468
|
+
timezone: string;
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
export type DownloadCategory = "brochure" | "menu" | "application" | "price_sheet" | "other";
|
|
472
|
+
/** `url` is Site Manager's tracked link: it counts the download and sends the visitor on to the file. */
|
|
473
|
+
export type Download = { id: string; title: string; category: DownloadCategory; description: string | null; url: string; filename: string; mimeType: string; sizeBytes: number };
|
|
474
|
+
export type BlogTerm = { slug: string; name: string };
|
|
475
|
+
/**
|
|
476
|
+
* Who wrote a post. A `team` author is one of the site's Team members: `slug` is theirs, and `url` their page address,
|
|
477
|
+
* null until the site has a Team page pattern in Site Manager. Both are absent for other kinds and older Site Manager.
|
|
478
|
+
*/
|
|
479
|
+
export type BlogAuthor = { name: string; kind: "user" | "byline" | "team"; slug?: string; url?: string | null };
|
|
480
|
+
|
|
481
|
+
/** A post in a list: everything but the body. `url` is null until the site has a blog page pattern in Site Manager. */
|
|
482
|
+
export type BlogPostSummary = {
|
|
483
|
+
slug: string;
|
|
484
|
+
title: string;
|
|
485
|
+
excerpt: string | null;
|
|
486
|
+
url: string | null;
|
|
487
|
+
publishedAt: string | null;
|
|
488
|
+
publishedAtLocal: string | null;
|
|
489
|
+
updatedAt: string;
|
|
490
|
+
timezone: string;
|
|
491
|
+
cover: Image | null;
|
|
492
|
+
author: BlogAuthor | null;
|
|
493
|
+
categories: BlogTerm[];
|
|
494
|
+
tags: BlogTerm[];
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
export type BlogPost = BlogPostSummary & {
|
|
498
|
+
/** Short takeaways, in order, up to seven. */
|
|
499
|
+
keyPoints: string[];
|
|
500
|
+
bodyHtml: string;
|
|
501
|
+
wordCount: number;
|
|
502
|
+
/** The post's search listing from Site Manager, or null while it has no page. */
|
|
503
|
+
seo: { title: string; description: string | null; canonical: string | null; ogImage: Image | null } | null;
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
export type BlogCategory = BlogTerm & { description: string | null; postCount: number };
|
|
507
|
+
export type BlogTag = BlogTerm & { postCount: number };
|
|
508
|
+
export type Paging = { page: number; perPage: number; total: number; totalPages: number };
|
|
509
|
+
export type BlogPage = { posts: BlogPostSummary[]; paging: Paging };
|
|
510
|
+
|
|
511
|
+
export type BlogFeedItem = {
|
|
512
|
+
guid: string;
|
|
513
|
+
guidIsPermaLink: boolean;
|
|
514
|
+
title: string;
|
|
515
|
+
link: string | null;
|
|
516
|
+
summary: string | null;
|
|
517
|
+
contentHtml: string;
|
|
518
|
+
author: string | null;
|
|
519
|
+
categories: string[];
|
|
520
|
+
image: Image | null;
|
|
521
|
+
publishedAt: string | null;
|
|
522
|
+
pubDate: string | null;
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
export type BlogFeed = {
|
|
526
|
+
title: string;
|
|
527
|
+
description: string | null;
|
|
528
|
+
link: string | null;
|
|
529
|
+
feedUrl: string | null;
|
|
530
|
+
language: string;
|
|
531
|
+
updatedAt: string | null;
|
|
532
|
+
items: BlogFeedItem[];
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
/** What a waitlist form can offer, from getWaitlistOptions(). `available` is whether the plan has a home open now. */
|
|
536
|
+
export type WaitlistOptions = {
|
|
537
|
+
floorPlans: { slug: string; name: string; bedrooms: number; available: boolean }[];
|
|
538
|
+
bedrooms: { value: number; label: string }[];
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
export type WaitlistResult =
|
|
542
|
+
| { ok: true; waitingFor: string | null }
|
|
543
|
+
| { ok: false; error: "invalid"; fields: Record<string, string> }
|
|
544
|
+
| { ok: false; error: "rate_limited" | "unavailable" };
|
|
545
|
+
|
|
546
|
+
/** A glossary term in the index: enough to list it and to link the term where it appears in text. */
|
|
547
|
+
export type GlossaryTermSummary = {
|
|
548
|
+
slug: string;
|
|
549
|
+
term: string;
|
|
550
|
+
/** The term's page on the site, or null when Site Manager has no glossary page address set. */
|
|
551
|
+
url: string | null;
|
|
552
|
+
shortDefinition: string;
|
|
553
|
+
category: { slug: string; name: string } | null;
|
|
554
|
+
/** Works differently in the site's own state. */
|
|
555
|
+
isLocal: boolean;
|
|
556
|
+
alsoKnownAs: string[];
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
export type GlossaryRef = { slug: string; term: string; url: string | null };
|
|
560
|
+
|
|
561
|
+
export type Glossary = { categories: { slug: string; name: string }[]; terms: GlossaryTermSummary[] };
|
|
562
|
+
|
|
563
|
+
export type GlossaryTerm = GlossaryTermSummary & {
|
|
564
|
+
pronunciation: string | null;
|
|
565
|
+
localNote: string | null;
|
|
566
|
+
inPracticeHtml: string;
|
|
567
|
+
steps: { heading: string; body: string }[];
|
|
568
|
+
confusedWith: GlossaryRef[];
|
|
569
|
+
confusedNote: string | null;
|
|
570
|
+
related: GlossaryRef[];
|
|
571
|
+
/** Pages this term leads to. Skip any whose `href` is null. */
|
|
572
|
+
relatedLinks: CopyLink[];
|
|
573
|
+
faqs: { question: string; answerHtml: string }[];
|
|
574
|
+
reviewStatus: "needs_review" | "reviewed_by_staff" | "reviewed_by_expert";
|
|
575
|
+
reviewedBy: string | null;
|
|
576
|
+
reviewedAt: string | null;
|
|
577
|
+
updatedAt: string;
|
|
578
|
+
/** The terms before and after this one, alphabetically. */
|
|
579
|
+
previous: GlossaryRef | null;
|
|
580
|
+
next: GlossaryRef | null;
|
|
581
|
+
};
|