@distinctagency/cms-client 1.1.1 → 1.2.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/dist/client.d.mts +68 -0
- package/dist/client.d.ts +68 -0
- package/dist/client.js +208 -0
- package/dist/client.js.map +1 -0
- package/dist/client.mjs +181 -0
- package/dist/client.mjs.map +1 -0
- package/dist/index.d.mts +1 -68
- package/dist/index.d.ts +1 -68
- package/dist/index.js +3 -181
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +0 -176
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -2
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
interface CmsAnalyticsProps {
|
|
2
|
+
/** The CMS tracking endpoint URL (required — your CMS app URL + /api/track) */
|
|
3
|
+
trackingUrl: string;
|
|
4
|
+
/** Tenant API key (public, read-only — safe for client-side use) */
|
|
5
|
+
apiKey: string;
|
|
6
|
+
/** Content item ID (from CMS) */
|
|
7
|
+
contentItemId?: string;
|
|
8
|
+
/** Content type slug (e.g. "blog_posts") */
|
|
9
|
+
contentTypeSlug: string;
|
|
10
|
+
/** Item slug (e.g. "my-post") */
|
|
11
|
+
itemSlug: string;
|
|
12
|
+
/** Enable scroll depth tracking (default: true) */
|
|
13
|
+
trackScroll?: boolean;
|
|
14
|
+
/** Enable time-on-page tracking (default: true) */
|
|
15
|
+
trackTime?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Drop-in analytics component for CMS content pages.
|
|
19
|
+
* Tracks page views, scroll depth, and time on page.
|
|
20
|
+
*
|
|
21
|
+
* The apiKey is a public, read-only tenant identifier — it only grants
|
|
22
|
+
* access to published content and is safe for client-side use.
|
|
23
|
+
*
|
|
24
|
+
* Usage:
|
|
25
|
+
* ```tsx
|
|
26
|
+
* <CmsAnalytics
|
|
27
|
+
* trackingUrl={process.env.NEXT_PUBLIC_CMS_URL + "/api/track"}
|
|
28
|
+
* apiKey={process.env.NEXT_PUBLIC_CMS_API_KEY!}
|
|
29
|
+
* contentTypeSlug="blog_posts"
|
|
30
|
+
* itemSlug={params.slug}
|
|
31
|
+
* />
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
declare function CmsAnalytics({ trackingUrl, apiKey, contentItemId, contentTypeSlug, itemSlug, trackScroll, trackTime, }: CmsAnalyticsProps): null;
|
|
35
|
+
|
|
36
|
+
interface PageTrackerProps {
|
|
37
|
+
/** The CMS tracking endpoint URL (required — your CMS app URL + /api/track) */
|
|
38
|
+
trackingUrl: string;
|
|
39
|
+
/** Tenant API key (public, read-only — safe for client-side use) */
|
|
40
|
+
apiKey: string;
|
|
41
|
+
/** Enable scroll depth tracking (default: true) */
|
|
42
|
+
trackScroll?: boolean;
|
|
43
|
+
/** Enable time-on-page tracking (default: true) */
|
|
44
|
+
trackTime?: boolean;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Site-wide page tracker. Add once in root layout to track all pages.
|
|
48
|
+
* Automatically detects CMS content pages from URL structure.
|
|
49
|
+
*
|
|
50
|
+
* The apiKey is a public, read-only tenant identifier — it only grants
|
|
51
|
+
* access to published content and is safe for client-side use.
|
|
52
|
+
*
|
|
53
|
+
* Usage in src/app/layout.tsx:
|
|
54
|
+
* ```tsx
|
|
55
|
+
* import { PageTracker } from "@distinctagency/cms-client"
|
|
56
|
+
*
|
|
57
|
+
* <body>
|
|
58
|
+
* {children}
|
|
59
|
+
* <PageTracker
|
|
60
|
+
* trackingUrl={process.env.NEXT_PUBLIC_CMS_URL + "/api/track"}
|
|
61
|
+
* apiKey={process.env.NEXT_PUBLIC_CMS_API_KEY!}
|
|
62
|
+
* />
|
|
63
|
+
* </body>
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
declare function PageTracker({ trackingUrl, apiKey, trackScroll, trackTime, }: PageTrackerProps): null;
|
|
67
|
+
|
|
68
|
+
export { CmsAnalytics, PageTracker };
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
interface CmsAnalyticsProps {
|
|
2
|
+
/** The CMS tracking endpoint URL (required — your CMS app URL + /api/track) */
|
|
3
|
+
trackingUrl: string;
|
|
4
|
+
/** Tenant API key (public, read-only — safe for client-side use) */
|
|
5
|
+
apiKey: string;
|
|
6
|
+
/** Content item ID (from CMS) */
|
|
7
|
+
contentItemId?: string;
|
|
8
|
+
/** Content type slug (e.g. "blog_posts") */
|
|
9
|
+
contentTypeSlug: string;
|
|
10
|
+
/** Item slug (e.g. "my-post") */
|
|
11
|
+
itemSlug: string;
|
|
12
|
+
/** Enable scroll depth tracking (default: true) */
|
|
13
|
+
trackScroll?: boolean;
|
|
14
|
+
/** Enable time-on-page tracking (default: true) */
|
|
15
|
+
trackTime?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Drop-in analytics component for CMS content pages.
|
|
19
|
+
* Tracks page views, scroll depth, and time on page.
|
|
20
|
+
*
|
|
21
|
+
* The apiKey is a public, read-only tenant identifier — it only grants
|
|
22
|
+
* access to published content and is safe for client-side use.
|
|
23
|
+
*
|
|
24
|
+
* Usage:
|
|
25
|
+
* ```tsx
|
|
26
|
+
* <CmsAnalytics
|
|
27
|
+
* trackingUrl={process.env.NEXT_PUBLIC_CMS_URL + "/api/track"}
|
|
28
|
+
* apiKey={process.env.NEXT_PUBLIC_CMS_API_KEY!}
|
|
29
|
+
* contentTypeSlug="blog_posts"
|
|
30
|
+
* itemSlug={params.slug}
|
|
31
|
+
* />
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
declare function CmsAnalytics({ trackingUrl, apiKey, contentItemId, contentTypeSlug, itemSlug, trackScroll, trackTime, }: CmsAnalyticsProps): null;
|
|
35
|
+
|
|
36
|
+
interface PageTrackerProps {
|
|
37
|
+
/** The CMS tracking endpoint URL (required — your CMS app URL + /api/track) */
|
|
38
|
+
trackingUrl: string;
|
|
39
|
+
/** Tenant API key (public, read-only — safe for client-side use) */
|
|
40
|
+
apiKey: string;
|
|
41
|
+
/** Enable scroll depth tracking (default: true) */
|
|
42
|
+
trackScroll?: boolean;
|
|
43
|
+
/** Enable time-on-page tracking (default: true) */
|
|
44
|
+
trackTime?: boolean;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Site-wide page tracker. Add once in root layout to track all pages.
|
|
48
|
+
* Automatically detects CMS content pages from URL structure.
|
|
49
|
+
*
|
|
50
|
+
* The apiKey is a public, read-only tenant identifier — it only grants
|
|
51
|
+
* access to published content and is safe for client-side use.
|
|
52
|
+
*
|
|
53
|
+
* Usage in src/app/layout.tsx:
|
|
54
|
+
* ```tsx
|
|
55
|
+
* import { PageTracker } from "@distinctagency/cms-client"
|
|
56
|
+
*
|
|
57
|
+
* <body>
|
|
58
|
+
* {children}
|
|
59
|
+
* <PageTracker
|
|
60
|
+
* trackingUrl={process.env.NEXT_PUBLIC_CMS_URL + "/api/track"}
|
|
61
|
+
* apiKey={process.env.NEXT_PUBLIC_CMS_API_KEY!}
|
|
62
|
+
* />
|
|
63
|
+
* </body>
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
declare function PageTracker({ trackingUrl, apiKey, trackScroll, trackTime, }: PageTrackerProps): null;
|
|
67
|
+
|
|
68
|
+
export { CmsAnalytics, PageTracker };
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
"use client";
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
21
|
+
|
|
22
|
+
// src/client.ts
|
|
23
|
+
var client_exports = {};
|
|
24
|
+
__export(client_exports, {
|
|
25
|
+
CmsAnalytics: () => CmsAnalytics,
|
|
26
|
+
PageTracker: () => PageTracker
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(client_exports);
|
|
29
|
+
|
|
30
|
+
// src/analytics.tsx
|
|
31
|
+
var import_react = require("react");
|
|
32
|
+
function CmsAnalytics({
|
|
33
|
+
trackingUrl,
|
|
34
|
+
apiKey,
|
|
35
|
+
contentItemId,
|
|
36
|
+
contentTypeSlug,
|
|
37
|
+
itemSlug,
|
|
38
|
+
trackScroll = true,
|
|
39
|
+
trackTime = true
|
|
40
|
+
}) {
|
|
41
|
+
const sentRef = (0, import_react.useRef)(false);
|
|
42
|
+
const maxScrollRef = (0, import_react.useRef)(0);
|
|
43
|
+
const startTimeRef = (0, import_react.useRef)(Date.now());
|
|
44
|
+
const sessionIdRef = (0, import_react.useRef)(getSessionId());
|
|
45
|
+
(0, import_react.useEffect)(() => {
|
|
46
|
+
if (sentRef.current) return;
|
|
47
|
+
sentRef.current = true;
|
|
48
|
+
sendEvent(trackingUrl, {
|
|
49
|
+
api_key: apiKey,
|
|
50
|
+
content_item_id: contentItemId,
|
|
51
|
+
content_type_slug: contentTypeSlug,
|
|
52
|
+
item_slug: itemSlug,
|
|
53
|
+
event_type: "page_view",
|
|
54
|
+
referrer: document.referrer || null,
|
|
55
|
+
session_id: sessionIdRef.current
|
|
56
|
+
});
|
|
57
|
+
let scrollHandler = null;
|
|
58
|
+
if (trackScroll) {
|
|
59
|
+
scrollHandler = () => {
|
|
60
|
+
const scrollHeight = document.documentElement.scrollHeight - window.innerHeight;
|
|
61
|
+
if (scrollHeight <= 0) return;
|
|
62
|
+
const pct = Math.round(window.scrollY / scrollHeight * 100);
|
|
63
|
+
if (pct > maxScrollRef.current) maxScrollRef.current = pct;
|
|
64
|
+
};
|
|
65
|
+
window.addEventListener("scroll", scrollHandler, { passive: true });
|
|
66
|
+
}
|
|
67
|
+
const handleUnload = () => {
|
|
68
|
+
const timeOnPage = Math.round((Date.now() - startTimeRef.current) / 1e3);
|
|
69
|
+
const payload = JSON.stringify({
|
|
70
|
+
api_key: apiKey,
|
|
71
|
+
content_item_id: contentItemId,
|
|
72
|
+
content_type_slug: contentTypeSlug,
|
|
73
|
+
item_slug: itemSlug,
|
|
74
|
+
event_type: "engagement",
|
|
75
|
+
session_id: sessionIdRef.current,
|
|
76
|
+
metadata: {
|
|
77
|
+
scroll_depth: maxScrollRef.current,
|
|
78
|
+
time_on_page: timeOnPage
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
if (navigator.sendBeacon) {
|
|
82
|
+
navigator.sendBeacon(trackingUrl, payload);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
if (trackScroll || trackTime) {
|
|
86
|
+
window.addEventListener("beforeunload", handleUnload);
|
|
87
|
+
}
|
|
88
|
+
return () => {
|
|
89
|
+
if (scrollHandler) window.removeEventListener("scroll", scrollHandler);
|
|
90
|
+
if (trackScroll || trackTime) window.removeEventListener("beforeunload", handleUnload);
|
|
91
|
+
};
|
|
92
|
+
}, [trackingUrl, apiKey, contentItemId, contentTypeSlug, itemSlug, trackScroll, trackTime]);
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
function sendEvent(url, data) {
|
|
96
|
+
fetch(url, {
|
|
97
|
+
method: "POST",
|
|
98
|
+
headers: { "Content-Type": "application/json" },
|
|
99
|
+
body: JSON.stringify(data),
|
|
100
|
+
keepalive: true
|
|
101
|
+
}).catch(() => {
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function getSessionId() {
|
|
105
|
+
if (typeof window === "undefined") return "";
|
|
106
|
+
const key = "__cms_sid";
|
|
107
|
+
let sid = sessionStorage.getItem(key);
|
|
108
|
+
if (!sid) {
|
|
109
|
+
sid = Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
110
|
+
sessionStorage.setItem(key, sid);
|
|
111
|
+
}
|
|
112
|
+
return sid;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/page-tracker.tsx
|
|
116
|
+
var import_react2 = require("react");
|
|
117
|
+
var import_navigation = require("next/navigation");
|
|
118
|
+
function PageTracker({
|
|
119
|
+
trackingUrl,
|
|
120
|
+
apiKey,
|
|
121
|
+
trackScroll = true,
|
|
122
|
+
trackTime = true
|
|
123
|
+
}) {
|
|
124
|
+
const pathname = (0, import_navigation.usePathname)();
|
|
125
|
+
const prevPathRef = (0, import_react2.useRef)("");
|
|
126
|
+
const maxScrollRef = (0, import_react2.useRef)(0);
|
|
127
|
+
const startTimeRef = (0, import_react2.useRef)(Date.now());
|
|
128
|
+
const sessionIdRef = (0, import_react2.useRef)(getSessionId2());
|
|
129
|
+
(0, import_react2.useEffect)(() => {
|
|
130
|
+
if (pathname === prevPathRef.current) return;
|
|
131
|
+
prevPathRef.current = pathname;
|
|
132
|
+
maxScrollRef.current = 0;
|
|
133
|
+
startTimeRef.current = Date.now();
|
|
134
|
+
const segments = pathname.split("/").filter(Boolean);
|
|
135
|
+
const contentTypeSlug = segments[0] ?? null;
|
|
136
|
+
const itemSlug = segments.length >= 2 ? segments[segments.length - 1] : null;
|
|
137
|
+
sendEvent2(trackingUrl, {
|
|
138
|
+
api_key: apiKey,
|
|
139
|
+
event_type: "page_view",
|
|
140
|
+
content_type_slug: contentTypeSlug,
|
|
141
|
+
item_slug: itemSlug,
|
|
142
|
+
referrer: document.referrer || null,
|
|
143
|
+
session_id: sessionIdRef.current,
|
|
144
|
+
metadata: { path: pathname }
|
|
145
|
+
});
|
|
146
|
+
}, [pathname, trackingUrl, apiKey]);
|
|
147
|
+
(0, import_react2.useEffect)(() => {
|
|
148
|
+
if (!trackScroll) return;
|
|
149
|
+
const handleScroll = () => {
|
|
150
|
+
const scrollHeight = document.documentElement.scrollHeight - window.innerHeight;
|
|
151
|
+
if (scrollHeight <= 0) return;
|
|
152
|
+
const pct = Math.round(window.scrollY / scrollHeight * 100);
|
|
153
|
+
if (pct > maxScrollRef.current) maxScrollRef.current = pct;
|
|
154
|
+
};
|
|
155
|
+
window.addEventListener("scroll", handleScroll, { passive: true });
|
|
156
|
+
return () => window.removeEventListener("scroll", handleScroll);
|
|
157
|
+
}, [trackScroll]);
|
|
158
|
+
(0, import_react2.useEffect)(() => {
|
|
159
|
+
if (!trackScroll && !trackTime) return;
|
|
160
|
+
const handleUnload = () => {
|
|
161
|
+
const timeOnPage = Math.round((Date.now() - startTimeRef.current) / 1e3);
|
|
162
|
+
const segments = prevPathRef.current.split("/").filter(Boolean);
|
|
163
|
+
const payload = JSON.stringify({
|
|
164
|
+
api_key: apiKey,
|
|
165
|
+
event_type: "engagement",
|
|
166
|
+
content_type_slug: segments[0] ?? null,
|
|
167
|
+
item_slug: segments.length >= 2 ? segments[segments.length - 1] : null,
|
|
168
|
+
session_id: sessionIdRef.current,
|
|
169
|
+
metadata: {
|
|
170
|
+
scroll_depth: maxScrollRef.current,
|
|
171
|
+
time_on_page: timeOnPage,
|
|
172
|
+
path: prevPathRef.current
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
if (navigator.sendBeacon) {
|
|
176
|
+
navigator.sendBeacon(trackingUrl, payload);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
window.addEventListener("beforeunload", handleUnload);
|
|
180
|
+
return () => window.removeEventListener("beforeunload", handleUnload);
|
|
181
|
+
}, [trackingUrl, apiKey, trackScroll, trackTime]);
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
function sendEvent2(url, data) {
|
|
185
|
+
fetch(url, {
|
|
186
|
+
method: "POST",
|
|
187
|
+
headers: { "Content-Type": "application/json" },
|
|
188
|
+
body: JSON.stringify(data),
|
|
189
|
+
keepalive: true
|
|
190
|
+
}).catch(() => {
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
function getSessionId2() {
|
|
194
|
+
if (typeof window === "undefined") return "";
|
|
195
|
+
const key = "__cms_sid";
|
|
196
|
+
let sid = sessionStorage.getItem(key);
|
|
197
|
+
if (!sid) {
|
|
198
|
+
sid = Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
199
|
+
sessionStorage.setItem(key, sid);
|
|
200
|
+
}
|
|
201
|
+
return sid;
|
|
202
|
+
}
|
|
203
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
204
|
+
0 && (module.exports = {
|
|
205
|
+
CmsAnalytics,
|
|
206
|
+
PageTracker
|
|
207
|
+
});
|
|
208
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/analytics.tsx","../src/page-tracker.tsx"],"sourcesContent":["\"use client\"\n\n// Client-side React components — requires \"use client\" context\nexport { CmsAnalytics } from \"./analytics\"\nexport { PageTracker } from \"./page-tracker\"\n","\"use client\"\n\nimport { useEffect, useRef } from \"react\"\n\ninterface CmsAnalyticsProps {\n /** The CMS tracking endpoint URL (required — your CMS app URL + /api/track) */\n trackingUrl: string\n /** Tenant API key (public, read-only — safe for client-side use) */\n apiKey: string\n /** Content item ID (from CMS) */\n contentItemId?: string\n /** Content type slug (e.g. \"blog_posts\") */\n contentTypeSlug: string\n /** Item slug (e.g. \"my-post\") */\n itemSlug: string\n /** Enable scroll depth tracking (default: true) */\n trackScroll?: boolean\n /** Enable time-on-page tracking (default: true) */\n trackTime?: boolean\n}\n\n/**\n * Drop-in analytics component for CMS content pages.\n * Tracks page views, scroll depth, and time on page.\n *\n * The apiKey is a public, read-only tenant identifier — it only grants\n * access to published content and is safe for client-side use.\n *\n * Usage:\n * ```tsx\n * <CmsAnalytics\n * trackingUrl={process.env.NEXT_PUBLIC_CMS_URL + \"/api/track\"}\n * apiKey={process.env.NEXT_PUBLIC_CMS_API_KEY!}\n * contentTypeSlug=\"blog_posts\"\n * itemSlug={params.slug}\n * />\n * ```\n */\nexport function CmsAnalytics({\n trackingUrl,\n apiKey,\n contentItemId,\n contentTypeSlug,\n itemSlug,\n trackScroll = true,\n trackTime = true,\n}: CmsAnalyticsProps) {\n const sentRef = useRef(false)\n const maxScrollRef = useRef(0)\n const startTimeRef = useRef(Date.now())\n const sessionIdRef = useRef(getSessionId())\n\n useEffect(() => {\n // Only send page view once per mount\n if (sentRef.current) return\n sentRef.current = true\n\n // Page view event\n sendEvent(trackingUrl, {\n api_key: apiKey,\n content_item_id: contentItemId,\n content_type_slug: contentTypeSlug,\n item_slug: itemSlug,\n event_type: \"page_view\",\n referrer: document.referrer || null,\n session_id: sessionIdRef.current,\n })\n\n // Scroll tracking\n let scrollHandler: (() => void) | null = null\n if (trackScroll) {\n scrollHandler = () => {\n const scrollHeight = document.documentElement.scrollHeight - window.innerHeight\n if (scrollHeight <= 0) return\n const pct = Math.round((window.scrollY / scrollHeight) * 100)\n if (pct > maxScrollRef.current) maxScrollRef.current = pct\n }\n window.addEventListener(\"scroll\", scrollHandler, { passive: true })\n }\n\n // Send engagement data on page leave\n const handleUnload = () => {\n const timeOnPage = Math.round((Date.now() - startTimeRef.current) / 1000)\n const payload = JSON.stringify({\n api_key: apiKey,\n content_item_id: contentItemId,\n content_type_slug: contentTypeSlug,\n item_slug: itemSlug,\n event_type: \"engagement\",\n session_id: sessionIdRef.current,\n metadata: {\n scroll_depth: maxScrollRef.current,\n time_on_page: timeOnPage,\n },\n })\n\n // Use sendBeacon for reliable delivery on page unload\n if (navigator.sendBeacon) {\n navigator.sendBeacon(trackingUrl, payload)\n }\n }\n\n if (trackScroll || trackTime) {\n window.addEventListener(\"beforeunload\", handleUnload)\n }\n\n return () => {\n if (scrollHandler) window.removeEventListener(\"scroll\", scrollHandler)\n if (trackScroll || trackTime) window.removeEventListener(\"beforeunload\", handleUnload)\n }\n }, [trackingUrl, apiKey, contentItemId, contentTypeSlug, itemSlug, trackScroll, trackTime])\n\n return null\n}\n\nfunction sendEvent(url: string, data: Record<string, unknown>) {\n fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(data),\n keepalive: true,\n }).catch(() => {}) // fire and forget\n}\n\n/** Generate a random session ID (persists for the browser session) */\nfunction getSessionId(): string {\n if (typeof window === \"undefined\") return \"\"\n const key = \"__cms_sid\"\n let sid = sessionStorage.getItem(key)\n if (!sid) {\n sid = Math.random().toString(36).slice(2) + Date.now().toString(36)\n sessionStorage.setItem(key, sid)\n }\n return sid\n}\n","\"use client\"\n\nimport { useEffect, useRef } from \"react\"\nimport { usePathname } from \"next/navigation\"\n\ninterface PageTrackerProps {\n /** The CMS tracking endpoint URL (required — your CMS app URL + /api/track) */\n trackingUrl: string\n /** Tenant API key (public, read-only — safe for client-side use) */\n apiKey: string\n /** Enable scroll depth tracking (default: true) */\n trackScroll?: boolean\n /** Enable time-on-page tracking (default: true) */\n trackTime?: boolean\n}\n\n/**\n * Site-wide page tracker. Add once in root layout to track all pages.\n * Automatically detects CMS content pages from URL structure.\n *\n * The apiKey is a public, read-only tenant identifier — it only grants\n * access to published content and is safe for client-side use.\n *\n * Usage in src/app/layout.tsx:\n * ```tsx\n * import { PageTracker } from \"@distinctagency/cms-client\"\n *\n * <body>\n * {children}\n * <PageTracker\n * trackingUrl={process.env.NEXT_PUBLIC_CMS_URL + \"/api/track\"}\n * apiKey={process.env.NEXT_PUBLIC_CMS_API_KEY!}\n * />\n * </body>\n * ```\n */\nexport function PageTracker({\n trackingUrl,\n apiKey,\n trackScroll = true,\n trackTime = true,\n}: PageTrackerProps) {\n const pathname = usePathname()\n const prevPathRef = useRef(\"\")\n const maxScrollRef = useRef(0)\n const startTimeRef = useRef(Date.now())\n const sessionIdRef = useRef(getSessionId())\n\n useEffect(() => {\n // Skip if same path (prevents double-fire on mount)\n if (pathname === prevPathRef.current) return\n prevPathRef.current = pathname\n\n // Reset engagement tracking for new page\n maxScrollRef.current = 0\n startTimeRef.current = Date.now()\n\n // Try to extract content type and item slug from URL\n // Supports patterns like /events/future-finance, /blog/my-post, etc.\n const segments = pathname.split(\"/\").filter(Boolean)\n const contentTypeSlug = segments[0] ?? null\n const itemSlug = segments.length >= 2 ? segments[segments.length - 1] : null\n\n // Fire page view\n sendEvent(trackingUrl, {\n api_key: apiKey,\n event_type: \"page_view\",\n content_type_slug: contentTypeSlug,\n item_slug: itemSlug,\n referrer: document.referrer || null,\n session_id: sessionIdRef.current,\n metadata: { path: pathname },\n })\n }, [pathname, trackingUrl, apiKey])\n\n // Scroll tracking\n useEffect(() => {\n if (!trackScroll) return\n\n const handleScroll = () => {\n const scrollHeight = document.documentElement.scrollHeight - window.innerHeight\n if (scrollHeight <= 0) return\n const pct = Math.round((window.scrollY / scrollHeight) * 100)\n if (pct > maxScrollRef.current) maxScrollRef.current = pct\n }\n\n window.addEventListener(\"scroll\", handleScroll, { passive: true })\n return () => window.removeEventListener(\"scroll\", handleScroll)\n }, [trackScroll])\n\n // Send engagement data on page leave\n useEffect(() => {\n if (!trackScroll && !trackTime) return\n\n const handleUnload = () => {\n const timeOnPage = Math.round((Date.now() - startTimeRef.current) / 1000)\n const segments = prevPathRef.current.split(\"/\").filter(Boolean)\n\n const payload = JSON.stringify({\n api_key: apiKey,\n event_type: \"engagement\",\n content_type_slug: segments[0] ?? null,\n item_slug: segments.length >= 2 ? segments[segments.length - 1] : null,\n session_id: sessionIdRef.current,\n metadata: {\n scroll_depth: maxScrollRef.current,\n time_on_page: timeOnPage,\n path: prevPathRef.current,\n },\n })\n\n if (navigator.sendBeacon) {\n navigator.sendBeacon(trackingUrl, payload)\n }\n }\n\n window.addEventListener(\"beforeunload\", handleUnload)\n return () => window.removeEventListener(\"beforeunload\", handleUnload)\n }, [trackingUrl, apiKey, trackScroll, trackTime])\n\n return null\n}\n\nfunction sendEvent(url: string, data: Record<string, unknown>) {\n fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(data),\n keepalive: true,\n }).catch(() => {})\n}\n\nfunction getSessionId(): string {\n if (typeof window === \"undefined\") return \"\"\n const key = \"__cms_sid\"\n let sid = sessionStorage.getItem(key)\n if (!sid) {\n sid = Math.random().toString(36).slice(2) + Date.now().toString(36)\n sessionStorage.setItem(key, sid)\n }\n return sid\n}\n"],"mappings":"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","names":["import_react","getSessionId","sendEvent"]}
|
package/dist/client.mjs
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
// src/analytics.tsx
|
|
5
|
+
import { useEffect, useRef } from "react";
|
|
6
|
+
function CmsAnalytics({
|
|
7
|
+
trackingUrl,
|
|
8
|
+
apiKey,
|
|
9
|
+
contentItemId,
|
|
10
|
+
contentTypeSlug,
|
|
11
|
+
itemSlug,
|
|
12
|
+
trackScroll = true,
|
|
13
|
+
trackTime = true
|
|
14
|
+
}) {
|
|
15
|
+
const sentRef = useRef(false);
|
|
16
|
+
const maxScrollRef = useRef(0);
|
|
17
|
+
const startTimeRef = useRef(Date.now());
|
|
18
|
+
const sessionIdRef = useRef(getSessionId());
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (sentRef.current) return;
|
|
21
|
+
sentRef.current = true;
|
|
22
|
+
sendEvent(trackingUrl, {
|
|
23
|
+
api_key: apiKey,
|
|
24
|
+
content_item_id: contentItemId,
|
|
25
|
+
content_type_slug: contentTypeSlug,
|
|
26
|
+
item_slug: itemSlug,
|
|
27
|
+
event_type: "page_view",
|
|
28
|
+
referrer: document.referrer || null,
|
|
29
|
+
session_id: sessionIdRef.current
|
|
30
|
+
});
|
|
31
|
+
let scrollHandler = null;
|
|
32
|
+
if (trackScroll) {
|
|
33
|
+
scrollHandler = () => {
|
|
34
|
+
const scrollHeight = document.documentElement.scrollHeight - window.innerHeight;
|
|
35
|
+
if (scrollHeight <= 0) return;
|
|
36
|
+
const pct = Math.round(window.scrollY / scrollHeight * 100);
|
|
37
|
+
if (pct > maxScrollRef.current) maxScrollRef.current = pct;
|
|
38
|
+
};
|
|
39
|
+
window.addEventListener("scroll", scrollHandler, { passive: true });
|
|
40
|
+
}
|
|
41
|
+
const handleUnload = () => {
|
|
42
|
+
const timeOnPage = Math.round((Date.now() - startTimeRef.current) / 1e3);
|
|
43
|
+
const payload = JSON.stringify({
|
|
44
|
+
api_key: apiKey,
|
|
45
|
+
content_item_id: contentItemId,
|
|
46
|
+
content_type_slug: contentTypeSlug,
|
|
47
|
+
item_slug: itemSlug,
|
|
48
|
+
event_type: "engagement",
|
|
49
|
+
session_id: sessionIdRef.current,
|
|
50
|
+
metadata: {
|
|
51
|
+
scroll_depth: maxScrollRef.current,
|
|
52
|
+
time_on_page: timeOnPage
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
if (navigator.sendBeacon) {
|
|
56
|
+
navigator.sendBeacon(trackingUrl, payload);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
if (trackScroll || trackTime) {
|
|
60
|
+
window.addEventListener("beforeunload", handleUnload);
|
|
61
|
+
}
|
|
62
|
+
return () => {
|
|
63
|
+
if (scrollHandler) window.removeEventListener("scroll", scrollHandler);
|
|
64
|
+
if (trackScroll || trackTime) window.removeEventListener("beforeunload", handleUnload);
|
|
65
|
+
};
|
|
66
|
+
}, [trackingUrl, apiKey, contentItemId, contentTypeSlug, itemSlug, trackScroll, trackTime]);
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
function sendEvent(url, data) {
|
|
70
|
+
fetch(url, {
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: { "Content-Type": "application/json" },
|
|
73
|
+
body: JSON.stringify(data),
|
|
74
|
+
keepalive: true
|
|
75
|
+
}).catch(() => {
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function getSessionId() {
|
|
79
|
+
if (typeof window === "undefined") return "";
|
|
80
|
+
const key = "__cms_sid";
|
|
81
|
+
let sid = sessionStorage.getItem(key);
|
|
82
|
+
if (!sid) {
|
|
83
|
+
sid = Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
84
|
+
sessionStorage.setItem(key, sid);
|
|
85
|
+
}
|
|
86
|
+
return sid;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/page-tracker.tsx
|
|
90
|
+
import { useEffect as useEffect2, useRef as useRef2 } from "react";
|
|
91
|
+
import { usePathname } from "next/navigation";
|
|
92
|
+
function PageTracker({
|
|
93
|
+
trackingUrl,
|
|
94
|
+
apiKey,
|
|
95
|
+
trackScroll = true,
|
|
96
|
+
trackTime = true
|
|
97
|
+
}) {
|
|
98
|
+
const pathname = usePathname();
|
|
99
|
+
const prevPathRef = useRef2("");
|
|
100
|
+
const maxScrollRef = useRef2(0);
|
|
101
|
+
const startTimeRef = useRef2(Date.now());
|
|
102
|
+
const sessionIdRef = useRef2(getSessionId2());
|
|
103
|
+
useEffect2(() => {
|
|
104
|
+
if (pathname === prevPathRef.current) return;
|
|
105
|
+
prevPathRef.current = pathname;
|
|
106
|
+
maxScrollRef.current = 0;
|
|
107
|
+
startTimeRef.current = Date.now();
|
|
108
|
+
const segments = pathname.split("/").filter(Boolean);
|
|
109
|
+
const contentTypeSlug = segments[0] ?? null;
|
|
110
|
+
const itemSlug = segments.length >= 2 ? segments[segments.length - 1] : null;
|
|
111
|
+
sendEvent2(trackingUrl, {
|
|
112
|
+
api_key: apiKey,
|
|
113
|
+
event_type: "page_view",
|
|
114
|
+
content_type_slug: contentTypeSlug,
|
|
115
|
+
item_slug: itemSlug,
|
|
116
|
+
referrer: document.referrer || null,
|
|
117
|
+
session_id: sessionIdRef.current,
|
|
118
|
+
metadata: { path: pathname }
|
|
119
|
+
});
|
|
120
|
+
}, [pathname, trackingUrl, apiKey]);
|
|
121
|
+
useEffect2(() => {
|
|
122
|
+
if (!trackScroll) return;
|
|
123
|
+
const handleScroll = () => {
|
|
124
|
+
const scrollHeight = document.documentElement.scrollHeight - window.innerHeight;
|
|
125
|
+
if (scrollHeight <= 0) return;
|
|
126
|
+
const pct = Math.round(window.scrollY / scrollHeight * 100);
|
|
127
|
+
if (pct > maxScrollRef.current) maxScrollRef.current = pct;
|
|
128
|
+
};
|
|
129
|
+
window.addEventListener("scroll", handleScroll, { passive: true });
|
|
130
|
+
return () => window.removeEventListener("scroll", handleScroll);
|
|
131
|
+
}, [trackScroll]);
|
|
132
|
+
useEffect2(() => {
|
|
133
|
+
if (!trackScroll && !trackTime) return;
|
|
134
|
+
const handleUnload = () => {
|
|
135
|
+
const timeOnPage = Math.round((Date.now() - startTimeRef.current) / 1e3);
|
|
136
|
+
const segments = prevPathRef.current.split("/").filter(Boolean);
|
|
137
|
+
const payload = JSON.stringify({
|
|
138
|
+
api_key: apiKey,
|
|
139
|
+
event_type: "engagement",
|
|
140
|
+
content_type_slug: segments[0] ?? null,
|
|
141
|
+
item_slug: segments.length >= 2 ? segments[segments.length - 1] : null,
|
|
142
|
+
session_id: sessionIdRef.current,
|
|
143
|
+
metadata: {
|
|
144
|
+
scroll_depth: maxScrollRef.current,
|
|
145
|
+
time_on_page: timeOnPage,
|
|
146
|
+
path: prevPathRef.current
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
if (navigator.sendBeacon) {
|
|
150
|
+
navigator.sendBeacon(trackingUrl, payload);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
window.addEventListener("beforeunload", handleUnload);
|
|
154
|
+
return () => window.removeEventListener("beforeunload", handleUnload);
|
|
155
|
+
}, [trackingUrl, apiKey, trackScroll, trackTime]);
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
function sendEvent2(url, data) {
|
|
159
|
+
fetch(url, {
|
|
160
|
+
method: "POST",
|
|
161
|
+
headers: { "Content-Type": "application/json" },
|
|
162
|
+
body: JSON.stringify(data),
|
|
163
|
+
keepalive: true
|
|
164
|
+
}).catch(() => {
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
function getSessionId2() {
|
|
168
|
+
if (typeof window === "undefined") return "";
|
|
169
|
+
const key = "__cms_sid";
|
|
170
|
+
let sid = sessionStorage.getItem(key);
|
|
171
|
+
if (!sid) {
|
|
172
|
+
sid = Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
173
|
+
sessionStorage.setItem(key, sid);
|
|
174
|
+
}
|
|
175
|
+
return sid;
|
|
176
|
+
}
|
|
177
|
+
export {
|
|
178
|
+
CmsAnalytics,
|
|
179
|
+
PageTracker
|
|
180
|
+
};
|
|
181
|
+
//# sourceMappingURL=client.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/analytics.tsx","../src/page-tracker.tsx"],"sourcesContent":["\"use client\"\n\nimport { useEffect, useRef } from \"react\"\n\ninterface CmsAnalyticsProps {\n /** The CMS tracking endpoint URL (required — your CMS app URL + /api/track) */\n trackingUrl: string\n /** Tenant API key (public, read-only — safe for client-side use) */\n apiKey: string\n /** Content item ID (from CMS) */\n contentItemId?: string\n /** Content type slug (e.g. \"blog_posts\") */\n contentTypeSlug: string\n /** Item slug (e.g. \"my-post\") */\n itemSlug: string\n /** Enable scroll depth tracking (default: true) */\n trackScroll?: boolean\n /** Enable time-on-page tracking (default: true) */\n trackTime?: boolean\n}\n\n/**\n * Drop-in analytics component for CMS content pages.\n * Tracks page views, scroll depth, and time on page.\n *\n * The apiKey is a public, read-only tenant identifier — it only grants\n * access to published content and is safe for client-side use.\n *\n * Usage:\n * ```tsx\n * <CmsAnalytics\n * trackingUrl={process.env.NEXT_PUBLIC_CMS_URL + \"/api/track\"}\n * apiKey={process.env.NEXT_PUBLIC_CMS_API_KEY!}\n * contentTypeSlug=\"blog_posts\"\n * itemSlug={params.slug}\n * />\n * ```\n */\nexport function CmsAnalytics({\n trackingUrl,\n apiKey,\n contentItemId,\n contentTypeSlug,\n itemSlug,\n trackScroll = true,\n trackTime = true,\n}: CmsAnalyticsProps) {\n const sentRef = useRef(false)\n const maxScrollRef = useRef(0)\n const startTimeRef = useRef(Date.now())\n const sessionIdRef = useRef(getSessionId())\n\n useEffect(() => {\n // Only send page view once per mount\n if (sentRef.current) return\n sentRef.current = true\n\n // Page view event\n sendEvent(trackingUrl, {\n api_key: apiKey,\n content_item_id: contentItemId,\n content_type_slug: contentTypeSlug,\n item_slug: itemSlug,\n event_type: \"page_view\",\n referrer: document.referrer || null,\n session_id: sessionIdRef.current,\n })\n\n // Scroll tracking\n let scrollHandler: (() => void) | null = null\n if (trackScroll) {\n scrollHandler = () => {\n const scrollHeight = document.documentElement.scrollHeight - window.innerHeight\n if (scrollHeight <= 0) return\n const pct = Math.round((window.scrollY / scrollHeight) * 100)\n if (pct > maxScrollRef.current) maxScrollRef.current = pct\n }\n window.addEventListener(\"scroll\", scrollHandler, { passive: true })\n }\n\n // Send engagement data on page leave\n const handleUnload = () => {\n const timeOnPage = Math.round((Date.now() - startTimeRef.current) / 1000)\n const payload = JSON.stringify({\n api_key: apiKey,\n content_item_id: contentItemId,\n content_type_slug: contentTypeSlug,\n item_slug: itemSlug,\n event_type: \"engagement\",\n session_id: sessionIdRef.current,\n metadata: {\n scroll_depth: maxScrollRef.current,\n time_on_page: timeOnPage,\n },\n })\n\n // Use sendBeacon for reliable delivery on page unload\n if (navigator.sendBeacon) {\n navigator.sendBeacon(trackingUrl, payload)\n }\n }\n\n if (trackScroll || trackTime) {\n window.addEventListener(\"beforeunload\", handleUnload)\n }\n\n return () => {\n if (scrollHandler) window.removeEventListener(\"scroll\", scrollHandler)\n if (trackScroll || trackTime) window.removeEventListener(\"beforeunload\", handleUnload)\n }\n }, [trackingUrl, apiKey, contentItemId, contentTypeSlug, itemSlug, trackScroll, trackTime])\n\n return null\n}\n\nfunction sendEvent(url: string, data: Record<string, unknown>) {\n fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(data),\n keepalive: true,\n }).catch(() => {}) // fire and forget\n}\n\n/** Generate a random session ID (persists for the browser session) */\nfunction getSessionId(): string {\n if (typeof window === \"undefined\") return \"\"\n const key = \"__cms_sid\"\n let sid = sessionStorage.getItem(key)\n if (!sid) {\n sid = Math.random().toString(36).slice(2) + Date.now().toString(36)\n sessionStorage.setItem(key, sid)\n }\n return sid\n}\n","\"use client\"\n\nimport { useEffect, useRef } from \"react\"\nimport { usePathname } from \"next/navigation\"\n\ninterface PageTrackerProps {\n /** The CMS tracking endpoint URL (required — your CMS app URL + /api/track) */\n trackingUrl: string\n /** Tenant API key (public, read-only — safe for client-side use) */\n apiKey: string\n /** Enable scroll depth tracking (default: true) */\n trackScroll?: boolean\n /** Enable time-on-page tracking (default: true) */\n trackTime?: boolean\n}\n\n/**\n * Site-wide page tracker. Add once in root layout to track all pages.\n * Automatically detects CMS content pages from URL structure.\n *\n * The apiKey is a public, read-only tenant identifier — it only grants\n * access to published content and is safe for client-side use.\n *\n * Usage in src/app/layout.tsx:\n * ```tsx\n * import { PageTracker } from \"@distinctagency/cms-client\"\n *\n * <body>\n * {children}\n * <PageTracker\n * trackingUrl={process.env.NEXT_PUBLIC_CMS_URL + \"/api/track\"}\n * apiKey={process.env.NEXT_PUBLIC_CMS_API_KEY!}\n * />\n * </body>\n * ```\n */\nexport function PageTracker({\n trackingUrl,\n apiKey,\n trackScroll = true,\n trackTime = true,\n}: PageTrackerProps) {\n const pathname = usePathname()\n const prevPathRef = useRef(\"\")\n const maxScrollRef = useRef(0)\n const startTimeRef = useRef(Date.now())\n const sessionIdRef = useRef(getSessionId())\n\n useEffect(() => {\n // Skip if same path (prevents double-fire on mount)\n if (pathname === prevPathRef.current) return\n prevPathRef.current = pathname\n\n // Reset engagement tracking for new page\n maxScrollRef.current = 0\n startTimeRef.current = Date.now()\n\n // Try to extract content type and item slug from URL\n // Supports patterns like /events/future-finance, /blog/my-post, etc.\n const segments = pathname.split(\"/\").filter(Boolean)\n const contentTypeSlug = segments[0] ?? null\n const itemSlug = segments.length >= 2 ? segments[segments.length - 1] : null\n\n // Fire page view\n sendEvent(trackingUrl, {\n api_key: apiKey,\n event_type: \"page_view\",\n content_type_slug: contentTypeSlug,\n item_slug: itemSlug,\n referrer: document.referrer || null,\n session_id: sessionIdRef.current,\n metadata: { path: pathname },\n })\n }, [pathname, trackingUrl, apiKey])\n\n // Scroll tracking\n useEffect(() => {\n if (!trackScroll) return\n\n const handleScroll = () => {\n const scrollHeight = document.documentElement.scrollHeight - window.innerHeight\n if (scrollHeight <= 0) return\n const pct = Math.round((window.scrollY / scrollHeight) * 100)\n if (pct > maxScrollRef.current) maxScrollRef.current = pct\n }\n\n window.addEventListener(\"scroll\", handleScroll, { passive: true })\n return () => window.removeEventListener(\"scroll\", handleScroll)\n }, [trackScroll])\n\n // Send engagement data on page leave\n useEffect(() => {\n if (!trackScroll && !trackTime) return\n\n const handleUnload = () => {\n const timeOnPage = Math.round((Date.now() - startTimeRef.current) / 1000)\n const segments = prevPathRef.current.split(\"/\").filter(Boolean)\n\n const payload = JSON.stringify({\n api_key: apiKey,\n event_type: \"engagement\",\n content_type_slug: segments[0] ?? null,\n item_slug: segments.length >= 2 ? segments[segments.length - 1] : null,\n session_id: sessionIdRef.current,\n metadata: {\n scroll_depth: maxScrollRef.current,\n time_on_page: timeOnPage,\n path: prevPathRef.current,\n },\n })\n\n if (navigator.sendBeacon) {\n navigator.sendBeacon(trackingUrl, payload)\n }\n }\n\n window.addEventListener(\"beforeunload\", handleUnload)\n return () => window.removeEventListener(\"beforeunload\", handleUnload)\n }, [trackingUrl, apiKey, trackScroll, trackTime])\n\n return null\n}\n\nfunction sendEvent(url: string, data: Record<string, unknown>) {\n fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(data),\n keepalive: true,\n }).catch(() => {})\n}\n\nfunction getSessionId(): string {\n if (typeof window === \"undefined\") return \"\"\n const key = \"__cms_sid\"\n let sid = sessionStorage.getItem(key)\n if (!sid) {\n sid = Math.random().toString(36).slice(2) + Date.now().toString(36)\n sessionStorage.setItem(key, sid)\n }\n return sid\n}\n"],"mappings":"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","names":["useEffect","useRef","getSessionId","sendEvent"]}
|
package/dist/index.d.mts
CHANGED
|
@@ -296,73 +296,6 @@ declare function createShopClient(supabase: SupabaseClient, options: {
|
|
|
296
296
|
createOrder(params: CreateOrderParams): Promise<CreateOrderResult>;
|
|
297
297
|
};
|
|
298
298
|
|
|
299
|
-
interface CmsAnalyticsProps {
|
|
300
|
-
/** The CMS tracking endpoint URL (required — your CMS app URL + /api/track) */
|
|
301
|
-
trackingUrl: string;
|
|
302
|
-
/** Tenant API key (public, read-only — safe for client-side use) */
|
|
303
|
-
apiKey: string;
|
|
304
|
-
/** Content item ID (from CMS) */
|
|
305
|
-
contentItemId?: string;
|
|
306
|
-
/** Content type slug (e.g. "blog_posts") */
|
|
307
|
-
contentTypeSlug: string;
|
|
308
|
-
/** Item slug (e.g. "my-post") */
|
|
309
|
-
itemSlug: string;
|
|
310
|
-
/** Enable scroll depth tracking (default: true) */
|
|
311
|
-
trackScroll?: boolean;
|
|
312
|
-
/** Enable time-on-page tracking (default: true) */
|
|
313
|
-
trackTime?: boolean;
|
|
314
|
-
}
|
|
315
|
-
/**
|
|
316
|
-
* Drop-in analytics component for CMS content pages.
|
|
317
|
-
* Tracks page views, scroll depth, and time on page.
|
|
318
|
-
*
|
|
319
|
-
* The apiKey is a public, read-only tenant identifier — it only grants
|
|
320
|
-
* access to published content and is safe for client-side use.
|
|
321
|
-
*
|
|
322
|
-
* Usage:
|
|
323
|
-
* ```tsx
|
|
324
|
-
* <CmsAnalytics
|
|
325
|
-
* trackingUrl={process.env.NEXT_PUBLIC_CMS_URL + "/api/track"}
|
|
326
|
-
* apiKey={process.env.NEXT_PUBLIC_CMS_API_KEY!}
|
|
327
|
-
* contentTypeSlug="blog_posts"
|
|
328
|
-
* itemSlug={params.slug}
|
|
329
|
-
* />
|
|
330
|
-
* ```
|
|
331
|
-
*/
|
|
332
|
-
declare function CmsAnalytics({ trackingUrl, apiKey, contentItemId, contentTypeSlug, itemSlug, trackScroll, trackTime, }: CmsAnalyticsProps): null;
|
|
333
|
-
|
|
334
|
-
interface PageTrackerProps {
|
|
335
|
-
/** The CMS tracking endpoint URL (required — your CMS app URL + /api/track) */
|
|
336
|
-
trackingUrl: string;
|
|
337
|
-
/** Tenant API key (public, read-only — safe for client-side use) */
|
|
338
|
-
apiKey: string;
|
|
339
|
-
/** Enable scroll depth tracking (default: true) */
|
|
340
|
-
trackScroll?: boolean;
|
|
341
|
-
/** Enable time-on-page tracking (default: true) */
|
|
342
|
-
trackTime?: boolean;
|
|
343
|
-
}
|
|
344
|
-
/**
|
|
345
|
-
* Site-wide page tracker. Add once in root layout to track all pages.
|
|
346
|
-
* Automatically detects CMS content pages from URL structure.
|
|
347
|
-
*
|
|
348
|
-
* The apiKey is a public, read-only tenant identifier — it only grants
|
|
349
|
-
* access to published content and is safe for client-side use.
|
|
350
|
-
*
|
|
351
|
-
* Usage in src/app/layout.tsx:
|
|
352
|
-
* ```tsx
|
|
353
|
-
* import { PageTracker } from "@distinctagency/cms-client"
|
|
354
|
-
*
|
|
355
|
-
* <body>
|
|
356
|
-
* {children}
|
|
357
|
-
* <PageTracker
|
|
358
|
-
* trackingUrl={process.env.NEXT_PUBLIC_CMS_URL + "/api/track"}
|
|
359
|
-
* apiKey={process.env.NEXT_PUBLIC_CMS_API_KEY!}
|
|
360
|
-
* />
|
|
361
|
-
* </body>
|
|
362
|
-
* ```
|
|
363
|
-
*/
|
|
364
|
-
declare function PageTracker({ trackingUrl, apiKey, trackScroll, trackTime, }: PageTrackerProps): null;
|
|
365
|
-
|
|
366
299
|
/**
|
|
367
300
|
* CDN image transform helpers using Supabase Storage's built-in image transformation.
|
|
368
301
|
*
|
|
@@ -426,4 +359,4 @@ declare const IMAGE_PRESETS: {
|
|
|
426
359
|
};
|
|
427
360
|
};
|
|
428
361
|
|
|
429
|
-
export {
|
|
362
|
+
export { type CmsClientOptions, type ContentItem, type ContentQueryOptions, type ContentType, type ContentTypeSeoConfig, type CreateOrderParams, type CreateOrderResult, type FieldDefinition, type FieldType, IMAGE_PRESETS, type ImageConfig, type ImageTransformOptions, type MediaItem, type OrderAddress, type Product, type ProductOption, type ProductQueryOptions, type ProductVariant, type Profile, type Tenant, type TenantMembership, createCmsClient, createShopClient, getSrcSet, getTransformUrl };
|