@escape-game-over/atlas 0.1.40 → 0.1.42

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@escape-game-over/atlas",
3
- "version": "0.1.40",
3
+ "version": "0.1.42",
4
4
  "type": "module",
5
5
  "description": "Typed, data-driven machinery for static multi-locale, multi-deployment Astro sites.",
6
6
  "private": false,
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Where a visit came from, for a lead form to report: the campaign tags it
3
+ * landed with, and the Google Ads click behind it.
4
+ *
5
+ * A lead is the one record that outlives the visit — an email, a row in a
6
+ * sheet — so it is where a conversion import has to find the click, and where
7
+ * anyone asking "which campaign brought this?" will look.
8
+ *
9
+ * The two are kept differently, on purpose:
10
+ *
11
+ * - **Campaign tags** (`utm_*`) are labels a campaign shares with everyone who
12
+ * clicked it, not an identifier. They are kept for the tab in
13
+ * `sessionStorage`, so a visitor who lands on one page and fills a form on
14
+ * another still reports them. `rememberCampaign()` on every page does that.
15
+ * - **The ad click id** (`gclid`, `gbraid`, `wbraid`) identifies one click, and
16
+ * is never stored here. Google keeps it under the visitor's consent: on the
17
+ * URL while marketing is denied — `urlPassthrough` in `GoogleSettings` — and
18
+ * in its own `_gcl_aw` cookie once granted. `adClick()` reads it from there.
19
+ */
20
+
21
+ /** The campaign tags a link can carry. */
22
+ export type CampaignTag =
23
+ | "utm_source"
24
+ | "utm_medium"
25
+ | "utm_campaign"
26
+ | "utm_term"
27
+ | "utm_content";
28
+
29
+ const CAMPAIGN_TAGS: readonly CampaignTag[] = [
30
+ "utm_source",
31
+ "utm_medium",
32
+ "utm_campaign",
33
+ "utm_term",
34
+ "utm_content",
35
+ ];
36
+
37
+ /** The ids Google Ads puts on a landing URL. */
38
+ export type AdClickId = "gclid" | "gbraid" | "wbraid";
39
+
40
+ const AD_CLICK_IDS: readonly AdClickId[] = ["gclid", "gbraid", "wbraid"];
41
+
42
+ export type Campaign = Partial<Record<CampaignTag, string>>;
43
+ export type AdClick = Partial<Record<AdClickId, string>>;
44
+
45
+ /** The `sessionStorage` key the tags are kept under. */
46
+ const KEY = "atlas-campaign";
47
+
48
+ /** The given parameters present on this page's URL. */
49
+ function fromUrl<K extends string>(
50
+ names: readonly K[]
51
+ ): Partial<Record<K, string>> {
52
+ const params = new URLSearchParams(location.search);
53
+ const found: Partial<Record<K, string>> = {};
54
+ for (const name of names) {
55
+ const value = params.get(name);
56
+ if (value) found[name] = value;
57
+ }
58
+ return found;
59
+ }
60
+
61
+ /** Keeps this page's campaign tags for the tab, if it has any. */
62
+ export function rememberCampaign(): void {
63
+ const landed = fromUrl(CAMPAIGN_TAGS);
64
+ if (Object.keys(landed).length === 0) return;
65
+ try {
66
+ sessionStorage.setItem(KEY, JSON.stringify(landed));
67
+ } catch {
68
+ // Storage refused: the tags still count on the page they arrived on.
69
+ }
70
+ }
71
+
72
+ /** The tags on this page's URL, else the ones the visit landed with. */
73
+ export function campaign(): Campaign {
74
+ const here = fromUrl(CAMPAIGN_TAGS);
75
+ if (Object.keys(here).length > 0) return here;
76
+ try {
77
+ const kept: unknown = JSON.parse(sessionStorage.getItem(KEY) ?? "{}");
78
+ if (typeof kept !== "object" || kept === null) return {};
79
+ const found: Campaign = {};
80
+ for (const tag of CAMPAIGN_TAGS) {
81
+ const value = (kept as Record<string, unknown>)[tag];
82
+ if (typeof value === "string" && value !== "") found[tag] = value;
83
+ }
84
+ return found;
85
+ } catch {
86
+ return {};
87
+ }
88
+ }
89
+
90
+ /**
91
+ * A moment as a Google Ads conversion import reads it:
92
+ * `2026-09-24 12:42:10+00:00`. In UTC, so the file needs no time zone setting.
93
+ */
94
+ export function conversionTime(at: Date = new Date()): string {
95
+ return `${at.toISOString().slice(0, 19).replace("T", " ")}+00:00`;
96
+ }
97
+
98
+ /**
99
+ * A cookie's value. `document.cookie` is always `name=value; name=value`, so a
100
+ * split reads it — `cookieStore.get` would too, but is async and newer than
101
+ * the Safari still in use.
102
+ */
103
+ function cookie(name: string): string | undefined {
104
+ const prefix = `${name}=`;
105
+ return document.cookie
106
+ .split("; ")
107
+ .find((pair) => pair.startsWith(prefix))
108
+ ?.slice(prefix.length);
109
+ }
110
+
111
+ /**
112
+ * The Google Ads click behind this visit: from the URL, else `gclid` from
113
+ * Google's `_gcl_aw` cookie, whose value is `GCL.<time>.<gclid>`.
114
+ */
115
+ export function adClick(): AdClick {
116
+ const click = fromUrl(AD_CLICK_IDS);
117
+ if (click.gclid === undefined) {
118
+ const gclid = cookie("_gcl_aw")?.split(".").slice(2).join(".");
119
+ if (gclid) click.gclid = gclid;
120
+ }
121
+ return click;
122
+ }
@@ -9,8 +9,12 @@
9
9
  * import, so a component's contract can be imported from frontmatter too.
10
10
  */
11
11
 
12
+ export * from "./attribution.ts";
12
13
  export * from "./background-video.ts";
13
14
  export * from "./carousel.ts";
15
+ // Reading only: a form that reports what the visitor agreed to. Recording an
16
+ // answer stays with the banner.
17
+ export { type ConsentRecord, readConsent } from "./consent.ts";
14
18
  export {
15
19
  type Connect,
16
20
  type ElementContext,
@@ -20,4 +24,5 @@ export {
20
24
  export * from "./filters.ts";
21
25
  export * from "./filters-view.ts";
22
26
  export { data, ref, refs } from "./ref.ts";
27
+ export * from "./umami-event.ts";
23
28
  export * from "./youtube.ts";
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Records a named event with Umami — a form sent, a deck downloaded — from a
3
+ * client script.
4
+ *
5
+ * Umami's own script defines `window.umami` once it has loaded; atlas emits
6
+ * that script from `analytics.umami`. Before then, or where a blocker stopped
7
+ * it, there is nothing to call, and the event is dropped rather than thrown:
8
+ * an analytics call must never be the reason a page misbehaves.
9
+ *
10
+ * The data goes to Umami as it is, so it should carry what the visitor chose,
11
+ * never who they are. For an event on a plain click, Umami's
12
+ * `data-umami-event` attribute needs no script at all.
13
+ */
14
+ export function umamiEvent(
15
+ name: string,
16
+ data?: Readonly<Record<string, string>>
17
+ ): void {
18
+ const umami = (
19
+ window as unknown as {
20
+ umami?: { track(event: string, data?: object): void };
21
+ }
22
+ ).umami;
23
+ umami?.track(name, data);
24
+ }