@jmp-technologies/analytics 0.1.17 → 0.1.19

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.
Files changed (40) hide show
  1. package/README.md +3 -1
  2. package/dist/{chunk-3ELQETAA.js → chunk-EPNF76VK.js} +2 -2
  3. package/dist/{chunk-MUHLIN7P.js → chunk-GB37RVJJ.js} +339 -103
  4. package/dist/chunk-GB37RVJJ.js.map +1 -0
  5. package/dist/index.cjs +348 -104
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +112 -35
  8. package/dist/index.d.ts +112 -35
  9. package/dist/index.js +21 -5
  10. package/dist/next/content.cjs +322 -0
  11. package/dist/next/content.cjs.map +1 -0
  12. package/dist/next/content.d.cts +37 -0
  13. package/dist/next/content.d.ts +37 -0
  14. package/dist/next/content.js +172 -0
  15. package/dist/next/content.js.map +1 -0
  16. package/dist/next/instrumentation.cjs +131 -0
  17. package/dist/next/instrumentation.cjs.map +1 -0
  18. package/dist/next/instrumentation.d.cts +7 -0
  19. package/dist/next/instrumentation.d.ts +7 -0
  20. package/dist/next/instrumentation.js +18 -0
  21. package/dist/next/instrumentation.js.map +1 -0
  22. package/dist/next/middleware.cjs +147 -0
  23. package/dist/next/middleware.cjs.map +1 -0
  24. package/dist/next/middleware.d.cts +13 -0
  25. package/dist/next/middleware.d.ts +13 -0
  26. package/dist/next/middleware.js +22 -0
  27. package/dist/next/middleware.js.map +1 -0
  28. package/dist/next.cjs +322 -86
  29. package/dist/next.cjs.map +1 -1
  30. package/dist/next.d.cts +24 -8
  31. package/dist/next.d.ts +24 -8
  32. package/dist/next.js +41 -13
  33. package/dist/next.js.map +1 -1
  34. package/dist/react.cjs +279 -69
  35. package/dist/react.cjs.map +1 -1
  36. package/dist/react.js +2 -2
  37. package/docs/legal-and-privacy.md +1 -0
  38. package/package.json +17 -2
  39. package/dist/chunk-MUHLIN7P.js.map +0 -1
  40. /package/dist/{chunk-3ELQETAA.js.map → chunk-EPNF76VK.js.map} +0 -0
