@m13v/seo-components 0.31.2 → 0.31.4
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 +1 -1
- package/src/components/BookCallCTA.tsx +11 -0
- package/src/lib/track.ts +35 -0
package/package.json
CHANGED
|
@@ -43,6 +43,13 @@ export interface BookCallCTAProps {
|
|
|
43
43
|
* redirected to Cal/Calendly immediately without a confirmation.
|
|
44
44
|
*/
|
|
45
45
|
successMessage?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Render a custom trigger element instead of the default button. Receives
|
|
48
|
+
* an `onClick` handler that opens the inline email form. Only honored by
|
|
49
|
+
* `appearance="hero"` (the click-to-expand variant). Other appearances are
|
|
50
|
+
* already standalone cards/forms that don't have a separable trigger.
|
|
51
|
+
*/
|
|
52
|
+
renderTrigger?: (props: { onClick: () => void; disabled?: boolean }) => React.ReactNode;
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
export function BookCallCTA(props: BookCallCTAProps) {
|
|
@@ -281,6 +288,7 @@ function HeroBookCall({
|
|
|
281
288
|
endpoint = "/api/book-call",
|
|
282
289
|
emailPlaceholder = "you@example.com",
|
|
283
290
|
submitLabel,
|
|
291
|
+
renderTrigger,
|
|
284
292
|
}: BookCallCTAProps & {
|
|
285
293
|
text: string;
|
|
286
294
|
section: string;
|
|
@@ -296,6 +304,9 @@ function HeroBookCall({
|
|
|
296
304
|
});
|
|
297
305
|
|
|
298
306
|
if (!open && gate.status !== "success") {
|
|
307
|
+
if (renderTrigger) {
|
|
308
|
+
return <>{renderTrigger({ onClick: () => setOpen(true) })}</>;
|
|
309
|
+
}
|
|
299
310
|
return (
|
|
300
311
|
<button
|
|
301
312
|
type="button"
|
package/src/lib/track.ts
CHANGED
|
@@ -8,6 +8,38 @@
|
|
|
8
8
|
|
|
9
9
|
import { captureFromWindow } from "./analytics-context";
|
|
10
10
|
|
|
11
|
+
// Rage-click dedup window. A real visitor revisiting the page later still
|
|
12
|
+
// records; only same-event+same-destination+same-page firings within this
|
|
13
|
+
// window collapse to one. 2026-05-03: claude-meter.com had 11 reported
|
|
14
|
+
// `get_started_click` events in 24h that were actually 2 unique users
|
|
15
|
+
// rage-mashing the install button (5-6 clicks within 4-14s each). Dedup
|
|
16
|
+
// at the helper layer catches every callsite (GetStartedCTA, InlineCta,
|
|
17
|
+
// StickyBottomCta, InstallEmailGate, ClaudeMeterCta, BookCallCTA,
|
|
18
|
+
// SiteNavbar, and any custom caller in consumer sites).
|
|
19
|
+
const _DEDUP_WINDOW_MS = 1500;
|
|
20
|
+
const _lastFire = new Map<string, number>();
|
|
21
|
+
|
|
22
|
+
function _shouldSuppressRecent(key: string, windowMs = _DEDUP_WINDOW_MS): boolean {
|
|
23
|
+
if (typeof window === "undefined") return false;
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
const last = _lastFire.get(key);
|
|
26
|
+
if (last != null && now - last < windowMs) return true;
|
|
27
|
+
_lastFire.set(key, now);
|
|
28
|
+
// Keep the map from growing unbounded on long-lived SPAs.
|
|
29
|
+
if (_lastFire.size > 256) {
|
|
30
|
+
const cutoff = now - 60_000;
|
|
31
|
+
for (const [k, t] of _lastFire) {
|
|
32
|
+
if (t < cutoff) _lastFire.delete(k);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function _dedupKey(event: string, destination: string | undefined): string {
|
|
39
|
+
const page = typeof window !== "undefined" ? window.location.pathname : "";
|
|
40
|
+
return event + "::" + (destination || "") + "::" + page;
|
|
41
|
+
}
|
|
42
|
+
|
|
11
43
|
/**
|
|
12
44
|
* Rewrite a booking URL (Cal.com, Calendly, etc.) so the booking webhook can
|
|
13
45
|
* attribute the booking back to the specific landing page it came from.
|
|
@@ -81,6 +113,7 @@ export interface ScheduleClickProps {
|
|
|
81
113
|
*/
|
|
82
114
|
export function trackScheduleClick(props: ScheduleClickProps): void {
|
|
83
115
|
const { destination, site, section, text, component, extra } = props;
|
|
116
|
+
if (_shouldSuppressRecent(_dedupKey("schedule_click", destination))) return;
|
|
84
117
|
captureFromWindow("schedule_click", {
|
|
85
118
|
...(extra || {}),
|
|
86
119
|
destination,
|
|
@@ -119,6 +152,7 @@ export interface GetStartedClickProps {
|
|
|
119
152
|
*/
|
|
120
153
|
export function trackGetStartedClick(props: GetStartedClickProps): void {
|
|
121
154
|
const { destination, site, section, text, component, extra } = props;
|
|
155
|
+
if (_shouldSuppressRecent(_dedupKey("get_started_click", destination))) return;
|
|
122
156
|
captureFromWindow("get_started_click", {
|
|
123
157
|
...(extra || {}),
|
|
124
158
|
destination,
|
|
@@ -176,6 +210,7 @@ export interface CrossProductClickProps {
|
|
|
176
210
|
*/
|
|
177
211
|
export function trackCrossProductClick(props: CrossProductClickProps): void {
|
|
178
212
|
const { destination, site, targetProduct, section, text, component, extra } = props;
|
|
213
|
+
if (_shouldSuppressRecent(_dedupKey("cross_product_click", destination))) return;
|
|
179
214
|
captureFromWindow("cross_product_click", {
|
|
180
215
|
...(extra || {}),
|
|
181
216
|
destination,
|