@distinctagency/cms-client 1.1.2 → 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.
@@ -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 };
@@ -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"]}
@@ -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 { CmsAnalytics, 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, PageTracker, type Product, type ProductOption, type ProductQueryOptions, type ProductVariant, type Profile, type Tenant, type TenantMembership, createCmsClient, createShopClient, getSrcSet, getTransformUrl };
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 };