@sparkletree/core 0.12.0 → 0.13.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 +18 -19
- package/dist/analytics.d.ts +7 -1
- package/dist/analytics.d.ts.map +1 -1
- package/dist/analytics.js +2 -1
- package/dist/analytics.js.map +1 -1
- package/dist/client.d.ts +8 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +6 -6
- package/dist/client.js.map +1 -1
- package/dist/context.d.ts +8 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +21 -0
- package/dist/context.js.map +1 -1
- package/dist/fragments.d.ts +21 -24
- package/dist/fragments.d.ts.map +1 -1
- package/dist/fragments.js +10 -7
- package/dist/fragments.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/protocol.d.ts +12 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +5 -2
- package/dist/protocol.js.map +1 -1
- package/dist/server.d.ts +144 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +463 -0
- package/dist/server.js.map +1 -0
- package/dist/slots.d.ts +66 -0
- package/dist/slots.d.ts.map +1 -0
- package/dist/slots.js +102 -0
- package/dist/slots.js.map +1 -0
- package/dist/state.d.ts +282 -239
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +434 -413
- package/dist/state.js.map +1 -1
- package/dist/variant.d.ts +70 -1
- package/dist/variant.d.ts.map +1 -1
- package/dist/variant.js +85 -1
- package/dist/variant.js.map +1 -1
- package/dist/visitorContext.d.ts +99 -0
- package/dist/visitorContext.d.ts.map +1 -0
- package/dist/visitorContext.js +84 -0
- package/dist/visitorContext.js.map +1 -0
- package/package.json +5 -1
package/dist/server.js
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@sparkletree/core/server` — server-side reads for pages rendered on the
|
|
3
|
+
* server.
|
|
4
|
+
*
|
|
5
|
+
* Text a visitor can see is never replaced, so live copy is decided before a
|
|
6
|
+
* slot is shown. A page rendered on the server can ask, at request time, for
|
|
7
|
+
* the live copy already cached for this visitor's context and render it as
|
|
8
|
+
* final text. When there is none, the slot keeps its reservation and the
|
|
9
|
+
* browser's stream and hold take over as usual.
|
|
10
|
+
*
|
|
11
|
+
* Server only. Nothing here belongs in a browser bundle, and calling it in a
|
|
12
|
+
* browser returns empty results without a request.
|
|
13
|
+
*/
|
|
14
|
+
import { resolvePublishableConfig } from "./fragments.js";
|
|
15
|
+
import { warnDev } from "./devwarn.js";
|
|
16
|
+
import { deviceTypeForViewport, hourInTimeZone, isValidTimeZone, timeOfDayForHour, } from "./visitorContext.js";
|
|
17
|
+
import { flattenCustomContext } from "./context.js";
|
|
18
|
+
import { randomId } from "./analytics.js";
|
|
19
|
+
import { ISLAND_COPY_FIELDS, lengthBudgets, setLengthParams } from "./slots.js";
|
|
20
|
+
/** Default total budget for a server read, in milliseconds. */
|
|
21
|
+
export const SERVER_COPY_BUDGET_MS = 250;
|
|
22
|
+
/** The largest budget a server read accepts, in milliseconds. */
|
|
23
|
+
export const SERVER_COPY_MAX_BUDGET_MS = 1000;
|
|
24
|
+
/** Path of the server read on the embed API. */
|
|
25
|
+
const ISLAND_COPY_PATH = "/api/embed/v1/islands/copy";
|
|
26
|
+
const ISLAND_FIELDS = ISLAND_COPY_FIELDS;
|
|
27
|
+
/** The edge's bound on one parameter's value. */
|
|
28
|
+
const MAX_PARAM_LENGTH = 128;
|
|
29
|
+
/**
|
|
30
|
+
* The visitor context for one request, from its headers only.
|
|
31
|
+
*
|
|
32
|
+
* Reads exactly these headers and nothing else:
|
|
33
|
+
*
|
|
34
|
+
* - `Accept-Language`: the language;
|
|
35
|
+
* - `Sec-CH-UA-Mobile`, then `User-Agent`: the device bucket (`mobile` for
|
|
36
|
+
* phones, `desktop` for everything else, tablets included: the browser
|
|
37
|
+
* buckets by viewport width, and tablets are 768 CSS pixels or wider);
|
|
38
|
+
* - `x-vercel-ip-timezone`, then `cf-timezone`: the time zone, from which
|
|
39
|
+
* the time of day is bucketed exactly as the browser does.
|
|
40
|
+
*
|
|
41
|
+
* The visitor's IP, cookies, full user agent, referrer and location are
|
|
42
|
+
* never read or forwarded. Never throws.
|
|
43
|
+
*/
|
|
44
|
+
export function deriveServerContext(headers, options = {}) {
|
|
45
|
+
const context = {};
|
|
46
|
+
try {
|
|
47
|
+
const read = headerReader(headers);
|
|
48
|
+
const language = primaryLanguage(read("accept-language"));
|
|
49
|
+
if (language)
|
|
50
|
+
context.language = language;
|
|
51
|
+
const deviceType = deviceTypeFromHeaders(read("sec-ch-ua-mobile"), read("user-agent"));
|
|
52
|
+
if (deviceType)
|
|
53
|
+
context.deviceType = deviceType;
|
|
54
|
+
const timezone = timeZoneFromHeaders(read) ?? validTimeZone(options.fallbackTimezone);
|
|
55
|
+
if (timezone)
|
|
56
|
+
context.timezone = timezone;
|
|
57
|
+
const now = options.now?.() ?? new Date();
|
|
58
|
+
context.timeOfDay = timeOfDayForHour(hourInTimeZone(now, timezone ?? "UTC"));
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Context is an enhancement; a page must never fail on it.
|
|
62
|
+
}
|
|
63
|
+
return context;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Read the cached live copy for this request's visitor, to render as final
|
|
67
|
+
* text on the server.
|
|
68
|
+
*
|
|
69
|
+
* Strictly a read: nothing is generated, and no impression is recorded (the
|
|
70
|
+
* browser records it when the island is seen). One call decides one variant
|
|
71
|
+
* for every island it names; call it once per request (for example through
|
|
72
|
+
* React's `cache`) and share the result between the islands.
|
|
73
|
+
*
|
|
74
|
+
* Never throws and never waits past `budgetMs`. A miss, an error, a timeout,
|
|
75
|
+
* a refused key or a call in a browser all return `null` islands, with the
|
|
76
|
+
* visitor context still derived.
|
|
77
|
+
*/
|
|
78
|
+
export async function getIslandCopy(options) {
|
|
79
|
+
const started = Date.now();
|
|
80
|
+
const islands = uniqueIslands(options.islands);
|
|
81
|
+
const context = deriveServerContext(options.headers, {
|
|
82
|
+
fallbackTimezone: options.fallbackTimezone,
|
|
83
|
+
now: options.now,
|
|
84
|
+
});
|
|
85
|
+
// A variant the caller already pinned stays the page's, whatever the read
|
|
86
|
+
// returns.
|
|
87
|
+
const pinKey = options.surfaceId ?? options.campaignId;
|
|
88
|
+
// One read is one page view: the provider scopes the browser's pins and
|
|
89
|
+
// impressions to it.
|
|
90
|
+
const viewed = { ...context, viewId: randomId() };
|
|
91
|
+
const requestContext = pinKey && options.variantId ? { ...viewed, pins: { [pinKey]: options.variantId } } : viewed;
|
|
92
|
+
const result = (reasons) => ({
|
|
93
|
+
islands: Object.fromEntries(islands.map((island) => [island, null])),
|
|
94
|
+
campaignId: null,
|
|
95
|
+
variantId: options.variantId ?? null,
|
|
96
|
+
context: requestContext,
|
|
97
|
+
reasons,
|
|
98
|
+
elapsedMs: Date.now() - started,
|
|
99
|
+
});
|
|
100
|
+
if (typeof window !== "undefined" && typeof document !== "undefined") {
|
|
101
|
+
warnDev("server-copy:browser", "getIslandCopy is a server-side read and was called in a browser. " +
|
|
102
|
+
"It returns no copy there; the islands hold and stream as usual.");
|
|
103
|
+
return result({ read: "browser" });
|
|
104
|
+
}
|
|
105
|
+
let organizationId;
|
|
106
|
+
let apiBase;
|
|
107
|
+
try {
|
|
108
|
+
({ organizationId, apiBase } = resolvePublishableConfig({
|
|
109
|
+
publishableKey: options.publishableKey,
|
|
110
|
+
organizationId: options.organizationId,
|
|
111
|
+
apiBase: options.apiBase,
|
|
112
|
+
}, "getIslandCopy"));
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
warnDev("server-copy:config", String(error?.message ?? error));
|
|
116
|
+
return result({ read: "config" });
|
|
117
|
+
}
|
|
118
|
+
if (islands.length === 0)
|
|
119
|
+
return result({});
|
|
120
|
+
const budgetMs = clampBudget(options.budgetMs);
|
|
121
|
+
const url = buildIslandCopyUrl({
|
|
122
|
+
apiBase,
|
|
123
|
+
organizationId,
|
|
124
|
+
campaignId: options.campaignId,
|
|
125
|
+
surfaceId: options.surfaceId,
|
|
126
|
+
variantId: options.variantId,
|
|
127
|
+
islands,
|
|
128
|
+
context,
|
|
129
|
+
acceptLanguage: compactAcceptLanguage(headerReader(options.headers)("accept-language")),
|
|
130
|
+
declared: options.declared,
|
|
131
|
+
lengths: Object.assign({}, ...islands.map((island) => lengthBudgets(options.fallbacks?.[island], ISLAND_FIELDS[island]))),
|
|
132
|
+
budgetMs,
|
|
133
|
+
});
|
|
134
|
+
const controller = new AbortController();
|
|
135
|
+
const timer = setTimeout(() => controller.abort(), budgetMs);
|
|
136
|
+
let body;
|
|
137
|
+
try {
|
|
138
|
+
const doFetch = options.fetchImpl ?? fetch;
|
|
139
|
+
const init = {
|
|
140
|
+
method: "GET",
|
|
141
|
+
headers: {
|
|
142
|
+
Accept: "application/json",
|
|
143
|
+
Authorization: `Bearer ${options.publishableKey}`,
|
|
144
|
+
},
|
|
145
|
+
// Per visitor: never served from or stored in a fetch cache (in
|
|
146
|
+
// Next.js this also keeps the route dynamic).
|
|
147
|
+
cache: "no-store",
|
|
148
|
+
redirect: "error",
|
|
149
|
+
signal: controller.signal,
|
|
150
|
+
};
|
|
151
|
+
let response;
|
|
152
|
+
try {
|
|
153
|
+
response = await doFetch(url, init);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
// Some server runtimes reject the `cache` option outright rather than
|
|
157
|
+
// ignoring it. Without a fetch cache there is nothing to opt out of,
|
|
158
|
+
// so ask again without it.
|
|
159
|
+
if (controller.signal.aborted || !(error instanceof TypeError) || !/cache/i.test(error.message)) {
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
const { cache: _cache, ...withoutCache } = init;
|
|
163
|
+
response = await doFetch(url, withoutCache);
|
|
164
|
+
}
|
|
165
|
+
if (response.status === 401) {
|
|
166
|
+
const reason = await readReason(response);
|
|
167
|
+
if (reason === "live_key_required") {
|
|
168
|
+
warnDev("server-copy:test-key", "getIslandCopy needs a live publishable key (st_pk_live_). With a test key " +
|
|
169
|
+
"every island is a miss: it holds and streams in the browser as usual.");
|
|
170
|
+
}
|
|
171
|
+
return result({ read: reason ?? "error" });
|
|
172
|
+
}
|
|
173
|
+
if (!response.ok)
|
|
174
|
+
return result({ read: "error" });
|
|
175
|
+
body = await response.json();
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return result({ read: controller.signal.aborted ? "deadline" : "error" });
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
clearTimeout(timer);
|
|
182
|
+
}
|
|
183
|
+
return parseIslandCopyResponse(body, {
|
|
184
|
+
islands,
|
|
185
|
+
organizationId,
|
|
186
|
+
campaignId: options.campaignId,
|
|
187
|
+
surfaceId: options.surfaceId,
|
|
188
|
+
variantId: options.variantId,
|
|
189
|
+
context: requestContext,
|
|
190
|
+
fallbacks: options.fallbacks,
|
|
191
|
+
started,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
// --- request ------------------------------------------------------------------
|
|
195
|
+
function buildIslandCopyUrl(input) {
|
|
196
|
+
const url = new URL(ISLAND_COPY_PATH, input.apiBase);
|
|
197
|
+
const params = url.searchParams;
|
|
198
|
+
params.set("organization", input.organizationId);
|
|
199
|
+
// Same addressing rule as the stream: a surface wins over a campaign.
|
|
200
|
+
if (input.surfaceId)
|
|
201
|
+
params.set("surface", input.surfaceId);
|
|
202
|
+
else if (input.campaignId)
|
|
203
|
+
params.set("campaign", input.campaignId);
|
|
204
|
+
params.set("islands", input.islands.join(","));
|
|
205
|
+
if (input.variantId)
|
|
206
|
+
params.set("variant", input.variantId);
|
|
207
|
+
if (input.context.deviceType)
|
|
208
|
+
params.set("deviceType", input.context.deviceType);
|
|
209
|
+
if (input.context.timeOfDay)
|
|
210
|
+
params.set("timeOfDay", input.context.timeOfDay);
|
|
211
|
+
if (input.context.timezone)
|
|
212
|
+
params.set("timezone", input.context.timezone);
|
|
213
|
+
if (input.acceptLanguage)
|
|
214
|
+
params.set("language", input.acceptLanguage);
|
|
215
|
+
for (const [key, value] of Object.entries(declaredParams(input.declared))) {
|
|
216
|
+
params.set(key, value);
|
|
217
|
+
}
|
|
218
|
+
setLengthParams(params, input.lengths);
|
|
219
|
+
params.set("budgetMs", String(input.budgetMs));
|
|
220
|
+
return url.toString();
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Declared placement as the stream sends it: long names and `x.*` customs,
|
|
224
|
+
* flattened by the same function the stream uses (`custom` entries win over
|
|
225
|
+
* literal `x.*` keys). Unknown keys are dropped.
|
|
226
|
+
*/
|
|
227
|
+
function declaredParams(declared) {
|
|
228
|
+
const out = {};
|
|
229
|
+
if (!declared)
|
|
230
|
+
return out;
|
|
231
|
+
const flat = flattenCustomContext(declared);
|
|
232
|
+
const put = (key, value) => {
|
|
233
|
+
if (typeof value === "string" && value.trim() !== "")
|
|
234
|
+
out[key] = value;
|
|
235
|
+
};
|
|
236
|
+
put("page", flat.page);
|
|
237
|
+
put("intent", flat.intent);
|
|
238
|
+
put("audience", flat.audience);
|
|
239
|
+
put("notes", flat.notes);
|
|
240
|
+
for (const [key, value] of Object.entries(flat)) {
|
|
241
|
+
if (key.startsWith("x."))
|
|
242
|
+
put(key, value);
|
|
243
|
+
}
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
function clampBudget(budgetMs) {
|
|
247
|
+
if (typeof budgetMs !== "number" || !Number.isFinite(budgetMs) || budgetMs <= 0) {
|
|
248
|
+
return SERVER_COPY_BUDGET_MS;
|
|
249
|
+
}
|
|
250
|
+
return Math.min(Math.round(budgetMs), SERVER_COPY_MAX_BUDGET_MS) || 1;
|
|
251
|
+
}
|
|
252
|
+
function uniqueIslands(islands) {
|
|
253
|
+
const out = [];
|
|
254
|
+
for (const island of islands ?? []) {
|
|
255
|
+
if ((island === "hero" || island === "cta") && !out.includes(island))
|
|
256
|
+
out.push(island);
|
|
257
|
+
}
|
|
258
|
+
return out;
|
|
259
|
+
}
|
|
260
|
+
async function readReason(response) {
|
|
261
|
+
try {
|
|
262
|
+
const body = (await response.json());
|
|
263
|
+
return typeof body?.reason === "string" ? body.reason : undefined;
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// --- response -----------------------------------------------------------------
|
|
270
|
+
function parseIslandCopyResponse(body, request) {
|
|
271
|
+
const empty = (reasons) => ({
|
|
272
|
+
islands: Object.fromEntries(request.islands.map((island) => [island, null])),
|
|
273
|
+
campaignId: null,
|
|
274
|
+
variantId: request.variantId ?? null,
|
|
275
|
+
context: request.context,
|
|
276
|
+
reasons,
|
|
277
|
+
elapsedMs: Date.now() - request.started,
|
|
278
|
+
});
|
|
279
|
+
if (!isRecord(body))
|
|
280
|
+
return empty({ read: "error" });
|
|
281
|
+
const campaignId = nonEmptyString(body.campaignId);
|
|
282
|
+
const variantId = nonEmptyString(body.variantId);
|
|
283
|
+
const bodyReason = nonEmptyString(body.reason);
|
|
284
|
+
// The answer must be about what was asked: this organization, the pinned
|
|
285
|
+
// campaign and the pinned variant. Anything else is not shown.
|
|
286
|
+
if ((nonEmptyString(body.organizationId) ?? request.organizationId) !== request.organizationId ||
|
|
287
|
+
(!request.surfaceId && request.campaignId && campaignId && campaignId !== request.campaignId) ||
|
|
288
|
+
(request.variantId && variantId && variantId !== request.variantId)) {
|
|
289
|
+
return empty({ read: "identity_mismatch" });
|
|
290
|
+
}
|
|
291
|
+
// The variant the server decided is the page's, hit or miss: the
|
|
292
|
+
// browser's stream then fills the cache entry this read missed.
|
|
293
|
+
const pinKey = request.surfaceId ?? request.campaignId;
|
|
294
|
+
const context = pinKey && variantId ? { ...request.context, pins: { [pinKey]: variantId } } : request.context;
|
|
295
|
+
const identity = { campaignId: campaignId ?? null, variantId: variantId ?? null };
|
|
296
|
+
const islandsBody = isRecord(body.islands) ? body.islands : {};
|
|
297
|
+
const found = {};
|
|
298
|
+
const reasons = {};
|
|
299
|
+
if (bodyReason)
|
|
300
|
+
reasons.read = bodyReason;
|
|
301
|
+
for (const island of request.islands) {
|
|
302
|
+
const entry = islandsBody[island];
|
|
303
|
+
const parsed = isRecord(entry)
|
|
304
|
+
? parseIsland(island, entry, identity, request.fallbacks?.[island])
|
|
305
|
+
: { copy: null, reason: bodyReason ?? "miss" };
|
|
306
|
+
if (parsed.copy)
|
|
307
|
+
found[island] = parsed.copy;
|
|
308
|
+
else
|
|
309
|
+
reasons[island] = parsed.reason;
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
islands: Object.fromEntries(request.islands.map((island) => [island, found[island] ?? null])),
|
|
313
|
+
campaignId: identity.campaignId,
|
|
314
|
+
variantId: identity.variantId,
|
|
315
|
+
context,
|
|
316
|
+
reasons,
|
|
317
|
+
elapsedMs: Date.now() - request.started,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function parseIsland(island, entry, identity, fallback) {
|
|
321
|
+
if (entry.source !== "cache") {
|
|
322
|
+
return { copy: null, reason: nonEmptyString(entry.reason) ?? "miss" };
|
|
323
|
+
}
|
|
324
|
+
// Every island of one read carries the read's campaign and variant.
|
|
325
|
+
const campaignId = nonEmptyString(entry.campaignId) ?? identity.campaignId;
|
|
326
|
+
const variantId = nonEmptyString(entry.variantId) ?? identity.variantId;
|
|
327
|
+
if (!campaignId ||
|
|
328
|
+
!variantId ||
|
|
329
|
+
(identity.campaignId && campaignId !== identity.campaignId) ||
|
|
330
|
+
(identity.variantId && variantId !== identity.variantId)) {
|
|
331
|
+
return { copy: null, reason: "identity_mismatch" };
|
|
332
|
+
}
|
|
333
|
+
const fieldsBody = isRecord(entry.fields) ? entry.fields : {};
|
|
334
|
+
const copy = {};
|
|
335
|
+
let any = false;
|
|
336
|
+
for (const field of ISLAND_FIELDS[island]) {
|
|
337
|
+
// A field the island's fallback does not have has no slot.
|
|
338
|
+
if (fallback && !(typeof fallback[field] === "string" && fallback[field].trim() !== "")) {
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
const text = fieldsBody[field];
|
|
342
|
+
if (typeof text === "string" && text.trim() !== "") {
|
|
343
|
+
copy[field] = text;
|
|
344
|
+
any = true;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (!any)
|
|
348
|
+
return { copy: null, reason: "miss" };
|
|
349
|
+
const stale = entry.contentSource === "cached-stale" || entry.freshness === "stale";
|
|
350
|
+
return {
|
|
351
|
+
copy: {
|
|
352
|
+
copy,
|
|
353
|
+
contentSource: "cached",
|
|
354
|
+
freshness: stale ? "stale" : "fresh",
|
|
355
|
+
variantId,
|
|
356
|
+
campaignId,
|
|
357
|
+
},
|
|
358
|
+
reason: "miss",
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
// --- headers ------------------------------------------------------------------
|
|
362
|
+
function headerReader(headers) {
|
|
363
|
+
if (!headers)
|
|
364
|
+
return () => undefined;
|
|
365
|
+
if (typeof headers.get === "function") {
|
|
366
|
+
const source = headers;
|
|
367
|
+
return (name) => {
|
|
368
|
+
try {
|
|
369
|
+
return source.get(name) ?? undefined;
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
return undefined;
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
const record = headers;
|
|
377
|
+
const lowered = new Map();
|
|
378
|
+
for (const [key, value] of Object.entries(record))
|
|
379
|
+
lowered.set(key.toLowerCase(), value);
|
|
380
|
+
return (name) => {
|
|
381
|
+
const value = lowered.get(name);
|
|
382
|
+
return Array.isArray(value) ? value.join(", ") : value;
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
/** Phones and other small-screen devices, by user agent. */
|
|
386
|
+
const MOBILE_UA = /Mobi|iPhone|iPod|Android.+Mobile|Windows Phone|BlackBerry|BB10|Opera Mini|IEMobile/i;
|
|
387
|
+
/** Tablets, by user agent. iPadOS reports a desktop user agent and lands as desktop. */
|
|
388
|
+
const TABLET_UA = /iPad|Tablet|Android(?!.*Mobile)|Kindle|Silk|PlayBook/i;
|
|
389
|
+
/**
|
|
390
|
+
* The device bucket, from `Sec-CH-UA-Mobile` when the browser sends it,
|
|
391
|
+
* else the user agent. Tablets bucket as desktop, as they do in the
|
|
392
|
+
* browser. Undefined when neither header is present.
|
|
393
|
+
*/
|
|
394
|
+
function deviceTypeFromHeaders(chMobile, userAgent) {
|
|
395
|
+
const hint = chMobile?.trim();
|
|
396
|
+
if (hint === "?1")
|
|
397
|
+
return deviceTypeForViewport(true);
|
|
398
|
+
if (hint === "?0")
|
|
399
|
+
return deviceTypeForViewport(false);
|
|
400
|
+
if (!userAgent)
|
|
401
|
+
return undefined;
|
|
402
|
+
if (TABLET_UA.test(userAgent))
|
|
403
|
+
return deviceTypeForViewport(false);
|
|
404
|
+
return deviceTypeForViewport(MOBILE_UA.test(userAgent));
|
|
405
|
+
}
|
|
406
|
+
function timeZoneFromHeaders(read) {
|
|
407
|
+
for (const name of ["x-vercel-ip-timezone", "cf-timezone"]) {
|
|
408
|
+
const value = validTimeZone(read(name));
|
|
409
|
+
if (value)
|
|
410
|
+
return value;
|
|
411
|
+
}
|
|
412
|
+
return undefined;
|
|
413
|
+
}
|
|
414
|
+
function validTimeZone(value) {
|
|
415
|
+
const trimmed = value?.trim();
|
|
416
|
+
return trimmed && isValidTimeZone(trimmed) ? trimmed : undefined;
|
|
417
|
+
}
|
|
418
|
+
const LANGUAGE_ENTRY = /^([A-Za-z]{1,8}(?:-[A-Za-z0-9]{1,8})*|\*)(?:\s*;\s*q\s*=\s*([0-9.]+))?$/;
|
|
419
|
+
function languageEntries(header) {
|
|
420
|
+
if (!header)
|
|
421
|
+
return [];
|
|
422
|
+
const entries = [];
|
|
423
|
+
for (const part of header.split(",")) {
|
|
424
|
+
const trimmed = part.trim();
|
|
425
|
+
const match = LANGUAGE_ENTRY.exec(trimmed);
|
|
426
|
+
if (!match)
|
|
427
|
+
continue;
|
|
428
|
+
const q = match[2] === undefined ? 1 : Number.parseFloat(match[2]);
|
|
429
|
+
if (!Number.isFinite(q) || q <= 0)
|
|
430
|
+
continue;
|
|
431
|
+
entries.push({ tag: match[1], q: Math.min(q, 1), raw: trimmed.replace(/\s+/g, "") });
|
|
432
|
+
}
|
|
433
|
+
return entries;
|
|
434
|
+
}
|
|
435
|
+
/** The visitor's preferred language tag, e.g. `sv-SE`. */
|
|
436
|
+
function primaryLanguage(header) {
|
|
437
|
+
const best = languageEntries(header)
|
|
438
|
+
.filter((entry) => entry.tag !== "*")
|
|
439
|
+
.sort((a, b) => b.q - a.q)[0];
|
|
440
|
+
return best?.tag;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* The Accept-Language value to forward: well-formed entries only, in the
|
|
444
|
+
* order the browser sent them, whole entries up to the parameter bound.
|
|
445
|
+
*/
|
|
446
|
+
function compactAcceptLanguage(header) {
|
|
447
|
+
let out = "";
|
|
448
|
+
for (const entry of languageEntries(header)) {
|
|
449
|
+
const next = out ? `${out},${entry.raw}` : entry.raw;
|
|
450
|
+
if (next.length > MAX_PARAM_LENGTH)
|
|
451
|
+
break;
|
|
452
|
+
out = next;
|
|
453
|
+
}
|
|
454
|
+
return out || undefined;
|
|
455
|
+
}
|
|
456
|
+
// --- small helpers ------------------------------------------------------------
|
|
457
|
+
function isRecord(value) {
|
|
458
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
459
|
+
}
|
|
460
|
+
function nonEmptyString(value) {
|
|
461
|
+
return typeof value === "string" && value !== "" ? value : undefined;
|
|
462
|
+
}
|
|
463
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
package/dist/slots.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copy slots: what each island's slots are, how long their copy may be, and
|
|
3
|
+
* when visible copy is allowed to change.
|
|
4
|
+
*
|
|
5
|
+
* Two rules live here because the browser and the server must apply them
|
|
6
|
+
* identically:
|
|
7
|
+
*
|
|
8
|
+
* - The fallback sets the size. Each field's fallback defines its slot, so
|
|
9
|
+
* its length is sent with every request as that field's length budget
|
|
10
|
+
* (`len.<field>=<characters>`), and the server writes copy to fit. The
|
|
11
|
+
* server read and the browser's stream compute the budget with the same
|
|
12
|
+
* function, so they ask for, and cache, the same copy.
|
|
13
|
+
* - Visible copy changes only on a visitor's action. A change to what an
|
|
14
|
+
* island would show (a context or campaign change) replaces visible text
|
|
15
|
+
* only when the visitor caused it: the interaction handler calls
|
|
16
|
+
* `markVisitorChange()` before it updates state. Any other change waits
|
|
17
|
+
* until the slot is off screen or the next page view.
|
|
18
|
+
*/
|
|
19
|
+
import type { CopyField } from "./state.js";
|
|
20
|
+
/** The copy fields each island has. */
|
|
21
|
+
export declare const ISLAND_COPY_FIELDS: Readonly<Record<"hero" | "cta", readonly CopyField[]>>;
|
|
22
|
+
/** Prefix of the length-budget query parameters: `len.headline=42`. */
|
|
23
|
+
export declare const LENGTH_PARAM_PREFIX = "len.";
|
|
24
|
+
/**
|
|
25
|
+
* The length of copy as a length budget counts it: Unicode code points, so
|
|
26
|
+
* an emoji or an accented letter written as one character counts as one.
|
|
27
|
+
*/
|
|
28
|
+
export declare function copyLength(text: string): number;
|
|
29
|
+
/**
|
|
30
|
+
* The length budget per field: the length of each field's fallback, for the
|
|
31
|
+
* fields named in `fields` whose fallback is not empty. A field without a
|
|
32
|
+
* fallback has no slot and no budget.
|
|
33
|
+
*/
|
|
34
|
+
export declare function lengthBudgets(fallback: Partial<Record<CopyField, string | undefined>> | null | undefined, fields: readonly CopyField[]): Partial<Record<CopyField, number>>;
|
|
35
|
+
/** Add `len.<field>` parameters for `budgets`, in field order. */
|
|
36
|
+
export declare function setLengthParams(params: URLSearchParams, budgets: Partial<Record<CopyField, number>> | null | undefined): void;
|
|
37
|
+
/**
|
|
38
|
+
* How long a `markVisitorChange()` call stays valid for the change it
|
|
39
|
+
* announces: the state update it precedes renders well inside this.
|
|
40
|
+
*/
|
|
41
|
+
export declare const VISITOR_CHANGE_WINDOW_MS = 1000;
|
|
42
|
+
/**
|
|
43
|
+
* Announce that the visitor caused the next change to what islands and
|
|
44
|
+
* fragments show — call it in the interaction handler (a click, a submit, a
|
|
45
|
+
* choice) before the state update that changes their campaign, context or
|
|
46
|
+
* declaration:
|
|
47
|
+
*
|
|
48
|
+
* ```tsx
|
|
49
|
+
* onClick={() => {
|
|
50
|
+
* markVisitorChange();
|
|
51
|
+
* setAudience("teams");
|
|
52
|
+
* }}
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* Visible copy then changes once, with one cross-fade. Without it, a change
|
|
56
|
+
* keeps the visible copy until the slot is off screen or the next page view.
|
|
57
|
+
*/
|
|
58
|
+
export declare function markVisitorChange(): void;
|
|
59
|
+
/** Internal: the current visitor-change generation, to remember as seen. */
|
|
60
|
+
export declare function visitorChangeGeneration(): number;
|
|
61
|
+
/**
|
|
62
|
+
* Internal: has the visitor announced a change since generation `seen`, recently
|
|
63
|
+
* enough that the change being rendered now is the one they caused?
|
|
64
|
+
*/
|
|
65
|
+
export declare function isVisitorChange(seen: number): boolean;
|
|
66
|
+
//# sourceMappingURL=slots.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slots.d.ts","sourceRoot":"","sources":["../src/slots.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,uCAAuC;AACvC,eAAO,MAAM,kBAAkB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,GAAG,KAAK,EAAE,SAAS,SAAS,EAAE,CAAC,CAGrF,CAAC;AAEF,uEAAuE;AACvE,eAAO,MAAM,mBAAmB,SAAS,CAAC;AAE1C;;;GAGG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,GAAG,IAAI,GAAG,SAAS,EAC3E,MAAM,EAAE,SAAS,SAAS,EAAE,GAC3B,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,CAOpC;AAED,kEAAkE;AAClE,wBAAgB,eAAe,CAC7B,MAAM,EAAE,eAAe,EACvB,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,GAAG,IAAI,GAAG,SAAS,GAC7D,IAAI,CAQN;AAID;;;GAGG;AACH,eAAO,MAAM,wBAAwB,OAAO,CAAC;AAW7C;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAGxC;AAED,4EAA4E;AAC5E,wBAAgB,uBAAuB,IAAI,MAAM,CAEhD;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAErD"}
|
package/dist/slots.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copy slots: what each island's slots are, how long their copy may be, and
|
|
3
|
+
* when visible copy is allowed to change.
|
|
4
|
+
*
|
|
5
|
+
* Two rules live here because the browser and the server must apply them
|
|
6
|
+
* identically:
|
|
7
|
+
*
|
|
8
|
+
* - The fallback sets the size. Each field's fallback defines its slot, so
|
|
9
|
+
* its length is sent with every request as that field's length budget
|
|
10
|
+
* (`len.<field>=<characters>`), and the server writes copy to fit. The
|
|
11
|
+
* server read and the browser's stream compute the budget with the same
|
|
12
|
+
* function, so they ask for, and cache, the same copy.
|
|
13
|
+
* - Visible copy changes only on a visitor's action. A change to what an
|
|
14
|
+
* island would show (a context or campaign change) replaces visible text
|
|
15
|
+
* only when the visitor caused it: the interaction handler calls
|
|
16
|
+
* `markVisitorChange()` before it updates state. Any other change waits
|
|
17
|
+
* until the slot is off screen or the next page view.
|
|
18
|
+
*/
|
|
19
|
+
/** The copy fields each island has. */
|
|
20
|
+
export const ISLAND_COPY_FIELDS = {
|
|
21
|
+
hero: ["greeting", "headline", "body"],
|
|
22
|
+
cta: ["cta"],
|
|
23
|
+
};
|
|
24
|
+
/** Prefix of the length-budget query parameters: `len.headline=42`. */
|
|
25
|
+
export const LENGTH_PARAM_PREFIX = "len.";
|
|
26
|
+
/**
|
|
27
|
+
* The length of copy as a length budget counts it: Unicode code points, so
|
|
28
|
+
* an emoji or an accented letter written as one character counts as one.
|
|
29
|
+
*/
|
|
30
|
+
export function copyLength(text) {
|
|
31
|
+
return Array.from(text).length;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The length budget per field: the length of each field's fallback, for the
|
|
35
|
+
* fields named in `fields` whose fallback is not empty. A field without a
|
|
36
|
+
* fallback has no slot and no budget.
|
|
37
|
+
*/
|
|
38
|
+
export function lengthBudgets(fallback, fields) {
|
|
39
|
+
const out = {};
|
|
40
|
+
for (const field of fields) {
|
|
41
|
+
const text = fallback?.[field];
|
|
42
|
+
if (typeof text === "string" && text.trim() !== "")
|
|
43
|
+
out[field] = copyLength(text);
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
/** Add `len.<field>` parameters for `budgets`, in field order. */
|
|
48
|
+
export function setLengthParams(params, budgets) {
|
|
49
|
+
if (!budgets)
|
|
50
|
+
return;
|
|
51
|
+
for (const field of ["greeting", "headline", "body", "cta"]) {
|
|
52
|
+
const value = budgets[field];
|
|
53
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
54
|
+
params.set(`${LENGTH_PARAM_PREFIX}${field}`, String(Math.round(value)));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// --- visitor-initiated changes ------------------------------------------------
|
|
59
|
+
/**
|
|
60
|
+
* How long a `markVisitorChange()` call stays valid for the change it
|
|
61
|
+
* announces: the state update it precedes renders well inside this.
|
|
62
|
+
*/
|
|
63
|
+
export const VISITOR_CHANGE_WINDOW_MS = 1000;
|
|
64
|
+
let visitorGeneration = 0;
|
|
65
|
+
let visitorMarkedAt = Number.NEGATIVE_INFINITY;
|
|
66
|
+
function clock() {
|
|
67
|
+
return typeof performance !== "undefined" && typeof performance.now === "function"
|
|
68
|
+
? performance.now()
|
|
69
|
+
: Date.now();
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Announce that the visitor caused the next change to what islands and
|
|
73
|
+
* fragments show — call it in the interaction handler (a click, a submit, a
|
|
74
|
+
* choice) before the state update that changes their campaign, context or
|
|
75
|
+
* declaration:
|
|
76
|
+
*
|
|
77
|
+
* ```tsx
|
|
78
|
+
* onClick={() => {
|
|
79
|
+
* markVisitorChange();
|
|
80
|
+
* setAudience("teams");
|
|
81
|
+
* }}
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* Visible copy then changes once, with one cross-fade. Without it, a change
|
|
85
|
+
* keeps the visible copy until the slot is off screen or the next page view.
|
|
86
|
+
*/
|
|
87
|
+
export function markVisitorChange() {
|
|
88
|
+
visitorGeneration += 1;
|
|
89
|
+
visitorMarkedAt = clock();
|
|
90
|
+
}
|
|
91
|
+
/** Internal: the current visitor-change generation, to remember as seen. */
|
|
92
|
+
export function visitorChangeGeneration() {
|
|
93
|
+
return visitorGeneration;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Internal: has the visitor announced a change since generation `seen`, recently
|
|
97
|
+
* enough that the change being rendered now is the one they caused?
|
|
98
|
+
*/
|
|
99
|
+
export function isVisitorChange(seen) {
|
|
100
|
+
return visitorGeneration !== seen && clock() - visitorMarkedAt <= VISITOR_CHANGE_WINDOW_MS;
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=slots.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slots.js","sourceRoot":"","sources":["../src/slots.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,uCAAuC;AACvC,MAAM,CAAC,MAAM,kBAAkB,GAA2D;IACxF,IAAI,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,MAAM,CAAC;IACtC,GAAG,EAAE,CAAC,KAAK,CAAC;CACb,CAAC;AAEF,uEAAuE;AACvE,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC;AAE1C;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC;AACjC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAC3B,QAA2E,EAC3E,MAA4B;IAE5B,MAAM,GAAG,GAAuC,EAAE,CAAC;IACnD,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,IAAI,GAAG,QAAQ,EAAE,CAAC,KAAK,CAAC,CAAC;QAC/B,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE;YAAE,GAAG,CAAC,KAAK,CAAC,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;IACpF,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,eAAe,CAC7B,MAAuB,EACvB,OAA8D;IAE9D,IAAI,CAAC,OAAO;QAAE,OAAO;IACrB,KAAK,MAAM,KAAK,IAAI,CAAC,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,CAAU,EAAE,CAAC;QACrE,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;QAC7B,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;YACrE,MAAM,CAAC,GAAG,CAAC,GAAG,mBAAmB,GAAG,KAAK,EAAE,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAC1E,CAAC;IACH,CAAC;AACH,CAAC;AAED,iFAAiF;AAEjF;;;GAGG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,IAAI,CAAC;AAE7C,IAAI,iBAAiB,GAAG,CAAC,CAAC;AAC1B,IAAI,eAAe,GAAG,MAAM,CAAC,iBAAiB,CAAC;AAE/C,SAAS,KAAK;IACZ,OAAO,OAAO,WAAW,KAAK,WAAW,IAAI,OAAO,WAAW,CAAC,GAAG,KAAK,UAAU;QAChF,CAAC,CAAC,WAAW,CAAC,GAAG,EAAE;QACnB,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;AACjB,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,iBAAiB;IAC/B,iBAAiB,IAAI,CAAC,CAAC;IACvB,eAAe,GAAG,KAAK,EAAE,CAAC;AAC5B,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,uBAAuB;IACrC,OAAO,iBAAiB,CAAC;AAC3B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,OAAO,iBAAiB,KAAK,IAAI,IAAI,KAAK,EAAE,GAAG,eAAe,IAAI,wBAAwB,CAAC;AAC7F,CAAC"}
|