@jmp-technologies/analytics 0.1.26 → 0.1.30

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 (86) hide show
  1. package/README.md +43 -0
  2. package/bin/jmp-init.mjs +34 -0
  3. package/dist/astro/client.cjs +227 -10
  4. package/dist/astro/client.cjs.map +1 -1
  5. package/dist/astro/client.d.cts +2 -2
  6. package/dist/astro/client.d.ts +2 -2
  7. package/dist/astro/client.js +2 -2
  8. package/dist/astro/content.cjs +340 -14
  9. package/dist/astro/content.cjs.map +1 -1
  10. package/dist/astro/content.d.cts +16 -3
  11. package/dist/astro/content.d.ts +16 -3
  12. package/dist/astro/content.js +11 -3
  13. package/dist/astro/integration.cjs +229 -11
  14. package/dist/astro/integration.cjs.map +1 -1
  15. package/dist/astro/integration.js +3 -3
  16. package/dist/astro/middleware.cjs +2 -1
  17. package/dist/astro/middleware.cjs.map +1 -1
  18. package/dist/astro/middleware.d.cts +2 -2
  19. package/dist/astro/middleware.d.ts +2 -2
  20. package/dist/astro/middleware.js +2 -2
  21. package/dist/astro/register.cjs +229 -11
  22. package/dist/astro/register.cjs.map +1 -1
  23. package/dist/astro/register.js +3 -3
  24. package/dist/{chunk-A4RRTY5D.js → chunk-AIFJ7DVO.js} +3 -2
  25. package/dist/{chunk-A4RRTY5D.js.map → chunk-AIFJ7DVO.js.map} +1 -1
  26. package/dist/{chunk-TFHOTBOL.js → chunk-FS4BZ7DR.js} +66 -6
  27. package/dist/chunk-FS4BZ7DR.js.map +1 -0
  28. package/dist/{chunk-RCOHQAGL.js → chunk-GTPKRBFR.js} +284 -12
  29. package/dist/chunk-GTPKRBFR.js.map +1 -0
  30. package/dist/{chunk-HODG4NJQ.js → chunk-HFLU7HQE.js} +2 -2
  31. package/dist/{chunk-WFTTIRDJ.js → chunk-LLTB7ZFC.js} +2 -2
  32. package/dist/{chunk-PKGUBYZR.js → chunk-LTPLLPOR.js} +3 -3
  33. package/dist/{chunk-XQZOGWF2.js → chunk-ZEC22LFP.js} +3 -3
  34. package/dist/{create-jmp-analytics-JKAsJAWb.d.ts → create-jmp-analytics-BeuuU6as.d.ts} +1 -1
  35. package/dist/{create-jmp-analytics-Mau-oxji.d.cts → create-jmp-analytics-Ce2CrhhR.d.cts} +1 -1
  36. package/dist/index.cjs +289 -11
  37. package/dist/index.cjs.map +1 -1
  38. package/dist/index.d.cts +16 -7
  39. package/dist/index.d.ts +16 -7
  40. package/dist/index.js +12 -2
  41. package/dist/next/content.cjs +343 -14
  42. package/dist/next/content.cjs.map +1 -1
  43. package/dist/next/content.d.cts +8 -5
  44. package/dist/next/content.d.ts +8 -5
  45. package/dist/next/content.js +14 -3
  46. package/dist/next/content.js.map +1 -1
  47. package/dist/next/instrumentation.cjs +229 -11
  48. package/dist/next/instrumentation.cjs.map +1 -1
  49. package/dist/next/instrumentation.js +2 -2
  50. package/dist/next/landing-pages.cjs +875 -0
  51. package/dist/next/landing-pages.cjs.map +1 -0
  52. package/dist/next/landing-pages.d.cts +103 -0
  53. package/dist/next/landing-pages.d.ts +103 -0
  54. package/dist/next/landing-pages.js +297 -0
  55. package/dist/next/landing-pages.js.map +1 -0
  56. package/dist/next/middleware.cjs +2 -1
  57. package/dist/next/middleware.cjs.map +1 -1
  58. package/dist/next/middleware.d.cts +2 -2
  59. package/dist/next/middleware.d.ts +2 -2
  60. package/dist/next/middleware.js +2 -2
  61. package/dist/next/site-root-server.cjs +229 -11
  62. package/dist/next/site-root-server.cjs.map +1 -1
  63. package/dist/next/site-root-server.js +4 -4
  64. package/dist/next.cjs +227 -10
  65. package/dist/next.cjs.map +1 -1
  66. package/dist/next.d.cts +3 -3
  67. package/dist/next.d.ts +3 -3
  68. package/dist/next.js +4 -4
  69. package/dist/react.cjs +227 -10
  70. package/dist/react.cjs.map +1 -1
  71. package/dist/react.d.cts +2 -2
  72. package/dist/react.d.ts +2 -2
  73. package/dist/react.js +3 -3
  74. package/dist/{site-config-CXUdGLqw.d.cts → site-config-B4uEj4xX.d.ts} +22 -2
  75. package/dist/{site-config-w_Db9YSk.d.ts → site-config-DIFmnwqv.d.cts} +22 -2
  76. package/dist/{track-page-view-CkuSYuco.d.cts → track-page-view-Bv0BAvxG.d.cts} +1 -1
  77. package/dist/{track-page-view-QRSk8vap.d.ts → track-page-view-CkgIp9-j.d.ts} +1 -1
  78. package/dist/{types-C1eq8E5u.d.ts → types-B2iP79lb.d.cts} +99 -2
  79. package/dist/{types-C1eq8E5u.d.cts → types-B2iP79lb.d.ts} +99 -2
  80. package/package.json +11 -2
  81. package/dist/chunk-RCOHQAGL.js.map +0 -1
  82. package/dist/chunk-TFHOTBOL.js.map +0 -1
  83. /package/dist/{chunk-HODG4NJQ.js.map → chunk-HFLU7HQE.js.map} +0 -0
  84. /package/dist/{chunk-WFTTIRDJ.js.map → chunk-LLTB7ZFC.js.map} +0 -0
  85. /package/dist/{chunk-PKGUBYZR.js.map → chunk-LTPLLPOR.js.map} +0 -0
  86. /package/dist/{chunk-XQZOGWF2.js.map → chunk-ZEC22LFP.js.map} +0 -0