package/dist/index.d.ts CHANGED
@@ -1,19 +1,95 @@
1
- type EarlyPageViewBeaconArgs = {
2
- baseUrl: string;
3
- trackingKey: string;
1
+ /**
2
+ * Classify outbound contact links for auto-tracking.
3
+ *
4
+ * Keep channel set in sync with `lib/ingest/contact-click.ts` on the dashboard side.
5
+ *
6
+ * Channels detected from `href` alone (no widget / iframe detection):
7
+ *
8
+ * - `call` — `tel:` (delegates to existing `trackCallClick`)
9
+ * - `email` — `mailto:` (delegates to existing `trackEmailClick`)
10
+ * - `sms` — `sms:` / `smsto:`
11
+ * - `whatsapp` — `wa.me`, `api.whatsapp.com`, `whatsapp.com/send`
12
+ * - `maps` — Google Maps, Apple Maps, `maps.app.goo.gl`, `geo:`
13
+ * - `booking` — fixed allowlist of common scheduling hosts (see {@link BOOKING_LINK_HOSTS})
14
+ *
15
+ * `linkHost` returned in `contact_click` results is the hostname only — never the full URL
16
+ * (avoids leaking phone numbers in `tel:`/`sms:` URLs and PII-laden booking query strings).
17
+ */
18
+ type ContactLinkChannel = "sms" | "whatsapp" | "maps" | "booking";
19
+ type ClassifiedContactLink = {
20
+ kind: "call";
21
+ } | {
22
+ kind: "email";
23
+ } | {
24
+ kind: "contact_click";
25
+ channel: ContactLinkChannel;
26
+ linkHost: string | null;
27
+ };
28
+ /** v1 booking hosts (~80%+ SMB coverage). Add hosts here when a client uses something rare. */
29
+ declare const BOOKING_LINK_HOSTS: readonly ["calendly.com", "cal.com", "acuityscheduling.com", "calendar.google.com", "calendar.app.google", "bookings.microsoft.com", "outlook.office.com", "outlook.office365.com", "book.ms", "setmore.com", "simplybook.me", "oncehub.com", "scheduleonce.com", "youcanbook.me", "savvycal.com", "tidycal.com", "zohobookings.com", "square.site", "squareup.com", "meetings.hubspot.com", "jobber.com", "housecallpro.com", "servicetitan.com", "thryv.com", "honeybook.com", "dubsado.com", "fresha.com", "booksy.com", "vagaro.com", "mindbodyonline.com", "mindbody.io", "janeapp.com", "cliniko.com", "practicebetter.io"];
30
+ /** Returns `null` when the link should not be tracked as a contact link. */
31
+ declare function classifyContactHref(href: string): ClassifiedContactLink | null;
32
+
33
+ /** Inline script for Next.js `beforeInteractive` — capture referrer/UTM only (no network). */
34
+ declare function buildLandingCaptureInlineScript(): string;
35
+
36
+ /** Per-site values used when the dashboard profile is missing or fields are empty. */
37
+ type JmpSiteContactFallback = {
38
+ displayName?: string;
39
+ founderName?: string;
40
+ email?: string;
41
+ phone?: string | null;
42
+ linkedin?: string | null;
43
+ hours?: string | null;
44
+ serviceArea?: string | null;
45
+ dashboardDemoUrl?: string;
46
+ profilePhotoSrc?: string;
47
+ privacyUrl?: string;
48
+ termsUrl?: string;
49
+ };
50
+ type JmpSiteDefinition = {
51
+ /** Content tabs this site uses — reported via instrumentation capabilities. */
52
+ contentWrappers: JmpContentWrapper[];
53
+ /**
54
+ * Public site origin (no trailing slash), e.g. `https://example.com`.
55
+ * Used to resolve relative profile image paths in structured data.
56
+ */
57
+ siteUrl?: string;
58
+ contactFallback?: JmpSiteContactFallback;
59
+ /**
60
+ * `extras` keys to read from the dashboard site profile (hyphenated keys from admin).
61
+ */
62
+ contactExtraKeys?: {
63
+ founderName?: readonly string[];
64
+ dashboardDemoUrl?: readonly string[];
65
+ profilePhotoSrc?: readonly string[];
66
+ hours?: readonly string[];
67
+ serviceArea?: readonly string[];
68
+ };
4
69
  };
5
70
  /**
6
- * Capture landing referrer/UTMs and send an early page_view (fetch keepalive).
7
- * Safe to call from inline script or module init.
71
+ * Register site-specific JMP integration options once (typically from `jmp.config.ts`).
72
+ * Call at module load time before server content or instrumentation runs.
8
73
  */
9
- declare function sendEarlyPageViewBeacon(args: EarlyPageViewBeaconArgs): boolean;
10
- /** Inline script for Next.js `beforeInteractive` — earliest capture + beacon. */
11
- declare function buildLandingCaptureInlineScript(args: {
12
- baseUrl: string;
13
- trackingKey: string;
14
- }): string;
74
+ declare function defineJmpSite(definition: JmpSiteDefinition): JmpSiteDefinition;
75
+ declare function getJmpSiteDefinition(): JmpSiteDefinition | null;
76
+ /** Server content + lead config: env vars + wrappers from {@link defineJmpSite}. */
77
+ declare function getJmpContentConfig(): JmpContentConfig | null;
15
78
 
16
- type JmpEventType = "page_view" | "form_submit" | "call_click" | "email_click";
79
+ /** Published package version sent with capabilities for admin readiness. */
80
+ declare const JMP_ANALYTICS_SDK_VERSION = "0.1.19";
81
+ /** `localStorage` key when {@link JmpAnalyticsConfig.requireConsent} is enabled. */
82
+ declare const JMP_TRACKING_CONSENT_STORAGE_KEY = "jmp_analytics_consent_v1";
83
+ /** Whether analytics events may be sent (after CMP / cookie banner). */
84
+ declare function isJmpTrackingConsentGranted(): boolean;
85
+ /** Subscribe to consent changes (for React `useSyncExternalStore`). */
86
+ declare function subscribeJmpTrackingConsent(listener: () => void): () => void;
87
+ /**
88
+ * Grant or deny analytics. Persists in `localStorage` when `requireConsent` is used.
89
+ * Call from your cookie banner before tracking runs.
90
+ */
91
+ declare function setJmpTrackingConsent(granted: boolean): void;
92
+ type JmpEventType = "page_view" | "form_submit" | "call_click" | "email_click" | "contact_click";
17
93
  type JmpAnalyticsConfig = {
18
94
  /** Dashboard origin, e.g. https://analytics.example.com (no trailing slash) */
19
95
  baseUrl: string;
@@ -28,6 +104,21 @@ type JmpAnalyticsConfig = {
28
104
  * Helps with React Strict Mode double-mount; typical: 500–1000.
29
105
  */
30
106
  pageViewDedupeMs?: number;
107
+ /**
108
+ * When true, no events are sent until {@link setJmpTrackingConsent}(true).
109
+ * Pair with your cookie banner; see `NEXT_PUBLIC_JMP_ANALYTICS_REQUIRE_CONSENT`.
110
+ */
111
+ requireConsent?: boolean;
112
+ /**
113
+ * When true, register a single delegated `click` listener that classifies links
114
+ * (SMS, WhatsApp, maps, scheduling) and records them via {@link trackContactClick}.
115
+ *
116
+ * Off by default — pair with privacy disclosure / consent before enabling.
117
+ * `tel:` / `mailto:` continue to use {@link trackCallClick} / {@link trackEmailClick}.
118
+ *
119
+ * Opt out per-link with `data-jmp-no-track` on an `<a>` (or an ancestor).
120
+ */
121
+ autoTrackContactLinks?: boolean;
31
122
  };
32
123
  type JmpContentConfig = Pick<JmpAnalyticsConfig, "baseUrl" | "trackingKey" | "debug" | "fetchImpl"> & {
33
124
  /**
@@ -36,15 +127,9 @@ type JmpContentConfig = Pick<JmpAnalyticsConfig, "baseUrl" | "trackingKey" | "de
36
127
  * Call {@link registerJmpContentWrappers} at startup / deploy.
37
128
  */
38
129
  contentWrappers?: JmpContentWrapper[];
39
- /**
40
- * @deprecated Use `contentWrappers` (e.g. `trust.testimonials` instead of `testimonials`).
41
- */
42
- trustSlots?: JmpTrustSlot[];
43
130
  };
44
131
  /** Top-level and trust sub-wrappers the client can declare. */
45
132
  type JmpContentWrapper = "blog" | "faq" | "contact" | "trust" | "trust.summary" | "trust.testimonials" | "trust.reviews" | "trust.stats" | "trust.logos" | "trust.credentials";
46
- /** @deprecated Trust-only shorthand; prefer `JmpContentWrapper`. */
47
- type JmpTrustSlot = "summary" | "testimonials" | "reviews" | "stats" | "logos" | "credentials";
48
133
  type JmpBlogPost = {
49
134
  id: string;
50
135
  title: string;
@@ -241,8 +326,16 @@ declare function createJmpAnalytics(config: JmpAnalyticsConfig): {
241
326
  trackFormSubmit: (path: string, metadata?: Record<string, unknown>) => Promise<void>;
242
327
  trackCallClick: (path: string, metadata?: Record<string, unknown>) => Promise<void>;
243
328
  trackEmailClick: (path: string, metadata?: Record<string, unknown>) => Promise<void>;
329
+ trackContactClick: (path: string, options: {
330
+ channel: ContactLinkChannel;
331
+ linkHost?: string | null;
332
+ metadata?: Record<string, unknown>;
333
+ }) => Promise<void>;
244
334
  trackLead: (options?: TrackLeadOptions) => Promise<void>;
245
335
  subscribeToSpaNavigation: () => () => void;
336
+ subscribeContactLinkAutoTrack: () => () => void;
337
+ setTrackingConsent: typeof setJmpTrackingConsent;
338
+ isTrackingConsentGranted: typeof isJmpTrackingConsentGranted;
246
339
  };
247
340
  type JmpAnalytics = ReturnType<typeof createJmpAnalytics>;
248
341
  declare function trackJmpServerPageView(config: JmpContentConfig, options?: TrackServerPageViewOptions): Promise<void>;
@@ -263,14 +356,6 @@ declare function registerJmpContentWrappers(config: JmpContentConfig): Promise<{
263
356
  ok: false;
264
357
  skipped: true;
265
358
  }>;
266
- /** @deprecated Use {@link registerJmpContentWrappers} with `contentWrappers`. */
267
- declare function registerJmpTrustCapabilities(config: JmpContentConfig): Promise<{
268
- ok: true;
269
- slots: JmpTrustSlot[];
270
- } | {
271
- ok: false;
272
- skipped: true;
273
- }>;
274
359
  declare function createJmpContentClient(config: JmpContentConfig): {
275
360
  getBlogPosts: () => Promise<JmpBlogPost[]>;
276
361
  getBlogPost: (slug: string) => Promise<JmpBlogPost | null>;
@@ -285,14 +370,6 @@ declare function createJmpContentClient(config: JmpContentConfig): {
285
370
  ok: false;
286
371
  skipped: true;
287
372
  }>;
288
- /** @deprecated Use registerContentWrappers */
289
- registerTrustCapabilities: () => Promise<{
290
- ok: true;
291
- slots: JmpTrustSlot[];
292
- } | {
293
- ok: false;
294
- skipped: true;
295
- }>;
296
373
  };
297
374
  /** Default `pageViewDedupeMs` when resolving browser config from env (Strict Mode / SPA). */
298
375
  declare const DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS = 800;
@@ -321,4 +398,4 @@ type JmpContentClient = ReturnType<typeof createJmpContentClient>;
321
398
  */
322
399
  declare function createJmpContentClientFromServerEnv(overrides?: Partial<JmpContentConfig>): JmpContentClient | null;
323
400
 
324
- export { DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS, type JmpAnalytics, type JmpAnalyticsConfig, type JmpBlogPost, type JmpContentClient, type JmpContentConfig, type JmpContentWrapper, type JmpCredential, type JmpEventType, type JmpFaqItem, type JmpPartnerLogo, type JmpServerRequestLike, type JmpSiteProfile, type JmpTestimonial, type JmpTrustBundle, type JmpTrustSlot, type JmpTrustStat, type JmpTrustSummary, type LeadMetadata, type LeadMetadataValue, type TrackLeadOptions, type TrackPageViewOptions, type TrackServerPageViewOptions, buildLandingCaptureInlineScript, createJmpAnalytics, createJmpContentClient, createJmpContentClientFromServerEnv, getJmpBlogPost, getJmpBlogPosts, getJmpBrowserAnalyticsConfigFromEnv, getJmpFaqs, getJmpRobotsTxt, getJmpServerContentConfigFromEnv, getJmpSiteProfile, getJmpTrustBundle, getOrCreateJmpAnalyticsFromBrowserEnv, isJmpBrowserAnalyticsEnvConfigured, isJmpServerContentEnvConfigured, registerJmpContentWrappers, registerJmpTrustCapabilities, sendEarlyPageViewBeacon, trackJmpServerPageView };
401
+ export { BOOKING_LINK_HOSTS, type ClassifiedContactLink, type ContactLinkChannel, DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS, JMP_ANALYTICS_SDK_VERSION, JMP_TRACKING_CONSENT_STORAGE_KEY, type JmpAnalytics, type JmpAnalyticsConfig, type JmpBlogPost, type JmpContentClient, type JmpContentConfig, type JmpContentWrapper, type JmpCredential, type JmpEventType, type JmpFaqItem, type JmpPartnerLogo, type JmpServerRequestLike, type JmpSiteContactFallback, type JmpSiteDefinition, type JmpSiteProfile, type JmpTestimonial, type JmpTrustBundle, type JmpTrustStat, type JmpTrustSummary, type LeadMetadata, type LeadMetadataValue, type TrackLeadOptions, type TrackPageViewOptions, type TrackServerPageViewOptions, buildLandingCaptureInlineScript, classifyContactHref, createJmpAnalytics, createJmpContentClient, createJmpContentClientFromServerEnv, defineJmpSite, getJmpBlogPost, getJmpBlogPosts, getJmpBrowserAnalyticsConfigFromEnv, getJmpContentConfig, getJmpFaqs, getJmpRobotsTxt, getJmpServerContentConfigFromEnv, getJmpSiteDefinition, getJmpSiteProfile, getJmpTrustBundle, getOrCreateJmpAnalyticsFromBrowserEnv, isJmpBrowserAnalyticsEnvConfigured, isJmpServerContentEnvConfigured, isJmpTrackingConsentGranted, registerJmpContentWrappers, setJmpTrackingConsent, subscribeJmpTrackingConsent, trackJmpServerPageView };
package/dist/index.js CHANGED
@@ -1,45 +1,61 @@
1
1
  import {
2
+ BOOKING_LINK_HOSTS,
2
3
  DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS,
4
+ JMP_ANALYTICS_SDK_VERSION,
5
+ JMP_TRACKING_CONSENT_STORAGE_KEY,
3
6
  buildLandingCaptureInlineScript,
7
+ classifyContactHref,
4
8
  createJmpAnalytics,
5
9
  createJmpContentClient,
6
10
  createJmpContentClientFromServerEnv,
11
+ defineJmpSite,
7
12
  getJmpBlogPost,
8
13
  getJmpBlogPosts,
9
14
  getJmpBrowserAnalyticsConfigFromEnv,
15
+ getJmpContentConfig,
10
16
  getJmpFaqs,
11
17
  getJmpRobotsTxt,
12
18
  getJmpServerContentConfigFromEnv,
19
+ getJmpSiteDefinition,
13
20
  getJmpSiteProfile,
14
21
  getJmpTrustBundle,
15
22
  getOrCreateJmpAnalyticsFromBrowserEnv,
16
23
  isJmpBrowserAnalyticsEnvConfigured,
17
24
  isJmpServerContentEnvConfigured,
25
+ isJmpTrackingConsentGranted,
18
26
  registerJmpContentWrappers,
19
- registerJmpTrustCapabilities,
20
- sendEarlyPageViewBeacon,
27
+ setJmpTrackingConsent,
28
+ subscribeJmpTrackingConsent,
21
29
  trackJmpServerPageView
22
- } from "./chunk-MUHLIN7P.js";
30
+ } from "./chunk-GB37RVJJ.js";
23
31
  export {
32
+ BOOKING_LINK_HOSTS,
24
33
  DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS,
34
+ JMP_ANALYTICS_SDK_VERSION,
35
+ JMP_TRACKING_CONSENT_STORAGE_KEY,
25
36
  buildLandingCaptureInlineScript,
37
+ classifyContactHref,
26
38
  createJmpAnalytics,
27
39
  createJmpContentClient,
28
40
  createJmpContentClientFromServerEnv,
41
+ defineJmpSite,
29
42
  getJmpBlogPost,
30
43
  getJmpBlogPosts,
31
44
  getJmpBrowserAnalyticsConfigFromEnv,
45
+ getJmpContentConfig,
32
46
  getJmpFaqs,
33
47
  getJmpRobotsTxt,
34
48
  getJmpServerContentConfigFromEnv,
49
+ getJmpSiteDefinition,
35
50
  getJmpSiteProfile,
36
51
  getJmpTrustBundle,
37
52
  getOrCreateJmpAnalyticsFromBrowserEnv,
38
53
  isJmpBrowserAnalyticsEnvConfigured,
39
54
  isJmpServerContentEnvConfigured,
55
+ isJmpTrackingConsentGranted,
40
56
  registerJmpContentWrappers,
41
- registerJmpTrustCapabilities,
42
- sendEarlyPageViewBeacon,
57
+ setJmpTrackingConsent,
58
+ subscribeJmpTrackingConsent,
43
59
  trackJmpServerPageView
44
60
  };
45
61
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,322 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/next/content.ts
21
+ var content_exports = {};
22
+ __export(content_exports, {
23
+ absoluteSiteAssetUrl: () => absoluteSiteAssetUrl,
24
+ filterFeaturedTrustQuotes: () => filterFeaturedTrustQuotes,
25
+ getJmpContentConfig: () => getJmpContentConfig,
26
+ getResolvedSiteContact: () => getResolvedSiteContact,
27
+ loadBlogPost: () => loadBlogPost,
28
+ loadBlogPosts: () => loadBlogPosts,
29
+ loadFaqBySlug: () => loadFaqBySlug,
30
+ loadFaqs: () => loadFaqs,
31
+ loadSiteProfile: () => loadSiteProfile,
32
+ loadTrustBundle: () => loadTrustBundle,
33
+ resolveSiteContact: () => resolveSiteContact
34
+ });
35
+ module.exports = __toCommonJS(content_exports);
36
+ var import_react = require("react");
37
+
38
+ // src/landing-capture.ts
39
+ var LANDING_REFERER_COOKIE_MAX_AGE_SEC = 30 * 60;
40
+
41
+ // src/site-config.ts
42
+ var registeredSite = null;
43
+ function readNextPublicEnv(key) {
44
+ if (typeof process === "undefined" || !process.env) return void 0;
45
+ const v = key === "NEXT_PUBLIC_JMP_ANALYTICS_URL" ? process.env.NEXT_PUBLIC_JMP_ANALYTICS_URL : process.env.NEXT_PUBLIC_JMP_TRACKING_KEY;
46
+ return typeof v === "string" && v.length > 0 ? v : void 0;
47
+ }
48
+ function getServerConfigFromEnv() {
49
+ const baseUrl = readNextPublicEnv("NEXT_PUBLIC_JMP_ANALYTICS_URL");
50
+ const trackingKey = readNextPublicEnv("NEXT_PUBLIC_JMP_TRACKING_KEY");
51
+ if (!baseUrl || !trackingKey) return null;
52
+ return { baseUrl, trackingKey };
53
+ }
54
+ function getJmpSiteDefinition() {
55
+ return registeredSite;
56
+ }
57
+ function getJmpContentConfig() {
58
+ const fromEnv = getServerConfigFromEnv();
59
+ if (!fromEnv) {
60
+ return null;
61
+ }
62
+ const site = getJmpSiteDefinition();
63
+ return {
64
+ ...fromEnv,
65
+ contentWrappers: site?.contentWrappers ?? fromEnv.contentWrappers
66
+ };
67
+ }
68
+
69
+ // src/index.ts
70
+ function responseLooksLikeHtml(body) {
71
+ const s = body.trimStart();
72
+ return s.startsWith("<!") || s.toLowerCase().startsWith("<html") || s.startsWith("<") && /<html[\s>]/i.test(s.slice(0, 500));
73
+ }
74
+ function normalizeBaseUrl(baseUrl) {
75
+ return baseUrl.replace(/\/$/, "");
76
+ }
77
+ async function getJmpBlogPosts(config) {
78
+ const data = await getContent(
79
+ config,
80
+ "/api/content/blog"
81
+ );
82
+ return data.posts;
83
+ }
84
+ async function getJmpBlogPost(config, slug) {
85
+ try {
86
+ const data = await getContent(
87
+ config,
88
+ `/api/content/blog/${encodeURIComponent(slug)}`
89
+ );
90
+ return data.post;
91
+ } catch (error) {
92
+ if (error instanceof Error && error.message.includes("HTTP 404")) {
93
+ return null;
94
+ }
95
+ throw error;
96
+ }
97
+ }
98
+ async function getJmpFaqs(config) {
99
+ const data = await getContent(
100
+ config,
101
+ "/api/content/faqs"
102
+ );
103
+ return data.faqs;
104
+ }
105
+ async function getJmpTrustBundle(config) {
106
+ return getContent(config, "/api/content/trust");
107
+ }
108
+ async function getJmpSiteProfile(config) {
109
+ const data = await getContent(
110
+ config,
111
+ "/api/content/site"
112
+ );
113
+ return data.site;
114
+ }
115
+ async function getContent(config, path) {
116
+ const text = await getContentText(config, path);
117
+ try {
118
+ return JSON.parse(text);
119
+ } catch {
120
+ const baseUrl = normalizeBaseUrl(config.baseUrl);
121
+ const url = `${baseUrl}${path}`;
122
+ throw new Error(`JMP content HTTP 200 \u2014 ${url} returned invalid JSON.`);
123
+ }
124
+ }
125
+ async function getContentText(config, path) {
126
+ const baseUrl = normalizeBaseUrl(config.baseUrl);
127
+ const resolvedFetch = config.fetchImpl ?? (typeof globalThis !== "undefined" && globalThis.fetch ? globalThis.fetch.bind(globalThis) : void 0);
128
+ if (!resolvedFetch) {
129
+ throw new Error("JMP content: fetch is not available; pass fetchImpl in config");
130
+ }
131
+ const url = `${baseUrl}${path}`;
132
+ if (config.debug) {
133
+ console.debug("[@jmp-technologies/analytics]", "GET", url);
134
+ }
135
+ const res = await resolvedFetch(url, {
136
+ method: "GET",
137
+ headers: {
138
+ "X-JMP-Tracking-Key": config.trackingKey
139
+ }
140
+ });
141
+ const text = await res.text();
142
+ if (!res.ok) {
143
+ if (responseLooksLikeHtml(text)) {
144
+ throw new Error(
145
+ `JMP content HTTP ${res.status} \u2014 ${url} returned HTML (not the JSON API). Set baseUrl to your JMP Analytics dashboard origin.`
146
+ );
147
+ }
148
+ let parsed = {};
149
+ try {
150
+ parsed = JSON.parse(text);
151
+ } catch {
152
+ parsed = { error: text };
153
+ }
154
+ throw new Error(
155
+ [`JMP content HTTP ${res.status}`, parsed.error, parsed.hint].filter(Boolean).join(" \u2014 ")
156
+ );
157
+ }
158
+ return text;
159
+ }
160
+
161
+ // src/next/content.ts
162
+ var DEFAULT_EXTRA_KEYS = {
163
+ founderName: ["founder-name", "founder_name"],
164
+ dashboardDemoUrl: ["dashboard-demo-url", "sample-dashboard-url"],
165
+ profilePhotoSrc: ["profile-photo-url", "profile_photo_url"],
166
+ hours: ["hours", "business-hours"],
167
+ serviceArea: ["service-area", "service_area", "service-area-text"]
168
+ };
169
+ function sortBlogPosts(posts) {
170
+ return [...posts].sort((a, b) => {
171
+ const da = a.published_at ?? a.updated_at;
172
+ const db = b.published_at ?? b.updated_at;
173
+ return db.localeCompare(da);
174
+ });
175
+ }
176
+ function sortFaqs(items) {
177
+ return [...items].sort((a, b) => a.sort_order - b.sort_order);
178
+ }
179
+ function readProfileExtra(profile, keys) {
180
+ for (const key of keys) {
181
+ const value = profile?.extras?.[key]?.trim();
182
+ if (value) {
183
+ return value;
184
+ }
185
+ }
186
+ return null;
187
+ }
188
+ function absoluteSiteAssetUrl(src) {
189
+ const trimmed = src.trim();
190
+ if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
191
+ return trimmed;
192
+ }
193
+ const siteUrl = getJmpSiteDefinition()?.siteUrl?.replace(/\/$/, "");
194
+ if (!siteUrl) {
195
+ return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
196
+ }
197
+ return `${siteUrl}${trimmed.startsWith("/") ? trimmed : `/${trimmed}`}`;
198
+ }
199
+ var loadSiteProfile = (0, import_react.cache)(async () => {
200
+ const config = getJmpContentConfig();
201
+ if (!config) {
202
+ return null;
203
+ }
204
+ try {
205
+ return await getJmpSiteProfile(config);
206
+ } catch {
207
+ return null;
208
+ }
209
+ });
210
+ function resolveSiteContact(profile) {
211
+ const site = getJmpSiteDefinition();
212
+ const fallback = site?.contactFallback ?? {};
213
+ const extraKeys = {
214
+ ...DEFAULT_EXTRA_KEYS,
215
+ ...site?.contactExtraKeys
216
+ };
217
+ const email = profile?.email?.trim() || fallback.email || "";
218
+ const linkedin = profile?.social?.linkedin?.trim() || fallback.linkedin || null;
219
+ const phone = profile?.phone?.trim() || fallback.phone || null;
220
+ return {
221
+ displayName: profile?.display_name?.trim() || profile?.name?.trim() || fallback.displayName || "",
222
+ founderName: readProfileExtra(profile, extraKeys.founderName ?? []) || fallback.founderName || "",
223
+ dashboardDemoUrl: readProfileExtra(profile, extraKeys.dashboardDemoUrl ?? []) || fallback.dashboardDemoUrl || "",
224
+ profilePhotoSrc: readProfileExtra(profile, extraKeys.profilePhotoSrc ?? []) || fallback.profilePhotoSrc || "",
225
+ email,
226
+ linkedin,
227
+ phone,
228
+ hours: readProfileExtra(profile, extraKeys.hours ?? []) || fallback.hours || null,
229
+ serviceArea: readProfileExtra(profile, extraKeys.serviceArea ?? []) || fallback.serviceArea || null,
230
+ privacyUrl: profile?.legal?.privacy_policy_url?.trim() || fallback.privacyUrl || "/privacy",
231
+ termsUrl: profile?.legal?.terms_url?.trim() || fallback.termsUrl || "/terms",
232
+ cta: {
233
+ label: profile?.cta?.label?.trim() || null,
234
+ url: profile?.cta?.url?.trim() || null
235
+ }
236
+ };
237
+ }
238
+ async function getResolvedSiteContact() {
239
+ const profile = await loadSiteProfile();
240
+ return resolveSiteContact(profile);
241
+ }
242
+ var loadBlogPosts = (0, import_react.cache)(async () => {
243
+ const c = getJmpContentConfig();
244
+ if (!c) {
245
+ return [];
246
+ }
247
+ try {
248
+ const posts = await getJmpBlogPosts(c);
249
+ return sortBlogPosts(posts);
250
+ } catch {
251
+ return [];
252
+ }
253
+ });
254
+ var loadBlogPost = (0, import_react.cache)(async (slug) => {
255
+ const c = getJmpContentConfig();
256
+ if (!c) {
257
+ return null;
258
+ }
259
+ try {
260
+ return await getJmpBlogPost(c, slug);
261
+ } catch {
262
+ return null;
263
+ }
264
+ });
265
+ var loadFaqs = (0, import_react.cache)(async () => {
266
+ const c = getJmpContentConfig();
267
+ if (!c) {
268
+ return [];
269
+ }
270
+ try {
271
+ const faqs = await getJmpFaqs(c);
272
+ return sortFaqs(faqs);
273
+ } catch {
274
+ return [];
275
+ }
276
+ });
277
+ async function loadFaqBySlug(slug) {
278
+ const normalized = slug.trim();
279
+ if (!normalized) {
280
+ return null;
281
+ }
282
+ const items = await loadFaqs();
283
+ return items.find((item) => item.slug === normalized || item.id === normalized) ?? null;
284
+ }
285
+ var loadTrustBundle = (0, import_react.cache)(async () => {
286
+ const c = getJmpContentConfig();
287
+ if (!c) {
288
+ return null;
289
+ }
290
+ try {
291
+ return await getJmpTrustBundle(c);
292
+ } catch {
293
+ return null;
294
+ }
295
+ });
296
+ function filterFeaturedTrustQuotes(testimonials, placement, limit = 2) {
297
+ return testimonials.filter((item) => {
298
+ if (!item.featured) {
299
+ return false;
300
+ }
301
+ const bucket = item.placement?.trim().toLowerCase();
302
+ if (placement === "reviews") {
303
+ return bucket === "reviews";
304
+ }
305
+ return !bucket || bucket === "testimonials";
306
+ }).sort((a, b) => a.sort_order - b.sort_order).slice(0, limit);
307
+ }
308
+ // Annotate the CommonJS export names for ESM import in node:
309
+ 0 && (module.exports = {
310
+ absoluteSiteAssetUrl,
311
+ filterFeaturedTrustQuotes,
312
+ getJmpContentConfig,
313
+ getResolvedSiteContact,
314
+ loadBlogPost,
315
+ loadBlogPosts,
316
+ loadFaqBySlug,
317
+ loadFaqs,
318
+ loadSiteProfile,
319
+ loadTrustBundle,
320
+ resolveSiteContact
321
+ });
322
+ //# sourceMappingURL=content.cjs.map