@jmp-technologies/analytics 0.1.27 → 0.1.32

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 (100) hide show
  1. package/dist/astro/client.cjs +256 -11
  2. package/dist/astro/client.cjs.map +1 -1
  3. package/dist/astro/client.d.cts +2 -2
  4. package/dist/astro/client.d.ts +2 -2
  5. package/dist/astro/client.js +2 -2
  6. package/dist/astro/content.cjs +308 -13
  7. package/dist/astro/content.cjs.map +1 -1
  8. package/dist/astro/content.d.cts +12 -3
  9. package/dist/astro/content.d.ts +12 -3
  10. package/dist/astro/content.js +9 -3
  11. package/dist/astro/integration.cjs +228 -10
  12. package/dist/astro/integration.cjs.map +1 -1
  13. package/dist/astro/integration.js +3 -3
  14. package/dist/astro/middleware.cjs +2 -1
  15. package/dist/astro/middleware.cjs.map +1 -1
  16. package/dist/astro/middleware.d.cts +2 -2
  17. package/dist/astro/middleware.d.ts +2 -2
  18. package/dist/astro/middleware.js +2 -2
  19. package/dist/astro/register.cjs +228 -10
  20. package/dist/astro/register.cjs.map +1 -1
  21. package/dist/astro/register.js +3 -3
  22. package/dist/{chunk-KZ4WVDGL.js → chunk-7623G3XL.js} +3 -3
  23. package/dist/{chunk-A4RRTY5D.js → chunk-AIFJ7DVO.js} +3 -2
  24. package/dist/{chunk-A4RRTY5D.js.map → chunk-AIFJ7DVO.js.map} +1 -1
  25. package/dist/{chunk-YCVHRMM6.js → chunk-G5BMWPQ6.js} +2 -3
  26. package/dist/{chunk-YCVHRMM6.js.map → chunk-G5BMWPQ6.js.map} +1 -1
  27. package/dist/{chunk-HODG4NJQ.js → chunk-HFLU7HQE.js} +2 -2
  28. package/dist/{chunk-PG3IOI5O.js → chunk-HKMDWTA2.js} +53 -6
  29. package/dist/chunk-HKMDWTA2.js.map +1 -0
  30. package/dist/{chunk-DNYJF7BG.js → chunk-KPTMZQCY.js} +3 -3
  31. package/dist/{chunk-FNWE2DT5.js → chunk-NDSPSAWQ.js} +18 -8
  32. package/dist/chunk-NDSPSAWQ.js.map +1 -0
  33. package/dist/{chunk-UVYF33XE.js → chunk-NI7R6QZQ.js} +293 -13
  34. package/dist/chunk-NI7R6QZQ.js.map +1 -0
  35. package/dist/chunk-RI3KKDPX.js +75 -0
  36. package/dist/chunk-RI3KKDPX.js.map +1 -0
  37. package/dist/{create-jmp-analytics-D4Qw_d6o.d.cts → create-jmp-analytics-D1dUsqCQ.d.cts} +1 -2
  38. package/dist/{create-jmp-analytics-DpG5Hdl5.d.ts → create-jmp-analytics-htx2oOEf.d.ts} +1 -2
  39. package/dist/index.cjs +295 -12
  40. package/dist/index.cjs.map +1 -1
  41. package/dist/index.d.cts +8 -6
  42. package/dist/index.d.ts +8 -6
  43. package/dist/index.js +10 -2
  44. package/dist/next/content.cjs +310 -13
  45. package/dist/next/content.cjs.map +1 -1
  46. package/dist/next/content.d.cts +7 -5
  47. package/dist/next/content.d.ts +7 -5
  48. package/dist/next/content.js +11 -3
  49. package/dist/next/content.js.map +1 -1
  50. package/dist/next/instrumentation.cjs +228 -10
  51. package/dist/next/instrumentation.cjs.map +1 -1
  52. package/dist/next/instrumentation.js +2 -2
  53. package/dist/next/landing-capture-script-server.cjs +1 -2
  54. package/dist/next/landing-capture-script-server.cjs.map +1 -1
  55. package/dist/next/landing-capture-script-server.js +1 -1
  56. package/dist/next/landing-pages.cjs +228 -12
  57. package/dist/next/landing-pages.cjs.map +1 -1
  58. package/dist/next/landing-pages.d.cts +4 -16
  59. package/dist/next/landing-pages.d.ts +4 -16
  60. package/dist/next/landing-pages.js +13 -67
  61. package/dist/next/landing-pages.js.map +1 -1
  62. package/dist/next/locations.cjs +737 -0
  63. package/dist/next/locations.cjs.map +1 -0
  64. package/dist/next/locations.d.cts +55 -0
  65. package/dist/next/locations.d.ts +55 -0
  66. package/dist/next/locations.js +158 -0
  67. package/dist/next/locations.js.map +1 -0
  68. package/dist/next/middleware.cjs +2 -1
  69. package/dist/next/middleware.cjs.map +1 -1
  70. package/dist/next/middleware.d.cts +2 -2
  71. package/dist/next/middleware.d.ts +2 -2
  72. package/dist/next/middleware.js +2 -2
  73. package/dist/next/site-root-server.cjs +259 -14
  74. package/dist/next/site-root-server.cjs.map +1 -1
  75. package/dist/next/site-root-server.js +5 -5
  76. package/dist/next.cjs +270 -17
  77. package/dist/next.cjs.map +1 -1
  78. package/dist/next.d.cts +3 -3
  79. package/dist/next.d.ts +3 -3
  80. package/dist/next.js +4 -4
  81. package/dist/react.cjs +389 -136
  82. package/dist/react.cjs.map +1 -1
  83. package/dist/react.d.cts +2 -2
  84. package/dist/react.d.ts +2 -2
  85. package/dist/react.js +3 -3
  86. package/dist/{site-config-UTzDG-UG.d.ts → site-config-B7USGGzu.d.cts} +22 -2
  87. package/dist/{site-config-CJqYnZm7.d.cts → site-config-C4bcUoLI.d.ts} +22 -2
  88. package/dist/{track-page-view-tX0Magkd.d.cts → track-page-view-C4TbbULE.d.cts} +1 -1
  89. package/dist/{track-page-view-CMAqyhD_.d.ts → track-page-view-RiB-xgIx.d.ts} +1 -1
  90. package/dist/{types-B-111Wht.d.ts → types-DRljY1Yy.d.cts} +92 -2
  91. package/dist/{types-B-111Wht.d.cts → types-DRljY1Yy.d.ts} +92 -2
  92. package/dist/utils-BajG58pj.d.cts +15 -0
  93. package/dist/utils-CHT3SQFv.d.ts +15 -0
  94. package/package.json +7 -2
  95. package/dist/chunk-FNWE2DT5.js.map +0 -1
  96. package/dist/chunk-PG3IOI5O.js.map +0 -1
  97. package/dist/chunk-UVYF33XE.js.map +0 -1
  98. /package/dist/{chunk-KZ4WVDGL.js.map → chunk-7623G3XL.js.map} +0 -0
  99. /package/dist/{chunk-HODG4NJQ.js.map → chunk-HFLU7HQE.js.map} +0 -0
  100. /package/dist/{chunk-DNYJF7BG.js.map → chunk-KPTMZQCY.js.map} +0 -0
