@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
package/README.md CHANGED
@@ -253,6 +253,41 @@ export default async function BlogPage() {
253
253
  }
254
254
  ```
255
255
 
256
+ ## Service & location landing pages (≥ 0.1.27)
257
+
258
+ CMS-managed pages at `/areas/{slug}` and `/services/{slug}` (plus optional `/[locale]/…`).
259
+
260
+ **Thin route files** — import from `@jmp-technologies/analytics/next/landing-pages`:
261
+
262
+ ```tsx
263
+ // app/services/[slug]/page.tsx
264
+ import {
265
+ JmpLandingDetailPage,
266
+ buildJmpLandingMetadata,
267
+ generateJmpLandingStaticParams,
268
+ } from "@jmp-technologies/analytics/next/landing-pages"
269
+
270
+ export function generateStaticParams() {
271
+ return generateJmpLandingStaticParams("service")
272
+ }
273
+
274
+ export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
275
+ const { slug } = await params
276
+ return buildJmpLandingMetadata({ kind: "service", slug })
277
+ }
278
+
279
+ export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
280
+ const { slug } = await params
281
+ return <JmpLandingDetailPage kind="service" slug={slug} />
282
+ }
283
+ ```
284
+
285
+ Optional **`components`** prop for site Markdown, breadcrumbs, and JSON-LD. **`buildJmpLandingSitemapRoutes`** merges landing URLs into `app/sitemap.ts`.
286
+
287
+ Data loaders remain on `@jmp-technologies/analytics/next/content`: `loadLandingPages`, `loadLandingPageDetail`, `loadLandingPageSitemap`.
288
+
289
+ ---
290
+
256
291
  Available helpers:
257
292
 
258
293
  FAQ items include a stable `slug` field. Use that slug, rather than the `id`,
@@ -271,6 +306,13 @@ When present, `category` is an optional section label you can use to group FAQs
271
306
  - `JmpNextAnalytics` / `JmpNextPageViewTracker` — App Router page views
272
307
  - Re-exports: `JmpTrackedTelLink`, `JmpTrackedMailtoLink`, `useJmpAnalytics`
273
308
 
309
+ **Next.js landing routes (`@jmp-technologies/analytics/next/landing-pages`, ≥ 0.1.27)**
310
+
311
+ - `generateJmpLandingStaticParams`, `generateJmpLocalizedLandingStaticParams`
312
+ - `buildJmpLandingMetadata`, `buildJmpLandingIndexMetadata`
313
+ - `JmpLandingDetailPage`, `JmpLandingIndexPage`
314
+ - `buildJmpLandingSitemapRoutes`
315
+
274
316
  **React (`@jmp-technologies/analytics/react`)**
275
317
 
276
318
  - `JmpTrackedTelLink`, `JmpTrackedMailtoLink` (since **0.1.10**), `useJmpAnalytics` — Vite, CRA, etc. (no automatic route tracking; call `trackPageView` yourself or use your router)
@@ -284,6 +326,7 @@ When present, `category` is an optional section label you can use to group FAQs
284
326
  - `getJmpBlogPosts(config)`
285
327
  - `getJmpBlogPost(config, slug)`
286
328
  - `getJmpFaqs(config)`
329
+ - `getJmpLandingPages`, `getJmpLandingPageDetail`, `getJmpLandingPageSitemap` (since **0.1.26**; route helpers **0.1.27**)
287
330
  - `getJmpSiteProfile(config)` — contact + `extras` (since **0.1.8**)
288
331
  - `getJmpRobotsTxt(config)`
289
332
  - `createJmpContentClient(config)` — includes `getSiteProfile()`
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Client-facing entry: npx @jmp-technologies/analytics init
4
+ * Delegates to create-jmp-client when available (monorepo or installed sibling).
5
+ */
6
+
7
+ import { spawn } from "node:child_process"
8
+ import fs from "node:fs"
9
+ import path from "node:path"
10
+ import { fileURLToPath } from "node:url"
11
+
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
13
+ const analyticsRoot = path.resolve(__dirname, "..")
14
+
15
+ const candidates = [
16
+ path.join(analyticsRoot, "../create-jmp-client/bin/create-jmp-client.mjs"),
17
+ path.join(analyticsRoot, "node_modules/@jmp-technologies/create-jmp-client/bin/create-jmp-client.mjs"),
18
+ ]
19
+
20
+ const cli = candidates.find((p) => fs.existsSync(p))
21
+
22
+ if (!cli) {
23
+ console.error(
24
+ "jmp-init: create-jmp-client not found.\n" +
25
+ "From jmp-dashboard: npm run create-jmp-client -- --tier cms -d .\n" +
26
+ "Or install: npm i -D @jmp-technologies/create-jmp-client",
27
+ )
28
+ process.exit(1)
29
+ }
30
+
31
+ const child = spawn(process.execPath, [cli, ...process.argv.slice(2)], {
32
+ stdio: "inherit",
33
+ })
34
+ child.on("close", (code) => process.exit(code ?? 1))
@@ -288,7 +288,7 @@ function classifyContactHref(href) {
288
288
  }
289
289
 
290
290
  // src/constants.ts
291
- var JMP_ANALYTICS_SDK_VERSION = "0.1.25";
291
+ var JMP_ANALYTICS_SDK_VERSION = "0.1.26";
292
292
  var JMP_TRACKING_CONSENT_STORAGE_KEY = "jmp_analytics_consent_v1";
293
293
  var DEFAULT_JMP_BROWSER_PAGE_VIEW_DEDUPE_MS = 2500;
294
294
 
@@ -755,6 +755,209 @@ function createJmpAnalytics(config) {
755
755
  };
756
756
  }
757
757
 
758
+ // src/content/manifest.ts
759
+ var JMP_CONTENT_MANIFEST_VERSION = 1;
760
+ var TRUST_SECTIONS = /* @__PURE__ */ new Set([
761
+ "summary",
762
+ "testimonials",
763
+ "reviews",
764
+ "stats",
765
+ "logos",
766
+ "credentials"
767
+ ]);
768
+ function normalizeKey(value) {
769
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
770
+ }
771
+ function isValidExtraKey(key) {
772
+ return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(key);
773
+ }
774
+ var MAX_EXTERNAL_ADMIN_LINKS = 10;
775
+ var MAX_EXTERNAL_ADMIN_LINK_LABEL_LENGTH = 80;
776
+ var EXTERNAL_ADMIN_LINK_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
777
+ function tryNormalizeHttpUrl(input) {
778
+ if (typeof input !== "string" || !input.trim()) {
779
+ return null;
780
+ }
781
+ try {
782
+ const url = new URL(input.trim());
783
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
784
+ return null;
785
+ }
786
+ return url.toString().replace(/\/$/, "");
787
+ } catch {
788
+ return null;
789
+ }
790
+ }
791
+ function normalizeShopifyExternalSource(shopify) {
792
+ if (!shopify || typeof shopify !== "object") {
793
+ return null;
794
+ }
795
+ const adminUrl = tryNormalizeHttpUrl(shopify.adminUrl);
796
+ const storeUrl = tryNormalizeHttpUrl(shopify.storeUrl);
797
+ if (!adminUrl && !storeUrl) {
798
+ return null;
799
+ }
800
+ return {
801
+ ...adminUrl ? { adminUrl } : {},
802
+ ...storeUrl ? { storeUrl } : {}
803
+ };
804
+ }
805
+ function normalizeExternalAdminLinkEntries(links) {
806
+ if (!Array.isArray(links)) {
807
+ return [];
808
+ }
809
+ const seenIds = /* @__PURE__ */ new Set();
810
+ const out = [];
811
+ for (const raw of links) {
812
+ if (!raw || typeof raw !== "object" || out.length >= MAX_EXTERNAL_ADMIN_LINKS) {
813
+ continue;
814
+ }
815
+ const id = typeof raw.id === "string" ? raw.id.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "") : "";
816
+ const label = typeof raw.label === "string" ? raw.label.trim().slice(0, MAX_EXTERNAL_ADMIN_LINK_LABEL_LENGTH) : "";
817
+ const url = tryNormalizeHttpUrl(raw.url);
818
+ if (!id || !label || !url || !EXTERNAL_ADMIN_LINK_ID_PATTERN.test(id) || seenIds.has(id)) {
819
+ continue;
820
+ }
821
+ seenIds.add(id);
822
+ out.push({ id, label, url });
823
+ }
824
+ return out;
825
+ }
826
+ function normalizeExternalSources(externalSources) {
827
+ if (!externalSources || typeof externalSources !== "object") {
828
+ return null;
829
+ }
830
+ const studioUrl = tryNormalizeHttpUrl(externalSources.sanity?.studioUrl);
831
+ const shopify = normalizeShopifyExternalSource(externalSources.shopify);
832
+ const links = normalizeExternalAdminLinkEntries(externalSources.links);
833
+ if (!studioUrl && !shopify && links.length === 0) {
834
+ return null;
835
+ }
836
+ return {
837
+ ...studioUrl ? { sanity: { studioUrl } } : {},
838
+ ...shopify ? { shopify } : {},
839
+ ...links.length > 0 ? { links } : {}
840
+ };
841
+ }
842
+ function normalizeJmpContentManifest(input) {
843
+ if (!input || typeof input !== "object") {
844
+ return null;
845
+ }
846
+ const routes = normalizeRoutes(input.routes);
847
+ const externalSources = normalizeExternalSources(input.externalSources);
848
+ const trust = normalizeTrust(input.trust);
849
+ const contact = normalizeContact(input.contact);
850
+ if (routes.length === 0 && !externalSources && !trust && !contact) {
851
+ return null;
852
+ }
853
+ return {
854
+ version: JMP_CONTENT_MANIFEST_VERSION,
855
+ ...routes.length > 0 ? { routes } : {},
856
+ ...externalSources ? { externalSources } : {},
857
+ ...trust ? { trust } : {},
858
+ ...contact ? { contact } : {}
859
+ };
860
+ }
861
+ function normalizeRoutes(routes) {
862
+ if (!Array.isArray(routes)) {
863
+ return [];
864
+ }
865
+ const seen = /* @__PURE__ */ new Set();
866
+ const out = [];
867
+ for (const raw of routes) {
868
+ if (!raw || typeof raw !== "object") continue;
869
+ const wrapper = typeof raw.wrapper === "string" ? raw.wrapper.trim().toLowerCase() : "";
870
+ const pattern = typeof raw.pattern === "string" ? raw.pattern.trim() : "";
871
+ if (!wrapper || !pattern) continue;
872
+ const dedupe = `${wrapper}:${pattern}`;
873
+ if (seen.has(dedupe)) continue;
874
+ seen.add(dedupe);
875
+ out.push({
876
+ wrapper,
877
+ pattern,
878
+ ...typeof raw.index === "string" && raw.index.trim() ? { index: raw.index.trim() } : {},
879
+ ...typeof raw.label === "string" && raw.label.trim() ? { label: raw.label.trim() } : {}
880
+ });
881
+ }
882
+ return out;
883
+ }
884
+ function normalizeTrust(trust) {
885
+ if (!trust || typeof trust !== "object") return null;
886
+ const blocks = normalizeTrustBlocks(trust.blocks);
887
+ const labels = normalizeTrustLabels(trust.labels);
888
+ const mergeTestimonialsReviews = trust.mergeTestimonialsReviews === true;
889
+ if (blocks.length === 0 && !labels && !mergeTestimonialsReviews) return null;
890
+ return {
891
+ ...blocks.length > 0 ? { blocks } : {},
892
+ ...labels ? { labels } : {},
893
+ ...mergeTestimonialsReviews ? { mergeTestimonialsReviews: true } : {}
894
+ };
895
+ }
896
+ function normalizeTrustBlocks(blocks) {
897
+ if (!Array.isArray(blocks)) return [];
898
+ const seen = /* @__PURE__ */ new Set();
899
+ const out = [];
900
+ for (const raw of blocks) {
901
+ if (!raw || typeof raw !== "object") continue;
902
+ const section = typeof raw.section === "string" ? raw.section.trim().toLowerCase() : "";
903
+ if (!TRUST_SECTIONS.has(section) || seen.has(section)) continue;
904
+ seen.add(section);
905
+ out.push({
906
+ section,
907
+ ...typeof raw.label === "string" && raw.label.trim() ? { label: raw.label.trim() } : {}
908
+ });
909
+ }
910
+ return out;
911
+ }
912
+ function normalizeTrustLabels(labels) {
913
+ if (!labels || typeof labels !== "object") return null;
914
+ const out = {};
915
+ for (const [key, value] of Object.entries(labels)) {
916
+ if (TRUST_SECTIONS.has(key) && typeof value === "string" && value.trim()) {
917
+ out[key] = value.trim();
918
+ }
919
+ }
920
+ return Object.keys(out).length > 0 ? out : null;
921
+ }
922
+ function normalizeContact(contact) {
923
+ if (!contact || typeof contact !== "object") return null;
924
+ const extras = normalizeContactExtras(contact.extras);
925
+ return extras.length > 0 ? { extras } : null;
926
+ }
927
+ function normalizeContactExtras(extras) {
928
+ if (!Array.isArray(extras)) return [];
929
+ const seen = /* @__PURE__ */ new Set();
930
+ const out = [];
931
+ for (const raw of extras) {
932
+ if (!raw || typeof raw !== "object") continue;
933
+ const label = typeof raw.label === "string" ? raw.label.trim() : "";
934
+ const key = normalizeKey(
935
+ typeof raw.key === "string" && raw.key.trim() ? raw.key : label
936
+ );
937
+ if (!key || !label || !isValidExtraKey(key) || seen.has(key)) continue;
938
+ seen.add(key);
939
+ out.push({ key, label });
940
+ }
941
+ return out;
942
+ }
943
+ function stableJmpManifestFingerprint(manifest) {
944
+ return manifest ? JSON.stringify(manifest) : "";
945
+ }
946
+ function inferWrapperSlotsFromManifest(manifest) {
947
+ const slots = /* @__PURE__ */ new Set();
948
+ for (const route of manifest?.routes ?? []) {
949
+ slots.add(route.wrapper);
950
+ }
951
+ for (const block of manifest?.trust?.blocks ?? []) {
952
+ slots.add(`trust.${block.section}`);
953
+ slots.add("trust");
954
+ }
955
+ if (manifest?.contact?.extras?.length) {
956
+ slots.add("contact");
957
+ }
958
+ return [...slots];
959
+ }
960
+
758
961
  // src/content/wrapper-discovery.ts
759
962
  var WRAPPER_SLOT_ORDER = [
760
963
  "blog",
@@ -774,16 +977,22 @@ var syncHandler = null;
774
977
  function getDiscoveredContentWrappers() {
775
978
  return sortWrapperSlots([...discovered]);
776
979
  }
980
+ function resolveContentManifestForReport(config) {
981
+ return normalizeJmpContentManifest(config.contentManifest);
982
+ }
777
983
  function resolveContentWrappersForReport(config) {
778
984
  const explicit = config.contentWrappers ?? [];
779
985
  const auto = getDiscoveredContentWrappers();
986
+ const fromManifest = inferWrapperSlotsFromManifest(
987
+ resolveContentManifestForReport(config)
988
+ );
780
989
  if (config.contentWrapperMode === "manual") {
781
- return sortWrapperSlots([...explicit]);
990
+ return sortWrapperSlots([...explicit, ...fromManifest]);
782
991
  }
783
992
  if (config.contentWrapperMode === "explicit-only") {
784
- return sortWrapperSlots([...explicit]);
993
+ return sortWrapperSlots([...explicit, ...fromManifest]);
785
994
  }
786
- const merged = /* @__PURE__ */ new Set([...explicit, ...auto]);
995
+ const merged = /* @__PURE__ */ new Set([...explicit, ...auto, ...fromManifest]);
787
996
  return sortWrapperSlots([...merged]);
788
997
  }
789
998
  function hasReportedContentWrappers(dedupeKey) {
@@ -813,7 +1022,8 @@ async function registerJmpContentWrappers(config) {
813
1022
  if (!wrappers.length) {
814
1023
  return { ok: false, skipped: true };
815
1024
  }
816
- const dedupeKey = `${config.trackingKey}:${[...wrappers].sort().join(",")}`;
1025
+ const manifest = resolveContentManifestForReport(config);
1026
+ const dedupeKey = `${config.trackingKey}:${[...wrappers].sort().join(",")}:${stableJmpManifestFingerprint(manifest)}`;
817
1027
  if (hasReportedContentWrappers(dedupeKey)) {
818
1028
  return { ok: true, wrappers };
819
1029
  }
@@ -826,7 +1036,17 @@ async function registerJmpContentWrappers(config) {
826
1036
  }
827
1037
  const url = `${baseUrl}/api/content/capabilities`;
828
1038
  if (config.debug) {
829
- console.debug("[@jmp-technologies/analytics]", "POST", url, { wrappers });
1039
+ console.debug("[@jmp-technologies/analytics]", "POST", url, {
1040
+ wrappers,
1041
+ manifest
1042
+ });
1043
+ }
1044
+ const body = {
1045
+ wrappers,
1046
+ sdkVersion: JMP_ANALYTICS_SDK_VERSION
1047
+ };
1048
+ if (manifest) {
1049
+ body.manifest = manifest;
830
1050
  }
831
1051
  const res = await resolvedFetch(url, {
832
1052
  method: "POST",
@@ -834,10 +1054,7 @@ async function registerJmpContentWrappers(config) {
834
1054
  "Content-Type": "application/json",
835
1055
  "X-JMP-Tracking-Key": config.trackingKey
836
1056
  },
837
- body: JSON.stringify({
838
- wrappers,
839
- sdkVersion: JMP_ANALYTICS_SDK_VERSION
840
- })
1057
+ body: JSON.stringify(body)
841
1058
  });
842
1059
  const text = await res.text();
843
1060
  if (!res.ok) {