@@ -24,10 +24,14 @@ __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,
30
31
  loadFaqs: () => loadFaqs,
32
+ loadLandingPageDetail: () => loadLandingPageDetail,
33
+ loadLandingPageSitemap: () => loadLandingPageSitemap,
34
+ loadLandingPages: () => loadLandingPages,
31
35
  loadSiteProfile: () => loadSiteProfile,
32
36
  loadTrustBundle: () => loadTrustBundle,
33
37
  resolveSiteContact: () => resolveSiteContact
@@ -38,7 +42,7 @@ module.exports = __toCommonJS(content_exports);
38
42
  var LANDING_REFERER_COOKIE_MAX_AGE_SEC = 30 * 60;
39
43
 
40
44
  // src/constants.ts
41
- var JMP_ANALYTICS_SDK_VERSION = "0.1.25";
45
+ var JMP_ANALYTICS_SDK_VERSION = "0.1.26";
42
46
 
43
47
  // src/http.ts
44
48
  function responseLooksLikeHtml(body) {
@@ -49,6 +53,209 @@ function normalizeBaseUrl(baseUrl) {
49
53
  return baseUrl.replace(/\/$/, "");
50
54
  }
51
55
 
56
+ // src/content/manifest.ts
57
+ var JMP_CONTENT_MANIFEST_VERSION = 1;
58
+ var TRUST_SECTIONS = /* @__PURE__ */ new Set([
59
+ "summary",
60
+ "testimonials",
61
+ "reviews",
62
+ "stats",
63
+ "logos",
64
+ "credentials"
65
+ ]);
66
+ function normalizeKey(value) {
67
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
68
+ }
69
+ function isValidExtraKey(key) {
70
+ return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(key);
71
+ }
72
+ var MAX_EXTERNAL_ADMIN_LINKS = 10;
73
+ var MAX_EXTERNAL_ADMIN_LINK_LABEL_LENGTH = 80;
74
+ var EXTERNAL_ADMIN_LINK_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
75
+ function tryNormalizeHttpUrl(input) {
76
+ if (typeof input !== "string" || !input.trim()) {
77
+ return null;
78
+ }
79
+ try {
80
+ const url = new URL(input.trim());
81
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
82
+ return null;
83
+ }
84
+ return url.toString().replace(/\/$/, "");
85
+ } catch {
86
+ return null;
87
+ }
88
+ }
89
+ function normalizeShopifyExternalSource(shopify) {
90
+ if (!shopify || typeof shopify !== "object") {
91
+ return null;
92
+ }
93
+ const adminUrl = tryNormalizeHttpUrl(shopify.adminUrl);
94
+ const storeUrl = tryNormalizeHttpUrl(shopify.storeUrl);
95
+ if (!adminUrl && !storeUrl) {
96
+ return null;
97
+ }
98
+ return {
99
+ ...adminUrl ? { adminUrl } : {},
100
+ ...storeUrl ? { storeUrl } : {}
101
+ };
102
+ }
103
+ function normalizeExternalAdminLinkEntries(links) {
104
+ if (!Array.isArray(links)) {
105
+ return [];
106
+ }
107
+ const seenIds = /* @__PURE__ */ new Set();
108
+ const out = [];
109
+ for (const raw of links) {
110
+ if (!raw || typeof raw !== "object" || out.length >= MAX_EXTERNAL_ADMIN_LINKS) {
111
+ continue;
112
+ }
113
+ const id = typeof raw.id === "string" ? raw.id.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "") : "";
114
+ const label = typeof raw.label === "string" ? raw.label.trim().slice(0, MAX_EXTERNAL_ADMIN_LINK_LABEL_LENGTH) : "";
115
+ const url = tryNormalizeHttpUrl(raw.url);
116
+ if (!id || !label || !url || !EXTERNAL_ADMIN_LINK_ID_PATTERN.test(id) || seenIds.has(id)) {
117
+ continue;
118
+ }
119
+ seenIds.add(id);
120
+ out.push({ id, label, url });
121
+ }
122
+ return out;
123
+ }
124
+ function normalizeExternalSources(externalSources) {
125
+ if (!externalSources || typeof externalSources !== "object") {
126
+ return null;
127
+ }
128
+ const studioUrl = tryNormalizeHttpUrl(externalSources.sanity?.studioUrl);
129
+ const shopify = normalizeShopifyExternalSource(externalSources.shopify);
130
+ const links = normalizeExternalAdminLinkEntries(externalSources.links);
131
+ if (!studioUrl && !shopify && links.length === 0) {
132
+ return null;
133
+ }
134
+ return {
135
+ ...studioUrl ? { sanity: { studioUrl } } : {},
136
+ ...shopify ? { shopify } : {},
137
+ ...links.length > 0 ? { links } : {}
138
+ };
139
+ }
140
+ function normalizeJmpContentManifest(input) {
141
+ if (!input || typeof input !== "object") {
142
+ return null;
143
+ }
144
+ const routes = normalizeRoutes(input.routes);
145
+ const externalSources = normalizeExternalSources(input.externalSources);
146
+ const trust = normalizeTrust(input.trust);
147
+ const contact = normalizeContact(input.contact);
148
+ if (routes.length === 0 && !externalSources && !trust && !contact) {
149
+ return null;
150
+ }
151
+ return {
152
+ version: JMP_CONTENT_MANIFEST_VERSION,
153
+ ...routes.length > 0 ? { routes } : {},
154
+ ...externalSources ? { externalSources } : {},
155
+ ...trust ? { trust } : {},
156
+ ...contact ? { contact } : {}
157
+ };
158
+ }
159
+ function normalizeRoutes(routes) {
160
+ if (!Array.isArray(routes)) {
161
+ return [];
162
+ }
163
+ const seen = /* @__PURE__ */ new Set();
164
+ const out = [];
165
+ for (const raw of routes) {
166
+ if (!raw || typeof raw !== "object") continue;
167
+ const wrapper = typeof raw.wrapper === "string" ? raw.wrapper.trim().toLowerCase() : "";
168
+ const pattern = typeof raw.pattern === "string" ? raw.pattern.trim() : "";
169
+ if (!wrapper || !pattern) continue;
170
+ const dedupe = `${wrapper}:${pattern}`;
171
+ if (seen.has(dedupe)) continue;
172
+ seen.add(dedupe);
173
+ out.push({
174
+ wrapper,
175
+ pattern,
176
+ ...typeof raw.index === "string" && raw.index.trim() ? { index: raw.index.trim() } : {},
177
+ ...typeof raw.label === "string" && raw.label.trim() ? { label: raw.label.trim() } : {}
178
+ });
179
+ }
180
+ return out;
181
+ }
182
+ function normalizeTrust(trust) {
183
+ if (!trust || typeof trust !== "object") return null;
184
+ const blocks = normalizeTrustBlocks(trust.blocks);
185
+ const labels = normalizeTrustLabels(trust.labels);
186
+ const mergeTestimonialsReviews = trust.mergeTestimonialsReviews === true;
187
+ if (blocks.length === 0 && !labels && !mergeTestimonialsReviews) return null;
188
+ return {
189
+ ...blocks.length > 0 ? { blocks } : {},
190
+ ...labels ? { labels } : {},
191
+ ...mergeTestimonialsReviews ? { mergeTestimonialsReviews: true } : {}
192
+ };
193
+ }
194
+ function normalizeTrustBlocks(blocks) {
195
+ if (!Array.isArray(blocks)) return [];
196
+ const seen = /* @__PURE__ */ new Set();
197
+ const out = [];
198
+ for (const raw of blocks) {
199
+ if (!raw || typeof raw !== "object") continue;
200
+ const section = typeof raw.section === "string" ? raw.section.trim().toLowerCase() : "";
201
+ if (!TRUST_SECTIONS.has(section) || seen.has(section)) continue;
202
+ seen.add(section);
203
+ out.push({
204
+ section,
205
+ ...typeof raw.label === "string" && raw.label.trim() ? { label: raw.label.trim() } : {}
206
+ });
207
+ }
208
+ return out;
209
+ }
210
+ function normalizeTrustLabels(labels) {
211
+ if (!labels || typeof labels !== "object") return null;
212
+ const out = {};
213
+ for (const [key, value] of Object.entries(labels)) {
214
+ if (TRUST_SECTIONS.has(key) && typeof value === "string" && value.trim()) {
215
+ out[key] = value.trim();
216
+ }
217
+ }
218
+ return Object.keys(out).length > 0 ? out : null;
219
+ }
220
+ function normalizeContact(contact) {
221
+ if (!contact || typeof contact !== "object") return null;
222
+ const extras = normalizeContactExtras(contact.extras);
223
+ return extras.length > 0 ? { extras } : null;
224
+ }
225
+ function normalizeContactExtras(extras) {
226
+ if (!Array.isArray(extras)) return [];
227
+ const seen = /* @__PURE__ */ new Set();
228
+ const out = [];
229
+ for (const raw of extras) {
230
+ if (!raw || typeof raw !== "object") continue;
231
+ const label = typeof raw.label === "string" ? raw.label.trim() : "";
232
+ const key = normalizeKey(
233
+ typeof raw.key === "string" && raw.key.trim() ? raw.key : label
234
+ );
235
+ if (!key || !label || !isValidExtraKey(key) || seen.has(key)) continue;
236
+ seen.add(key);
237
+ out.push({ key, label });
238
+ }
239
+ return out;
240
+ }
241
+ function stableJmpManifestFingerprint(manifest) {
242
+ return manifest ? JSON.stringify(manifest) : "";
243
+ }
244
+ function inferWrapperSlotsFromManifest(manifest) {
245
+ const slots = /* @__PURE__ */ new Set();
246
+ for (const route of manifest?.routes ?? []) {
247
+ slots.add(route.wrapper);
248
+ }
249
+ for (const block of manifest?.trust?.blocks ?? []) {
250
+ slots.add(`trust.${block.section}`);
251
+ slots.add("trust");
252
+ }
253
+ if (manifest?.contact?.extras?.length) {
254
+ slots.add("contact");
255
+ }
256
+ return [...slots];
257
+ }
258
+
52
259
  // src/content/fetch.ts