@@ -24,6 +24,7 @@ __export(content_exports, {
24
24
  filterFeaturedTrustQuotes: () => filterFeaturedTrustQuotes,
25
25
  getJmpContentConfig: () => getJmpContentConfig,
26
26
  getResolvedSiteContact: () => getResolvedSiteContact,
27
+ listResolvedSocialUrls: () => listResolvedSocialUrls,
27
28
  loadBlogPost: () => loadBlogPost,
28
29
  loadBlogPosts: () => loadBlogPosts,
29
30
  loadFaqBySlug: () => loadFaqBySlug,
@@ -31,6 +32,8 @@ __export(content_exports, {
31
32
  loadLandingPageDetail: () => loadLandingPageDetail,
32
33
  loadLandingPageSitemap: () => loadLandingPageSitemap,
33
34
  loadLandingPages: () => loadLandingPages,
35
+ loadLocationDetail: () => loadLocationDetail,
36
+ loadLocations: () => loadLocations,
34
37
  loadSiteProfile: () => loadSiteProfile,
35
38
  loadTrustBundle: () => loadTrustBundle,
36
39
  resolveSiteContact: () => resolveSiteContact
@@ -52,6 +55,209 @@ function normalizeBaseUrl(baseUrl) {
52
55
  return baseUrl.replace(/\/$/, "");
53
56
  }
54
57
 
58
+ // src/content/manifest.ts
59
+ var JMP_CONTENT_MANIFEST_VERSION = 1;
60
+ var TRUST_SECTIONS = /* @__PURE__ */ new Set([
61
+ "summary",
62
+ "testimonials",
63
+ "reviews",
64
+ "stats",
65
+ "logos",
66
+ "credentials"
67
+ ]);
68
+ function normalizeKey(value) {
69
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
70
+ }
71
+ function isValidExtraKey(key) {
72
+ return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(key);
73
+ }
74
+ var MAX_EXTERNAL_ADMIN_LINKS = 10;
75
+ var MAX_EXTERNAL_ADMIN_LINK_LABEL_LENGTH = 80;
76
+ var EXTERNAL_ADMIN_LINK_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
77
+ function tryNormalizeHttpUrl(input) {
78
+ if (typeof input !== "string" || !input.trim()) {
79
+ return null;
80
+ }
81
+ try {
82
+ const url = new URL(input.trim());
83
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
84
+ return null;
85
+ }
86
+ return url.toString().replace(/\/$/, "");
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+ function normalizeShopifyExternalSource(shopify) {
92
+ if (!shopify || typeof shopify !== "object") {
93
+ return null;
94
+ }
95
+ const adminUrl = tryNormalizeHttpUrl(shopify.adminUrl);
96
+ const storeUrl = tryNormalizeHttpUrl(shopify.storeUrl);
97
+ if (!adminUrl && !storeUrl) {
98
+ return null;
99
+ }
100
+ return {
101
+ ...adminUrl ? { adminUrl } : {},
102
+ ...storeUrl ? { storeUrl } : {}
103
+ };
104
+ }
105
+ function normalizeExternalAdminLinkEntries(links) {
106
+ if (!Array.isArray(links)) {
107
+ return [];
108
+ }
109
+ const seenIds = /* @__PURE__ */ new Set();
110
+ const out = [];
111
+ for (const raw of links) {
112
+ if (!raw || typeof raw !== "object" || out.length >= MAX_EXTERNAL_ADMIN_LINKS) {
113
+ continue;
114
+ }
115
+ const id = typeof raw.id === "string" ? raw.id.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "") : "";
116
+ const label = typeof raw.label === "string" ? raw.label.trim().slice(0, MAX_EXTERNAL_ADMIN_LINK_LABEL_LENGTH) : "";
117
+ const url = tryNormalizeHttpUrl(raw.url);
118
+ if (!id || !label || !url || !EXTERNAL_ADMIN_LINK_ID_PATTERN.test(id) || seenIds.has(id)) {
119
+ continue;
120
+ }
121
+ seenIds.add(id);
122
+ out.push({ id, label, url });
123
+ }
124
+ return out;
125
+ }
126
+ function normalizeExternalSources(externalSources) {
127
+ if (!externalSources || typeof externalSources !== "object") {
128
+ return null;
129
+ }
130
+ const studioUrl = tryNormalizeHttpUrl(externalSources.sanity?.studioUrl);
131
+ const shopify = normalizeShopifyExternalSource(externalSources.shopify);
132
+ const links = normalizeExternalAdminLinkEntries(externalSources.links);
133
+ if (!studioUrl && !shopify && links.length === 0) {
134
+ return null;
135
+ }
136
+ return {
137
+ ...studioUrl ? { sanity: { studioUrl } } : {},
138
+ ...shopify ? { shopify } : {},
139
+ ...links.length > 0 ? { links } : {}
140
+ };
141
+ }
142
+ function normalizeJmpContentManifest(input) {
143
+ if (!input || typeof input !== "object") {
144
+ return null;
145
+ }
146
+ const routes = normalizeRoutes(input.routes);
147
+ const externalSources = normalizeExternalSources(input.externalSources);
148
+ const trust = normalizeTrust(input.trust);
149
+ const contact = normalizeContact(input.contact);
150
+ if (routes.length === 0 && !externalSources && !trust && !contact) {
151
+ return null;
152
+ }
153
+ return {
154
+ version: JMP_CONTENT_MANIFEST_VERSION,
155
+ ...routes.length > 0 ? { routes } : {},
156
+ ...externalSources ? { externalSources } : {},
157
+ ...trust ? { trust } : {},
158
+ ...contact ? { contact } : {}
159
+ };
160
+ }
161
+ function normalizeRoutes(routes) {
162
+ if (!Array.isArray(routes)) {
163
+ return [];
164
+ }
165
+ const seen = /* @__PURE__ */ new Set();
166
+ const out = [];
167
+ for (const raw of routes) {
168
+ if (!raw || typeof raw !== "object") continue;
169
+ const wrapper = typeof raw.wrapper === "string" ? raw.wrapper.trim().toLowerCase() : "";
170
+ const pattern = typeof raw.pattern === "string" ? raw.pattern.trim() : "";
171
+ if (!wrapper || !pattern) continue;
172
+ const dedupe = `${wrapper}:${pattern}`;
173
+ if (seen.has(dedupe)) continue;
174
+ seen.add(dedupe);
175
+ out.push({
176
+ wrapper,
177
+ pattern,
178
+ ...typeof raw.index === "string" && raw.index.trim() ? { index: raw.index.trim() } : {},
179
+ ...typeof raw.label === "string" && raw.label.trim() ? { label: raw.label.trim() } : {}
180
+ });
181
+ }
182
+ return out;
183
+ }
184
+ function normalizeTrust(trust) {
185
+ if (!trust || typeof trust !== "object") return null;
186
+ const blocks = normalizeTrustBlocks(trust.blocks);
187
+ const labels = normalizeTrustLabels(trust.labels);
188
+ const mergeTestimonialsReviews = trust.mergeTestimonialsReviews === true;
189
+ if (blocks.length === 0 && !labels && !mergeTestimonialsReviews) return null;
190
+ return {
191
+ ...blocks.length > 0 ? { blocks } : {},
192
+ ...labels ? { labels } : {},
193
+ ...mergeTestimonialsReviews ? { mergeTestimonialsReviews: true } : {}
194
+ };
195
+ }
196
+ function normalizeTrustBlocks(blocks) {
197
+ if (!Array.isArray(blocks)) return [];
198
+ const seen = /* @__PURE__ */ new Set();
199
+ const out = [];
200
+ for (const raw of blocks) {
201
+ if (!raw || typeof raw !== "object") continue;
202
+ const section = typeof raw.section === "string" ? raw.section.trim().toLowerCase() : "";
203
+ if (!TRUST_SECTIONS.has(section) || seen.has(section)) continue;
204
+ seen.add(section);
205
+ out.push({
206
+ section,
207
+ ...typeof raw.label === "string" && raw.label.trim() ? { label: raw.label.trim() } : {}
208
+ });
209
+ }
210
+ return out;
211
+ }
212
+ function normalizeTrustLabels(labels) {
213
+ if (!labels || typeof labels !== "object") return null;
214
+ const out = {};
215
+ for (const [key, value] of Object.entries(labels)) {
216
+ if (TRUST_SECTIONS.has(key) && typeof value === "string" && value.trim()) {
217
+ out[key] = value.trim();
218
+ }
219
+ }
220
+ return Object.keys(out).length > 0 ? out : null;
221
+ }
222
+ function normalizeContact(contact) {
223
+ if (!contact || typeof contact !== "object") return null;
224
+ const extras = normalizeContactExtras(contact.extras);
225
+ return extras.length > 0 ? { extras } : null;
226
+ }
227
+ function normalizeContactExtras(extras) {
228
+ if (!Array.isArray(extras)) return [];
229
+ const seen = /* @__PURE__ */ new Set();
230
+ const out = [];
231
+ for (const raw of extras) {
232
+ if (!raw || typeof raw !== "object") continue;
233
+ const label = typeof raw.label === "string" ? raw.label.trim() : "";
234
+ const key = normalizeKey(
235
+ typeof raw.key === "string" && raw.key.trim() ? raw.key : label
236
+ );
237
+ if (!key || !label || !isValidExtraKey(key) || seen.has(key)) continue;
238
+ seen.add(key);
239
+ out.push({ key, label });
240
+ }
241
+ return out;
242
+ }
243
+ function stableJmpManifestFingerprint(manifest) {
244
+ return manifest ? JSON.stringify(manifest) : "";
245
+ }
246
+ function inferWrapperSlotsFromManifest(manifest) {
247
+ const slots = /* @__PURE__ */ new Set();
248
+ for (const route of manifest?.routes ?? []) {
249
+ slots.add(route.wrapper);
250
+ }
251
+ for (const block of manifest?.trust?.blocks ?? []) {
252
+ slots.add(`trust.${block.section}`);
253
+ slots.add("trust");
254
+ }
255
+ if (manifest?.contact?.extras?.length) {
256
+ slots.add("contact");
257
+ }
258
+ return [...slots];
259
+ }
260
+
55
261
  // src/content/fetch.ts
56
262
  async function getContent(config, path) {
57
263
  const text = await getContentText(config, path);
@@ -166,16 +372,22 @@ function inferTrustWrapperSlots(bundle) {
166
372
  function getDiscoveredContentWrappers() {
167
373
  return sortWrapperSlots([...discovered]);
168
374
  }
375
+ function resolveContentManifestForReport(config) {
376
+ return normalizeJmpContentManifest(config.contentManifest);
377
+ }
169
378
  function resolveContentWrappersForReport(config) {
170
379
  const explicit = config.contentWrappers ?? [];
171
380
  const auto = getDiscoveredContentWrappers();
381
+ const fromManifest = inferWrapperSlotsFromManifest(
382
+ resolveContentManifestForReport(config)
383
+ );
172
384
  if (config.contentWrapperMode === "manual") {
173
- return sortWrapperSlots([...explicit]);
385
+ return sortWrapperSlots([...explicit, ...fromManifest]);
174
386
  }
175
387
  if (config.contentWrapperMode === "explicit-only") {
176
- return sortWrapperSlots([...explicit]);
388
+ return sortWrapperSlots([...explicit, ...fromManifest]);
177
389
  }
178
- const merged = /* @__PURE__ */ new Set([...explicit, ...auto]);
390
+ const merged = /* @__PURE__ */ new Set([...explicit, ...auto, ...fromManifest]);
179
391
  return sortWrapperSlots([...merged]);
180
392
  }
181
393
  function hasReportedContentWrappers(dedupeKey) {
@@ -303,6 +515,35 @@ async function getJmpLandingPageSitemap(config) {
303
515
  afterContentWrapperUse(config);
304
516
  return data.entries;
305
517
  }
518
+ async function getJmpLocations(config) {
519
+ const data = await getContent(
520
+ config,
521
+ "/api/content/locations"
522
+ );
523
+ markContentWrapper("locations");
524
+ afterContentWrapperUse(config);
525
+ return data.locations;
526
+ }
527
+ async function getJmpLocationDetail(config, slug) {
528
+ const normalized = slug.trim();
529
+ if (!normalized) {
530
+ return null;
531
+ }
532
+ try {
533
+ const detail = await getContent(
534
+ config,
535
+ `/api/content/locations/${encodeURIComponent(normalized)}`
536
+ );
537
+ markContentWrapper("locations");
538
+ afterContentWrapperUse(config);
539
+ return detail;
540
+ } catch (error) {
541
+ if (error instanceof Error && error.message.includes("HTTP 404")) {
542
+ return null;
543
+ }
544
+ throw error;
545
+ }
546
+ }
306
547
  async function getJmpTrustBundle(config) {
307
548
  const bundle = await getContent(config, "/api/content/trust");
308
549
  markTrustBundleWrappers(bundle);
@@ -323,7 +564,8 @@ async function registerJmpContentWrappers(config) {
323
564
  if (!wrappers.length) {
324
565
  return { ok: false, skipped: true };
325
566
  }
326
- const dedupeKey = `${config.trackingKey}:${[...wrappers].sort().join(",")}`;
567
+ const manifest = resolveContentManifestForReport(config);
568
+ const dedupeKey = `${config.trackingKey}:${[...wrappers].sort().join(",")}:${stableJmpManifestFingerprint(manifest)}`;
327
569
  if (hasReportedContentWrappers(dedupeKey)) {
328
570
  return { ok: true, wrappers };
329
571
  }
@@ -336,7 +578,17 @@ async function registerJmpContentWrappers(config) {
336
578
  }
337
579
  const url = `${baseUrl}/api/content/capabilities`;
338
580
  if (config.debug) {
339
- console.debug("[@jmp-technologies/analytics]", "POST", url, { wrappers });
581
+ console.debug("[@jmp-technologies/analytics]", "POST", url, {
582
+ wrappers,
583
+ manifest
584
+ });
585
+ }
586
+ const body = {
587
+ wrappers,
588
+ sdkVersion: JMP_ANALYTICS_SDK_VERSION
589
+ };
590
+ if (manifest) {
591
+ body.manifest = manifest;
340
592
  }
341
593
  const res = await resolvedFetch(url, {
342
594
  method: "POST",
@@ -344,10 +596,7 @@ async function registerJmpContentWrappers(config) {
344
596
  "Content-Type": "application/json",
345
597
  "X-JMP-Tracking-Key": config.trackingKey
346
598
  },
347
- body: JSON.stringify({
348
- wrappers,
349
- sdkVersion: JMP_ANALYTICS_SDK_VERSION
350
- })
599
+ body: JSON.stringify(body)
351
600
  });
352
601
  const text = await res.text();
353
602
  if (!res.ok) {
@@ -426,7 +675,8 @@ function getJmpContentConfig() {
426
675
  return {
427
676
  baseUrl: analyticsUrl,
428
677
  trackingKey,
429
- contentWrappers: site?.contentWrappers ?? void 0
678
+ contentWrappers: site?.contentWrappers ?? void 0,
679
+ contentManifest: site?.contentManifest ?? void 0
430
680
  };
431
681
  }
432
682
 
@@ -436,7 +686,8 @@ var DEFAULT_EXTRA_KEYS = {
436
686
  dashboardDemoUrl: ["dashboard-demo-url", "sample-dashboard-url"],
437
687
  profilePhotoSrc: ["profile-photo-url", "profile_photo_url"],
438
688
  hours: ["hours", "business-hours"],
439
- serviceArea: ["service-area", "service_area", "service-area-text"]
689
+ serviceArea: ["service-area", "service_area", "service-area-text"],
690
+ googleMapsEmbedUrl: ["google-maps-embed-url", "google_maps_embed_url"]
440
691
  };
441
692
  function sortBlogPosts(posts) {
442
693
  return [...posts].sort((a, b) => {
@@ -487,7 +738,13 @@ function resolveSiteContact(profile) {
487
738
  ...site?.contactExtraKeys
488
739
  };
489
740
  const email = profile?.email?.trim() || fallback.email || "";
490
- const linkedin = profile?.social?.linkedin?.trim() || fallback.linkedin || null;
741
+ const social = {
742
+ linkedin: profile?.social?.linkedin?.trim() || fallback.linkedin || null,
743
+ instagram: profile?.social?.instagram?.trim() || fallback.instagram || null,
744
+ facebook: profile?.social?.facebook?.trim() || fallback.facebook || null,
745
+ x: profile?.social?.x?.trim() || fallback.x || null,
746
+ youtube: profile?.social?.youtube?.trim() || fallback.youtube || null
747
+ };
491
748
  const phone = profile?.phone?.trim() || fallback.phone || null;
492
749
  return {
493
750
  displayName: profile?.display_name?.trim() || profile?.name?.trim() || fallback.displayName || "",
@@ -495,8 +752,12 @@ function resolveSiteContact(profile) {
495
752
  dashboardDemoUrl: readProfileExtra(profile, extraKeys.dashboardDemoUrl ?? []) || fallback.dashboardDemoUrl || "",
496
753
  profilePhotoSrc: readProfileExtra(profile, extraKeys.profilePhotoSrc ?? []) || fallback.profilePhotoSrc || "",
497
754
  email,
498
- linkedin,
755
+ linkedin: social.linkedin,
756
+ social,
499
757
  phone,
758
+ address: profile?.address?.trim() || fallback.address || null,
759
+ googleMapsUrl: profile?.google_maps_url?.trim() || fallback.googleMapsUrl || null,
760
+ googleMapsEmbedUrl: readProfileExtra(profile, extraKeys.googleMapsEmbedUrl ?? []) || fallback.googleMapsEmbedUrl || null,
500
761
  hours: readProfileExtra(profile, extraKeys.hours ?? []) || fallback.hours || null,
501
762
  serviceArea: readProfileExtra(profile, extraKeys.serviceArea ?? []) || fallback.serviceArea || null,
502
763
  privacyUrl: profile?.legal?.privacy_policy_url?.trim() || fallback.privacyUrl || "/privacy",
@@ -511,6 +772,15 @@ async function getResolvedSiteContact() {
511
772
  const profile = await loadSiteProfile();
512
773
  return resolveSiteContact(profile);
513
774
  }
775
+ function listResolvedSocialUrls(social) {
776
+ return [
777
+ social.linkedin,
778
+ social.instagram,
779
+ social.facebook,
780
+ social.x,
781
+ social.youtube
782
+ ].filter((url) => Boolean(url));
783
+ }
514
784
  async function loadBlogPosts() {
515
785
  const c = getJmpContentConfig();
516
786
  if (!c) {
@@ -587,6 +857,28 @@ async function loadLandingPageSitemap() {
587
857
  return [];
588
858
  }
589
859
  }
860
+ async function loadLocations() {
861
+ const c = getJmpContentConfig();
862
+ if (!c) {
863
+ return [];
864
+ }
865
+ try {
866
+ return await getJmpLocations(c);
867
+ } catch {
868
+ return [];
869
+ }
870
+ }
871
+ async function loadLocationDetail(slug) {
872
+ const c = getJmpContentConfig();
873
+ if (!c) {
874
+ return null;
875
+ }
876
+ try {
877
+ return await getJmpLocationDetail(c, slug);
878
+ } catch {
879
+ return null;
880
+ }
881
+ }
590
882
  async function loadTrustBundle() {
591
883
  const c = getJmpContentConfig();
592
884
  if (!c) {
@@ -616,6 +908,7 @@ function filterFeaturedTrustQuotes(testimonials, placement, limit = 2) {
616
908
  filterFeaturedTrustQuotes,
617
909
  getJmpContentConfig,
618
910
  getResolvedSiteContact,
911
+ listResolvedSocialUrls,
619
912
  loadBlogPost,
620
913
  loadBlogPosts,
621
914
  loadFaqBySlug,
@@ -623,6 +916,8 @@ function filterFeaturedTrustQuotes(testimonials, placement, limit = 2) {
623
916
  loadLandingPageDetail,
624
917
  loadLandingPageSitemap,
625
918
  loadLandingPages,
919
+ loadLocationDetail,
920
+ loadLocations,
626
921
  loadSiteProfile,
627
922
  loadTrustBundle,
628
923
  resolveSiteContact