@v-office/website-headless 0.1.1 → 1.0.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.
@@ -0,0 +1,72 @@
1
+ import { k as acquireShared } from "./parts-CS1VBrPt.mjs";
2
+ //#region src/sdk/errors.ts
3
+ /** Nesting deeper than this is cut: a cause chain is a handful of levels, never more. */
4
+ const MAX_DEPTH = 6;
5
+ /**
6
+ * Every failure arrives as a `CoreSDKError` (`source`, `operation`, `message`, `cause`), and the
7
+ * sentence that names the fault is in `cause` — a GraphQL `errors` array, a transport
8
+ * `{ status, statusText, body }`, or a nested `CoreSDKError`. `console.error(error)` does not print it,
9
+ * and neither does `Object.entries`: `Error` declares `name`, `message` and `cause` non-enumerable
10
+ * (verified on the real class: `Object.keys(new CoreSDKError(…))` is `["source", "operation", "_tag"]`).
11
+ * So those three are read by hand, `cause` first after the message so the reason reads under it.
12
+ */
13
+ function unwrap(value, depth, seen) {
14
+ if (value === null || typeof value !== "object") return value;
15
+ if (depth > MAX_DEPTH) return "[truncated]";
16
+ if (seen.has(value)) return "[circular]";
17
+ seen.add(value);
18
+ if (Array.isArray(value)) return value.map((entry) => unwrap(entry, depth + 1, seen));
19
+ const out = {};
20
+ if (value instanceof Error) {
21
+ out["name"] = value.name;
22
+ out["message"] = value.message;
23
+ if (value.cause !== void 0) out["cause"] = unwrap(value.cause, depth + 1, seen);
24
+ }
25
+ for (const [key, entry] of Object.entries(value)) if (key !== "stack") out[key] = unwrap(entry, depth + 1, seen);
26
+ return out;
27
+ }
28
+ /** See {@link DescribeSdkError}. */
29
+ const describeSdkError = function describeSdkError(error) {
30
+ try {
31
+ return JSON.stringify(unwrap(error, 0, /* @__PURE__ */ new WeakSet()), null, 2) ?? String(error);
32
+ } catch {
33
+ return String(error);
34
+ }
35
+ };
36
+ /** See {@link LogSdkFailure}. */
37
+ const logSdkFailure = function logSdkFailure(scope, error) {
38
+ console.error(`[sdk] ${scope} failed:`, describeSdkError(error));
39
+ };
40
+ //#endregion
41
+ //#region src/sdk/claim.ts
42
+ /** See {@link SdkInput}. */
43
+ const sdkInput = function sdkInput(setup, mode) {
44
+ const options = mode === "all-rentals-at-once" ? {
45
+ ...setup.options,
46
+ searchAllRentalsAtOnce: true
47
+ } : setup.options;
48
+ return options ? {
49
+ config: setup.config,
50
+ options
51
+ } : { config: setup.config };
52
+ };
53
+ /** JSON with object keys sorted at every level, so two equal setups written in a different key order
54
+ * share one instance. `undefined` members drop out, as in JSON. */
55
+ function stableJson(value) {
56
+ return JSON.stringify(value, (_key, entry) => entry !== null && typeof entry === "object" && !Array.isArray(entry) ? Object.fromEntries(Object.entries(entry).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) : entry);
57
+ }
58
+ /** See {@link ClaimSdk}. */
59
+ const claimSdk = function claimSdk(setup, mode, construct) {
60
+ const claim = acquireShared("sdk", stableJson({
61
+ setup,
62
+ mode
63
+ }), () => construct(setup, mode), (sdk) => {
64
+ sdk.dispose().catch((error) => logSdkFailure("sdk.dispose", error));
65
+ });
66
+ return {
67
+ sdk: claim.value,
68
+ release: claim.release
69
+ };
70
+ };
71
+ //#endregion
72
+ export { logSdkFailure as i, sdkInput as n, describeSdkError as r, claimSdk as t };
@@ -0,0 +1,9 @@
1
+ import { n as sdkInput } from "./sdk-claim-CsYFzvEc.mjs";
2
+ import { createWebsiteSDK } from "@v-office/website-sdk";
3
+ //#region src/sdk/create.ts
4
+ /** See {@link CreateSdk}. */
5
+ const createSdk = function createSdk(setup, mode = "default") {
6
+ return createWebsiteSDK(sdkInput(setup, mode));
7
+ };
8
+ //#endregion
9
+ export { createSdk as t };
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ <!-- generated by `node .github/release/cli.mts index`; do not edit by hand -->
4
+
5
+ Release notes of `@v-office/website-headless`, newest first. Each version has its own directory.
6
+
7
+ - `versions/1.0.0/CHANGELOG.md`: release notes for 1.0.0
8
+ - `versions/0.2.0/CHANGELOG.md`: release notes for 0.2.0
9
+ - `versions/0.1.1/CHANGELOG.md`: release notes for 0.1.1
@@ -0,0 +1,9 @@
1
+ # Migration
2
+
3
+ <!-- generated by `node .github/release/cli.mts index`; do not edit by hand -->
4
+
5
+ Migration guides of `@v-office/website-headless`, newest first. Apply them in order when skipping versions.
6
+
7
+ - `versions/1.0.0/MIGRATION.md`: Migration: 0.2.0 to 1.0.0
8
+ - `versions/0.2.0/MIGRATION.md`: Migration: 0.1.1 to 0.2.0
9
+ - `versions/0.1.1/MIGRATION.md`: Migration to 0.1.1
@@ -0,0 +1,7 @@
1
+ # Changelog: 0.1.1
2
+
3
+ Baseline of the shipped release notes for `@v-office/website-headless`. The package is the framework-free behavior layer of the website platform: the checkout flow as a store, validation and insurance rules, the checkout's structural requirements and the progressive-enhancement engines.
4
+
5
+ Changes before 0.1.1 are recorded in this repository's git history only. From
6
+ this version on, every release carries its own `CHANGELOG.md` and
7
+ `MIGRATION.md` in `instructions/versions/<version>/`.
@@ -0,0 +1,4 @@
1
+ # Migration to 0.1.1
2
+
3
+ No changes required. 0.1.1 is the version that was published when the shipped
4
+ release notes were introduced; consumers already on it stay as they are.
@@ -0,0 +1,3 @@
1
+ {
2
+ "componentsAffectedByHeadlessChange": []
3
+ }
@@ -0,0 +1,17 @@
1
+ # Changelog: 0.2.0
2
+
3
+ The initial availability of the booking date picker now carries the v9 rules for children and
4
+ pets, so a guest selector can stop offering a party the quote would refuse.
5
+
6
+ ## Added
7
+
8
+ - `fetchInitialAvailability` returns `childrenAllowed` and `maxPets` in its
9
+ `InitialAvailability`. On v9 with `@v-office/website-sdk` 2.28.0 or later,
10
+ `childrenAllowed` is `false` when the rental takes no children, babies included, and
11
+ `maxPets` is the most pets the quote accepts, `0` for none. Both are `null` on v10, with an
12
+ older SDK and when the rental states no rule; `null` means no limit.
13
+
14
+ ## Unchanged
15
+
16
+ - `maxPersons` and `maxAdults` keep their meaning. On v9 they carry the rental's limits from
17
+ `@v-office/website-sdk` 2.28.0 on, where they were `null` before.
@@ -0,0 +1,16 @@
1
+ # Migration: 0.1.1 to 0.2.0
2
+
3
+ ## 1. Upgrade
4
+
5
+ ```sh
6
+ pnpm add @v-office/website-headless@0.2.0
7
+ ```
8
+
9
+ No changes are required for code that reads `fetchInitialAvailability`. Code that builds an
10
+ `InitialAvailability` itself, such as a test fixture, adds `childrenAllowed: null` and
11
+ `maxPets: null`.
12
+
13
+ A site with its own guest selector can use the two fields: offer neither children nor babies
14
+ when `childrenAllowed` is `false`, and cap pets at `maxPets`, hiding them at `0`. Keep a
15
+ selection the guest arrived with visible until they remove it, so a party the quote refuses
16
+ can still be corrected.
@@ -0,0 +1,3 @@
1
+ {
2
+ "componentsAffectedByHeadlessChange": ["quote", "booking"]
3
+ }
@@ -0,0 +1,170 @@
1
+ # Changelog: 1.0.0
2
+
3
+ 1.0.0 is a redesign of the behaviour layer, with no 0.x compatibility. Every behaviour the
4
+ website surfaces need now lives here as a store or a pure rule (the search form, the live search,
5
+ the results map, the quote, the checkout and the contact form), every runner takes the SDK first,
6
+ one SDK instance serves a whole page, and the fleet's per-site rulings became options with safe
7
+ defaults instead of one site's behaviour for everyone. `@v-office/website-components` 1.0.0 renders
8
+ these stores; a site that builds its own UI uses the same stores and is held to the same rules.
9
+ Storage keys, URL keys and the German sort values are unchanged, so links and every guest's
10
+ Merkliste survive the switch. `MIGRATION.md` lists what to change, in order.
11
+
12
+ ## Added
13
+
14
+ ### SDK and wiring
15
+
16
+ - `SdkSetup` (`{ config, options? }`): the one plain-data SDK input every surface takes, written
17
+ once per site.
18
+ - `sdkConfigFromEnv(env)` with `SDK_ENV` and `SdkEnvError`: the fleet's env names
19
+ (`CMS_BACKEND`, `CMS_STAGE`, `HUB_API_KEY`, `HUB_GRAPHQL_URL`, `HUB_V0_API_BASE_URL`,
20
+ `HUB_V1_API_BASE_URL`, `IMAGE_PROXY_BASE_URL`, `VOFFICE_LOCAL_DEV_ACCESS_TOKEN`,
21
+ `VOFFICE_API_ENDPOINT`, `VOFFICE_SEARCH_ENDPOINT`, `VOFFICE_IMAGE_BASE_URL`) to an
22
+ `SdkConfig`. No backend default; one error names every missing or conflicting variable, never a
23
+ value.
24
+ - `shareSdk(setup, mode?)` and `loadSharedSdk(setup, mode?)`: every surface on a page with an
25
+ equal setup gets the same instance, so one 1 request/second queue and one cache serve the
26
+ page. `loadSharedSdk` imports the SDK on first use. `resolveSdk`, `SdkSource` (an instance or a
27
+ getter) and `SdkMode` (`"default"`, `"all-rentals-at-once"`).
28
+ - `walkSearchPages`, `fetchSearchPage`, `SEARCH_PAGE_SIZE`: the one paged walker every search
29
+ uses, 100 results per request.
30
+
31
+ ### Stores and rules, per surface
32
+
33
+ - **Search form:** `createSearchForm` (the bar's state, a GET form that never calls the SDK),
34
+ `createDateSearch`, `createGuestSearch`, `describePeriod`, `formatStayRange`, and the rental
35
+ name jump (`matchRentals`, `loadRentalIndex`, `rentalIndexEntries`).
36
+ - **URL parameters:** `SEARCH_PARAMS`, `readSearch`, `writeSearch`, `searchHref`,
37
+ `SearchContract` (`backend`, `nightRangeSearch`, `aliases`), `readParty`, `partyPairs`.
38
+ - **Guests:** one party model (`Party`, `GuestOptions`, `GuestLimits`, `guestBounds`,
39
+ `clampParty`, `agesComplete`) for the search bar, the quote and the checkout. `limits.pets`: 0 =
40
+ no dogs, 1 = a toggle, more = a stepper. `GuestCaps` takes 0.2.0's v9 `childrenAllowed` and
41
+ `maxPets`; `guestBounds` applies them as 0.2.0's components did.
42
+ - **Search store:** `createSearchStore` (the page's one live search, with filters, sort, chips,
43
+ favourites scope and the empty and error outcomes), `acquireSearchStore`, `connectSearchStore`,
44
+ result items for prerendered listings (`catalogueResultItem`, `resultCardSource`), sort
45
+ (`SortKey`, `SORT_URL_VALUE`, `DEFAULT_SORT_OPTIONS`, `sortResults`), filter curation
46
+ (`FilterSpec`, `resolveFilters`, `measureFilters`), links (`handoverQuery`,
47
+ `externalSearchHref`, `sanitizeSearch`, `resultsQueryFlagScript`), `describeOutcome`.
48
+ - **Offers:** `offersWindowQuery`, `findOfferWindows`, `findCampaignStart`, `campaignGroup`,
49
+ `campaignChips`, `offerWindowHref`; result links carry `offer=` labels to the rental page.
50
+ - **Results map:** `createResultsMapController` over the search store, pins, framing, same-spot
51
+ layout, and the address decision `rentalAddressLine` with `RentalAddressMode` (no default).
52
+ - **Rental catalogue:** `loadRentalCatalogue(sdk, { locale, … })` gives `RentalDetail[]` for the
53
+ rental pages, JSON-LD and cards; `fetchRentalFlags`, slugs, counts, distances, amenity and
54
+ description previews, reviewer names, image URL rules.
55
+ - **Cards:** `CardSource` and `cardSource(rental, { href, … })`, the one card input;
56
+ `toCardModel`, `rentalFactsMap`, `loadRentalFacts`, `matches`.
57
+ - **Showcase:** `buildShowcaseGroups`, `selectRentals`, `selectRelated`, `rotationWindow`,
58
+ `resolveSearchGroups`.
59
+ - **Quote:** `createQuoteFlow` and `acquireQuoteFlow` (the booking widget as a store),
60
+ `createStayPicker`, `dayPriceLabel` with `DayPrices`, the rental page's URL contract
61
+ (`readQuoteHandover`, `writeQuoteParams`, `checkoutHref`, `stayQuery`), the cancellation-policy
62
+ rules (`describePolicyOffer`, `offeredPolicies`, `policyStance`, `policyTitle`, `policyTerms`,
63
+ `PolicyChoiceMode`), `BookableFrom` for a site that is not bookable online yet.
64
+ - **Checkout:** `readCheckoutHandover`, `checkoutSearch`, `createGuestForm` (the guest data form
65
+ with the fleet's validation timing), `readPaymentReturn`, option bookings (`?kind=option`).
66
+ - **Contact form:** `createContactForm` with field specs, validation and the PMS submit;
67
+ `DEFAULT_CONTACT_FIELDS`, `CONTACT_LIMITS`.
68
+ - **Favourites:** `favoritesStorageKey(namespace)`, `createFavorites`, `acquireFavorites`.
69
+ - **Countries and legal documents:** `countryOptions`, `dialCode`, `fetchLegalDocuments`.
70
+ - **Shared helpers:** `IsoDay` / `IsoMonth` and their arithmetic, `formatPriceLabel` with
71
+ `PriceCents`, `parseTextBlocks` and `normalizeStructuredDocument` (sanitized PMS text),
72
+ `routeHref`, `browserStorage`, the tracking-parameter list, the closed `GLYPHS` vocabulary.
73
+
74
+ ### Events and part vocabularies
75
+
76
+ - `QUOTE_EVENT` (`voffice:quote`), `CHECKOUT_EVENT` (`voffice:checkout`) and `CONTACT_EVENT`
77
+ (`voffice:contact`) with their detail types. The stores hand them to `onEvent`; the components
78
+ dispatch them on `window`, and a site's own UI dispatches the same shapes. No detail carries
79
+ personal data.
80
+ - `ENHANCE_PARTS` and `GATE_MARKERS`, exported from both entries: the markup contract of the
81
+ enhance engines and the markers the platform's page gates read.
82
+ - `CHECKOUT_PARTS` gains `consent` and `insurancePremium`; `CHECKOUT_STRUCTURE` gains
83
+ `consents-before-submit` and `premium-before-submit`; `SUBMIT_WORDING` (per booking kind, German
84
+ and English) and `BOOKING_KIND_ATTRIBUTE` (`data-booking-kind` on the order button).
85
+
86
+ ### Copy templates
87
+
88
+ - `fillTemplate(template, values)`: fills `{token}`s; an unknown token stays visible.
89
+ - `Template<K>`: the type of a copy string whose tokens are `K`. `fillTemplate` needs a value for
90
+ every token a template declares and takes no other.
91
+ - `defineCopy<T>()(overrides)`: checks a site's copy overrides at compile time (unknown keys, wrong
92
+ types, tokens the key does not declare) and returns them unchanged. `TokensOf` and
93
+ `CheckedCopy` are the types behind it.
94
+ - The contact messages type their templates: `extraRequired` and `fieldInvalid` take `{label}`,
95
+ `messageTooLong` `{max}`, `sendFailed` `{email}`.
96
+
97
+ ### `./enhance`
98
+
99
+ - `enhance(root, options)`: one call in the layout binds every engine below the root, idempotent,
100
+ returns a `Dispose`. Engines: `bindStrip`, `bindLayer`, `bindGallery`, `bindReveal`,
101
+ `bindClamp`, `bindShare`, `bindImageFade`, `bindFavorites`, and the MapLibre engines
102
+ (`createMapSurface`, `bindResultMarkers`, `bindPlaceMarkers`, `observeResultCardHover`). The
103
+ map engines take the `maplibre-gl` loader from the caller; the package does not depend on it.
104
+
105
+ ## Changed
106
+
107
+ - **Peer dependency:** `@v-office/website-sdk >=2.25.0 <3` (was `>=2.13.0 <3`). 1.0.0 uses the
108
+ unavailable `code` (2.16), v9 `matchingPeriods` (2.22), `searchAllRentalsAtOnce` (2.23), the Hub
109
+ `/Setup` fix and `v9Reviews.criteria` (2.25).
110
+ - **`createSdk(setup, mode?)`** takes an `SdkSetup` (was `createSdk(config, options?)`).
111
+ `SdkConfig` is the SDK's own config type, with `backend` explicit on v10.
112
+ - **Every SDK runner takes the SDK first:** `fetchQuote(sdk, request)`,
113
+ `runLiveSearch(sdk, search, options)` (returns a `SearchPage`), `submitBooking(sdk, quote,
114
+ contact, options)`, `payWithOption(sdk, option)`, `fetchInitialAvailability(sdk, input)`,
115
+ `fetchEndAvailability(sdk, input)`, `addQuoteService`, `removeQuoteService`,
116
+ `clearQuoteServices`, `selectQuotePolicy`, `selectQuoteInsurance`,
117
+ `pricePolicyBranches(sdk, base)`.
118
+ - **`createCheckoutFlow(sdk, options)`:** `handover`, `returnPath` and `messages` are required;
119
+ the state is `phase`, `stay`, `services`, `policy`, `insurance`, `voucher`, `consents`,
120
+ `blockers` (why the order button is off), `submit`, `confirmation` and `payment`. New actions
121
+ `setDates`, `setParty`, `loadGuestCaps` (the arrival map's caps for the guest editor, loaded
122
+ when it opens), `selectPolicy`, `setConsent`, `retry`; `openInsuranceDocument(url)` and
123
+ `confirmInsuranceDocuments(confirmed)`; `pay(option)`. Events go to `onEvent`. The flow never
124
+ scrolls the window.
125
+ - **Reshaped types:** `Stay` is `{ start, end, party }` in ISO days; `SearchResultItem` carries
126
+ `price: StayPrice`, `counts` and `periods`; `AlternativePeriod`, `FilterDef`, `ActiveFilter`
127
+ and `CheckoutIssue` are reshaped; `adaptGuestQuote` returns a `QuoteView` whose `price` is a
128
+ `PriceView`; `stayKeyOf` and `occupancyOf` take a `Stay` / `Party`;
129
+ `freeCancellationUntil(policy, arrival)` takes one policy and an `IsoDay`;
130
+ `detectCountry(countries, languages, fallback?)` no longer reads `navigator`.
131
+ - **Dates** are `IsoDay` / `IsoMonth` strings at every public boundary.
132
+
133
+ ## Removed
134
+
135
+ - **Site-specific defaults:** `CHECKOUT_RETURN_PATH`, `DEFAULT_RENTALS_PATH`,
136
+ `LiveSearchOptions.rentalsPath` and the German `SORT_OPTIONS` labels. Routes and copy are the
137
+ site's.
138
+ - **0.1.x search, sort, filter and region helpers:** `SearchPayload`, `SearchImage`,
139
+ `SORT_PARAM`, `SortValue`, `SortOption`, `DEFAULT_SORT`, `activeSort`, `isPricedQuery`,
140
+ `isSortAvailable`, `resolveSdkSort`, `FilterType`, `isBooleanFilterActive`,
141
+ `activeFiltersFromQuery`, `PETS_PARAM`, `PETS_COUNT_PARAM`, `petsCountInQuery`,
142
+ `REGION_PARAM`, `activeRegions`, `scopeToRegions`.
143
+ - **0.1.x date and price helpers:** `NIGHT_RANGE_ENABLED`, `NIGHT_RANGE_KEYS`,
144
+ `parseNightRange`, `parseParam`, `parseGermanParam`, `isoDate`, `stayParams`, `tidyPrice`,
145
+ `nightlyPrice`.
146
+ - **0.1.x quote and checkout names:** `QuoteLine`, `PriceQuote`, `fullRefundRule`,
147
+ `policyFreeCancellationUntil`, `BookingContactForm`, `saveBookingSnapshot` and
148
+ `BookingSnapshotGuests` (`submitBooking` writes the snapshot), `VALIDATED_FIELDS`, `fieldError`,
149
+ `CheckoutFlowApi`, `CheckoutFlowState`, `CheckoutFlowActions`, `CheckoutFlowMessages`,
150
+ `BUTTON_SOLUTION_PATTERN`, `CountryGroupsLike`, and the checkout flow's `getSdk`, `stay`,
151
+ `chosenPolicy`, `filterOfferedPolicies` and `onRateConfigurationFault` options.
152
+ - **`./enhance`:** `initImageCarousels`, `initRentalCarousel`, `initHistoryOverlay`,
153
+ `initGallery`, `HistoryOverlayOptions`, `GalleryOptions`, `GalleryLayer`, and the
154
+ `[data-rental-tabs]` / `.rental-track` selectors. Dots are repainted through `data-active`; no
155
+ Tailwind class is written.
156
+
157
+ ## Unchanged
158
+
159
+ - Storage keys: `<namespace>-merkliste` (favourites), `voffice:checkout-context` and
160
+ `voffice:booking-return` (payment return). The `merkliste:changed` event.
161
+ - Every URL key and the German sort values (`empfohlen`, `preis-auf`, `preis-ab`, `bewertung`,
162
+ `groesse`, `groesse-auf`, `gaeste`, `schlafzimmer`, `zufall`).
163
+ - The gate markers on the same elements (`data-rental-stats`, `data-result-card`, `data-fav*`,
164
+ `data-img-*`, …), and legacy `.card-images` / `data-img-*` strips still bind.
165
+ - `book()`, `pay()` and the contact `submit()` stay user-initiated only, and the § 7 VVG
166
+ insurance document gate is not configurable.
167
+ - `describeSdkError` and `logSdkFailure`.
168
+ - `fetchInitialAvailability` returns 0.2.0's `childrenAllowed` and `maxPets`. The `StayPicker`'s
169
+ `caps` carry them for the quote, and the checkout's `loadGuestCaps` for its guest editor, so
170
+ both bound the party by the v9 rules.
@@ -0,0 +1,252 @@
1
+ # Migration: 0.2.0 to 1.0.0
2
+
3
+ 1.0.0 is not compatible with 0.x. Do the steps in this order, in one change. A site that also
4
+ renders `@v-office/website-components` moves it to 1.0.0 in the same change and follows that
5
+ package's `MIGRATION.md` after this one. A site that has only copied kit code and never imported
6
+ 0.x follows the same steps: replace the kit's SDK seam, stores and helpers with the 1.0.0 names
7
+ below. The README (in this package) has every option and default.
8
+
9
+ ## 1. Dependencies
10
+
11
+ Pin exact versions and install, so the lockfile matches:
12
+
13
+ ```sh
14
+ pnpm add --save-exact @v-office/website-headless@1.0.0 @v-office/website-sdk@2.28.1
15
+ # with the components:
16
+ pnpm add --save-exact @v-office/website-components@1.0.0
17
+ # so the build reports the contact surface:
18
+ pnpm add --save-exact @v-office/site-manifest@0.3.0
19
+ ```
20
+
21
+ - Pin `@v-office/website-sdk` 2.28.1. From 2.28.0, v9 availability states the occupancy, children
22
+ and pets rules the guest selectors apply; never pin below the version a site already has. From
23
+ 2.27.0, with site-manifest 0.3.0, the build reports the SDK it
24
+ constructed, and the platform keeps the website's public token and SDK options current. Construct
25
+ the SDK from one setup only (`createSdk` and `shareSdk` with the same `SdkSetup` count as one).
26
+ `sdkMode: "all-rentals-at-once"` beside it counts as a second SDK on the same token: the token
27
+ stays current, the stored SDK options are kept. A second setup with another token corrects neither.
28
+ - The peer range is `@v-office/website-sdk >=2.25.0 <3`. 2.25.0 fixes the v9 Hub `/Setup` answer
29
+ without configured add-ons; delete a site's `fetch` patch for `/api/v1/Setup` in this change,
30
+ never earlier.
31
+ - `maplibre-gl` is not a dependency of this package. A site that shows a map pins `maplibre-gl`
32
+ 5.x itself.
33
+
34
+ ## 2. The SDK setup
35
+
36
+ Write the SDK input once, in `src/data/sdk.ts`, and import it everywhere. Spell out every env
37
+ name: Vite inlines only the `import.meta.env.NAME` it sees, so `import.meta.env` passed whole can
38
+ arrive without them in a build.
39
+
40
+ ```ts
41
+ import { sdkConfigFromEnv, type SdkSetup } from "@v-office/website-headless";
42
+
43
+ export const SDK: SdkSetup = {
44
+ config: sdkConfigFromEnv({
45
+ CMS_BACKEND: import.meta.env.CMS_BACKEND,
46
+ CMS_STAGE: import.meta.env.CMS_STAGE,
47
+ HUB_API_KEY: import.meta.env.HUB_API_KEY,
48
+ HUB_GRAPHQL_URL: import.meta.env.HUB_GRAPHQL_URL,
49
+ HUB_V0_API_BASE_URL: import.meta.env.HUB_V0_API_BASE_URL,
50
+ HUB_V1_API_BASE_URL: import.meta.env.HUB_V1_API_BASE_URL,
51
+ IMAGE_PROXY_BASE_URL: import.meta.env.IMAGE_PROXY_BASE_URL,
52
+ VOFFICE_LOCAL_DEV_ACCESS_TOKEN: import.meta.env.VOFFICE_LOCAL_DEV_ACCESS_TOKEN,
53
+ VOFFICE_API_ENDPOINT: import.meta.env.VOFFICE_API_ENDPOINT,
54
+ VOFFICE_SEARCH_ENDPOINT: import.meta.env.VOFFICE_SEARCH_ENDPOINT,
55
+ VOFFICE_IMAGE_BASE_URL: import.meta.env.VOFFICE_IMAGE_BASE_URL,
56
+ }),
57
+ // Only the options the site passed to the SDK before (translationOverrides,
58
+ // customAttributes, rentalHighlightPrioritization, …): the setup is serialized into every island.
59
+ options: {},
60
+ };
61
+ ```
62
+
63
+ - **The env.** `CMS_BACKEND` must be `v9` or `v10`; there is no default any more (kit code read an
64
+ unset value as v9). The token is `HUB_API_KEY` on v9 and `VOFFICE_LOCAL_DEV_ACCESS_TOKEN` on
65
+ v10. Set either `CMS_STAGE` (`development` or `production`, the platform's endpoints) or every
66
+ URL variable of the backend (v9: `HUB_GRAPHQL_URL`, `HUB_V0_API_BASE_URL`,
67
+ `HUB_V1_API_BASE_URL`, `IMAGE_PROXY_BASE_URL`; v10: `VOFFICE_API_ENDPOINT`,
68
+ `VOFFICE_SEARCH_ENDPOINT`, `VOFFICE_IMAGE_BASE_URL`), never both. Do not rename a variable: the
69
+ platform's builder reads `HUB_API_KEY`. A missing or conflicting variable fails the build with an
70
+ `SdkEnvError` that names the variables.
71
+ - **Build time** (Astro frontmatter, loaders, scripts): one instance for every loader, disposed
72
+ after the last one.
73
+
74
+ ```ts
75
+ const sdk = createSdk(SDK);
76
+ try {
77
+ const rentals = await loadRentalCatalogue(sdk, { locale: "de-DE" });
78
+ // … every other loader takes the same sdk
79
+ } finally {
80
+ await sdk.dispose();
81
+ }
82
+ ```
83
+
84
+ - **Browser:** `const shared = shareSdk(SDK)` (or `await loadSharedSdk(SDK)` where the only SDK use
85
+ is a submit), pass `shared.sdk` to the stores, call `shared.release()` when the UI goes. Every
86
+ surface given the same `SDK` shares one instance and one rate limit; a different setup is a
87
+ second instance with its own 1 request/second queue.
88
+ - Replace `createSdk(config, options)` with `createSdk({ config, options })`, and every
89
+ `sdkConfigFor(…)` built per surface with the one `SDK` constant. Delete the site's own SDK
90
+ modules (a `voffice.ts`, a `backend.ts` / `backendKind()`, an SDK error helper).
91
+
92
+ ## 3. Site constants
93
+
94
+ Write each decision once in `src/data/` and pass the same constant to every place that needs it:
95
+
96
+ | constant | type | what it decides |
97
+ | ----------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
98
+ | `SDK` | `SdkSetup` | the SDK input (step 2) |
99
+ | `SEARCH_CONTRACT` | `SearchContract` (`backend`, `nightRangeSearch`, `aliases`) | how the search runs on this backend; `nightRangeSearch: false` where the v9 Hub answers the ± pair unpriced |
100
+ | `GUESTS` | `GuestOptions` (`limits`, `childAge`, `defaultAdults`) | the party limits; `limits.pets` 0 = no dogs, 1 = a toggle, more = a stepper |
101
+ | `DAY_PRICES` | `DayPrices` (`"rounded"`, `"exact"`, `"none"`) | prices in calendar cells; a site with a no-price ruling sets `"none"` |
102
+ | `POLICY_CHOICE` | `PolicyChoiceMode` (`"rental-page"`, `"checkout"`) | where the cancellation rate is chosen, one value for quote and checkout |
103
+ | `BOOKABLE` | `BookableFrom` (`from`, `noticeUntil?`, `contact?`) | a site that is not bookable online before a date |
104
+ | `ADDRESS` | `RentalAddressMode` | how much of the address is published; required, there is no default |
105
+ | `FAVORITES_KEY` | `favoritesStorageKey(namespace)` | the Merkliste's storage key |
106
+
107
+ `FAVORITES_KEY` must stay byte-identical to the key the site wrote before (`<namespace>-merkliste`).
108
+ Write the site's exact namespace expression, its fallback included (for example
109
+ `favoritesStorageKey(import.meta.env.PUBLIC_STORAGE_NS || "<the old fallback>")`), and compare the
110
+ key a deployed page writes before and after. A changed key empties every guest's list, and nothing
111
+ migrates it. `favoritesStorageKey` throws on an empty namespace.
112
+
113
+ ## 4. What replaces what
114
+
115
+ | 0.x | 1.0.0 |
116
+ | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
117
+ | `createSdk(config, options?)` | `createSdk({ config, options }, mode?)`; browser `shareSdk` / `loadSharedSdk` |
118
+ | `fetchQuote(rentalId, period, occupancy, sdk, locale, voucher?)` | `fetchQuote(sdk, { rentalId, period, occupancy, locale, voucher })`; a booking widget uses `createQuoteFlow` |
119
+ | `fetchInitialAvailability(sdk, rentalId, from, through, locale)`, `fetchEndAvailability(…)` | `fetchInitialAvailability(sdk, input)`, `fetchEndAvailability(sdk, input)`; a calendar uses `createStayPicker` |
120
+ | `submitBooking(quote, contact, sdk, returnPath?)`, `payWithOption(option, sdk)` | `submitBooking(sdk, quote, contact, { returnPath, kind? })`, `payWithOption(sdk, option)` |
121
+ | `runLiveSearch(sdk, search, locale, cursor?, options)` → `SearchPayload` | `runLiveSearch(sdk, search, { locale, backend, rentalHref, … })` → `SearchPage`; a results page uses `createSearchStore` |
122
+ | `DEFAULT_RENTALS_PATH`, `rentalsPath` | `rentalHref`, a required route template such as `"/unterkuenfte/{slug}"` |
123
+ | `CHECKOUT_RETURN_PATH` | `returnPath`, required on `createCheckoutFlow` and `submitBooking` |
124
+ | `SORT_PARAM`, `SortValue`, `SORT_OPTIONS`, `DEFAULT_SORT`, `activeSort`, `resolveSdkSort`, `isSortAvailable`, `isPricedQuery` | `SortKey`, `SORT_URL_VALUE`, `DEFAULT_SORT_OPTIONS`, `sortFromSearch`, `toSdkSort`, `sortResults`; labels are copy |
125
+ | `FilterType`, `FilterDef`, `activeFiltersFromQuery`, `isBooleanFilterActive` | `FilterSpec` + `resolveFilters(getFilters output, spec)`, `readSelection`, `writeSelection`, `activeFilters`, `withoutFilter` |
126
+ | `PETS_PARAM`, `PETS_COUNT_PARAM`, `petsCountInQuery` | `readParty`, `partyPairs` (`petsCount` on search pages, `pets` on rental and checkout pages) |
127
+ | `REGION_PARAM`, `activeRegions`, `scopeToRegions` | `readSearch` / `writeSearch` (`SEARCH_PARAMS.region`) and the search store's scope |
128
+ | `parseNightRange`, `NIGHT_RANGE_*`, `parseParam`, `parseGermanParam`, `stayParams` | `readSearch`, `writeSearch`, `searchHref` with `SEARCH_CONTRACT`; `stayQuery`, `checkoutHref` for rental links |
129
+ | `isoDate(Date)` | `IsoDay` strings: `todayIso`, `addDays`, `nightsBetween`, `formatIsoDate` |
130
+ | `tidyPrice`, `nightlyPrice` | `formatPriceLabel(label, cents)`; no amount is recomputed, so there is no nightly price |
131
+ | `QuoteLine`, `PriceQuote` | `PriceView`, `PriceLine` (`QuoteView.price`) |
132
+ | `fullRefundRule`, `policyFreeCancellationUntil`, `freeCancellationUntil(policies, Date)` | `freeCancellationUntil(policy, arrival)`, `describePolicyOffer`, `offeredPolicies`, `policyStance` |
133
+ | `SearchImage` | `CardImage` |
134
+ | `BookingContactForm` | `BookingContact` |
135
+ | `saveBookingSnapshot` | nothing: `submitBooking` writes it; the return page reads it with `readPaymentReturn` |
136
+ | `VALIDATED_FIELDS`, `fieldError(name, value, …)` | `createGuestForm`; field rules `emailError`, `phoneError`, `postalcodeError`, `streetWarning` |
137
+ | `BUTTON_SOLUTION_PATTERN` | `SUBMIT_WORDING` (per booking kind); `assertCheckoutStructure` checks it |
138
+ | `CountryGroupsLike`, a site's country list | `countryOptions({ locale, featured })` at build time, `dialCode`, `detectCountry(countries, languages)` |
139
+ | `CheckoutFlowState`, `CheckoutFlowActions`, `CheckoutFlowApi`, `CheckoutFlowMessages` | `CheckoutState`, `CheckoutActions`, `CheckoutFlow`, `CheckoutMessages` |
140
+
141
+ Types that kept their name changed shape: `Stay` is `{ start, end, party }` with `IsoDay` strings
142
+ (`stayKeyOf` and `occupancyOf` take it and a `Party`); `SearchResultItem` has `price: StayPrice`,
143
+ `counts` and `periods` instead of `priceLabel`, `pills`, `isNew` and the `guests` / `bedrooms` /
144
+ `area` fields; `AlternativePeriod`, `ActiveFilter` and `CheckoutIssue` are reshaped. Let the type
145
+ checker find every use.
146
+
147
+ ## 5. The checkout flow
148
+
149
+ ```ts
150
+ // 0.1.x
151
+ createCheckoutFlow({
152
+ rentalId,
153
+ getSdk,
154
+ stay,
155
+ chosenPolicy,
156
+ filterOfferedPolicies,
157
+ messages,
158
+ locale,
159
+ onRateConfigurationFault,
160
+ });
161
+
162
+ // 1.0.0
163
+ const flow = createCheckoutFlow(shared.sdk, {
164
+ rentalId,
165
+ locale,
166
+ returnPath: "/buchen/bestaetigung", // the site's route
167
+ handover: readCheckoutHandover(location.search, { allowOption: false }),
168
+ messages, // the five sentences of CheckoutMessages
169
+ policyChoice: POLICY_CHOICE,
170
+ guests: GUESTS,
171
+ onEvent: (detail) => window.dispatchEvent(new CustomEvent(CHECKOUT_EVENT, { detail })),
172
+ });
173
+ flow.start();
174
+ ```
175
+
176
+ - The stay comes from the handover and is edited in place (`setDates`, `setParty`); the rate rule
177
+ is internal (`selectPolicy` with `policyChoice: "checkout"`).
178
+ - Call `loadGuestCaps()` when your guest editor opens. `state.bounds` then follow the rental's
179
+ availability caps and, on v9, its children and pets rules, as 0.2.0's guest selector did.
180
+ - Disable the order button while `state.blockers` is not empty and name the first blocker;
181
+ `submit` replaces `bookState`, `payment` replaces `payBusyKey` / `payError`.
182
+ - `pay(option)` takes the option only. `openInsuranceDocument(url)` and
183
+ `confirmInsuranceDocuments(confirmed)` replace `openInsuranceDoc` and
184
+ `confirmInsuranceDocs(productKey, confirmed)`; `clearIssue` and `update` are gone.
185
+ - The flow does not scroll: scroll to the confirmation when `phase.kind` becomes `"booked"`.
186
+ - Run `assertCheckoutStructure` on the rendered checkout in a test. A custom checkout renders
187
+ `CHECKOUT_PARTS` (now with `checkout-consent` and `checkout-insurance-premium`) and
188
+ `BOOKING_KIND_ATTRIBUTE` on the order button.
189
+
190
+ ## 6. Enhancement: one `enhance()` call
191
+
192
+ Remove every `initImageCarousels`, `initRentalCarousel`, `initGallery` and `initHistoryOverlay`
193
+ call (and per-section init scripts). Call `enhance` once, in the layout:
194
+
195
+ ```ts
196
+ import { enhance } from "@v-office/website-headless/enhance";
197
+ import { FAVORITES_KEY } from "../data/favorites";
198
+
199
+ // With Astro's ClientRouter: bind on page-load, dispose before the swap.
200
+ let dispose = () => {};
201
+ document.addEventListener("astro:page-load", () => {
202
+ dispose = enhance(document, { favorites: { storageKey: FAVORITES_KEY } });
203
+ });
204
+ document.addEventListener("astro:before-swap", () => dispose());
205
+ ```
206
+
207
+ - Without ClientRouter, call `enhance(document, { favorites: { storageKey: FAVORITES_KEY } })`
208
+ once after the page loads. Favourites are bound only with a `storageKey`.
209
+ - Markup binds through `data-part` values (`ENHANCE_PARTS`: `strip`, `strip-track`, `layer`,
210
+ `reveal`, `clamp`, `share`, `favorite`, `rental-gallery`, …). Legacy `.card-images` strips with
211
+ `data-img-*`, `[data-share]` and `[data-fav]` markup still bind; `[data-rental-tabs]` and
212
+ `.rental-track` do not, so move those to `data-part="strip"` markup. A layer is a `<dialog
213
+ data-part="layer">` with an id.
214
+ - The engines write state attributes (`data-active`, `data-state`), never Tailwind classes. Site
215
+ CSS that keyed on a class an init function added keys on the attribute now.
216
+
217
+ ## 7. Events
218
+
219
+ - `onRateConfigurationFault` is gone. Pass `onEvent` to `createQuoteFlow` and
220
+ `createCheckoutFlow`; with the components, listen on `window` for `voffice:quote`,
221
+ `voffice:checkout` and `voffice:contact` (`detail.type`, no personal data).
222
+ - The kits' window globals (`__searchStore`, `__favoritesMode`, `__offersMode`,
223
+ `__coordsByRentalId`, `__slideLoader`, …) are gone. A script that read one goes silent without an
224
+ error: move it to the store it needs or to a `voffice:*` event.
225
+ - `merkliste:changed` (`{ id, saved }`) is unchanged.
226
+
227
+ ## 8. Delete
228
+
229
+ - The site's SDK seam and its `fetch` patch for the Hub `/Setup` answer (step 1 and 2).
230
+ - Copied kit stores, state machines and helpers that the names in step 4 replace: search, sort,
231
+ filters, regions, dates, prices, quote, checkout, validation, countries, favourites, carousel,
232
+ gallery and overlay code.
233
+ - Route and copy constants the package no longer provides (`CHECKOUT_RETURN_PATH`, German sort
234
+ labels): routes go into the site's props, copy into the UI.
235
+
236
+ ## 9. Check
237
+
238
+ Run the site's tests, `astro check` (0 errors) and a build. Never press the final booking, payment
239
+ or contact submit against a real PMS to test: they create a real reservation, start a real payment
240
+ and send a real request.
241
+
242
+ ## What stays the same
243
+
244
+ - Storage keys: `<namespace>-merkliste`, `voffice:checkout-context`, `voffice:booking-return`.
245
+ - Every URL key (`start`, `end`, `adults`, `children`, `childrenAges`, `petsCount`, `pets`,
246
+ `region`, `sort`, `policy`, `kind`, `offer`, …) and the German sort values (`empfohlen`,
247
+ `preis-auf`, …), so existing links, bookmarks and mails keep working.
248
+ - The gate markers (`data-rental-stats`, `data-result-card`, `data-fav*`, `data-img-*`, …) on the
249
+ same elements.
250
+ - `book()`, `pay()` and the contact `submit()` stay user-initiated only; the § 7 VVG insurance
251
+ document gate cannot be switched off.
252
+ - `describeSdkError` and `logSdkFailure`.
@@ -0,0 +1,10 @@
1
+ {
2
+ "componentsAffectedByHeadlessChange": [
3
+ "searchBar",
4
+ "showcase",
5
+ "searchResults",
6
+ "quote",
7
+ "booking",
8
+ "contact"
9
+ ]
10
+ }