53
260
  async function getContent(config, path) {
54
261
  const text = await getContentText(config, path);
@@ -163,16 +370,22 @@ function inferTrustWrapperSlots(bundle) {
163
370
  function getDiscoveredContentWrappers() {
164
371
  return sortWrapperSlots([...discovered]);
165
372
  }
373
+ function resolveContentManifestForReport(config) {
374
+ return normalizeJmpContentManifest(config.contentManifest);
375
+ }
166
376
  function resolveContentWrappersForReport(config) {
167
377
  const explicit = config.contentWrappers ?? [];
168
378
  const auto = getDiscoveredContentWrappers();
379
+ const fromManifest = inferWrapperSlotsFromManifest(
380
+ resolveContentManifestForReport(config)
381
+ );
169
382
  if (config.contentWrapperMode === "manual") {
170
- return sortWrapperSlots([...explicit]);
383
+ return sortWrapperSlots([...explicit, ...fromManifest]);
171
384
  }
172
385
  if (config.contentWrapperMode === "explicit-only") {
173
- return sortWrapperSlots([...explicit]);
386
+ return sortWrapperSlots([...explicit, ...fromManifest]);
174
387
  }
175
- const merged = /* @__PURE__ */ new Set([...explicit, ...auto]);
388
+ const merged = /* @__PURE__ */ new Set([...explicit, ...auto, ...fromManifest]);
176
389
  return sortWrapperSlots([...merged]);
177
390
  }
178
391
  function hasReportedContentWrappers(dedupeKey) {
@@ -253,6 +466,53 @@ async function getJmpFaqs(config) {
253
466
  afterContentWrapperUse(config);
254
467
  return data.faqs;
255
468
  }
469
+ async function getJmpLandingPages(config, filters) {
470
+ const params = new URLSearchParams();
471
+ if (filters?.kind) {
472
+ params.set("kind", filters.kind);
473
+ }
474
+ if (filters?.locale) {
475
+ params.set("locale", filters.locale);
476
+ }
477
+ const query = params.toString();
478
+ const data = await getContent(
479
+ config,
480
+ `/api/content/landing-pages${query ? `?${query}` : ""}`
481
+ );
482
+ markContentWrapper("landing");
483
+ afterContentWrapperUse(config);
484
+ return data.pages;
485
+ }
486
+ async function getJmpLandingPageDetail(config, kind, slug, locale) {
487
+ const params = new URLSearchParams();
488
+ if (locale) {
489
+ params.set("locale", locale);
490
+ }
491
+ const query = params.toString();
492
+ try {
493
+ const detail = await getContent(
494
+ config,
495
+ `/api/content/landing-pages/${encodeURIComponent(kind)}/${encodeURIComponent(slug)}${query ? `?${query}` : ""}`
496
+ );
497
+ markContentWrapper("landing");
498
+ afterContentWrapperUse(config);
499
+ return detail;
500
+ } catch (error) {
501
+ if (error instanceof Error && error.message.includes("HTTP 404")) {
502
+ return null;
503
+ }
504
+ throw error;
505
+ }
506
+ }
507
+ async function getJmpLandingPageSitemap(config) {
508
+ const data = await getContent(
509
+ config,
510
+ "/api/content/landing-pages/sitemap"
511
+ );
512
+ markContentWrapper("landing");
513
+ afterContentWrapperUse(config);
514
+ return data.entries;
515
+ }
256
516
  async function getJmpTrustBundle(config) {
257
517
  const bundle = await getContent(config, "/api/content/trust");
258
518
  markTrustBundleWrappers(bundle);
@@ -273,7 +533,8 @@ async function registerJmpContentWrappers(config) {
273
533
  if (!wrappers.length) {
274
534
  return { ok: false, skipped: true };
275
535
  }
276
- const dedupeKey = `${config.trackingKey}:${[...wrappers].sort().join(",")}`;
536
+ const manifest = resolveContentManifestForReport(config);
537
+ const dedupeKey = `${config.trackingKey}:${[...wrappers].sort().join(",")}:${stableJmpManifestFingerprint(manifest)}`;
277
538
  if (hasReportedContentWrappers(dedupeKey)) {
278
539
  return { ok: true, wrappers };
279
540
  }
@@ -286,7 +547,17 @@ async function registerJmpContentWrappers(config) {
286
547
  }
287
548
  const url = `${baseUrl}/api/content/capabilities`;
288
549
  if (config.debug) {
289
- console.debug("[@jmp-technologies/analytics]", "POST", url, { wrappers });
550
+ console.debug("[@jmp-technologies/analytics]", "POST", url, {
551
+ wrappers,
552
+ manifest
553
+ });
554
+ }
555
+ const body = {
556
+ wrappers,
557
+ sdkVersion: JMP_ANALYTICS_SDK_VERSION
558
+ };
559
+ if (manifest) {
560
+ body.manifest = manifest;
290
561
  }
291
562
  const res = await resolvedFetch(url, {
292
563
  method: "POST",
@@ -294,10 +565,7 @@ async function registerJmpContentWrappers(config) {
294
565
  "Content-Type": "application/json",
295
566
  "X-JMP-Tracking-Key": config.trackingKey
296
567
  },
297
- body: JSON.stringify({
298
- wrappers,
299
- sdkVersion: JMP_ANALYTICS_SDK_VERSION
300
- })
568
+ body: JSON.stringify(body)
301
569
  });
302
570
  const text = await res.text();
303
571
  if (!res.ok) {
@@ -376,7 +644,8 @@ function getJmpContentConfig() {
376
644
  return {
377
645
  baseUrl: analyticsUrl,
378
646
  trackingKey,
379
- contentWrappers: site?.contentWrappers ?? void 0
647
+ contentWrappers: site?.contentWrappers ?? void 0,
648
+ contentManifest: site?.contentManifest ?? void 0
380
649
  };
381
650
  }
382
651
 
@@ -386,7 +655,8 @@ var DEFAULT_EXTRA_KEYS = {
386
655
  dashboardDemoUrl: ["dashboard-demo-url", "sample-dashboard-url"],
387
656
  profilePhotoSrc: ["profile-photo-url", "profile_photo_url"],
388
657
  hours: ["hours", "business-hours"],
389
- serviceArea: ["service-area", "service_area", "service-area-text"]
658
+ serviceArea: ["service-area", "service_area", "service-area-text"],
659
+ googleMapsEmbedUrl: ["google-maps-embed-url", "google_maps_embed_url"]
390
660
  };
391
661
  function sortBlogPosts(posts) {
392
662
  return [...posts].sort((a, b) => {
@@ -437,7 +707,13 @@ function resolveSiteContact(profile) {
437
707
  ...site?.contactExtraKeys
438
708
  };
439
709
  const email = profile?.email?.trim() || fallback.email || "";
440
- const linkedin = profile?.social?.linkedin?.trim() || fallback.linkedin || null;
710
+ const social = {
711
+ linkedin: profile?.social?.linkedin?.trim() || fallback.linkedin || null,
712
+ instagram: profile?.social?.instagram?.trim() || fallback.instagram || null,
713
+ facebook: profile?.social?.facebook?.trim() || fallback.facebook || null,
714
+ x: profile?.social?.x?.trim() || fallback.x || null,
715
+ youtube: profile?.social?.youtube?.trim() || fallback.youtube || null
716
+ };
441
717
  const phone = profile?.phone?.trim() || fallback.phone || null;
442
718
  return {
443
719
  displayName: profile?.display_name?.trim() || profile?.name?.trim() || fallback.displayName || "",
@@ -445,8 +721,12 @@ function resolveSiteContact(profile) {
445
721
  dashboardDemoUrl: readProfileExtra(profile, extraKeys.dashboardDemoUrl ?? []) || fallback.dashboardDemoUrl || "",
446
722
  profilePhotoSrc: readProfileExtra(profile, extraKeys.profilePhotoSrc ?? []) || fallback.profilePhotoSrc || "",
447
723
  email,
448
- linkedin,
724
+ linkedin: social.linkedin,
725
+ social,
449
726
  phone,
727
+ address: profile?.address?.trim() || fallback.address || null,
728
+ googleMapsUrl: profile?.google_maps_url?.trim() || fallback.googleMapsUrl || null,
729
+ googleMapsEmbedUrl: readProfileExtra(profile, extraKeys.googleMapsEmbedUrl ?? []) || fallback.googleMapsEmbedUrl || null,
450
730
  hours: readProfileExtra(profile, extraKeys.hours ?? []) || fallback.hours || null,
451
731
  serviceArea: readProfileExtra(profile, extraKeys.serviceArea ?? []) || fallback.serviceArea || null,
452
732
  privacyUrl: profile?.legal?.privacy_policy_url?.trim() || fallback.privacyUrl || "/privacy",
@@ -461,6 +741,15 @@ async function getResolvedSiteContact() {
461
741
  const profile = await loadSiteProfile();
462
742
  return resolveSiteContact(profile);
463
743
  }
744
+ function listResolvedSocialUrls(social) {
745
+ return [
746
+ social.linkedin,
747
+ social.instagram,
748
+ social.facebook,
749
+ social.x,
750
+ social.youtube
751
+ ].filter((url) => Boolean(url));
752
+ }
464
753
  async function loadBlogPosts() {
465
754
  const c = getJmpContentConfig();
466
755
  if (!c) {
@@ -504,6 +793,39 @@ async function loadFaqBySlug(slug) {
504
793
  const items = await loadFaqs();
505
794
  return items.find((item) => item.slug === normalized || item.id === normalized) ?? null;
506
795
  }
796
+ async function loadLandingPages(filters) {
797
+ const c = getJmpContentConfig();
798
+ if (!c) {
799
+ return [];
800
+ }
801
+ try {
802
+ return await getJmpLandingPages(c, filters);
803
+ } catch {
804
+ return [];
805
+ }
806
+ }
807
+ async function loadLandingPageDetail(kind, slug, locale) {
808
+ const c = getJmpContentConfig();
809
+ if (!c) {
810
+ return null;
811
+ }
812
+ try {
813
+ return await getJmpLandingPageDetail(c, kind, slug, locale);
814
+ } catch {
815
+ return null;
816
+ }
817
+ }
818
+ async function loadLandingPageSitemap() {
819
+ const c = getJmpContentConfig();
820
+ if (!c) {
821
+ return [];
822
+ }
823
+ try {
824
+ return await getJmpLandingPageSitemap(c);
825
+ } catch {
826
+ return [];
827
+ }
828
+ }
507
829
  async function loadTrustBundle() {
508
830
  const c = getJmpContentConfig();
509
831
  if (!c) {
@@ -533,10 +855,14 @@ function filterFeaturedTrustQuotes(testimonials, placement, limit = 2) {
533
855
  filterFeaturedTrustQuotes,
534
856
  getJmpContentConfig,
535
857
  getResolvedSiteContact,
858
+ listResolvedSocialUrls,
536
859
  loadBlogPost,
537
860
  loadBlogPosts,
538
861
  loadFaqBySlug,
539
862
  loadFaqs,
863
+ loadLandingPageDetail,
864
+ loadLandingPageSitemap,
865
+ loadLandingPages,
540
866
  loadSiteProfile,
541
867
  loadTrustBundle,
542
868
  resolveSiteContact