keystone-design-bootstrap 1.0.53 → 1.0.54

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": "keystone-design-bootstrap",
3
- "version": "1.0.53",
3
+ "version": "1.0.54",
4
4
  "description": "Keystone Design Bootstrap - Sections, Elements, and Theme System for customer websites",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -3,7 +3,7 @@ import type { Metadata } from 'next';
3
3
 
4
4
  import { HeaderNavigation, FooterHome } from '../../design_system/sections';
5
5
  import { ThemeProvider } from '../../contexts';
6
- import { MetaPixel } from '../../tracking';
6
+ import { MetaPixel, MetaPixelTracker } from '../../tracking';
7
7
  import { ChatWidget } from '../../design_system/components/ChatWidget';
8
8
  import { FormDefinitionsProvider } from '../contexts/form-definitions';
9
9
  import { KeystoneSSRProvider } from '../providers/ssr-provider';
@@ -159,6 +159,7 @@ export async function KeystoneRootLayout(props: {
159
159
  <html lang="en" data-theme={theme}>
160
160
  <body>
161
161
  {metaPixelId ? <MetaPixel pixelId={metaPixelId} /> : null}
162
+ {metaPixelId ? <MetaPixelTracker bookingUrl={externalManagementUrl} /> : null}
162
163
  <ThemeProvider theme={theme}>
163
164
  <KeystoneSSRProvider>
164
165
  <FormDefinitionsProvider
@@ -0,0 +1,32 @@
1
+ 'use client';
2
+
3
+ import { useEffect } from 'react';
4
+ import { trackInitiateCheckout } from './trackInitiateCheckout';
5
+
6
+ type Props = {
7
+ bookingUrl: string | null | undefined;
8
+ };
9
+
10
+ /**
11
+ * Drop into the root layout to fire Meta Pixel InitiateCheckout whenever a visitor
12
+ * clicks any anchor whose href points to the external booking URL.
13
+ * Uses document-level click delegation — no changes needed to individual buttons.
14
+ */
15
+ export function BookingCtaTracker({ bookingUrl }: Props) {
16
+ useEffect(() => {
17
+ if (!bookingUrl) return;
18
+
19
+ const handleClick = (e: MouseEvent) => {
20
+ const anchor = (e.target as Element).closest('a');
21
+ if (!anchor) return;
22
+ if (anchor.href && anchor.href.startsWith(bookingUrl)) {
23
+ trackInitiateCheckout();
24
+ }
25
+ };
26
+
27
+ document.addEventListener('click', handleClick);
28
+ return () => document.removeEventListener('click', handleClick);
29
+ }, [bookingUrl]);
30
+
31
+ return null;
32
+ }
@@ -0,0 +1,91 @@
1
+ 'use client';
2
+
3
+ import { useEffect } from 'react';
4
+ import { usePathname } from 'next/navigation';
5
+ import { trackViewContent } from './trackViewContent';
6
+ import { trackInitiateCheckout } from './trackInitiateCheckout';
7
+
8
+ type RouteRule = {
9
+ pattern: RegExp;
10
+ getParams: (match: RegExpMatchArray) => { contentName: string; contentCategory: string };
11
+ };
12
+
13
+ // Checked in order — first match wins. More specific patterns come first.
14
+ const ROUTE_RULES: RouteRule[] = [
15
+ {
16
+ pattern: /^\/services\/(.+)$/,
17
+ getParams: ([, slug]) => ({ contentName: slugToTitle(slug), contentCategory: 'Service' }),
18
+ },
19
+ {
20
+ pattern: /^\/locations\/(.+)$/,
21
+ getParams: ([, slug]) => ({ contentName: slugToTitle(slug), contentCategory: 'Location' }),
22
+ },
23
+ {
24
+ pattern: /^\/services$/,
25
+ getParams: () => ({ contentName: 'Services', contentCategory: 'Services' }),
26
+ },
27
+ {
28
+ pattern: /^\/locations$/,
29
+ getParams: () => ({ contentName: 'Locations', contentCategory: 'Locations' }),
30
+ },
31
+ {
32
+ pattern: /^\/service-menu$/,
33
+ getParams: () => ({ contentName: 'Service Menu', contentCategory: 'Pricing' }),
34
+ },
35
+ {
36
+ pattern: /^\/faq$/,
37
+ getParams: () => ({ contentName: 'FAQ', contentCategory: 'FAQ' }),
38
+ },
39
+ {
40
+ pattern: /^\/contact$/,
41
+ getParams: () => ({ contentName: 'Contact', contentCategory: 'Contact' }),
42
+ },
43
+ ];
44
+
45
+ function slugToTitle(slug: string): string {
46
+ return slug
47
+ .split('-')
48
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
49
+ .join(' ');
50
+ }
51
+
52
+ type Props = {
53
+ /** External booking URL. When set, fires InitiateCheckout on any click targeting that URL. */
54
+ bookingUrl?: string | null;
55
+ };
56
+
57
+ /**
58
+ * Single client-side tracker placed once in KeystoneRootLayout.
59
+ * - Fires ViewContent on every route change for known page patterns.
60
+ * - Fires InitiateCheckout whenever a visitor clicks a link to the external booking URL.
61
+ */
62
+ export function MetaPixelTracker({ bookingUrl }: Props) {
63
+ const pathname = usePathname();
64
+
65
+ useEffect(() => {
66
+ for (const rule of ROUTE_RULES) {
67
+ const match = pathname.match(rule.pattern);
68
+ if (match) {
69
+ const { contentName, contentCategory } = rule.getParams(match);
70
+ trackViewContent(contentName, contentCategory);
71
+ break;
72
+ }
73
+ }
74
+ }, [pathname]);
75
+
76
+ useEffect(() => {
77
+ if (!bookingUrl) return;
78
+
79
+ const handleClick = (e: MouseEvent) => {
80
+ const anchor = (e.target as Element).closest('a');
81
+ if (anchor?.href?.startsWith(bookingUrl)) {
82
+ trackInitiateCheckout();
83
+ }
84
+ };
85
+
86
+ document.addEventListener('click', handleClick);
87
+ return () => document.removeEventListener('click', handleClick);
88
+ }, [bookingUrl]);
89
+
90
+ return null;
91
+ }
@@ -0,0 +1,21 @@
1
+ 'use client';
2
+
3
+ import { useEffect } from 'react';
4
+ import { trackViewContent } from './trackViewContent';
5
+
6
+ type Props = {
7
+ contentName?: string;
8
+ contentCategory?: string;
9
+ };
10
+
11
+ /**
12
+ * Drop into any server-rendered page to fire the Meta Pixel ViewContent event on mount.
13
+ * Renders nothing — purely a tracking side-effect component.
14
+ */
15
+ export function ViewContentTracker({ contentName, contentCategory }: Props) {
16
+ useEffect(() => {
17
+ trackViewContent(contentName, contentCategory);
18
+ }, [contentName, contentCategory]);
19
+
20
+ return null;
21
+ }
@@ -1,3 +1,9 @@
1
1
  export { MetaPixel } from './MetaPixel';
2
2
  export type { MetaPixelProps } from './MetaPixel';
3
3
  export { trackMetaLead } from './trackMetaLead';
4
+ export { trackViewContent } from './trackViewContent';
5
+ export { trackInitiateCheckout } from './trackInitiateCheckout';
6
+ export { MetaPixelTracker } from './MetaPixelTracker';
7
+ // Kept for custom use — MetaPixelTracker covers the standard cases automatically.
8
+ export { ViewContentTracker } from './ViewContentTracker';
9
+ export { BookingCtaTracker } from './BookingCtaTracker';
@@ -0,0 +1,11 @@
1
+ type FbqFn = (method: string, eventName: string, params?: object) => void;
2
+
3
+ /**
4
+ * Fires the client-side Meta Pixel InitiateCheckout event.
5
+ * Call this when a visitor clicks a booking / scheduling button.
6
+ */
7
+ export function trackInitiateCheckout(): void {
8
+ if (typeof window === 'undefined') return;
9
+ const fbq = (window as Window & { fbq?: FbqFn }).fbq;
10
+ if (fbq) fbq('track', 'InitiateCheckout');
11
+ }
@@ -0,0 +1,15 @@
1
+ type FbqFn = (method: string, eventName: string, params?: object) => void;
2
+
3
+ /**
4
+ * Fires the client-side Meta Pixel ViewContent event.
5
+ * Call this on service/offer detail pages so Meta knows which content is most engaging.
6
+ */
7
+ export function trackViewContent(contentName?: string, contentCategory?: string): void {
8
+ if (typeof window === 'undefined') return;
9
+ const fbq = (window as Window & { fbq?: FbqFn }).fbq;
10
+ if (!fbq) return;
11
+ const params: Record<string, string> = {};
12
+ if (contentName) params.content_name = contentName;
13
+ if (contentCategory) params.content_category = contentCategory;
14
+ fbq('track', 'ViewContent', params);
15
+ }