@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/src/astro/google-map.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { reportDevError } from "./dev-log.ts";
|
|
|
6
6
|
import { element } from "./element.ts";
|
|
7
7
|
import { triggered } from "./load-script.ts";
|
|
8
8
|
import { data } from "./ref.ts";
|
|
9
|
+
import { GOOGLE_MAPS_CALLBACK, vendors } from "./vendors.ts";
|
|
9
10
|
|
|
10
11
|
/** What `GoogleMap.astro` hands the element, serialised. */
|
|
11
12
|
export interface GoogleMapConfig {
|
|
@@ -27,20 +28,17 @@ export interface GoogleMapConfig {
|
|
|
27
28
|
}[];
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
const CALLBACK = "__atlasGoogleMaps";
|
|
31
|
-
|
|
32
31
|
let loading: Promise<typeof google.maps> | undefined;
|
|
33
32
|
|
|
34
33
|
/** The Maps JavaScript API, fetched once per page however many maps ask. */
|
|
35
34
|
export function loadGoogleMaps(apiKey: string): Promise<typeof google.maps> {
|
|
36
35
|
loading ??= new Promise((resolve, reject) => {
|
|
37
|
-
(
|
|
38
|
-
resolve(google.maps);
|
|
36
|
+
vendors()[GOOGLE_MAPS_CALLBACK] = () => resolve(google.maps);
|
|
39
37
|
const params = {
|
|
40
38
|
key: apiKey,
|
|
41
39
|
v: "weekly",
|
|
42
40
|
loading: "async",
|
|
43
|
-
callback:
|
|
41
|
+
callback: GOOGLE_MAPS_CALLBACK,
|
|
44
42
|
};
|
|
45
43
|
appendScript(
|
|
46
44
|
`https://maps.googleapis.com/maps/api/js?${new URLSearchParams(params)}`
|
package/src/astro/images.ts
CHANGED
|
@@ -331,36 +331,22 @@ export async function photoSet(
|
|
|
331
331
|
export type ImagePathOptions = Omit<Parameters<typeof getImage>[0], "src">;
|
|
332
332
|
|
|
333
333
|
/**
|
|
334
|
-
* The built URL of an optimised image, for
|
|
335
|
-
*
|
|
334
|
+
* The built URL of an optimised image, for places that take a path rather than
|
|
335
|
+
* an `<img>`: a CSS `url()` in an inline style or custom property, a
|
|
336
|
+
* `<link rel="preload">`, a manifest icon.
|
|
336
337
|
*
|
|
337
|
-
* ```
|
|
338
|
-
*
|
|
339
|
-
* import { imagePath } from "@escape-game-over/atlas/astro/images";
|
|
340
|
-
*
|
|
341
|
-
* const company = organization({
|
|
342
|
-
* logo: await imagePath(logo, { width: 512, height: 512, format: "png" }),
|
|
343
|
-
* // …
|
|
344
|
-
* });
|
|
338
|
+
* ```astro
|
|
339
|
+
* <section style={`--hero: url(${await imagePath(hero, { width: 1600, format: "webp" })})`}>
|
|
345
340
|
* ```
|
|
346
341
|
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
* assumption in as many places as there are consumers.
|
|
351
|
-
*
|
|
352
|
-
* **Not for entity data.** What comes back is `/_astro/logo.<hash>.png`, and the
|
|
353
|
-
* hash moves every time the source file is touched. That is right for a page
|
|
354
|
-
* resource, which is re-fetched from a document that was just re-fetched, and
|
|
355
|
-
* wrong for anything a search engine stores against an identity — an
|
|
356
|
-
* `Organization.logo` above all, which is held long after the build that
|
|
357
|
-
* produced it. Those want a stable path out of `public/`, written as `/logo.png`
|
|
358
|
-
* and joined to the site's own origin; both examples in this repository do that
|
|
359
|
-
* deliberately and say so at the call site.
|
|
342
|
+
* A function rather than `getImage().src` at each call site because that is
|
|
343
|
+
* typed `string`, and a path is wanted as `UrlPath`: narrowed once here, with
|
|
344
|
+
* no cast at every caller.
|
|
360
345
|
*
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
* `
|
|
346
|
+
* **Not for structured data.** What comes back is `/_astro/hero.<hash>.webp`,
|
|
347
|
+
* and the hash moves whenever the source changes. Right for a resource fetched
|
|
348
|
+
* with the page; wrong for an `Organization.logo` or anything else a search
|
|
349
|
+
* engine stores against an identity, which wants a stable path in `public/`.
|
|
364
350
|
*/
|
|
365
351
|
export async function imagePath(
|
|
366
352
|
src: ImageMetadata,
|
package/src/astro/index.ts
CHANGED
|
@@ -27,12 +27,5 @@
|
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
29
|
export { siteFonts } from "./fonts.ts";
|
|
30
|
-
export {
|
|
31
|
-
|
|
32
|
-
publicFiles,
|
|
33
|
-
} from "./public-files.ts";
|
|
34
|
-
export {
|
|
35
|
-
type SiteFiles,
|
|
36
|
-
type SiteRoutesOptions,
|
|
37
|
-
siteRoutes,
|
|
38
|
-
} from "./site-routes.ts";
|
|
30
|
+
export { publicFiles } from "./public-files.ts";
|
|
31
|
+
export { siteRoutes } from "./site-routes.ts";
|
package/src/astro/meta-event.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CurrencyCode } from "../currency.ts";
|
|
2
2
|
import type { DeclaredEvent, EventDataArgs } from "./event-data.ts";
|
|
3
|
+
import { vendors } from "./vendors.ts";
|
|
3
4
|
|
|
4
5
|
/** Meta's standard events, which its ad optimisation understands by name. */
|
|
5
6
|
const META_STANDARD_EVENTS = [
|
|
@@ -66,9 +67,6 @@ type MetaEventArgs<E extends MetaEventName> = E extends MetaStandardEvent
|
|
|
66
67
|
: EventDataArgs<MetaCustomEvents[E & keyof MetaCustomEvents]>;
|
|
67
68
|
|
|
68
69
|
/** What `meta-pixel.ts` defines once marketing is granted. */
|
|
69
|
-
interface MetaWindow {
|
|
70
|
-
fbq?: (...args: unknown[]) => void;
|
|
71
|
-
}
|
|
72
70
|
|
|
73
71
|
/**
|
|
74
72
|
* Records an event with Meta's pixel — a lead sent, a booking made. A declared
|
|
@@ -83,7 +81,7 @@ export function metaEvent<E extends MetaEventName>(
|
|
|
83
81
|
event: E,
|
|
84
82
|
...[data]: MetaEventArgs<E>
|
|
85
83
|
): void {
|
|
86
|
-
const fbq = (
|
|
84
|
+
const fbq = vendors().fbq;
|
|
87
85
|
const method = (META_STANDARD_EVENTS as readonly string[]).includes(event)
|
|
88
86
|
? "track"
|
|
89
87
|
: "trackCustom";
|
package/src/astro/meta-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
|
/** Meta's `fbq`: a queue until `fbevents.js` arrives and replays it. */
|
|
5
|
-
interface Fbq {
|
|
6
|
+
export interface Fbq {
|
|
6
7
|
(...args: unknown[]): void;
|
|
7
8
|
callMethod?: (...args: unknown[]) => void;
|
|
8
9
|
queue: unknown[];
|
|
@@ -11,16 +12,11 @@ interface Fbq {
|
|
|
11
12
|
version: string;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
interface PixelWindow {
|
|
15
|
-
fbq?: Fbq;
|
|
16
|
-
_fbq?: Fbq;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
15
|
const SCRIPT = "https://connect.facebook.net/en_US/fbevents.js";
|
|
20
16
|
|
|
21
17
|
/** Meta's own base code, as it defines `fbq` before the script loads. */
|
|
22
18
|
function defineFbq(): Fbq {
|
|
23
|
-
const global =
|
|
19
|
+
const global = vendors();
|
|
24
20
|
if (global.fbq !== undefined) return global.fbq;
|
|
25
21
|
// `arguments` is the list of whatever a call passed, which every regular
|
|
26
22
|
// `function` gets without declaring it (an arrow function does not). The
|
|
@@ -57,7 +53,7 @@ export function metaPixel(pixelIds: readonly string[]): ConsentGated {
|
|
|
57
53
|
appendScript(SCRIPT);
|
|
58
54
|
},
|
|
59
55
|
revoke: () => {
|
|
60
|
-
(
|
|
56
|
+
vendors().fbq?.("consent", "revoke");
|
|
61
57
|
},
|
|
62
58
|
};
|
|
63
59
|
}
|
|
@@ -2,13 +2,8 @@ import type { Dirent } from "node:fs";
|
|
|
2
2
|
import { readdir } from "node:fs/promises";
|
|
3
3
|
import type { AstroIntegration } from "astro";
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
|
|
7
|
-
*
|
|
8
|
-
* Types only, so nothing has to exist at runtime: a consumer reaches it with
|
|
9
|
-
* `import type`, which is erased before anything tries to resolve it.
|
|
10
|
-
*/
|
|
11
|
-
const PUBLIC_MODULE = "atlas:public";
|
|
5
|
+
/** The package the generated declaration merges into, as consumers import it. */
|
|
6
|
+
const LIB_MODULE = "@escape-game-over/atlas";
|
|
12
7
|
|
|
13
8
|
/** Files in `public/` that are not content: editor litter and placeholders. */
|
|
14
9
|
const IGNORED = new Set([".DS_Store", "Thumbs.db", ".gitkeep"]);
|
|
@@ -38,13 +33,9 @@ async function walk(dir: URL, prefix = ""): Promise<string[]> {
|
|
|
38
33
|
}
|
|
39
34
|
|
|
40
35
|
/**
|
|
41
|
-
* Types every file in `public/`, so linking to one is checked.
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* import type { PublicFile } from "atlas:public";
|
|
45
|
-
*
|
|
46
|
-
* const statement: PublicFile = "/reports/annual-2026.pdf";
|
|
47
|
-
* ```
|
|
36
|
+
* Types every file in `public/`, so linking to one is checked: `site.fileUrl()`
|
|
37
|
+
* and a redirect's `{ file }` target take `PublicFilePath`, which this narrows
|
|
38
|
+
* from any path to the files that are there.
|
|
48
39
|
*
|
|
49
40
|
* `public/` is the one part of a site the compiler cannot see: files are copied
|
|
50
41
|
* to the output verbatim, so a link to one is a string nothing verifies, and a
|
|
@@ -56,45 +47,19 @@ async function walk(dir: URL, prefix = ""): Promise<string[]> {
|
|
|
56
47
|
* commit or add to a tsconfig. A file added while the dev server runs is picked
|
|
57
48
|
* up when it restarts.
|
|
58
49
|
*/
|
|
59
|
-
export
|
|
60
|
-
/**
|
|
61
|
-
* How the consuming project imports this package.
|
|
62
|
-
*
|
|
63
|
-
* Defaults to the published name, which is what a project installing it
|
|
64
|
-
* from the registry writes. State it only where that is not the specifier
|
|
65
|
-
* in the import statements — a path alias, or a fork under another name.
|
|
66
|
-
*
|
|
67
|
-
* Needed because the generated file *augments* that module: the union has to
|
|
68
|
-
* reach `fileUrl` and the redirect target, and a declaration can only be
|
|
69
|
-
* merged into a module named the way the consumer names it.
|
|
70
|
-
*/
|
|
71
|
-
readonly libModule?: string;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function publicFiles(
|
|
75
|
-
options: PublicFilesOptions = {}
|
|
76
|
-
): AstroIntegration {
|
|
77
|
-
const libModule = options.libModule ?? "@escape-game-over/atlas";
|
|
78
|
-
|
|
50
|
+
export function publicFiles(): AstroIntegration {
|
|
79
51
|
return {
|
|
80
52
|
name: "public-files",
|
|
81
53
|
hooks: {
|
|
82
54
|
"astro:config:done": async ({ config, injectTypes, logger }) => {
|
|
83
55
|
const files = (await walk(config.publicDir)).sort();
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
*
|
|
87
|
-
* A count rather than the whitespace itself: the two call sites
|
|
88
|
-
* sit at different depths in the emitted file, and passing the
|
|
89
|
-
* indent as a literal means counting spaces by eye in a string
|
|
90
|
-
* that looks identical whether it holds eight of them or nine.
|
|
91
|
-
*/
|
|
92
|
-
const union = (spaces: number) =>
|
|
56
|
+
// One file per line, at the depth the declaration sits.
|
|
57
|
+
const union =
|
|
93
58
|
files.length === 0
|
|
94
59
|
? "never"
|
|
95
60
|
: files
|
|
96
61
|
.map((file) => `"${file}"`)
|
|
97
|
-
.join(
|
|
62
|
+
.join("\n | ");
|
|
98
63
|
|
|
99
64
|
injectTypes({
|
|
100
65
|
filename: "public-files.d.ts",
|
|
@@ -105,17 +70,11 @@ export function publicFiles(
|
|
|
105
70
|
// export lib has.
|
|
106
71
|
content: `export {};
|
|
107
72
|
|
|
108
|
-
declare module "${
|
|
109
|
-
/** Every file in \`public/\`, as it is served. */
|
|
110
|
-
export type PublicFile =
|
|
111
|
-
| ${union(8)};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
declare module "${libModule}" {
|
|
73
|
+
declare module "${LIB_MODULE}" {
|
|
115
74
|
/** Merged so \`fileUrl\` and redirect targets accept only these. */
|
|
116
75
|
interface PublicFileRegistry {
|
|
117
76
|
path:
|
|
118
|
-
| ${union
|
|
77
|
+
| ${union};
|
|
119
78
|
}
|
|
120
79
|
}
|
|
121
80
|
`,
|
package/src/astro/site-routes.ts
CHANGED
|
@@ -119,16 +119,11 @@ export function siteRoutes<Id extends string, L extends string>(
|
|
|
119
119
|
const describe = (file: GeneratedFile): string =>
|
|
120
120
|
`${file.name} (${size(file.body)})`;
|
|
121
121
|
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
// config was evaluated, and nothing re-evaluates that until the server
|
|
128
|
-
// restarts — so rebuilding per request derives the same answer from the same
|
|
129
|
-
// frozen inputs. Measured: editing a message refreshes the page that renders
|
|
130
|
-
// it and leaves the same string in `llms.txt` untouched. A build is always
|
|
131
|
-
// right, since it evaluates the config in a fresh process.
|
|
122
|
+
// Built when the dev server starts and when a build ends. The `site` and
|
|
123
|
+
// `llms` this closes over are fixed when the Astro config is evaluated, so
|
|
124
|
+
// in dev these files change only on a restart: editing a message refreshes
|
|
125
|
+
// the page that renders it, not `llms.txt`. A build is always right, since
|
|
126
|
+
// it evaluates the config in a fresh process.
|
|
132
127
|
const generate = (): GeneratedFile[] => {
|
|
133
128
|
const files: GeneratedFile[] = [...site.sitemap().files, site.robots()];
|
|
134
129
|
// Every page's head links to it, so one is written unless the config
|
|
@@ -352,35 +347,11 @@ export function siteRoutes<Id extends string, L extends string>(
|
|
|
352
347
|
* them out of the page graph.
|
|
353
348
|
*/
|
|
354
349
|
"astro:server:setup": ({ server, logger }) => {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
* middleware sees every request the dev server takes — each
|
|
361
|
-
* page, each asset, each Vite ping — and regenerating for all
|
|
362
|
-
* of them rebuilds the sitemap three times over (once directly,
|
|
363
|
-
* once inside `robots()`, once inside `llms()`) to answer a
|
|
364
|
-
* request for `/favicon.ico`.
|
|
365
|
-
*
|
|
366
|
-
* A name that appears only later is the one thing this can
|
|
367
|
-
* miss: a sitemap crossing `entryLimit` mid-session gains
|
|
368
|
-
* `sitemap-0.xml` and friends, which are not in the set until
|
|
369
|
-
* something regenerates. Acceptable because the set is
|
|
370
|
-
* refreshed on every hit and the dev server restarts on a
|
|
371
|
-
* config change — and because the alternative is paying for a
|
|
372
|
-
* full generate on every asset request to catch it.
|
|
373
|
-
*/
|
|
374
|
-
const served = new Set<string>();
|
|
375
|
-
const remember = (files: readonly GeneratedFile[]) => {
|
|
376
|
-
served.clear();
|
|
377
|
-
for (const file of files) served.add(`/${file.name}`);
|
|
378
|
-
return files;
|
|
379
|
-
};
|
|
380
|
-
|
|
381
|
-
remember(generate()).map((file) =>
|
|
382
|
-
logger.info(`generated ${describe(file)}`)
|
|
383
|
-
);
|
|
350
|
+
const files = new Map<string, GeneratedFile>();
|
|
351
|
+
for (const file of generate()) {
|
|
352
|
+
logger.info(`generated ${describe(file)}`);
|
|
353
|
+
files.set(`/${file.name}`, file);
|
|
354
|
+
}
|
|
384
355
|
server.middlewares.use((request, response, next) => {
|
|
385
356
|
const path = (request.url ?? "").split("?")[0] ?? "";
|
|
386
357
|
|
|
@@ -406,23 +377,8 @@ export function siteRoutes<Id extends string, L extends string>(
|
|
|
406
377
|
return;
|
|
407
378
|
}
|
|
408
379
|
|
|
409
|
-
|
|
410
|
-
// building anything. This is the check that keeps the cost
|
|
411
|
-
// below off every page and asset request in dev.
|
|
412
|
-
if (!served.has(path)) {
|
|
413
|
-
next();
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// Regenerated per request, so an edit to a route or a
|
|
418
|
-
// message shows up on reload rather than at restart.
|
|
419
|
-
const file = remember(generate()).find(
|
|
420
|
-
(candidate) => `/${candidate.name}` === path
|
|
421
|
-
);
|
|
380
|
+
const file = files.get(path);
|
|
422
381
|
if (file === undefined) {
|
|
423
|
-
// The set said this was ours and the fresh build
|
|
424
|
-
// disagrees — a file that stopped being generated, so
|
|
425
|
-
// it is now a 404 like any other missing path.
|
|
426
382
|
next();
|
|
427
383
|
return;
|
|
428
384
|
}
|
package/src/astro/snap-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
|
/** Snap's standard events that a website sends. */
|
|
5
6
|
export type SnapStandardEvent =
|
|
@@ -37,9 +38,6 @@ type SnapEventArgs<E extends SnapStandardEvent> = E extends "PURCHASE"
|
|
|
37
38
|
: [data?: SnapEventData];
|
|
38
39
|
|
|
39
40
|
/** What `snap-pixel.ts` defines once marketing is granted. */
|
|
40
|
-
interface SnapWindow {
|
|
41
|
-
snaptr?: (...args: unknown[]) => void;
|
|
42
|
-
}
|
|
43
41
|
|
|
44
42
|
/**
|
|
45
43
|
* Records a standard event with Snap's pixel — a lead sent, a booking made.
|
|
@@ -53,7 +51,7 @@ export function snapEvent<E extends SnapStandardEvent>(
|
|
|
53
51
|
...[data]: SnapEventArgs<E>
|
|
54
52
|
): void {
|
|
55
53
|
if (readConsent()?.marketing !== "granted") return;
|
|
56
|
-
const snaptr = (
|
|
54
|
+
const snaptr = vendors().snaptr;
|
|
57
55
|
if (data === undefined) snaptr?.("track", event);
|
|
58
56
|
else snaptr?.("track", event, data);
|
|
59
57
|
}
|
package/src/astro/snap-pixel.ts
CHANGED
|
@@ -1,22 +1,19 @@
|
|
|
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
|
/** Snap's `snaptr`: a queue until `scevent.min.js` arrives and replays it. */
|
|
5
|
-
interface Snaptr {
|
|
6
|
+
export interface Snaptr {
|
|
6
7
|
(...args: unknown[]): void;
|
|
7
8
|
handleRequest?: (...args: unknown[]) => void;
|
|
8
9
|
queue: unknown[];
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
interface PixelWindow {
|
|
12
|
-
snaptr?: Snaptr;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
12
|
const SCRIPT = "https://sc-static.net/scevent.min.js";
|
|
16
13
|
|
|
17
14
|
/** Snap's own base code, as it defines `snaptr` before the script loads. */
|
|
18
15
|
function defineSnaptr(): Snaptr {
|
|
19
|
-
const global =
|
|
16
|
+
const global = vendors();
|
|
20
17
|
if (global.snaptr !== undefined) return global.snaptr;
|
|
21
18
|
// `arguments` is the list of whatever a call passed, which every regular
|
|
22
19
|
// `function` gets without declaring it (an arrow function does not). The
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CurrencyCode } from "../currency.ts";
|
|
2
2
|
import type { DeclaredEvent, EventDataArgs } from "./event-data.ts";
|
|
3
|
+
import { vendors } from "./vendors.ts";
|
|
3
4
|
|
|
4
5
|
/** TikTok's standard events, by their current names (May 2025 onwards). */
|
|
5
6
|
export type TikTokStandardEvent =
|
|
@@ -63,9 +64,6 @@ type TikTokEventArgs<E extends TikTokEventName> = E extends TikTokStandardEvent
|
|
|
63
64
|
: EventDataArgs<TikTokCustomEvents[E & keyof TikTokCustomEvents]>;
|
|
64
65
|
|
|
65
66
|
/** What `tiktok-pixel.ts` defines once marketing is granted. */
|
|
66
|
-
interface TikTokWindow {
|
|
67
|
-
ttq?: { track(event: string, data?: object): void };
|
|
68
|
-
}
|
|
69
67
|
|
|
70
68
|
/**
|
|
71
69
|
* Records an event with TikTok's pixel — a lead sent, a booking made. Declared
|
|
@@ -80,7 +78,7 @@ export function tiktokEvent<E extends TikTokEventName>(
|
|
|
80
78
|
event: E,
|
|
81
79
|
...[data]: TikTokEventArgs<E>
|
|
82
80
|
): void {
|
|
83
|
-
const ttq = (
|
|
81
|
+
const ttq = vendors().ttq;
|
|
84
82
|
if (data === undefined) ttq?.track(event);
|
|
85
83
|
else ttq?.track(event, data);
|
|
86
84
|
}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { appendScript } from "./append-script.ts";
|
|
2
2
|
import type { ConsentGated } from "./consent-gate.ts";
|
|
3
|
+
import { vendors } from "./vendors.ts";
|
|
4
|
+
|
|
5
|
+
/** What a page calls on `ttq`: each queues, until `events.js` takes over. */
|
|
6
|
+
type TtqMethods = Record<
|
|
7
|
+
(typeof METHODS)[number],
|
|
8
|
+
(...args: unknown[]) => void
|
|
9
|
+
>;
|
|
3
10
|
|
|
4
11
|
/** TikTok's `ttq`: a queue of calls until `events.js` arrives and replays it. */
|
|
5
|
-
interface Ttq extends Array<unknown
|
|
12
|
+
export interface Ttq extends Array<unknown>, TtqMethods {
|
|
6
13
|
methods: readonly string[];
|
|
7
14
|
setAndDefer(target: unknown[], method: string): void;
|
|
8
15
|
instance(id: string): unknown[];
|
|
@@ -10,12 +17,6 @@ interface Ttq extends Array<unknown> {
|
|
|
10
17
|
_i: Record<string, unknown[] & { _u?: string }>;
|
|
11
18
|
_t: Record<string, number>;
|
|
12
19
|
_o: Record<string, object>;
|
|
13
|
-
[method: string]: unknown;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
interface PixelWindow {
|
|
17
|
-
ttq?: Ttq;
|
|
18
|
-
TiktokAnalyticsObject?: string;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
const SCRIPT = "https://analytics.tiktok.com/i18n/pixel/events.js";
|
|
@@ -39,42 +40,73 @@ const METHODS = [
|
|
|
39
40
|
"grantConsent",
|
|
40
41
|
] as const;
|
|
41
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Each method, queuing its call on `target`. Written out rather than looped
|
|
45
|
+
* over `METHODS`, so the type sees every one: the return type rejects a name
|
|
46
|
+
* missing here, or one `METHODS` lacks.
|
|
47
|
+
*/
|
|
48
|
+
function deferTo(target: unknown[]): TtqMethods {
|
|
49
|
+
const defer =
|
|
50
|
+
(method: string) =>
|
|
51
|
+
(...args: unknown[]) => {
|
|
52
|
+
target.push([method, ...args]);
|
|
53
|
+
};
|
|
54
|
+
return {
|
|
55
|
+
page: defer("page"),
|
|
56
|
+
track: defer("track"),
|
|
57
|
+
identify: defer("identify"),
|
|
58
|
+
instances: defer("instances"),
|
|
59
|
+
debug: defer("debug"),
|
|
60
|
+
on: defer("on"),
|
|
61
|
+
off: defer("off"),
|
|
62
|
+
once: defer("once"),
|
|
63
|
+
ready: defer("ready"),
|
|
64
|
+
alias: defer("alias"),
|
|
65
|
+
group: defer("group"),
|
|
66
|
+
enableCookie: defer("enableCookie"),
|
|
67
|
+
disableCookie: defer("disableCookie"),
|
|
68
|
+
holdConsent: defer("holdConsent"),
|
|
69
|
+
revokeConsent: defer("revokeConsent"),
|
|
70
|
+
grantConsent: defer("grantConsent"),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
42
74
|
/** TikTok's own base code, as it defines `ttq` before the script loads. */
|
|
43
75
|
function defineTtq(): Ttq {
|
|
44
|
-
const global =
|
|
76
|
+
const global = vendors();
|
|
45
77
|
if (global.ttq !== undefined) return global.ttq;
|
|
46
78
|
global.TiktokAnalyticsObject = "ttq";
|
|
47
|
-
const
|
|
48
|
-
ttq
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
79
|
+
const queue: unknown[] = [];
|
|
80
|
+
const ttq: Ttq = Object.assign(queue, deferTo(queue), {
|
|
81
|
+
methods: METHODS,
|
|
82
|
+
// TikTok's script calls this itself, with any name, so it stays as
|
|
83
|
+
// loose as the snippet's.
|
|
84
|
+
setAndDefer: (target: unknown[], method: string) => {
|
|
85
|
+
Object.assign(target, {
|
|
86
|
+
[method]: (...args: unknown[]) =>
|
|
87
|
+
target.push([method, ...args]),
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
instance: (id: string) => {
|
|
91
|
+
const instance = ttq._i[id] ?? [];
|
|
92
|
+
return Object.assign(instance, deferTo(instance));
|
|
93
|
+
},
|
|
94
|
+
load: (id: string, options: object = {}) => {
|
|
95
|
+
ttq._i[id] = Object.assign([], { _u: SCRIPT });
|
|
96
|
+
ttq._t[id] = Date.now();
|
|
97
|
+
ttq._o[id] = options;
|
|
98
|
+
appendScript(
|
|
99
|
+
`${SCRIPT}?${new URLSearchParams({ sdkid: id, lib: "ttq" })}`
|
|
100
|
+
);
|
|
101
|
+
},
|
|
102
|
+
_i: {},
|
|
103
|
+
_t: {},
|
|
104
|
+
_o: {},
|
|
105
|
+
});
|
|
71
106
|
global.ttq = ttq;
|
|
72
107
|
return ttq;
|
|
73
108
|
}
|
|
74
109
|
|
|
75
|
-
const call = (ttq: Ttq, method: (typeof METHODS)[number], ...args: unknown[]) =>
|
|
76
|
-
(ttq[method] as (...args: unknown[]) => void)(...args);
|
|
77
|
-
|
|
78
110
|
/** The pixel: loaded on the first grant, paused and resumed by later answers. */
|
|
79
111
|
export function tiktokPixel(pixelIds: readonly string[]): ConsentGated {
|
|
80
112
|
let loaded = false;
|
|
@@ -83,15 +115,14 @@ export function tiktokPixel(pixelIds: readonly string[]): ConsentGated {
|
|
|
83
115
|
const ttq = defineTtq();
|
|
84
116
|
// Also lifts the hold of TikTok's Cookie Consent Mode, where a
|
|
85
117
|
// pixel has it on in Events Manager.
|
|
86
|
-
|
|
118
|
+
ttq.grantConsent();
|
|
87
119
|
if (loaded) return;
|
|
88
120
|
loaded = true;
|
|
89
121
|
for (const id of pixelIds) ttq.load(id);
|
|
90
|
-
|
|
122
|
+
ttq.page();
|
|
91
123
|
},
|
|
92
124
|
revoke: () => {
|
|
93
|
-
|
|
94
|
-
if (ttq !== undefined) call(ttq, "revokeConsent");
|
|
125
|
+
vendors().ttq?.revokeConsent();
|
|
95
126
|
},
|
|
96
127
|
};
|
|
97
128
|
}
|
package/src/astro/umami-event.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DeclaredEvent, EventDataArgs } from "./event-data.ts";
|
|
2
|
+
import { vendors } from "./vendors.ts";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* The project's Umami events, each with the data it carries, `undefined` for
|
|
@@ -34,10 +35,5 @@ export function umamiEvent<E extends DeclaredEvent<UmamiEvents>>(
|
|
|
34
35
|
// Flat string values; anything else in a declaration fails here.
|
|
35
36
|
...[data]: EventDataArgs<UmamiEvents[E] & Readonly<Record<string, string>>>
|
|
36
37
|
): void {
|
|
37
|
-
|
|
38
|
-
window as unknown as {
|
|
39
|
-
umami?: { track(event: string, data?: object): void };
|
|
40
|
-
}
|
|
41
|
-
).umami;
|
|
42
|
-
umami?.track(name, data);
|
|
38
|
+
vendors().umami?.track(name, data);
|
|
43
39
|
}
|