@escape-game-over/atlas 0.1.115 → 0.1.117
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 +1 -1
- package/docs/NOT-BUILT.md +2 -2
- package/package.json +2 -2
- package/src/analytics/ids.ts +1 -13
- package/src/analytics/index.ts +18 -18
- package/src/analytics/storage.ts +34 -39
- package/src/analytics/vendor-settings.ts +162 -0
- package/src/astro/axon-event.ts +2 -4
- package/src/astro/axon-pixel.ts +3 -6
- package/src/astro/clarity-event.ts +2 -4
- package/src/astro/clarity.ts +3 -6
- package/src/astro/consent.ts +3 -6
- package/src/astro/drip-event.ts +2 -4
- package/src/astro/drip.ts +2 -7
- package/src/astro/escape-navigator-consent.ts +2 -6
- package/src/astro/escapeall-calendar.ts +2 -5
- package/src/astro/google-event.ts +3 -7
- package/src/astro/google-map.ts +3 -5
- package/src/astro/images.ts +12 -26
- package/src/astro/index.ts +2 -9
- package/src/astro/meta-event.ts +2 -4
- package/src/astro/meta-pixel.ts +4 -8
- package/src/astro/public-files.ts +11 -52
- package/src/astro/site-routes.ts +11 -55
- package/src/astro/snap-event.ts +2 -4
- package/src/astro/snap-pixel.ts +3 -6
- package/src/astro/tiktok-event.ts +2 -4
- package/src/astro/tiktok-pixel.ts +70 -39
- package/src/astro/umami-event.ts +2 -6
- package/src/astro/vendors.ts +64 -0
- package/src/config.ts +8 -17
- package/src/content/rich.ts +15 -18
- package/src/i18n/catalog.ts +20 -1
- package/src/i18n/define.ts +0 -5
- package/src/i18n/translate.ts +9 -48
- package/src/index.ts +24 -166
- package/src/jsonld/article.ts +7 -17
- package/src/jsonld/business.ts +27 -28
- package/src/jsonld/creative-work.ts +8 -20
- package/src/jsonld/node.ts +37 -7
- package/src/jsonld/organization.ts +21 -30
- package/src/jsonld/place.ts +2 -6
- package/src/jsonld/product.ts +16 -22
- package/src/jsonld/service.ts +21 -37
- package/src/jsonld/video.ts +4 -8
- package/src/jsonld/website.ts +3 -5
- package/src/meta/content.ts +14 -66
- package/src/meta/index.ts +10 -32
- package/src/project.ts +4 -18
- package/src/routes/define.ts +0 -13
- package/src/routes/family.ts +135 -3
- package/src/site/api.ts +1 -21
- package/src/site/create.ts +17 -26
- package/src/site/define.ts +13 -9
- package/src/site/page.ts +1 -10
- package/src/warn.ts +6 -6
- package/src/analytics/axon-pixel.ts +0 -24
- package/src/analytics/clarity.ts +0 -29
- package/src/analytics/drip.ts +0 -20
- package/src/analytics/meta-pixel.ts +0 -29
- package/src/analytics/snap-pixel.ts +0 -28
- package/src/analytics/tiktok-pixel.ts +0 -29
package/README.md
CHANGED
|
@@ -398,7 +398,7 @@ src/ the package — see the export map above for what is reachable
|
|
|
398
398
|
meta/ head tags: canonical, hreflang, Open Graph, Twitter, robots
|
|
399
399
|
jsonld/ the @graph — one file per node, each linked to Google's docs
|
|
400
400
|
i18n/ defineMessages(), t(), placeholder extraction
|
|
401
|
-
routes/ defineRoutes(),
|
|
401
|
+
routes/ defineRoutes(), route families, merging and collisions
|
|
402
402
|
analytics/ Umami and Google, the Consent Mode defaults, and whether a
|
|
403
403
|
visitor has to be asked anything at all
|
|
404
404
|
astro/ the only framework-aware code, plus the one component
|
package/docs/NOT-BUILT.md
CHANGED
|
@@ -209,10 +209,10 @@ something renders a currency picker, and not before.
|
|
|
209
209
|
|
|
210
210
|
`LocalBusinessInput.type` and `ProductInput.alsoA` take bare strings, so
|
|
211
211
|
`"EntertainmentBussiness"` compiles, gets emitted, and is ignored by every
|
|
212
|
-
reader — the silent failure that `RobotsDirective`
|
|
212
|
+
reader — the silent failure that `RobotsDirective` was
|
|
213
213
|
tightened to prevent.
|
|
214
214
|
|
|
215
|
-
They are not the same case.
|
|
215
|
+
They are not the same case. That vocabulary is closed; schema.org has
|
|
216
216
|
around 800 types and is still growing. A partial enumeration of an open
|
|
217
217
|
vocabulary rejects types that are perfectly valid, and the response to that is
|
|
218
218
|
to cast past it — at which point the check is theatre. A literal union that
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@escape-game-over/atlas",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.117",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Typed, data-driven machinery for static multi-locale, multi-deployment Astro sites.",
|
|
6
6
|
"private": false,
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"@biomejs/biome": "2.6.7",
|
|
68
68
|
"@types/node": "26.6.6",
|
|
69
69
|
"@vitest/coverage-istanbul": "5.1.0",
|
|
70
|
-
"astro": "7.4.
|
|
70
|
+
"astro": "7.4.12",
|
|
71
71
|
"typescript": "6.0.3",
|
|
72
72
|
"vitest": "5.1.0"
|
|
73
73
|
}
|
package/src/analytics/ids.ts
CHANGED
|
@@ -24,18 +24,6 @@ export function invalidId(at: string, message: string): never {
|
|
|
24
24
|
throw new Error(`${at}: ${message}`);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
/** A
|
|
28
|
-
export const META_PIXEL_ID = /^\d{15,16}$/;
|
|
29
|
-
|
|
30
|
-
/** A TikTok pixel id: 20 capital letters and digits. */
|
|
31
|
-
export const TIKTOK_PIXEL_ID = /^[A-Z0-9]{20}$/;
|
|
32
|
-
|
|
33
|
-
/** A Drip account id: digits. */
|
|
34
|
-
export const DRIP_ACCOUNT_ID = /^\d+$/;
|
|
35
|
-
|
|
36
|
-
/** A Clarity project id: 10 lowercase letters and digits. */
|
|
37
|
-
export const CLARITY_PROJECT_ID = /^[a-z0-9]{10}$/;
|
|
38
|
-
|
|
39
|
-
/** A UUID, as Umami, Snap and Axon issue them. */
|
|
27
|
+
/** A UUID, as Umami, Snap, Axon and the booking vendors issue them. */
|
|
40
28
|
export const UUID =
|
|
41
29
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
package/src/analytics/index.ts
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import type { MetaTag } from "../meta/tag.ts";
|
|
2
2
|
import type { HttpsUrl } from "../url.ts";
|
|
3
|
-
import { type AxonPixelSettings, checkAxonPixel } from "./axon-pixel.ts";
|
|
4
|
-
import { type ClaritySettings, checkClarity } from "./clarity.ts";
|
|
5
|
-
import { checkDrip, type DripSettings } from "./drip.ts";
|
|
6
3
|
import { type GoogleSettings, googleEmits, googleScripts } from "./google.ts";
|
|
7
|
-
import { checkMetaPixel, type MetaPixelSettings } from "./meta-pixel.ts";
|
|
8
|
-
import { checkSnapPixel, type SnapPixelSettings } from "./snap-pixel.ts";
|
|
9
4
|
import type { AnalyticsTags } from "./tags.ts";
|
|
10
|
-
import { checkTikTokPixel, type TikTokPixelSettings } from "./tiktok-pixel.ts";
|
|
11
5
|
import {
|
|
12
6
|
checkUmamiDomains,
|
|
13
7
|
type UmamiSettings,
|
|
14
8
|
umamiScripts,
|
|
15
9
|
} from "./umami.ts";
|
|
10
|
+
import {
|
|
11
|
+
type AxonPixelSettings,
|
|
12
|
+
type ClaritySettings,
|
|
13
|
+
checkVendorIds,
|
|
14
|
+
type DripSettings,
|
|
15
|
+
type MetaPixelSettings,
|
|
16
|
+
type SnapPixelSettings,
|
|
17
|
+
type TikTokPixelSettings,
|
|
18
|
+
} from "./vendor-settings.ts";
|
|
16
19
|
|
|
17
20
|
/**
|
|
18
21
|
* Analytics, as head tags.
|
|
@@ -29,9 +32,6 @@ import {
|
|
|
29
32
|
* project states ids; lib decides everything that follows from them.
|
|
30
33
|
*/
|
|
31
34
|
|
|
32
|
-
export type { AxonPixelSettings } from "./axon-pixel.ts";
|
|
33
|
-
export type { ClaritySettings } from "./clarity.ts";
|
|
34
|
-
export type { DripSettings } from "./drip.ts";
|
|
35
35
|
export {
|
|
36
36
|
CONSENT_UPDATE_GLOBAL,
|
|
37
37
|
type ConsentDefaults,
|
|
@@ -39,15 +39,20 @@ export {
|
|
|
39
39
|
type GoogleSettings,
|
|
40
40
|
} from "./google.ts";
|
|
41
41
|
export type { GoogleTagId, NumericId, TagManagerId, Uuid } from "./ids.ts";
|
|
42
|
-
export type { MetaPixelSettings } from "./meta-pixel.ts";
|
|
43
|
-
export type { SnapPixelSettings } from "./snap-pixel.ts";
|
|
44
42
|
export type { AnalyticsTags } from "./tags.ts";
|
|
45
|
-
export type { TikTokPixelSettings } from "./tiktok-pixel.ts";
|
|
46
43
|
export type {
|
|
47
44
|
UmamiReplay,
|
|
48
45
|
UmamiSettings,
|
|
49
46
|
UmamiTracker,
|
|
50
47
|
} from "./umami.ts";
|
|
48
|
+
export type {
|
|
49
|
+
AxonPixelSettings,
|
|
50
|
+
ClaritySettings,
|
|
51
|
+
DripSettings,
|
|
52
|
+
MetaPixelSettings,
|
|
53
|
+
SnapPixelSettings,
|
|
54
|
+
TikTokPixelSettings,
|
|
55
|
+
} from "./vendor-settings.ts";
|
|
51
56
|
|
|
52
57
|
/** What a project may switch on. A vendor is a key here. */
|
|
53
58
|
export interface AnalyticsSettings {
|
|
@@ -85,12 +90,7 @@ export function analyticsScripts(
|
|
|
85
90
|
if (analytics === undefined) return { head: [], body: [] };
|
|
86
91
|
|
|
87
92
|
checkUmamiDomains(analytics.umami, origin);
|
|
88
|
-
|
|
89
|
-
checkTikTokPixel(analytics.tiktok, origin);
|
|
90
|
-
checkSnapPixel(analytics.snapchat, origin);
|
|
91
|
-
checkAxonPixel(analytics.axon, origin);
|
|
92
|
-
checkDrip(analytics.drip, origin);
|
|
93
|
-
checkClarity(analytics.clarity, origin);
|
|
93
|
+
checkVendorIds(analytics, origin);
|
|
94
94
|
const google = googleScripts(analytics.google, origin);
|
|
95
95
|
return {
|
|
96
96
|
head: [...google.head, ...umamiScripts(analytics.umami)],
|
package/src/analytics/storage.ts
CHANGED
|
@@ -97,7 +97,7 @@ export interface StoredItem {
|
|
|
97
97
|
readonly consent: ConsentCategory | "none";
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
export interface StoredItemsOptions {
|
|
100
|
+
export interface StoredItemsOptions<Purpose> {
|
|
101
101
|
/** As passed to `Document`, and with its default: `true`. */
|
|
102
102
|
readonly rememberLocale?: boolean;
|
|
103
103
|
/** As passed to `Document`, and with its default: `false`. */
|
|
@@ -109,12 +109,17 @@ export interface StoredItemsOptions {
|
|
|
109
109
|
*/
|
|
110
110
|
readonly booking?: readonly BookingVendor[];
|
|
111
111
|
/**
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
112
|
+
* What each row is for, in the project's words — a message key, usually,
|
|
113
|
+
* since the words are the project's and in every locale.
|
|
114
|
+
*
|
|
115
|
+
* Required, and every row this deployment has must be named: a notice
|
|
116
|
+
* that lists a cookie without a purpose is the one thing it must not
|
|
117
|
+
* publish, so a vendor switched on, or a row a new Atlas adds, fails the
|
|
118
|
+
* build until the site says what it is for. Rows this deployment does not
|
|
119
|
+
* have may be named too; a site keeps its wording when a vendor is
|
|
120
|
+
* switched off.
|
|
116
121
|
*/
|
|
117
|
-
readonly
|
|
122
|
+
readonly purposes: Readonly<Partial<Record<StoredId, Purpose>>>;
|
|
118
123
|
}
|
|
119
124
|
|
|
120
125
|
interface Stored {
|
|
@@ -264,7 +269,7 @@ type VendorWithOwn = {
|
|
|
264
269
|
: never;
|
|
265
270
|
}[keyof typeof CATALOG];
|
|
266
271
|
|
|
267
|
-
/** Catalog order, which is
|
|
272
|
+
/** Catalog order, which is how rows that sort alike stay ordered. */
|
|
268
273
|
const VENDOR_ORDER: readonly StorageVendor[] = [
|
|
269
274
|
"google-analytics",
|
|
270
275
|
"google-ads",
|
|
@@ -372,8 +377,12 @@ const CONSENT_ORDER: readonly StoredItem["consent"][] = [
|
|
|
372
377
|
"marketing",
|
|
373
378
|
];
|
|
374
379
|
|
|
375
|
-
/**
|
|
376
|
-
|
|
380
|
+
/**
|
|
381
|
+
* Rows the site sets first, then by what has to be granted — nothing,
|
|
382
|
+
* analytics, marketing. Ties keep catalog order: Atlas's own, then each
|
|
383
|
+
* vendor's site row before its vendor-domain one.
|
|
384
|
+
*/
|
|
385
|
+
function compareStoredItems(a: StoredItem, b: StoredItem): number {
|
|
377
386
|
return (
|
|
378
387
|
Number(b.provider === "site") - Number(a.provider === "site") ||
|
|
379
388
|
CONSENT_ORDER.indexOf(a.consent) - CONSENT_ORDER.indexOf(b.consent)
|
|
@@ -381,45 +390,22 @@ export function compareStoredItems(a: StoredItem, b: StoredItem): number {
|
|
|
381
390
|
}
|
|
382
391
|
|
|
383
392
|
/**
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
* Throws, naming every row without one, so a vendor switched on, or a row a
|
|
388
|
-
* new Atlas adds, fails the build until the site has said what it is for: a
|
|
389
|
-
* notice that lists a cookie without a purpose is the one thing it must not
|
|
390
|
-
* publish. `purposes` may name rows this deployment does not have; a site
|
|
391
|
-
* keeps its wording when a vendor is switched off.
|
|
392
|
-
*/
|
|
393
|
-
export function withPurposes<K>(
|
|
394
|
-
rows: readonly StoredItem[],
|
|
395
|
-
purposes: Partial<Record<StoredId, K>>
|
|
396
|
-
): readonly (StoredItem & { readonly purpose: K })[] {
|
|
397
|
-
const missing = rows.filter((row) => purposes[row.id] === undefined);
|
|
398
|
-
if (missing.length > 0) {
|
|
399
|
-
throw new Error(
|
|
400
|
-
`The storage table has no purpose for ${missing.map((row) => `"${row.id}"`).join(", ")}. Add one for each to the purposes given to withPurposes.`
|
|
401
|
-
);
|
|
402
|
-
}
|
|
403
|
-
return rows.map((row) => ({ ...row, purpose: purposes[row.id] as K }));
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
/**
|
|
407
|
-
* Everything this deployment keeps on a device: Atlas's own keys, then each
|
|
408
|
-
* loaded vendor's.
|
|
393
|
+
* Everything this deployment keeps on a device, each row with its purpose:
|
|
394
|
+
* the site's own rows first, then by what has to be granted.
|
|
409
395
|
*
|
|
410
396
|
* Atlas's rows follow what the page renders, which `Document` decides from
|
|
411
397
|
* its props, so pass the same `rememberLocale` and `rememberCampaign` here.
|
|
412
398
|
* The consent record is listed whenever there is a banner to answer.
|
|
413
399
|
*/
|
|
414
|
-
export function storedItems(
|
|
400
|
+
export function storedItems<const Purpose>(
|
|
415
401
|
analytics: AnalyticsSettings | undefined,
|
|
416
|
-
options: StoredItemsOptions
|
|
417
|
-
): readonly StoredItem[] {
|
|
402
|
+
options: StoredItemsOptions<Purpose>
|
|
403
|
+
): readonly (StoredItem & { readonly purpose: Purpose })[] {
|
|
418
404
|
const {
|
|
419
405
|
rememberLocale = true,
|
|
420
406
|
rememberCampaign = false,
|
|
421
407
|
booking = [],
|
|
422
|
-
|
|
408
|
+
purposes,
|
|
423
409
|
} = options;
|
|
424
410
|
const asked = consentVendors(analytics).length > 0;
|
|
425
411
|
|
|
@@ -506,5 +492,14 @@ export function storedItems(
|
|
|
506
492
|
|
|
507
493
|
rows.push(...widgetRows);
|
|
508
494
|
|
|
509
|
-
|
|
495
|
+
const missing = rows.filter((row) => purposes[row.id] === undefined);
|
|
496
|
+
if (missing.length > 0) {
|
|
497
|
+
throw new Error(
|
|
498
|
+
`The storage table has no purpose for ${missing.map((row) => `"${row.id}"`).join(", ")}. Add one for each to storedItems' purposes.`
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
return rows.toSorted(compareStoredItems).flatMap((row) => {
|
|
502
|
+
const purpose = purposes[row.id];
|
|
503
|
+
return purpose === undefined ? [] : [{ ...row, purpose }];
|
|
504
|
+
});
|
|
510
505
|
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { NonEmpty } from "../types.ts";
|
|
2
|
+
import { invalidId, type NumericId, UUID, type Uuid } from "./ids.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Meta's pixel. Marketing: it loads only once a visitor grants that.
|
|
6
|
+
*
|
|
7
|
+
* @see https://developers.facebook.com/docs/meta-pixel/get-started
|
|
8
|
+
* @see https://developers.facebook.com/docs/meta-pixel/reference
|
|
9
|
+
* @see https://developers.facebook.com/docs/meta-pixel/implementation/gdpr
|
|
10
|
+
*/
|
|
11
|
+
export interface MetaPixelSettings {
|
|
12
|
+
/** From Events Manager: 15 or 16 digits. Several share one script. */
|
|
13
|
+
readonly pixelIds: NonEmpty<NumericId>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* TikTok's pixel. Marketing: it loads only once a visitor grants that.
|
|
18
|
+
*
|
|
19
|
+
* @see https://ads.tiktok.com/help/article/get-started-pixel
|
|
20
|
+
* @see https://ads.tiktok.com/help/article/standard-events-parameters
|
|
21
|
+
* @see https://ads.tiktok.com/help/article/how-to-use-cookies-with-tiktok-pixel
|
|
22
|
+
*/
|
|
23
|
+
export interface TikTokPixelSettings {
|
|
24
|
+
/** From TikTok Events Manager: 20 capital letters and digits. */
|
|
25
|
+
readonly pixelIds: NonEmpty<string>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Snapchat's pixel. Marketing: it loads only once a visitor grants that.
|
|
30
|
+
*
|
|
31
|
+
* @see https://businesshelp.snapchat.com/s/article/pixel-direct-implementation
|
|
32
|
+
* @see https://forbusiness.snapchat.com/en-US/blog/the-snap-pixel-how-it-works-and-how-to-install-it
|
|
33
|
+
*/
|
|
34
|
+
export interface SnapPixelSettings {
|
|
35
|
+
/** From Snap's Events Manager: a UUID. */
|
|
36
|
+
readonly pixelIds: NonEmpty<Uuid>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* AppLovin's Axon pixel. Marketing: it loads only once a visitor grants that.
|
|
41
|
+
*
|
|
42
|
+
* @see https://support.applovin.com/en/growth/promoting-your-websites/axon-pixel-integration/axon-pixel-native-js
|
|
43
|
+
* @see https://support.applovin.com/en/growth/promoting-your-websites/axon-pixel-integration/events-and-objects
|
|
44
|
+
*/
|
|
45
|
+
export interface AxonPixelSettings {
|
|
46
|
+
/** From the Axon dashboard: a UUID. */
|
|
47
|
+
readonly eventKey: Uuid;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Drip's site tracking. Marketing: it loads only once a visitor grants that.
|
|
52
|
+
*
|
|
53
|
+
* @see https://help.drip.com/hc/en-us/articles/4424702539789-Install-Your-JavaScript-Snippet
|
|
54
|
+
*/
|
|
55
|
+
export interface DripSettings {
|
|
56
|
+
/** From Drip's Site Setup: digits, the `_dcs.account` of its snippet. */
|
|
57
|
+
readonly accountId: NumericId;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Microsoft Clarity. It has a cookieless mode, so like Google it loads for
|
|
62
|
+
* every visitor: without consent it records each page view on its own and sets
|
|
63
|
+
* no cookie, and the visitor's answer switches its cookies on or off.
|
|
64
|
+
*
|
|
65
|
+
* @see https://learn.microsoft.com/en-us/clarity/setup-and-installation/clarity-setup
|
|
66
|
+
* @see https://learn.microsoft.com/en-us/clarity/setup-and-installation/consent-mode
|
|
67
|
+
* @see https://learn.microsoft.com/en-us/clarity/setup-and-installation/clarity-consent-api-v2
|
|
68
|
+
*/
|
|
69
|
+
export interface ClaritySettings {
|
|
70
|
+
/** From the Clarity project's settings: 10 lowercase letters and digits. */
|
|
71
|
+
readonly projectId: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** What these vendors' checks read: their slice of `AnalyticsSettings`. */
|
|
75
|
+
interface VendorSettings {
|
|
76
|
+
readonly meta?: MetaPixelSettings;
|
|
77
|
+
readonly tiktok?: TikTokPixelSettings;
|
|
78
|
+
readonly snapchat?: SnapPixelSettings;
|
|
79
|
+
readonly axon?: AxonPixelSettings;
|
|
80
|
+
readonly drip?: DripSettings;
|
|
81
|
+
readonly clarity?: ClaritySettings;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** One vendor's id: where it is set, its shape, and where to copy it from. */
|
|
85
|
+
interface IdCheck {
|
|
86
|
+
readonly ids: (settings: VendorSettings) => readonly string[];
|
|
87
|
+
/** What the id is called, as the vendor calls it. */
|
|
88
|
+
readonly name: string;
|
|
89
|
+
readonly shape: RegExp;
|
|
90
|
+
/** The shape in words, for the error. */
|
|
91
|
+
readonly is: string;
|
|
92
|
+
readonly from: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Each vendor's id check: one row each, read in one loop. Only the shape, the
|
|
97
|
+
* words and the place to copy it from differ between vendors.
|
|
98
|
+
*/
|
|
99
|
+
const ID_CHECKS: readonly IdCheck[] = [
|
|
100
|
+
{
|
|
101
|
+
ids: (settings) => settings.meta?.pixelIds ?? [],
|
|
102
|
+
name: "Meta pixel id",
|
|
103
|
+
shape: /^\d{15,16}$/,
|
|
104
|
+
is: "15 or 16 digits",
|
|
105
|
+
from: "Events Manager",
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
ids: (settings) => settings.tiktok?.pixelIds ?? [],
|
|
109
|
+
name: "TikTok pixel id",
|
|
110
|
+
shape: /^[A-Z0-9]{20}$/,
|
|
111
|
+
is: "20 capital letters and digits",
|
|
112
|
+
from: "TikTok Events Manager",
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
ids: (settings) => settings.snapchat?.pixelIds ?? [],
|
|
116
|
+
name: "Snap pixel id",
|
|
117
|
+
shape: UUID,
|
|
118
|
+
is: "a UUID",
|
|
119
|
+
from: "Snap's Events Manager",
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
ids: (settings) =>
|
|
123
|
+
settings.axon === undefined ? [] : [settings.axon.eventKey],
|
|
124
|
+
name: "Axon event key",
|
|
125
|
+
shape: UUID,
|
|
126
|
+
is: "a UUID",
|
|
127
|
+
from: "the Axon dashboard",
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
ids: (settings) =>
|
|
131
|
+
settings.drip === undefined ? [] : [settings.drip.accountId],
|
|
132
|
+
name: "Drip account id",
|
|
133
|
+
shape: /^\d+$/,
|
|
134
|
+
is: "all digits",
|
|
135
|
+
from: "Drip's Site Setup",
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
ids: (settings) =>
|
|
139
|
+
settings.clarity === undefined ? [] : [settings.clarity.projectId],
|
|
140
|
+
name: "Clarity project id",
|
|
141
|
+
shape: /^[a-z0-9]{10}$/,
|
|
142
|
+
is: "10 lowercase letters and digits",
|
|
143
|
+
from: "the Clarity project's settings",
|
|
144
|
+
},
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Fails the build on an id that is not what its vendor issues: configured,
|
|
149
|
+
* emitted, and reporting nowhere is the failure it prevents.
|
|
150
|
+
*/
|
|
151
|
+
export function checkVendorIds(settings: VendorSettings, at: string): void {
|
|
152
|
+
for (const check of ID_CHECKS) {
|
|
153
|
+
for (const id of check.ids(settings)) {
|
|
154
|
+
if (!check.shape.test(id)) {
|
|
155
|
+
invalidId(
|
|
156
|
+
at,
|
|
157
|
+
`${check.name} "${id}" is not ${check.is}, so it would report nowhere. Copy it from ${check.from}.`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
package/src/astro/axon-event.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CurrencyCode } from "../currency.ts";
|
|
2
2
|
import { readConsent } from "./consent.ts";
|
|
3
|
+
import { vendors } from "./vendors.ts";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Axon's events that a site without a cart sends. The cart and purchase events
|
|
@@ -15,9 +16,6 @@ export interface AxonEventData {
|
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
/** What `axon-pixel.ts` defines once marketing is granted. */
|
|
18
|
-
interface AxonWindow {
|
|
19
|
-
axon?: (...args: unknown[]) => void;
|
|
20
|
-
}
|
|
21
19
|
|
|
22
20
|
/**
|
|
23
21
|
* Records an event with Axon's pixel — a lead sent, an account made.
|
|
@@ -28,7 +26,7 @@ interface AxonWindow {
|
|
|
28
26
|
*/
|
|
29
27
|
export function axonEvent(event: AxonEvent, data?: AxonEventData): void {
|
|
30
28
|
if (readConsent()?.marketing !== "granted") return;
|
|
31
|
-
const axon = (
|
|
29
|
+
const axon = vendors().axon;
|
|
32
30
|
if (data === undefined) axon?.("track", event);
|
|
33
31
|
else axon?.("track", event, data);
|
|
34
32
|
}
|
package/src/astro/axon-pixel.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { appendScript } from "./append-script.ts";
|
|
2
2
|
import type { ConsentGated } from "./consent-gate.ts";
|
|
3
|
+
import { vendors } from "./vendors.ts";
|
|
3
4
|
|
|
4
5
|
/** Axon's `axon`: a queue until its scripts arrive and replay it. */
|
|
5
|
-
interface Axon {
|
|
6
|
+
export interface Axon {
|
|
6
7
|
(...args: unknown[]): void;
|
|
7
8
|
performOperation?: (...args: unknown[]) => void;
|
|
8
9
|
operationQueue: unknown[];
|
|
@@ -10,10 +11,6 @@ interface Axon {
|
|
|
10
11
|
eventKey: string;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
interface PixelWindow {
|
|
14
|
-
axon?: Axon;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
14
|
const SCRIPTS = [
|
|
18
15
|
"https://s.axon.ai/pixel.js",
|
|
19
16
|
"https://res4.applovin.com/p/l/loader.iife.js",
|
|
@@ -21,7 +18,7 @@ const SCRIPTS = [
|
|
|
21
18
|
|
|
22
19
|
/** Axon's own base code, as it defines `axon` before the scripts load. */
|
|
23
20
|
function defineAxon(eventKey: string): Axon {
|
|
24
|
-
const global =
|
|
21
|
+
const global = vendors();
|
|
25
22
|
if (global.axon !== undefined) return global.axon;
|
|
26
23
|
// `arguments` is the list of whatever a call passed, which every regular
|
|
27
24
|
// `function` gets without declaring it (an arrow function does not). The
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DeclaredEvent } from "./event-data.ts";
|
|
2
|
+
import { vendors } from "./vendors.ts";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* The project's Clarity events. Clarity carries a name and nothing else, so
|
|
@@ -17,9 +18,6 @@ import type { DeclaredEvent } from "./event-data.ts";
|
|
|
17
18
|
export interface ClarityEvents {}
|
|
18
19
|
|
|
19
20
|
/** What `clarity.ts` defines once the page has loaded. */
|
|
20
|
-
interface ClarityWindow {
|
|
21
|
-
clarity?: (...args: unknown[]) => void;
|
|
22
|
-
}
|
|
23
21
|
|
|
24
22
|
/**
|
|
25
23
|
* Marks the current Clarity recording with a named event — a form sent, a
|
|
@@ -29,5 +27,5 @@ interface ClarityWindow {
|
|
|
29
27
|
* sets no cookie. Dropped before Clarity loads, or where it is not configured.
|
|
30
28
|
*/
|
|
31
29
|
export function clarityEvent(name: DeclaredEvent<ClarityEvents>): void {
|
|
32
|
-
(
|
|
30
|
+
vendors().clarity?.("event", name);
|
|
33
31
|
}
|
package/src/astro/clarity.ts
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
import { appendScript } from "./append-script.ts";
|
|
2
2
|
import type { ConsentChoices } from "./consent.ts";
|
|
3
|
+
import { vendors } from "./vendors.ts";
|
|
3
4
|
|
|
4
5
|
/** Clarity's `clarity`: a queue until its script arrives and replays it. */
|
|
5
|
-
interface Clarity {
|
|
6
|
+
export interface Clarity {
|
|
6
7
|
(...args: unknown[]): void;
|
|
7
8
|
q?: unknown[];
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
interface ClarityWindow {
|
|
11
|
-
clarity?: Clarity;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
11
|
/** Clarity's own base code, as it defines `clarity` before the script loads. */
|
|
15
12
|
function defineClarity(): Clarity {
|
|
16
|
-
const global =
|
|
13
|
+
const global = vendors();
|
|
17
14
|
if (global.clarity !== undefined) return global.clarity;
|
|
18
15
|
// `arguments` is the list of whatever a call passed, which every regular
|
|
19
16
|
// `function` gets without declaring it (an arrow function does not). The
|
package/src/astro/consent.ts
CHANGED
|
@@ -12,6 +12,8 @@ export type {
|
|
|
12
12
|
ConsentChoices,
|
|
13
13
|
} from "../analytics/consent-storage.ts";
|
|
14
14
|
|
|
15
|
+
import { vendors } from "./vendors.ts";
|
|
16
|
+
|
|
15
17
|
/**
|
|
16
18
|
* The browser half of consent: remembering an answer, expiring it, and handing
|
|
17
19
|
* it to Google.
|
|
@@ -120,12 +122,7 @@ export function recordConsent(choices: ConsentChoices): void {
|
|
|
120
122
|
* copy of that list.
|
|
121
123
|
*/
|
|
122
124
|
export function applyConsent(choices: ConsentChoices): void {
|
|
123
|
-
const update = (
|
|
124
|
-
window as unknown as Record<
|
|
125
|
-
string,
|
|
126
|
-
((c: ConsentChoices) => void) | undefined
|
|
127
|
-
>
|
|
128
|
-
)[CONSENT_UPDATE_GLOBAL];
|
|
125
|
+
const update = vendors()[CONSENT_UPDATE_GLOBAL];
|
|
129
126
|
// Only the categories, so a record's date does not ride along.
|
|
130
127
|
update?.({ analytics: choices.analytics, marketing: choices.marketing });
|
|
131
128
|
}
|
package/src/astro/drip-event.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readConsent } from "./consent.ts";
|
|
2
2
|
import type { DeclaredEvent, EventDataArgs } from "./event-data.ts";
|
|
3
|
+
import { vendors } from "./vendors.ts";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* The project's Drip events — the actions its workflows start on — each with
|
|
@@ -18,9 +19,6 @@ import type { DeclaredEvent, EventDataArgs } from "./event-data.ts";
|
|
|
18
19
|
export interface DripEvents {}
|
|
19
20
|
|
|
20
21
|
/** What `drip.ts` defines once marketing is granted. */
|
|
21
|
-
interface DripWindow {
|
|
22
|
-
_dcq?: unknown[][];
|
|
23
|
-
}
|
|
24
22
|
|
|
25
23
|
/**
|
|
26
24
|
* Records an event with Drip, to start a workflow — a lead sent, a voucher
|
|
@@ -38,7 +36,7 @@ export function dripEvent<E extends DeclaredEvent<DripEvents>>(
|
|
|
38
36
|
>
|
|
39
37
|
): void {
|
|
40
38
|
if (readConsent()?.marketing !== "granted") return;
|
|
41
|
-
const queue = (
|
|
39
|
+
const queue = vendors()._dcq;
|
|
42
40
|
if (properties === undefined) queue?.push(["track", action]);
|
|
43
41
|
else queue?.push(["track", action, properties]);
|
|
44
42
|
}
|
package/src/astro/drip.ts
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import { appendScript } from "./append-script.ts";
|
|
2
2
|
import type { ConsentGated } from "./consent-gate.ts";
|
|
3
|
-
|
|
4
|
-
/** Drip's globals: a queue of calls, and the account its script reports to. */
|
|
5
|
-
interface DripWindow {
|
|
6
|
-
_dcq?: unknown[][];
|
|
7
|
-
_dcs?: { account?: string };
|
|
8
|
-
}
|
|
3
|
+
import { vendors } from "./vendors.ts";
|
|
9
4
|
|
|
10
5
|
/**
|
|
11
6
|
* Drip, loaded on the first grant. Its script records visits by itself, so
|
|
@@ -19,7 +14,7 @@ export function drip(accountId: string): ConsentGated {
|
|
|
19
14
|
grant: () => {
|
|
20
15
|
if (loaded) return;
|
|
21
16
|
loaded = true;
|
|
22
|
-
const global =
|
|
17
|
+
const global = vendors();
|
|
23
18
|
global._dcq ??= [];
|
|
24
19
|
global._dcs ??= {};
|
|
25
20
|
global._dcs.account = accountId;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ESCAPE_NAVIGATOR_LOADER } from "../booking/escape-navigator.ts";
|
|
2
2
|
import { appendScript } from "./append-script.ts";
|
|
3
3
|
import { CONSENT_EVENT, type ConsentChoices, readConsent } from "./consent.ts";
|
|
4
|
+
import { vendors } from "./vendors.ts";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Escape Navigator's consent, from the visitor's answer on this site: its
|
|
@@ -13,12 +14,7 @@ function escapeNavigatorConsent(choices: ConsentChoices | undefined) {
|
|
|
13
14
|
};
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
__pendingConsent?: { analytics: boolean; ads: boolean };
|
|
18
|
-
setConsent?: (consent: { analytics: boolean; ads: boolean }) => void;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const host = window as unknown as { EscapeNavigator?: EscapeNavigatorGlobal };
|
|
17
|
+
const host = vendors();
|
|
22
18
|
|
|
23
19
|
// Before the loader runs, which reads the pending answer as it starts: its
|
|
24
20
|
// tags never load on an answer the visitor has not given.
|
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
import { appendScript } from "./append-script.ts";
|
|
2
|
+
import { vendors } from "./vendors.ts";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Draws each EscapeAll calendar on the page, once EscapeAll's script has
|
|
5
6
|
* loaded: the call its store scripts make, with the settings the component
|
|
6
7
|
* wrote on the element.
|
|
7
8
|
*/
|
|
8
|
-
|
|
9
|
-
// calendar is named by its id, not handed over as an element.
|
|
10
|
-
type Calendar = (selector: string, settings: Record<string, unknown>) => void;
|
|
11
|
-
|
|
12
|
-
const host = window as unknown as { GetBookingCalendra?: Calendar };
|
|
9
|
+
const host = vendors();
|
|
13
10
|
|
|
14
11
|
const elements = [...document.querySelectorAll("[data-escapeall-company]")];
|
|
15
12
|
const [first] = elements;
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
GOOGLE_EVENT_GLOBAL,
|
|
3
|
-
type GoogleEventSink,
|
|
4
|
-
} from "../analytics/google.ts";
|
|
1
|
+
import { GOOGLE_EVENT_GLOBAL } from "../analytics/google.ts";
|
|
5
2
|
import type { CurrencyCode } from "../currency.ts";
|
|
6
3
|
import type { DeclaredEvent, EventDataArgs } from "./event-data.ts";
|
|
4
|
+
import { vendors } from "./vendors.ts";
|
|
7
5
|
|
|
8
6
|
/** GA4's recommended events that carry items and revenue. */
|
|
9
7
|
const ECOMMERCE_EVENTS = [
|
|
@@ -110,9 +108,7 @@ export function googleEvent<E extends GoogleEventName>(
|
|
|
110
108
|
event: E,
|
|
111
109
|
...[data]: GoogleEventArgs<E>
|
|
112
110
|
): void {
|
|
113
|
-
const send = (
|
|
114
|
-
window as unknown as Record<string, GoogleEventSink | undefined>
|
|
115
|
-
)[GOOGLE_EVENT_GLOBAL];
|
|
111
|
+
const send = vendors()[GOOGLE_EVENT_GLOBAL];
|
|
116
112
|
send?.(
|
|
117
113
|
event,
|
|
118
114
|
data,
|