@v-office/website-sdk 2.5.1 → 2.8.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 +28 -1
- package/dist/cli.mjs +25 -4
- package/dist/{client-C5ojrNQA.mjs → client-BNbBfJtO.mjs} +468 -189
- package/dist/index.d.mts +5 -4
- package/dist/index.mjs +3 -3
- package/dist/instructions/CHANGELOG.md +3 -0
- package/dist/instructions/MIGRATION.md +3 -0
- package/dist/instructions/README.md +8 -2
- package/dist/instructions/booking.md +39 -3
- package/dist/instructions/creation.md +65 -64
- package/dist/instructions/custom-attributes.md +556 -0
- package/dist/instructions/filter.md +51 -23
- package/dist/instructions/rentals.md +26 -2
- package/dist/instructions/search.md +29 -3
- package/dist/instructions/versions/2.5.0/MIGRATION.md +5 -0
- package/dist/instructions/versions/2.6.0/CHANGELOG.md +6 -0
- package/dist/instructions/versions/2.6.0/MIGRATION.md +6 -0
- package/dist/instructions/versions/2.7.0/CHANGELOG.md +44 -0
- package/dist/instructions/versions/2.7.0/MIGRATION.md +79 -0
- package/dist/instructions/versions/2.8.0/CHANGELOG.md +63 -0
- package/dist/instructions/versions/2.8.0/MIGRATION.md +252 -0
- package/dist/{rentals-Lw1qUKIj.mjs → rentals-cCejb_2d.mjs} +21 -21
- package/dist/{search-9yzAqHCg.mjs → search-Dzzra6IW.mjs} +16 -133
- package/dist/{to-rental-highlights-CxWlq76f.mjs → to-rental-highlights-OvNe9ZQS.mjs} +8 -6
- package/dist/translations/v10/de-DE/core.json +2 -0
- package/dist/translations/v10/en-US/core.json +2 -0
- package/instructions/CHANGELOG.md +3 -0
- package/instructions/MIGRATION.md +3 -0
- package/instructions/README.md +8 -2
- package/instructions/booking.md +39 -3
- package/instructions/creation.md +65 -64
- package/instructions/custom-attributes.md +556 -0
- package/instructions/filter.md +51 -23
- package/instructions/rentals.md +26 -2
- package/instructions/search.md +29 -3
- package/instructions/versions/2.5.0/MIGRATION.md +5 -0
- package/instructions/versions/2.6.0/CHANGELOG.md +6 -0
- package/instructions/versions/2.6.0/MIGRATION.md +6 -0
- package/instructions/versions/2.7.0/CHANGELOG.md +44 -0
- package/instructions/versions/2.7.0/MIGRATION.md +79 -0
- package/instructions/versions/2.8.0/CHANGELOG.md +63 -0
- package/instructions/versions/2.8.0/MIGRATION.md +252 -0
- package/package.json +5 -3
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
# Custom Attributes
|
|
2
|
+
|
|
3
|
+
Custom attributes are workspace-specific rental metadata: a sauna flag, a region, an
|
|
4
|
+
occupancy score, a marketing text. This document covers how to configure them, how
|
|
5
|
+
they behave on each backend, and what changes when a site moves from `v9` to `v10`.
|
|
6
|
+
|
|
7
|
+
They serve three separate concerns, and each is configured independently:
|
|
8
|
+
|
|
9
|
+
- **Filtering**: a stable query key a search UI can advertise and a visitor can use.
|
|
10
|
+
- **Display**: a localized value rendered in rental attributes or highlights.
|
|
11
|
+
- **Backend identity**: the workspace-specific v10 definition ID or v9 rental-data
|
|
12
|
+
field the SDK must talk to.
|
|
13
|
+
|
|
14
|
+
## Two Halves: Catalog and Policy
|
|
15
|
+
|
|
16
|
+
Configuration is a join of two things that have different owners.
|
|
17
|
+
|
|
18
|
+
| | Owner | Contains |
|
|
19
|
+
| ------------- | --------------------- | ------------------------------------------------------------------- |
|
|
20
|
+
| **Catalog** | the workspace backend | definition ID, backend type, option values |
|
|
21
|
+
| **Selection** | your site | which attributes to use, labels, filter exposure, display, category |
|
|
22
|
+
|
|
23
|
+
You never restate a catalog fact in your configuration. `defineCustomAttributes`
|
|
24
|
+
joins the two, validates the result, and returns a registry the SDK consumes.
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { defineCustomAttributes, defineWebsiteSDKOptions } from "@v-office/website-sdk";
|
|
28
|
+
|
|
29
|
+
const customAttributes = defineCustomAttributes({
|
|
30
|
+
catalog,
|
|
31
|
+
select: {
|
|
32
|
+
sauna: { label: { "de-DE": "Sauna", "en-US": "Sauna" } },
|
|
33
|
+
region: {
|
|
34
|
+
label: { "de-DE": "Region", "en-US": "Region" },
|
|
35
|
+
filter: true,
|
|
36
|
+
optionLabels: {
|
|
37
|
+
north: { "de-DE": "Norden", "en-US": "North" },
|
|
38
|
+
south: { "de-DE": "Süden", "en-US": "South" },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const options = defineWebsiteSDKOptions({ customAttributes });
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Invalid configuration throws while the SDK is being configured, which for a static
|
|
48
|
+
site means the build fails. Every problem is reported in one pass:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
Invalid custom attribute configuration (3 problems):
|
|
52
|
+
- sauna.label: missing a label for de-DE; the catalog supplies none, so it must be authored
|
|
53
|
+
- amenityTags: backend type MULTI_SELECT has no canonical SDK value type yet, so it cannot be selected
|
|
54
|
+
- region.optionLabels: a filterable option attribute needs a label for every option; missing south, east, west
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## The Catalog
|
|
58
|
+
|
|
59
|
+
A `Record` keyed by the backend's definition key:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
type CustomAttributeCatalogDefinitionInput = {
|
|
63
|
+
id: string; // 22-character v10 definition ID
|
|
64
|
+
type:
|
|
65
|
+
| "BOOLEAN"
|
|
66
|
+
| "NUMBER"
|
|
67
|
+
| "SINGLE_SELECT"
|
|
68
|
+
| "TEXT"
|
|
69
|
+
| "LOCALE_TEXT"
|
|
70
|
+
| "MULTI_SELECT"
|
|
71
|
+
| "LOCALE_DAY"
|
|
72
|
+
| "LOCAL_TIME"
|
|
73
|
+
| "LOCAL_TIMERANGE"
|
|
74
|
+
| "MONETARY_VALUE";
|
|
75
|
+
options?: readonly string[]; // SINGLE_SELECT identities; values only, no labels
|
|
76
|
+
label?: { "de-DE"?: string; "en-US"?: string };
|
|
77
|
+
};
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Where it comes from
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { fetchCustomAttributeCatalog } from "@v-office/website-sdk";
|
|
84
|
+
|
|
85
|
+
const catalog = await fetchCustomAttributeCatalog({ config });
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
This is a v10-only build-time read using `apiEndpoint` and `accessToken`, never a
|
|
89
|
+
per-request lookup. Nothing is generated and nothing is committed, so definition IDs
|
|
90
|
+
are never copied by hand and selected definitions stay aligned with the backend. It calls
|
|
91
|
+
`customattributes_listPublicCustomAttributeDefinition`; the configured token must be
|
|
92
|
+
authorized for that operation. When a website token lacks that permission, fetch the
|
|
93
|
+
catalog in an authorized build environment or pin a literal catalog. The operation
|
|
94
|
+
returns IDs, keys, types, and option identities, but no localized labels; author
|
|
95
|
+
labels in `select`. Definitions that the SDK cannot map are omitted with a warning,
|
|
96
|
+
and the remaining catalog is schema-validated. If multiple backend definitions share
|
|
97
|
+
one key, every definition for that ambiguous key is omitted with one warning rather
|
|
98
|
+
than binding whichever row arrived first.
|
|
99
|
+
|
|
100
|
+
You can also pin a literal catalog. Doing so is the only way to get compile-time
|
|
101
|
+
checking of your selection keys and option values, because a fetched value is
|
|
102
|
+
widened to `Record<string, …>`:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
const catalog = {
|
|
106
|
+
sauna: { id: "GQYKYgMVh28X7VLGursbkt", type: "BOOLEAN" },
|
|
107
|
+
} as const satisfies CustomAttributeCatalogInput;
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Both forms are accepted and validated identically. The only difference is _when_ a
|
|
111
|
+
mistake surfaces: a literal catalog catches an unknown key in the editor, a fetched
|
|
112
|
+
one catches it when `defineCustomAttributes` runs.
|
|
113
|
+
|
|
114
|
+
### Backend types the SDK can use
|
|
115
|
+
|
|
116
|
+
| Backend type | SDK type | Notes |
|
|
117
|
+
| -------------------- | --------- | -------------------------------------------- |
|
|
118
|
+
| `BOOLEAN` | `boolean` | |
|
|
119
|
+
| `NUMBER` | `int` | base-10 integers only |
|
|
120
|
+
| `SINGLE_SELECT` | `option` | option identities come from the catalog |
|
|
121
|
+
| `TEXT` | `string` | exact match |
|
|
122
|
+
| `LOCALE_TEXT` | — | unsupported: no locale-selection policy yet |
|
|
123
|
+
| `MULTI_SELECT` | — | unsupported: no AND/OR semantics yet |
|
|
124
|
+
| date, time, monetary | — | unsupported: no canonical type or parser yet |
|
|
125
|
+
|
|
126
|
+
Selecting an unsupported definition fails with a precise message rather than being
|
|
127
|
+
approximated. A definition you do not select is simply ignored, so unsupported types
|
|
128
|
+
elsewhere in the workspace never block a build.
|
|
129
|
+
|
|
130
|
+
## The Selection
|
|
131
|
+
|
|
132
|
+
Every field is optional except a label, and each default is chosen so that omitting
|
|
133
|
+
a field cannot expose or broaden anything.
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
type CustomAttributeOverridesInput = {
|
|
137
|
+
key?: string; // public query key, when the catalog key cannot be one
|
|
138
|
+
label?: { "de-DE"?: string; "en-US"?: string };
|
|
139
|
+
category?: CustomAttributeCategoryKey; // defaults to "OTHER"
|
|
140
|
+
display?: boolean; // defaults to true
|
|
141
|
+
v9?: string; // p_* rental-data binding
|
|
142
|
+
filter?:
|
|
143
|
+
| boolean // `true` means public
|
|
144
|
+
| "internal"
|
|
145
|
+
| {
|
|
146
|
+
exposure?: "public" | "internal";
|
|
147
|
+
numericBounds?: { min: number; max: number }; // int only, UI hint
|
|
148
|
+
allowsFalseValue?: boolean; // boolean only, defaults to false
|
|
149
|
+
};
|
|
150
|
+
optionLabels?: Record<string, { "de-DE": string; "en-US": string }>; // option only
|
|
151
|
+
};
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
| Field | Default | Why that default is safe |
|
|
155
|
+
| ------------------ | ------------------------- | ------------------------------------------------------------------------------ |
|
|
156
|
+
| `filter` | absent | silence never creates a public query capability |
|
|
157
|
+
| `display` | `true` | only public definitions can be selected, so there is nothing private to expose |
|
|
158
|
+
| `allowsFalseValue` | `false` | silence never broadens matching |
|
|
159
|
+
| `category` | `OTHER` | presentation only — grouping, never reachability |
|
|
160
|
+
| `label` | the catalog label, if any | **no safe default** — see below |
|
|
161
|
+
|
|
162
|
+
`category` governs **two** things: which group the filter appears under in
|
|
163
|
+
`getFilters`, and which group the value appears under in the rental attribute list.
|
|
164
|
+
Both backends resolve it the same way, so switching v9 → v10 never moves an
|
|
165
|
+
attribute between groups. An attribute whose category matches a built-in group
|
|
166
|
+
joins that group rather than starting a parallel one.
|
|
167
|
+
|
|
168
|
+
You author the category **key** (`ESSENTIALS`); output carries its **localized
|
|
169
|
+
label** (`Grundlagen` in `de-DE`), exactly like a built-in filter or attribute. Never
|
|
170
|
+
match on the output string — it changes with locale. Override the wording through
|
|
171
|
+
`translationOverrides` on `RentalAttributesCategory.<KEY>.label` if you need to.
|
|
172
|
+
|
|
173
|
+
`label` is the one field that is effectively required. Humanizing a key produces
|
|
174
|
+
English-ish text that is simply wrong for `de-DE`, so when the catalog supplies no
|
|
175
|
+
label you must author one. A partial override is merged over a partial catalog
|
|
176
|
+
label; the merged result must cover every locale.
|
|
177
|
+
|
|
178
|
+
A display-only option attribute may omit `optionLabels`; an unlabeled option then
|
|
179
|
+
renders with its raw catalog identity. Once `filter` is enabled, every catalog option
|
|
180
|
+
must have a complete label for each supported locale because `getFilters` must return
|
|
181
|
+
a usable localized option list.
|
|
182
|
+
|
|
183
|
+
### Renaming a key
|
|
184
|
+
|
|
185
|
+
Backend definition keys are human-authored and routinely contain spaces or
|
|
186
|
+
punctuation, which cannot appear in a query string. Use `key` to give such a
|
|
187
|
+
definition a public name:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
select: {
|
|
191
|
+
"Wäschepaket inkl.": {
|
|
192
|
+
key: "linenPackage",
|
|
193
|
+
label: { "de-DE": "Wäschepaket inklusive", "en-US": "Linen package included" },
|
|
194
|
+
filter: true,
|
|
195
|
+
},
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The attribute is then `linenPackage` everywhere downstream — in URLs, in
|
|
200
|
+
`getFilters`, in applied filters, and in highlight references. A catalog key that is
|
|
201
|
+
already URL-safe (`[A-Za-z0-9_-]+`) is used as-is and needs no override. Forgetting
|
|
202
|
+
the override on a key that needs one fails with:
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
- Wäschepaket inkl.: the catalog key is not usable as a public query key.
|
|
206
|
+
Add a "key" override containing only letters, digits, underscore, or hyphen.
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Renaming is product policy, so it lives in the selection rather than the catalog:
|
|
210
|
+
the catalog is re-read on every build and an authored field there would be lost.
|
|
211
|
+
|
|
212
|
+
### Public data only
|
|
213
|
+
|
|
214
|
+
**The website SDK only ever sees definitions the backend marks `PUBLIC`.** That is
|
|
215
|
+
enforced by the operations it calls: `listPublicCustomAttributeDefinition` for the
|
|
216
|
+
catalog and `listPublicCustomAttribute` for values. A private definition never
|
|
217
|
+
reaches the SDK at all, and there is no filter to opt out of.
|
|
218
|
+
|
|
219
|
+
Because of that, visibility is **not** part of any type you touch. There is no
|
|
220
|
+
visibility field on a catalog entry, none on a resolved attribute, and no rule you
|
|
221
|
+
have to remember to apply. Privacy is enforced where the data lives rather than by
|
|
222
|
+
configuration that could be got wrong.
|
|
223
|
+
|
|
224
|
+
This also closes an inference path that a display-only rule would leave open. An
|
|
225
|
+
`internal` filter is hidden from `getFilters` but is still parseable from a query
|
|
226
|
+
string, so if a private definition were reachable at all, a visitor who guessed the
|
|
227
|
+
key could send `?ownerNote=…` and infer values from which rentals came back. Nothing
|
|
228
|
+
private is reachable, so there is nothing to infer.
|
|
229
|
+
|
|
230
|
+
Because the values read is also a public operation, the backend decides at request
|
|
231
|
+
time what a website may see. That covers a definition whose visibility changed after
|
|
232
|
+
your site was built, which a build-time snapshot could not.
|
|
233
|
+
|
|
234
|
+
### Exposure versus display
|
|
235
|
+
|
|
236
|
+
Within public data, two decisions remain independent:
|
|
237
|
+
|
|
238
|
+
| | Meaning |
|
|
239
|
+
| --------------- | --------------------------------- |
|
|
240
|
+
| filter exposure | `public`, `internal`, or absent |
|
|
241
|
+
| display | whether the value may be rendered |
|
|
242
|
+
|
|
243
|
+
| Filter exposure | `getFilters` | query parsing | compositions | display |
|
|
244
|
+
| --------------- | ------------ | ------------- | -------------- | ----------- |
|
|
245
|
+
| `public` | included | allowed | allowed | independent |
|
|
246
|
+
| `internal` | excluded | allowed | allowed | independent |
|
|
247
|
+
| absent | excluded | not applied | invalid target | independent |
|
|
248
|
+
|
|
249
|
+
`internal` is a UI-visibility choice, not a security boundary — it keeps a filter out
|
|
250
|
+
of `getFilters` while leaving it usable by your own code and by compositions. Use it
|
|
251
|
+
for filters you drive yourself, never to hide sensitive data; the data behind it is
|
|
252
|
+
public either way.
|
|
253
|
+
|
|
254
|
+
## Value Types
|
|
255
|
+
|
|
256
|
+
| Type | Accepted query values | Rendered as |
|
|
257
|
+
| --------- | ----------------------------------------------------------------------- | --------------------------------------------- |
|
|
258
|
+
| `boolean` | `?key`, `?key=true`, `?key=1`; `false`/`0` only with `allowsFalseValue` | the label alone when true, omitted when false |
|
|
259
|
+
| `int` | base-10 integers only — no whitespace, fractions, `1e3`, or `0x1f` | `Label: 1.234` (localized) |
|
|
260
|
+
| `string` | any non-empty value, exact match | `Label: value` |
|
|
261
|
+
| `option` | only values the catalog declares | `Label: <option label>` |
|
|
262
|
+
|
|
263
|
+
Rendering dispatches on the declared type, never on the runtime shape of the value.
|
|
264
|
+
Both backends return values as strings, so a shape-based guess would read an `int`
|
|
265
|
+
of `"1"` as a boolean and drop a `string` of `"0"`.
|
|
266
|
+
|
|
267
|
+
When a value cannot be read as the declared type — an `int` of `"1.5"` or an option
|
|
268
|
+
value the catalog does not declare — the attribute is omitted rather than rendering
|
|
269
|
+
something misleading. A false boolean and an absent value are also omitted.
|
|
270
|
+
|
|
271
|
+
Scalar filters take **one** value. Given `?region=north,south`, the first valid value
|
|
272
|
+
is applied, but the key also remains in `unusedFilterKeys` because the extra token was
|
|
273
|
+
not applied. Combining scalar values would depend on backend semantics that are not
|
|
274
|
+
specified, and an AND of two option values can only return nothing.
|
|
275
|
+
|
|
276
|
+
`numericBounds` are UI hints for building a control. The parser does not enforce
|
|
277
|
+
them, so an advertised bound always round-trips.
|
|
278
|
+
|
|
279
|
+
## Highlights and Attributes
|
|
280
|
+
|
|
281
|
+
A custom attribute appears in `highlights` only when its key is listed in
|
|
282
|
+
`rentalHighlightPrioritization`, which owns order:
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
const options = defineWebsiteSDKOptions({
|
|
286
|
+
customAttributes,
|
|
287
|
+
rentalHighlightPrioritization: [
|
|
288
|
+
"bedrooms",
|
|
289
|
+
customAttributes.keys.region, // checked for a literal typed registry
|
|
290
|
+
"bathrooms",
|
|
291
|
+
],
|
|
292
|
+
});
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
The `keys` object is literal-typed when `defineCustomAttributes` receives a literal
|
|
296
|
+
catalog and selection. The JSON and `defineCustomAttributesFromUnknown` paths still
|
|
297
|
+
validate at runtime, but their keys are widened to `Record<string, string>` and
|
|
298
|
+
cannot catch a typo at compile time.
|
|
299
|
+
|
|
300
|
+
A built-in metadata key shadows a same-named custom attribute even when the built-in
|
|
301
|
+
value is absent — custom attributes are strictly a fallback there.
|
|
302
|
+
|
|
303
|
+
`getRentals` renders custom attributes into both `attributes` and `highlights`;
|
|
304
|
+
`search` renders them into `highlights` only.
|
|
305
|
+
|
|
306
|
+
In `attributes`, a custom attribute is placed in the group named by its resolved
|
|
307
|
+
`category`, alongside built-in attributes of that category rather than in a separate
|
|
308
|
+
bucket. The default `OTHER` therefore groups everything you did not categorize
|
|
309
|
+
together — set `category` when you want an attribute to sit with related built-ins.
|
|
310
|
+
|
|
311
|
+
Values are looked up by the attribute's **resolved** key, so a definition renamed
|
|
312
|
+
through `key` renders under its new name on every path.
|
|
313
|
+
|
|
314
|
+
## Backend Behaviour
|
|
315
|
+
|
|
316
|
+
The same registry is passed to both backends. Each uses the part it can bind.
|
|
317
|
+
|
|
318
|
+
| Registry entry | v10 | v9 |
|
|
319
|
+
| -------------- | ---------------- | ------- |
|
|
320
|
+
| `id` only | filter + display | ignored |
|
|
321
|
+
| `v9` only | ignored | display |
|
|
322
|
+
| both | filter + display | display |
|
|
323
|
+
|
|
324
|
+
### v10
|
|
325
|
+
|
|
326
|
+
Filtering and display both work. A filter resolves its stable key to the catalog
|
|
327
|
+
definition ID and sends `{ customAttributeDefinitionId, customAttributeValue }` to
|
|
328
|
+
REST search. Returned values resolve back through the ID index before
|
|
329
|
+
rendering.
|
|
330
|
+
|
|
331
|
+
### v9
|
|
332
|
+
|
|
333
|
+
Display only. v9 has **no custom-attribute search path**, so a custom attribute is
|
|
334
|
+
never advertised by v9 `getFilters` and a custom-attribute query key is always
|
|
335
|
+
reported in `unusedFilterKeys`. Values come from rental data, addressed by the `v9`
|
|
336
|
+
binding:
|
|
337
|
+
|
|
338
|
+
```ts
|
|
339
|
+
select: {
|
|
340
|
+
privateSauna: {
|
|
341
|
+
label: { "de-DE": "Sauna", "en-US": "Sauna" },
|
|
342
|
+
v9: "p_14418", // rental-data field, never the public query key
|
|
343
|
+
filter: true, // executes on v10; ignored by v9
|
|
344
|
+
},
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
An attribute that exists only in v9 has no catalog entry, so every fact is authored
|
|
349
|
+
and it carries no `filter` field at all:
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
const customAttributes = defineCustomAttributes({
|
|
353
|
+
catalog,
|
|
354
|
+
select: {},
|
|
355
|
+
v9Only: {
|
|
356
|
+
thatchedRoof: {
|
|
357
|
+
v9: "p_14418",
|
|
358
|
+
type: "boolean",
|
|
359
|
+
label: { "de-DE": "Reetdach", "en-US": "Thatched roof" },
|
|
360
|
+
},
|
|
361
|
+
locationType: {
|
|
362
|
+
v9: "p_10005",
|
|
363
|
+
type: "option",
|
|
364
|
+
label: { "de-DE": "Lage", "en-US": "Location type" },
|
|
365
|
+
// For v9 the authored keys are the option identities.
|
|
366
|
+
optionLabels: {
|
|
367
|
+
village: { "de-DE": "Dorf", "en-US": "Village" },
|
|
368
|
+
solitary: { "de-DE": "Alleinlage", "en-US": "Secluded" },
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
## Backend Filters and Compositions
|
|
376
|
+
|
|
377
|
+
These are **not** custom attributes and stay in `customAttributeFilterDefinitions`:
|
|
378
|
+
|
|
379
|
+
- `backendFilter`: a backend-native search key, in practice a v9 hub filter.
|
|
380
|
+
Advertised and executed by v9; ignored by v10.
|
|
381
|
+
- `composition`: a virtual boolean filter that expands into other configured
|
|
382
|
+
filters. Advertised only when every configured leaf can execute on the active
|
|
383
|
+
backend with its configured value.
|
|
384
|
+
|
|
385
|
+
```ts
|
|
386
|
+
const options = defineWebsiteSDKOptions({
|
|
387
|
+
customAttributes,
|
|
388
|
+
customAttributeFilterDefinitions: [
|
|
389
|
+
{
|
|
390
|
+
key: "atLeastTwoBedrooms",
|
|
391
|
+
source: "backendFilter",
|
|
392
|
+
type: "boolean",
|
|
393
|
+
category: "LIVING_SLEEPING",
|
|
394
|
+
label: { "de-DE": "Mind. zwei Schlafzimmer", "en-US": "At least two bedrooms" },
|
|
395
|
+
searchable: true,
|
|
396
|
+
internal: false,
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
key: "premium",
|
|
400
|
+
source: "composition",
|
|
401
|
+
type: "complex",
|
|
402
|
+
category: "ESSENTIALS",
|
|
403
|
+
label: { "de-DE": "Premium", "en-US": "Premium" },
|
|
404
|
+
searchable: true,
|
|
405
|
+
internal: false,
|
|
406
|
+
expression: { all: [{ filter: "atLeastTwoBedrooms", value: true }] },
|
|
407
|
+
},
|
|
408
|
+
],
|
|
409
|
+
});
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
A composition expression supports equality leaves combined with `all`. It is
|
|
413
|
+
**all-or-nothing at the backend-payload level**: every leaf is validated against the
|
|
414
|
+
active backend before any generated input is committed. If any leaf is unknown,
|
|
415
|
+
unsupported, or has an invalid configured value, no composition-generated leaf
|
|
416
|
+
reaches GraphQL or REST and only the composition key is reported in
|
|
417
|
+
`unusedFilterKeys`. A direct query filter remains independent even when a failed
|
|
418
|
+
composition references the same key.
|
|
419
|
+
|
|
420
|
+
When every leaf succeeds, `appliedFilters` contains the composition and its expanded
|
|
421
|
+
leaf entries. Consumers that render removable filter chips should decide whether to
|
|
422
|
+
show both levels or collapse leaf entries belonging to a visible composition.
|
|
423
|
+
|
|
424
|
+
An executable `internal` leaf may be used by a public composition without becoming
|
|
425
|
+
a standalone `getFilters` item. A composition cannot reference another composition;
|
|
426
|
+
option normalization rejects nested compositions while recursive expansion is
|
|
427
|
+
unsupported.
|
|
428
|
+
|
|
429
|
+
`category` remains a stable authored key in configuration. `getFilters` translates
|
|
430
|
+
it through `RentalAttributesCategory.<KEY>.label`, including for `backendFilter` and
|
|
431
|
+
`composition` definitions, and returns the localized display label.
|
|
432
|
+
|
|
433
|
+
Every search query key must have one owner. Option normalization rejects period and
|
|
434
|
+
occupancy keys such as `start`, `adults`, and `pets`; v10 also rejects filterable
|
|
435
|
+
custom attributes and compositions that reuse generated v10 filter keys; v9 rejects
|
|
436
|
+
configured filters that reuse stable v9 filter keys. A custom attribute and a
|
|
437
|
+
configured custom filter also cannot share a key, and two configured filter
|
|
438
|
+
definitions cannot repeat one. Use the custom attribute `key` override when a
|
|
439
|
+
workspace definition collides.
|
|
440
|
+
|
|
441
|
+
## Moving from v9 to v10
|
|
442
|
+
|
|
443
|
+
Switching backend changes capability. The registry and backend-specific filter
|
|
444
|
+
catalog determine which filters and compositions are executable, so `getFilters`
|
|
445
|
+
may add or remove entries.
|
|
446
|
+
|
|
447
|
+
### What is unchanged
|
|
448
|
+
|
|
449
|
+
- The configuration object. One `customAttributes` registry serves both backends.
|
|
450
|
+
- Stable query keys, and therefore existing URLs.
|
|
451
|
+
- Display: an attribute with both an `id` and a `v9` binding renders on both.
|
|
452
|
+
- Backend filters and compositions remain configured, but are advertised only on
|
|
453
|
+
backends that can execute all required leaves.
|
|
454
|
+
|
|
455
|
+
### What you gain
|
|
456
|
+
|
|
457
|
+
- **Custom-attribute filtering.** Definitions with a catalog `id` and an enabled
|
|
458
|
+
filter start appearing in `getFilters` and start being applied by search. On v9
|
|
459
|
+
those same keys were reported in `unusedFilterKeys`.
|
|
460
|
+
|
|
461
|
+
### What you must change
|
|
462
|
+
|
|
463
|
+
1. **Supply a catalog.** v9 needs none; v10 resolves definition IDs through it.
|
|
464
|
+
Without one, no custom attribute has an `id`, so none can filter.
|
|
465
|
+
2. **Add `id` bindings.** A v9-only entry keeps working for display but never
|
|
466
|
+
filters. Move it from `v9Only` into `select` with a `v9` override to get both.
|
|
467
|
+
3. **Re-check backend filters.** `source: "backendFilter"` is a v9 hub concept with
|
|
468
|
+
no v10 equivalent. v10 does not advertise or execute those keys. Any filter you
|
|
469
|
+
need on v10 must become a custom attribute or a built-in filter.
|
|
470
|
+
4. **Re-check compositions.** A composition is only as good as its leaves. One whose
|
|
471
|
+
leaves are v9 backend filters is omitted from v10 `getFilters`; if its key still
|
|
472
|
+
arrives through a saved or hand-written URL, it is reported unused without
|
|
473
|
+
applying any leaf.
|
|
474
|
+
|
|
475
|
+
### Worth knowing
|
|
476
|
+
|
|
477
|
+
- v9 exposes a small set of hub filters that have no v10 counterpart; a key such as
|
|
478
|
+
`pool` may exist on v9 and not on v10.
|
|
479
|
+
- v10 advertises far more built-in filters, so `getFilters` returns a much longer
|
|
480
|
+
list. Query keys are guaranteed unique within one response on both backends.
|
|
481
|
+
|
|
482
|
+
### A configuration that works on both
|
|
483
|
+
|
|
484
|
+
```ts
|
|
485
|
+
const customAttributes = defineCustomAttributes({
|
|
486
|
+
catalog, // empty object on a v9-only site
|
|
487
|
+
select: {
|
|
488
|
+
privateSauna: {
|
|
489
|
+
label: { "de-DE": "Sauna", "en-US": "Sauna" },
|
|
490
|
+
v9: "p_14418", // v9 renders it
|
|
491
|
+
filter: true, // v10 also filters on it
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
On v9 this renders a sauna highlight and reports `?privateSauna=true` as unused. On
|
|
498
|
+
v10 the same object renders the highlight _and_ advertises and applies the filter.
|
|
499
|
+
Nothing in the site's URLs or configuration changes.
|
|
500
|
+
|
|
501
|
+
## Untyped Configuration
|
|
502
|
+
|
|
503
|
+
Configuration that arrives without static types — a JSON options file, the CLI, or
|
|
504
|
+
an object assembled at runtime — goes through the same join:
|
|
505
|
+
|
|
506
|
+
```ts
|
|
507
|
+
import { defineCustomAttributesFromUnknown } from "@v-office/website-sdk";
|
|
508
|
+
|
|
509
|
+
const customAttributes = defineCustomAttributesFromUnknown(JSON.parse(raw));
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
The CLI does this for you. An options file passes the _inputs_, not a finished
|
|
513
|
+
registry:
|
|
514
|
+
|
|
515
|
+
```json
|
|
516
|
+
{
|
|
517
|
+
"customAttributes": {
|
|
518
|
+
"catalog": {
|
|
519
|
+
"privateSauna": { "id": "GQYKYgMVh28X7VLGursbkt", "type": "BOOLEAN" }
|
|
520
|
+
},
|
|
521
|
+
"select": {
|
|
522
|
+
"privateSauna": {
|
|
523
|
+
"label": { "de-DE": "Sauna", "en-US": "Sauna" },
|
|
524
|
+
"filter": true
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
},
|
|
528
|
+
"rentalHighlightPrioritization": ["bedrooms", "privateSauna"]
|
|
529
|
+
}
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
```sh
|
|
533
|
+
website-sdk --backend v10 --options ./website-sdk-options.json filters --locale de-DE
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
Every rule applies identically on this path; only the compile-time key and option
|
|
537
|
+
unions are lost.
|
|
538
|
+
|
|
539
|
+
## Reference
|
|
540
|
+
|
|
541
|
+
Exported from `@v-office/website-sdk`:
|
|
542
|
+
|
|
543
|
+
- `defineCustomAttributes` — join a catalog with a selection, typed.
|
|
544
|
+
- `defineCustomAttributesFromUnknown` — the same join for untyped input.
|
|
545
|
+
- `fetchCustomAttributeCatalog` — read the catalog from the definitions API.
|
|
546
|
+
- `parseCustomAttributeCatalog` — validate a catalog that arrives as plain data.
|
|
547
|
+
- `CustomAttributeCatalogSchema` — the catalog schema.
|
|
548
|
+
|
|
549
|
+
Types: `CustomAttributeCatalog`, `CustomAttributeCatalogInput`,
|
|
550
|
+
`CustomAttributeCatalogDefinitionInput`, `CustomAttributeCategoryKey`,
|
|
551
|
+
`CustomAttributeOverridesInput`, `CustomAttributeSelection`, `CustomAttributeRegistry`,
|
|
552
|
+
`CustomAttributeRegistrySnapshot`, `ResolvedCustomAttribute`,
|
|
553
|
+
`V9OnlyCustomAttributeInput`.
|
|
554
|
+
|
|
555
|
+
Related: `creation.md` for options, `filter.md` for `getFilters`, `search.md` for
|
|
556
|
+
query parsing and applied filters, `rentals.md` for attributes and highlights.
|
|
@@ -36,14 +36,14 @@ Sample:
|
|
|
36
36
|
[
|
|
37
37
|
{
|
|
38
38
|
"searchParameterQueryKey": "wifi",
|
|
39
|
-
"label": "
|
|
40
|
-
"category": "
|
|
39
|
+
"label": "WLAN",
|
|
40
|
+
"category": "Grundlagen",
|
|
41
41
|
"type": "BooleanFilter"
|
|
42
42
|
},
|
|
43
43
|
{
|
|
44
44
|
"searchParameterQueryKey": "bedrooms",
|
|
45
|
-
"label": "
|
|
46
|
-
"category": "
|
|
45
|
+
"label": "Schlafzimmer",
|
|
46
|
+
"category": "Wohn- und Schlafbereiche",
|
|
47
47
|
"type": "IntFilter",
|
|
48
48
|
"numericBounds": {
|
|
49
49
|
"min": 1,
|
|
@@ -53,22 +53,30 @@ Sample:
|
|
|
53
53
|
{
|
|
54
54
|
"searchParameterQueryKey": "region",
|
|
55
55
|
"label": "Region",
|
|
56
|
-
"category": "
|
|
56
|
+
"category": "Andere",
|
|
57
57
|
"type": "OptionFilter",
|
|
58
58
|
"options": [
|
|
59
59
|
{
|
|
60
60
|
"value": "north",
|
|
61
|
-
"label": "
|
|
61
|
+
"label": "Norden"
|
|
62
62
|
}
|
|
63
63
|
]
|
|
64
64
|
}
|
|
65
65
|
]
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
+
`searchParameterQueryKey` and each option `value` are **stable identifiers**: they are
|
|
69
|
+
what you put in a URL, and they never change with locale. `label` and `category` are
|
|
70
|
+
**localized display strings** for the requested locale, not keys — do not group or
|
|
71
|
+
match on them programmatically. This holds for custom attributes too: an attribute
|
|
72
|
+
authored with `category: "ESSENTIALS"` is advertised with `"category": "Grundlagen"`
|
|
73
|
+
in `de-DE`, exactly like a built-in filter.
|
|
74
|
+
|
|
68
75
|
Filter variants:
|
|
69
76
|
|
|
70
77
|
- `BooleanFilter`: on/off query parameter.
|
|
71
78
|
- `IntFilter`: numeric query parameter, optionally with `numericBounds`.
|
|
79
|
+
- `StringFilter`: non-empty exact-match query parameter.
|
|
72
80
|
- `OptionFilter`: query parameter with localized selectable `options`.
|
|
73
81
|
|
|
74
82
|
## Configuration
|
|
@@ -97,29 +105,26 @@ Filter variants:
|
|
|
97
105
|
}
|
|
98
106
|
```
|
|
99
107
|
|
|
100
|
-
Optional `WebsiteSDKOptions`
|
|
108
|
+
Optional `WebsiteSDKOptions` add two further sources of filters.
|
|
109
|
+
|
|
110
|
+
Custom attributes come from `customAttributes`, and a definition is advertised only
|
|
111
|
+
when it has a public filter and the selected backend can execute it. v9 has no
|
|
112
|
+
custom-attribute search path, so v9 never advertises one; see `custom-attributes.md`.
|
|
113
|
+
|
|
114
|
+
Backend filters and compositions come from `customAttributeFilterDefinitions`:
|
|
101
115
|
|
|
102
116
|
```json
|
|
103
117
|
{
|
|
104
118
|
"customAttributeFilterDefinitions": [
|
|
105
119
|
{
|
|
106
|
-
"key": "
|
|
107
|
-
"source": "
|
|
108
|
-
"category": "
|
|
120
|
+
"key": "atLeastTwoBedrooms",
|
|
121
|
+
"source": "backendFilter",
|
|
122
|
+
"category": "LIVING_SLEEPING",
|
|
109
123
|
"label": {
|
|
110
|
-
"de-DE": "
|
|
111
|
-
"en-US": "
|
|
124
|
+
"de-DE": "Mind. zwei Schlafzimmer",
|
|
125
|
+
"en-US": "At least two bedrooms"
|
|
112
126
|
},
|
|
113
|
-
"type": "
|
|
114
|
-
"options": [
|
|
115
|
-
{
|
|
116
|
-
"value": "north",
|
|
117
|
-
"label": {
|
|
118
|
-
"de-DE": "Norden",
|
|
119
|
-
"en-US": "North"
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
],
|
|
127
|
+
"type": "boolean",
|
|
123
128
|
"searchable": true,
|
|
124
129
|
"internal": false
|
|
125
130
|
}
|
|
@@ -128,7 +133,30 @@ Optional `WebsiteSDKOptions` can add public custom filters:
|
|
|
128
133
|
}
|
|
129
134
|
```
|
|
130
135
|
|
|
131
|
-
Only
|
|
136
|
+
Only definitions with `searchable: true` and `internal: false` are exposed by
|
|
137
|
+
`getFilters`. `backendFilter` entries are advertised by v9 only, because they have no
|
|
138
|
+
v10 execution path. Each query key has one owner: period and occupancy collisions
|
|
139
|
+
fail during option normalization, v9/v10 built-in filter collisions fail in the
|
|
140
|
+
corresponding backend configuration path, and a custom attribute cannot share a key
|
|
141
|
+
with a configured custom filter. Repeated keys within
|
|
142
|
+
`customAttributeFilterDefinitions` also fail during option normalization.
|
|
143
|
+
`getFilters` keeps a final uniqueness assertion so parse order never silently
|
|
144
|
+
decides ownership.
|
|
145
|
+
|
|
146
|
+
Compositions are additionally backend-aware: `getFilters` advertises one only when
|
|
147
|
+
every leaf exists on the active backend and its configured value is valid for that
|
|
148
|
+
leaf. Executable internal leaves may participate without being exposed separately.
|
|
149
|
+
Unknown, unsupported, or backend-specific leaves omit the composition. Nested
|
|
150
|
+
compositions are rejected during option normalization while recursive expansion is
|
|
151
|
+
unsupported.
|
|
152
|
+
|
|
153
|
+
During search, a successful composition and its expanded leaves are all returned in
|
|
154
|
+
the search output's `appliedFilters`; `getFilters` itself returns only discovery
|
|
155
|
+
metadata. See `search.md` for filter-chip guidance.
|
|
156
|
+
|
|
157
|
+
Configured category values remain stable keys such as `OUTDOORS`. The output
|
|
158
|
+
translates them through `RentalAttributesCategory.<KEY>.label`, honors
|
|
159
|
+
`translationOverrides`, and returns only the localized display label.
|
|
132
160
|
|
|
133
161
|
For v10, `getFilters` also exposes the mapped BooleanFilter `pets`, labeled `Dogs welcome` / `Hunde willkommen`. It maps to one pet in search occupancy; use `petsCount` when the UI needs an exact pet count instead.
|
|
134
162
|
|