@brandlab-digital/site-next 2.1.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analytics.d.ts +111 -52
- package/dist/analytics.d.ts.map +1 -1
- package/dist/analytics.js +281 -153
- package/dist/analytics.js.map +1 -1
- package/dist/blog.d.ts +4 -2
- package/dist/blog.d.ts.map +1 -1
- package/dist/blog.js +7 -7
- package/dist/blog.js.map +1 -1
- package/dist/contact/client-entry.d.ts +2 -0
- package/dist/contact/client-entry.d.ts.map +1 -1
- package/dist/contact/client-entry.js +1 -0
- package/dist/contact/client-entry.js.map +1 -1
- package/dist/contact/client.d.ts +12 -22
- package/dist/contact/client.d.ts.map +1 -1
- package/dist/contact/client.js +28 -21
- package/dist/contact/client.js.map +1 -1
- package/dist/contact/form-client.d.ts +159 -0
- package/dist/contact/form-client.d.ts.map +1 -0
- package/dist/contact/form-client.js +257 -0
- package/dist/contact/form-client.js.map +1 -0
- package/dist/contact/index.d.ts +1 -1
- package/dist/contact/index.d.ts.map +1 -1
- package/dist/contact/index.js +1 -1
- package/dist/contact/index.js.map +1 -1
- package/dist/contact/route-handler.d.ts +32 -0
- package/dist/contact/route-handler.d.ts.map +1 -0
- package/dist/contact/route-handler.js +44 -0
- package/dist/contact/route-handler.js.map +1 -0
- package/dist/contact/server-entry.d.ts +1 -0
- package/dist/contact/server-entry.d.ts.map +1 -1
- package/dist/contact/server-entry.js +1 -0
- package/dist/contact/server-entry.js.map +1 -1
- package/dist/contact/server.d.ts +5 -0
- package/dist/contact/server.d.ts.map +1 -1
- package/dist/contact/server.js +13 -7
- package/dist/contact/server.js.map +1 -1
- package/dist/contact/shared.d.ts +4 -0
- package/dist/contact/shared.d.ts.map +1 -1
- package/dist/contact/shared.js.map +1 -1
- package/dist/contact/testing.d.ts +29 -0
- package/dist/contact/testing.d.ts.map +1 -1
- package/dist/contact/testing.js +27 -0
- package/dist/contact/testing.js.map +1 -1
- package/dist/google-tag-plan.d.ts +1 -4
- package/dist/google-tag-plan.d.ts.map +1 -1
- package/dist/google-tag-plan.js +1 -14
- package/dist/google-tag-plan.js.map +1 -1
- package/dist/google-tag.d.ts +5 -4
- package/dist/google-tag.d.ts.map +1 -1
- package/dist/google-tag.js +5 -4
- package/dist/google-tag.js.map +1 -1
- package/dist/headers.d.ts +54 -27
- package/dist/headers.d.ts.map +1 -1
- package/dist/headers.js +122 -51
- package/dist/headers.js.map +1 -1
- package/dist/instrumentation-client.d.ts +21 -1
- package/dist/instrumentation-client.d.ts.map +1 -1
- package/dist/instrumentation-client.js +62 -7
- package/dist/instrumentation-client.js.map +1 -1
- package/dist/lead-registry.d.ts +34 -0
- package/dist/lead-registry.d.ts.map +1 -0
- package/dist/lead-registry.js +30 -0
- package/dist/lead-registry.js.map +1 -0
- package/dist/lead-reporting.d.ts +37 -0
- package/dist/lead-reporting.d.ts.map +1 -0
- package/dist/lead-reporting.js +74 -0
- package/dist/lead-reporting.js.map +1 -0
- package/dist/metadata.d.ts +10 -11
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +17 -19
- package/dist/metadata.js.map +1 -1
- package/dist/next-config.d.ts +113 -0
- package/dist/next-config.d.ts.map +1 -0
- package/dist/next-config.js +213 -0
- package/dist/next-config.js.map +1 -0
- package/dist/robots.d.ts +14 -6
- package/dist/robots.d.ts.map +1 -1
- package/dist/robots.js +26 -17
- package/dist/robots.js.map +1 -1
- package/dist/sitemaps.d.ts +34 -0
- package/dist/sitemaps.d.ts.map +1 -1
- package/dist/sitemaps.js +66 -1
- package/dist/sitemaps.js.map +1 -1
- package/dist/testing/server-only.d.ts +8 -0
- package/dist/testing/server-only.d.ts.map +1 -0
- package/dist/testing/server-only.js +8 -0
- package/dist/testing/server-only.js.map +1 -0
- package/dist/ui-client.d.ts +1 -12
- package/dist/ui-client.d.ts.map +1 -1
- package/dist/ui-client.js +0 -17
- package/dist/ui-client.js.map +1 -1
- package/dist/ui.d.ts +22 -2
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +67 -6
- package/dist/ui.js.map +1 -1
- package/package.json +16 -3
package/dist/headers.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"headers.js","sourceRoot":"","sources":["../src/headers.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Brandlab security/response headers (Brandlab Project Standard §2).\n *\n * One canonical header set for every client site, composed into\n * `next.config.ts`:\n *\n * async headers() {\n * return buildBrandlabHeaderRules({\n * isProduction,\n * profile: 'next-local-service',\n * })\n * }\n *\n * The X-Robots-Tag here carries snippet-display controls only; per-page\n * indexation stays owned by Metadata.robots (noindex pages override via\n * their <meta name=\"robots\">).\n */\n\nexport interface BrandlabHeader {\n key: string\n value: string\n}\n\nexport interface BrandlabHeaderRule {\n source: string\n headers: BrandlabHeader[]\n}\n\nexport const BRANDLAB_NEXT_HEADER_PROFILES = [\n 'next-local-service',\n 'next-root-level',\n] as const\n\nexport type BrandlabNextHeaderProfile = (typeof BRANDLAB_NEXT_HEADER_PROFILES)[number]\n\nexport const BRANDLAB_DEFAULT_PERMISSIONS_POLICY =\n 'camera=(), microphone=(), geolocation=()'\n\nexport const BRANDLAB_DEFAULT_X_ROBOTS_TAG =\n 'max-snippet:-1, max-image-preview:large, max-video-preview:-1'\n\nexport const BRANDLAB_HSTS_VALUE =\n 'max-age=31536000'\n\nexport interface BrandlabHstsExpansion {\n includeSubDomains: true\n preload?: true\n /** Reviewed proof that every affected subdomain is HTTPS-ready. */\n readinessEvidence: string\n}\n\nconst HEADER_NAME_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/\nconst PROTECTED_HEADER_NAMES = new Set([\n 'content-security-policy',\n 'content-security-policy-report-only',\n 'x-frame-options',\n 'x-content-type-options',\n 'referrer-policy',\n 'permissions-policy',\n 'x-robots-tag',\n 'strict-transport-security',\n])\n\nexport interface BrandlabContentSecurityPolicyExtensions {\n scriptSrc?: readonly string[]\n connectSrc?: readonly string[]\n frameSrc?: readonly string[]\n}\n\nexport type BrandlabContentSecurityPolicyMode = 'enforce' | 'report-only'\n\nexport interface BrandlabSecurityHeaderOptions {\n isProduction: boolean\n profile: BrandlabNextHeaderProfile\n permissionsPolicy?: string\n /** Pass null to omit the snippet-control header entirely. */\n xRobotsTag?: string | null\n extraHeaders?: readonly BrandlabHeader[]\n hsts?: BrandlabHstsExpansion\n contentSecurityPolicyMode?: BrandlabContentSecurityPolicyMode\n /** Reviewed HTTPS origins needed by an approved project integration. */\n contentSecurityPolicyExtensions?: BrandlabContentSecurityPolicyExtensions\n}\n\nexport function buildBrandlabContentSecurityPolicy(options: {\n isProduction: boolean\n profile: BrandlabNextHeaderProfile\n contentSecurityPolicyExtensions?: BrandlabContentSecurityPolicyExtensions\n}) {\n const profile = options.profile\n if (!BRANDLAB_NEXT_HEADER_PROFILES.includes(profile)) {\n throw new Error(`Unsupported Brandlab Next header profile \"${profile}\".`)\n }\n\n const scriptSources = [\n \"'self'\",\n \"'unsafe-inline'\",\n 'https://www.googletagmanager.com',\n 'https://www.googleadservices.com',\n 'https://www.google.com',\n 'https://www.gstatic.com',\n 'https://pagead2.googlesyndication.com',\n 'https://googleads.g.doubleclick.net',\n 'https://challenges.cloudflare.com',\n ...normalizeCspExtensions(options.contentSecurityPolicyExtensions?.scriptSrc, 'scriptSrc'),\n ...(options.isProduction ? [] : [\"'unsafe-eval'\", 'http://localhost:*']),\n ]\n const connectSources = [\n \"'self'\",\n 'https://www.google-analytics.com',\n 'https://*.google-analytics.com',\n 'https://www.googletagmanager.com',\n 'https://www.googleadservices.com',\n 'https://*.doubleclick.net',\n 'https://*.google.com',\n 'https://google.com',\n 'https://www.google.com.au',\n 'https://pagead2.googlesyndication.com',\n 'https://*.googletagmanager.com',\n 'https://*.analytics.google.com',\n 'https://challenges.cloudflare.com',\n ...normalizeCspExtensions(options.contentSecurityPolicyExtensions?.connectSrc, 'connectSrc'),\n ...(options.isProduction ? [] : ['http://localhost:*', 'ws://localhost:*']),\n ]\n const directives = [\n \"default-src 'self'\",\n \"base-uri 'self'\",\n \"object-src 'none'\",\n \"frame-ancestors 'none'\",\n \"form-action 'self'\",\n `script-src ${scriptSources.join(' ')}`,\n \"style-src 'self' 'unsafe-inline'\",\n \"font-src 'self' data:\",\n \"img-src 'self' data: blob: https:\",\n `connect-src ${connectSources.join(' ')}`,\n `frame-src 'self' https://challenges.cloudflare.com https://www.googletagmanager.com https://www.google.com ${normalizeCspExtensions(\n options.contentSecurityPolicyExtensions?.frameSrc,\n 'frameSrc',\n ).join(' ')}`.trim(),\n \"manifest-src 'self'\",\n \"worker-src 'self' blob:\",\n ...(options.isProduction ? ['upgrade-insecure-requests'] : []),\n ]\n return directives.join('; ')\n}\n\nfunction normalizeCspExtensions(\n sources: readonly string[] | undefined,\n field: keyof BrandlabContentSecurityPolicyExtensions,\n) {\n if (!sources) return []\n const normalized = sources.map((source) => {\n const value = source.trim().toLowerCase()\n if (!/^https:\\/\\/(?:\\*\\.)?[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?(?::\\d{1,5})?$/.test(value)) {\n throw new Error(`${field} entries must be HTTPS origins without paths or CSP keywords.`)\n }\n return value\n })\n if (new Set(normalized).size !== normalized.length) {\n throw new Error(`${field} entries must be unique.`)\n }\n return normalized\n}\n\nfunction assertHeaderValue(value: string, field: string) {\n if (!value || value.length > 4_096 || /[\\u0000-\\u001F\\u007F]/.test(value)) {\n throw new Error(`${field} must be a bounded single-line header value.`)\n }\n}\n\nfunction validateExtraHeaders(headers: readonly BrandlabHeader[]) {\n const names = new Set<string>()\n headers.forEach((header, index) => {\n if (!HEADER_NAME_PATTERN.test(header.key)) {\n throw new Error(`extraHeaders[${index}].key is invalid.`)\n }\n const name = header.key.toLowerCase()\n if (PROTECTED_HEADER_NAMES.has(name) || names.has(name)) {\n throw new Error(`extraHeaders[${index}] duplicates a protected or existing header.`)\n }\n names.add(name)\n assertHeaderValue(header.value, `extraHeaders[${index}].value`)\n })\n}\n\nexport function buildBrandlabSecurityHeaders(\n options: BrandlabSecurityHeaderOptions,\n): BrandlabHeader[] {\n if (typeof options.isProduction !== 'boolean') {\n throw new Error('isProduction must be boolean.')\n }\n const xRobotsTag =\n options.xRobotsTag === undefined ? BRANDLAB_DEFAULT_X_ROBOTS_TAG : options.xRobotsTag\n const permissionsPolicy =\n options.permissionsPolicy ?? BRANDLAB_DEFAULT_PERMISSIONS_POLICY\n assertHeaderValue(permissionsPolicy, 'permissionsPolicy')\n if (xRobotsTag !== null) assertHeaderValue(xRobotsTag, 'xRobotsTag')\n validateExtraHeaders(options.extraHeaders ?? [])\n if (options.hsts) {\n assertHeaderValue(options.hsts.readinessEvidence, 'hsts.readinessEvidence')\n if (options.hsts.preload && !options.hsts.includeSubDomains) {\n throw new Error('HSTS preload requires includeSubDomains readiness.')\n }\n }\n const hstsValue = options.hsts\n ? `${BRANDLAB_HSTS_VALUE}; includeSubDomains${options.hsts.preload ? '; preload' : ''}`\n : BRANDLAB_HSTS_VALUE\n const cspMode = options.contentSecurityPolicyMode ?? (\n options.isProduction ? 'enforce' : 'report-only'\n )\n if (!new Set<BrandlabContentSecurityPolicyMode>(['enforce', 'report-only']).has(cspMode)) {\n throw new Error(`Unsupported contentSecurityPolicyMode \"${cspMode}\".`)\n }\n if (options.isProduction && cspMode !== 'enforce') {\n throw new Error('Production Content Security Policy must use enforce mode.')\n }\n const cspHeader = [{\n key: cspMode === 'enforce'\n ? 'Content-Security-Policy'\n : 'Content-Security-Policy-Report-Only',\n value: buildBrandlabContentSecurityPolicy(options),\n }]\n\n return [\n { key: 'X-Frame-Options', value: 'DENY' },\n { key: 'X-Content-Type-Options', value: 'nosniff' },\n { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },\n {\n key: 'Permissions-Policy',\n value: permissionsPolicy,\n },\n ...(xRobotsTag ? [{ key: 'X-Robots-Tag', value: xRobotsTag }] : []),\n ...(options.isProduction\n ? [{ key: 'Strict-Transport-Security', value: hstsValue }]\n : []),\n ...cspHeader,\n ...(options.extraHeaders ?? []),\n ]\n}\n\nexport function buildBrandlabHeaderRules(options: BrandlabSecurityHeaderOptions & {\n source?: string\n}): BrandlabHeaderRule[] {\n const source = options.source ?? '/:path*'\n if (!source.startsWith('/') || /[\\u0000-\\u001F\\u007F]/.test(source)) {\n throw new Error('Header-rule source must be a root-relative single-line pattern.')\n }\n return [\n {\n source,\n headers: buildBrandlabSecurityHeaders(options),\n },\n ]\n}\n"]}
|
|
1
|
+
{"version":3,"file":"headers.js","sourceRoot":"","sources":["../src/headers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAiBH,MAAM,CAAC,MAAM,mCAAmC,GAC9C,0CAA0C,CAAA;AAE5C,MAAM,CAAC,MAAM,6BAA6B,GACxC,+DAA+D,CAAA;AAEjE,MAAM,CAAC,MAAM,6BAA6B,GAAG,8BAA8B,CAAA;AAE3E,MAAM,CAAC,MAAM,8BAA8B,GAAG,8BAA8B,CAAA;AAE5E,MAAM,CAAC,MAAM,mBAAmB,GAC9B,kBAAkB,CAAA;AAEpB,iFAAiF;AACjF,MAAM,CAAC,MAAM,mCAAmC,GAAG,qCAAqC,CAAA;AASxF,MAAM,mBAAmB,GAAG,+BAA+B,CAAA;AAC3D,MAAM,sBAAsB,GAAG,IAAI,GAAG,CAAC;IACrC,eAAe;IACf,yBAAyB;IACzB,qCAAqC;IACrC,iBAAiB;IACjB,wBAAwB;IACxB,iBAAiB;IACjB,oBAAoB;IACpB,cAAc;IACd,2BAA2B;CAC5B,CAAC,CAAA;AACF,MAAM,uBAAuB,GAAG,IAAI,GAAG,CAAgC;IACrE,YAAY;IACZ,SAAS;IACT,aAAa;CACd,CAAC,CAAA;AAoBF,SAAS,gBAAgB,CAAC,UAAmB;IAC3C,MAAM,SAAS,GAAG,UAAqD,CAAA;IACvE,IACE,CAAC,SAAS;QACV,OAAO,SAAS,KAAK,QAAQ;QAC7B,CAAC,uBAAuB,CAAC,GAAG,CAAC,SAAS,CAAC,WAA4C,CAAC;QACpF,OAAO,SAAS,CAAC,eAAe,KAAK,SAAS,EAC9C,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,kFAAkF,CAAC,CAAA;IACrG,CAAC;AACH,CAAC;AAED,MAAM,UAAU,kCAAkC,CAAC,OAGlD;IACC,gBAAgB,CAAC,OAAO,CAAC,UAAU,CAAC,CAAA;IACpC,MAAM,eAAe,GAAG,OAAO,CAAC,UAAU,CAAC,eAAe,CAAA;IAE1D,MAAM,aAAa,GAAG;QACpB,QAAQ;QACR,iBAAiB;QACjB,kCAAkC;QAClC,kCAAkC;QAClC,wBAAwB;QACxB,yBAAyB;QACzB,uCAAuC;QACvC,qCAAqC;QACrC,mCAAmC;QACnC,GAAG,sBAAsB,CAAC,OAAO,CAAC,+BAA+B,EAAE,SAAS,EAAE,WAAW,CAAC;QAC1F,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,eAAe,EAAE,oBAAoB,CAAC,CAAC;KACpE,CAAA;IACD,MAAM,cAAc,GAAG;QACrB,QAAQ;QACR,kCAAkC;QAClC,gCAAgC;QAChC,kCAAkC;QAClC,kCAAkC;QAClC,2BAA2B;QAC3B,sBAAsB;QACtB,oBAAoB;QACpB,2BAA2B;QAC3B,uCAAuC;QACvC,gCAAgC;QAChC,gCAAgC;QAChC,mCAAmC;QACnC,GAAG,sBAAsB,CAAC,OAAO,CAAC,+BAA+B,EAAE,UAAU,EAAE,YAAY,CAAC;QAC5F,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,oBAAoB,EAAE,kBAAkB,CAAC,CAAC;KACvE,CAAA;IACD,MAAM,UAAU,GAAG;QACjB,oBAAoB;QACpB,iBAAiB;QACjB,mBAAmB;QACnB,wBAAwB;QACxB,oBAAoB;QACpB,cAAc,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE;QACvC,kCAAkC;QAClC,uBAAuB;QACvB,mCAAmC;QACnC,eAAe,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE;QACzC,8GAA8G,sBAAsB,CAClI,OAAO,CAAC,+BAA+B,EAAE,QAAQ,EACjD,UAAU,CACX,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE;QACpB,qBAAqB;QACrB,yBAAyB;QACzB,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,2BAA2B,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;KAC1D,CAAA;IACD,OAAO,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AAC9B,CAAC;AAED,SAAS,sBAAsB,CAC7B,OAAsC,EACtC,KAAoD;IAEpD,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,CAAA;IACvB,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE;QACxC,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;QACzC,IAAI,CAAC,oEAAoE,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YACtF,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,+DAA+D,CAAC,CAAA;QAC1F,CAAC;QACD,OAAO,KAAK,CAAA;IACd,CAAC,CAAC,CAAA;IACF,IAAI,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,KAAK,UAAU,CAAC,MAAM,EAAE,CAAC;QACnD,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,0BAA0B,CAAC,CAAA;IACrD,CAAC;IACD,OAAO,UAAU,CAAA;AACnB,CAAC;AAED,SAAS,iBAAiB,CAAC,KAAa,EAAE,KAAa;IACrD,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,GAAG,KAAK,IAAI,uBAAuB,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1E,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,8CAA8C,CAAC,CAAA;IACzE,CAAC;AACH,CAAC;AAED,SAAS,oBAAoB,CAAC,OAAkC;IAC9D,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAA;IAC/B,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE;QAChC,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;YAC1C,MAAM,IAAI,KAAK,CAAC,gBAAgB,KAAK,mBAAmB,CAAC,CAAA;QAC3D,CAAC;QACD,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,WAAW,EAAE,CAAA;QACrC,IAAI,sBAAsB,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YACxD,MAAM,IAAI,KAAK,CAAC,gBAAgB,KAAK,8CAA8C,CAAC,CAAA;QACtF,CAAC;QACD,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QACf,iBAAiB,CAAC,MAAM,CAAC,KAAK,EAAE,gBAAgB,KAAK,SAAS,CAAC,CAAA;IACjE,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAAC;IACjC,YAAY;IACZ,MAAM;IACN,iCAAiC;IACjC,mBAAmB;IACnB,cAAc;CACf,CAAC,CAAA;AAEF,MAAM,UAAU,4BAA4B,CAC1C,OAAsC;IAEtC,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;QAC5C,MAAM,IAAI,SAAS,CAAC,8BAA8B,CAAC,CAAA;IACrD,CAAC;IACD,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,kBAAkB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAA;IACtF,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CACb,4CAA4C,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,sDAAsD,CACzH,CAAA;IACH,CAAC;IACD,gBAAgB,CAAC,OAAO,CAAC,UAAU,CAAC,CAAA;IACpC,MAAM,EAAE,WAAW,EAAE,eAAe,EAAE,GAAG,OAAO,CAAC,UAAU,CAAA;IAC3D,MAAM,iBAAiB,GACrB,OAAO,CAAC,iBAAiB,IAAI,mCAAmC,CAAA;IAClE,iBAAiB,CAAC,iBAAiB,EAAE,mBAAmB,CAAC,CAAA;IACzD,oBAAoB,CAAC,OAAO,CAAC,YAAY,IAAI,EAAE,CAAC,CAAA;IAChD,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;QACjB,iBAAiB,CAAC,OAAO,CAAC,IAAI,CAAC,iBAAiB,EAAE,wBAAwB,CAAC,CAAA;QAC3E,IAAI,OAAO,CAAC,IAAI,CAAC,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,iBAAiB,EAAE,CAAC;YAC5D,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAA;QACvE,CAAC;IACH,CAAC;IACD,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI;QAC5B,CAAC,CAAC,GAAG,mBAAmB,sBAAsB,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,EAAE;QACvF,CAAC,CAAC,mBAAmB,CAAA;IACvB,MAAM,OAAO,GAAG,WAAW,KAAK,SAAS,CAAA;IAEzC,OAAO;QACL,EAAE,GAAG,EAAE,iBAAiB,EAAE,KAAK,EAAE,MAAM,EAAE;QACzC,EAAE,GAAG,EAAE,wBAAwB,EAAE,KAAK,EAAE,SAAS,EAAE;QACnD,EAAE,GAAG,EAAE,iBAAiB,EAAE,KAAK,EAAE,iCAAiC,EAAE;QACpE;YACE,GAAG,EAAE,oBAAoB;YACzB,KAAK,EAAE,iBAAiB;SACzB;QACD;YACE,GAAG,EAAE,cAAc;YACnB,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,6BAA6B,CAAC,CAAC,CAAC,6BAA6B;SAC/E;QACD,GAAG,CAAC,eAAe;YACjB,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,2BAA2B,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;YAC1D,CAAC,CAAC,EAAE,CAAC;QACP;YACE,GAAG,EAAE,eAAe;gBAClB,CAAC,CAAC,yBAAyB;gBAC3B,CAAC,CAAC,qCAAqC;YACzC,KAAK,EAAE,kCAAkC,CAAC,OAAO,CAAC;SACnD;QACD,GAAG,CAAC,OAAO,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,MAAM,EAAE,CAAC,CAAC;QAChE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,eAAe,EAAE,KAAK,EAAE,8BAA8B,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;KACtF,CAAA;AACH,CAAC;AAED,MAAM,mBAAmB,GAAG,wEAAwE,CAAA;AACpG,MAAM,qBAAqB,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC,CAAA;AAClE,MAAM,wBAAwB,GAAG,EAAE,CAAA;AACnC,MAAM,uBAAuB,GAAG,GAAG,CAAA;AAenC;;;;;GAKG;AACH,MAAM,UAAU,6BAA6B,CAAC,OAAgB;IAC5D,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CAAC,mEAAmE,CAAC,CAAA;IACtF,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,GAAG,wBAAwB,EAAE,CAAC;QAC9C,MAAM,IAAI,KAAK,CAAC,kCAAkC,wBAAwB,kBAAkB,CAAC,CAAA;IAC/F,CAAC;IACD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAA;IAC9B,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,MAAe,EAAE,KAAK,EAAE,EAAE;QAC5C,MAAM,KAAK,GAAG,oBAAoB,KAAK,GAAG,CAAA;QAC1C,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,GAAG,uBAAuB,EAAE,CAAC;YAC1E,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,gCAAgC,uBAAuB,cAAc,CAAC,CAAA;QAChG,CAAC;QACD,IAAI,qBAAqB,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;YACzE,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,KAAK,MAAM,+CAA+C,CAAC,CAAA;QACrF,CAAC;QACD,MAAM,KAAK,GAAG,mBAAmB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QAC9C,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,MAAM,IAAI,KAAK,CACb,GAAG,KAAK,KAAK,MAAM,0GAA0G,CAC9H,CAAA;QACH,CAAC;QACD,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,CAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QAChD,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,CAAA;QAC1C,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,iBAAiB,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,CAAA;QACvF,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QACb,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,qBAAqB,EAAE,CAAA;IAC/F,CAAC,CAAC,CAAA;AACJ,CAAC;AAYD,MAAM,UAAU,wBAAwB,CAAC,OAAkC;IACzE,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;QAC5C,MAAM,IAAI,SAAS,CAAC,8BAA8B,CAAC,CAAA;IACrD,CAAC;IACD,MAAM,EAAE,gBAAgB,EAAE,GAAG,QAAQ,EAAE,GAAG,OAAO,CAAA;IACjD,MAAM,OAAO,GAAG,gBAAgB,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,6BAA6B,CAAC,gBAAgB,CAAC,CAAA;IACrG,MAAM,KAAK,GAAyB;QAClC;YACE,MAAM,EAAE,SAAS;YACjB,OAAO,EAAE,4BAA4B,CAAC,QAAQ,CAAC;SAChD;KACF,CAAA;IACD,oFAAoF;IACpF,MAAM,EAAE,WAAW,EAAE,eAAe,EAAE,GAAG,OAAO,CAAC,UAAU,CAAA;IAC3D,IAAI,eAAe,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QACjD,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC7B,KAAK,CAAC,IAAI,CAAC;gBACT,MAAM,EAAE,MAAM,CAAC,UAAU;gBACzB,OAAO,EAAE,CAAC,EAAE,GAAG,EAAE,eAAe,EAAE,KAAK,EAAE,mCAAmC,EAAE,CAAC;aAChF,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC","sourcesContent":["/**\n * Brandlab security/response headers: one canonical header set for every\n * client site, emitted by `defineBrandlabNextConfig`.\n *\n * The deployment decides everything environment-specific:\n * - a production build (`next build`, on Vercel or locally) enforces the\n * production Content Security Policy and sends HSTS;\n * - `next dev` sends a report-only development policy without HSTS;\n * - a Vercel Preview additionally sends `noindex, nofollow, noarchive` and\n * `private, no-store`, so previews never reach search results or caches.\n *\n * The production X-Robots-Tag carries snippet-display controls only; per-page\n * indexation stays owned by Metadata.robots.\n *\n * Declared public folders of static files (`staticAssetCache`) are served with\n * `public, max-age=31536000, immutable` by production builds; Previews and\n * `next dev` never cache them.\n */\n\nimport type {\n BrandlabDeployment,\n BrandlabDeploymentEnvironment,\n} from '@brandlab-digital/site-core/site'\n\nexport interface BrandlabHeader {\n key: string\n value: string\n}\n\nexport interface BrandlabHeaderRule {\n source: string\n headers: BrandlabHeader[]\n}\n\nexport const BRANDLAB_DEFAULT_PERMISSIONS_POLICY =\n 'camera=(), microphone=(), geolocation=()'\n\nexport const BRANDLAB_DEFAULT_X_ROBOTS_TAG =\n 'max-snippet:-1, max-image-preview:large, max-video-preview:-1'\n\nexport const BRANDLAB_PREVIEW_X_ROBOTS_TAG = 'noindex, nofollow, noarchive'\n\nexport const BRANDLAB_PREVIEW_CACHE_CONTROL = 'private, no-store, max-age=0'\n\nexport const BRANDLAB_HSTS_VALUE =\n 'max-age=31536000'\n\n/** Cache-Control for the declared static public folders on production builds. */\nexport const BRANDLAB_STATIC_ASSET_CACHE_CONTROL = 'public, max-age=31536000, immutable'\n\nexport interface BrandlabHstsExpansion {\n includeSubDomains: true\n preload?: true\n /** Reviewed proof that every affected subdomain is HTTPS-ready. */\n readinessEvidence: string\n}\n\nconst HEADER_NAME_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/\nconst PROTECTED_HEADER_NAMES = new Set([\n 'cache-control',\n 'content-security-policy',\n 'content-security-policy-report-only',\n 'x-frame-options',\n 'x-content-type-options',\n 'referrer-policy',\n 'permissions-policy',\n 'x-robots-tag',\n 'strict-transport-security',\n])\nconst DEPLOYMENT_ENVIRONMENTS = new Set<BrandlabDeploymentEnvironment>([\n 'production',\n 'preview',\n 'development',\n])\n\nexport interface BrandlabContentSecurityPolicyExtensions {\n scriptSrc?: readonly string[]\n connectSrc?: readonly string[]\n frameSrc?: readonly string[]\n}\n\nexport interface BrandlabSecurityHeaderOptions {\n /** From resolveBrandlabDeployment(process.env). */\n deployment: BrandlabDeployment\n hsts?: BrandlabHstsExpansion\n /** Reviewed HTTPS origins needed by an approved project integration. */\n contentSecurityPolicyExtensions?: BrandlabContentSecurityPolicyExtensions\n /** Reviewed project exception, for example `geolocation=(self)` for a location feature. */\n permissionsPolicy?: string\n /** Reviewed project headers; they cannot replace a protected header. */\n extraHeaders?: readonly BrandlabHeader[]\n}\n\nfunction assertDeployment(deployment: unknown): asserts deployment is BrandlabDeployment {\n const candidate = deployment as Partial<BrandlabDeployment> | undefined\n if (\n !candidate ||\n typeof candidate !== 'object' ||\n !DEPLOYMENT_ENVIRONMENTS.has(candidate.environment as BrandlabDeploymentEnvironment) ||\n typeof candidate.productionBuild !== 'boolean'\n ) {\n throw new Error('Header rules require the deployment from resolveBrandlabDeployment(process.env).')\n }\n}\n\nexport function buildBrandlabContentSecurityPolicy(options: {\n deployment: BrandlabDeployment\n contentSecurityPolicyExtensions?: BrandlabContentSecurityPolicyExtensions\n}) {\n assertDeployment(options.deployment)\n const productionBuild = options.deployment.productionBuild\n\n const scriptSources = [\n \"'self'\",\n \"'unsafe-inline'\",\n 'https://www.googletagmanager.com',\n 'https://www.googleadservices.com',\n 'https://www.google.com',\n 'https://www.gstatic.com',\n 'https://pagead2.googlesyndication.com',\n 'https://googleads.g.doubleclick.net',\n 'https://challenges.cloudflare.com',\n ...normalizeCspExtensions(options.contentSecurityPolicyExtensions?.scriptSrc, 'scriptSrc'),\n ...(productionBuild ? [] : [\"'unsafe-eval'\", 'http://localhost:*']),\n ]\n const connectSources = [\n \"'self'\",\n 'https://www.google-analytics.com',\n 'https://*.google-analytics.com',\n 'https://www.googletagmanager.com',\n 'https://www.googleadservices.com',\n 'https://*.doubleclick.net',\n 'https://*.google.com',\n 'https://google.com',\n 'https://www.google.com.au',\n 'https://pagead2.googlesyndication.com',\n 'https://*.googletagmanager.com',\n 'https://*.analytics.google.com',\n 'https://challenges.cloudflare.com',\n ...normalizeCspExtensions(options.contentSecurityPolicyExtensions?.connectSrc, 'connectSrc'),\n ...(productionBuild ? [] : ['http://localhost:*', 'ws://localhost:*']),\n ]\n const directives = [\n \"default-src 'self'\",\n \"base-uri 'self'\",\n \"object-src 'none'\",\n \"frame-ancestors 'none'\",\n \"form-action 'self'\",\n `script-src ${scriptSources.join(' ')}`,\n \"style-src 'self' 'unsafe-inline'\",\n \"font-src 'self' data:\",\n \"img-src 'self' data: blob: https:\",\n `connect-src ${connectSources.join(' ')}`,\n `frame-src 'self' https://challenges.cloudflare.com https://www.googletagmanager.com https://www.google.com ${normalizeCspExtensions(\n options.contentSecurityPolicyExtensions?.frameSrc,\n 'frameSrc',\n ).join(' ')}`.trim(),\n \"manifest-src 'self'\",\n \"worker-src 'self' blob:\",\n ...(productionBuild ? ['upgrade-insecure-requests'] : []),\n ]\n return directives.join('; ')\n}\n\nfunction normalizeCspExtensions(\n sources: readonly string[] | undefined,\n field: keyof BrandlabContentSecurityPolicyExtensions,\n) {\n if (!sources) return []\n const normalized = sources.map((source) => {\n const value = source.trim().toLowerCase()\n if (!/^https:\\/\\/(?:\\*\\.)?[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?(?::\\d{1,5})?$/.test(value)) {\n throw new Error(`${field} entries must be HTTPS origins without paths or CSP keywords.`)\n }\n return value\n })\n if (new Set(normalized).size !== normalized.length) {\n throw new Error(`${field} entries must be unique.`)\n }\n return normalized\n}\n\nfunction assertHeaderValue(value: string, field: string) {\n if (!value || value.length > 4_096 || /[\\u0000-\\u001F\\u007F]/.test(value)) {\n throw new Error(`${field} must be a bounded single-line header value.`)\n }\n}\n\nfunction validateExtraHeaders(headers: readonly BrandlabHeader[]) {\n const names = new Set<string>()\n headers.forEach((header, index) => {\n if (!HEADER_NAME_PATTERN.test(header.key)) {\n throw new Error(`extraHeaders[${index}].key is invalid.`)\n }\n const name = header.key.toLowerCase()\n if (PROTECTED_HEADER_NAMES.has(name) || names.has(name)) {\n throw new Error(`extraHeaders[${index}] duplicates a protected or existing header.`)\n }\n names.add(name)\n assertHeaderValue(header.value, `extraHeaders[${index}].value`)\n })\n}\n\nconst HEADER_OPTION_KEYS = new Set([\n 'deployment',\n 'hsts',\n 'contentSecurityPolicyExtensions',\n 'permissionsPolicy',\n 'extraHeaders',\n])\n\nexport function buildBrandlabSecurityHeaders(\n options: BrandlabSecurityHeaderOptions,\n): BrandlabHeader[] {\n if (!options || typeof options !== 'object') {\n throw new TypeError('Header options are required.')\n }\n const unsupported = Object.keys(options).filter((key) => !HEADER_OPTION_KEYS.has(key))\n if (unsupported.length > 0) {\n throw new Error(\n `Header options contain unsupported keys: ${unsupported.join(', ')}. The deployment decides CSP, HSTS and X-Robots-Tag.`,\n )\n }\n assertDeployment(options.deployment)\n const { environment, productionBuild } = options.deployment\n const permissionsPolicy =\n options.permissionsPolicy ?? BRANDLAB_DEFAULT_PERMISSIONS_POLICY\n assertHeaderValue(permissionsPolicy, 'permissionsPolicy')\n validateExtraHeaders(options.extraHeaders ?? [])\n if (options.hsts) {\n assertHeaderValue(options.hsts.readinessEvidence, 'hsts.readinessEvidence')\n if (options.hsts.preload && !options.hsts.includeSubDomains) {\n throw new Error('HSTS preload requires includeSubDomains readiness.')\n }\n }\n const hstsValue = options.hsts\n ? `${BRANDLAB_HSTS_VALUE}; includeSubDomains${options.hsts.preload ? '; preload' : ''}`\n : BRANDLAB_HSTS_VALUE\n const preview = environment === 'preview'\n\n return [\n { key: 'X-Frame-Options', value: 'DENY' },\n { key: 'X-Content-Type-Options', value: 'nosniff' },\n { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },\n {\n key: 'Permissions-Policy',\n value: permissionsPolicy,\n },\n {\n key: 'X-Robots-Tag',\n value: preview ? BRANDLAB_PREVIEW_X_ROBOTS_TAG : BRANDLAB_DEFAULT_X_ROBOTS_TAG,\n },\n ...(productionBuild\n ? [{ key: 'Strict-Transport-Security', value: hstsValue }]\n : []),\n {\n key: productionBuild\n ? 'Content-Security-Policy'\n : 'Content-Security-Policy-Report-Only',\n value: buildBrandlabContentSecurityPolicy(options),\n },\n ...(options.extraHeaders ?? []).map((header) => ({ ...header })),\n ...(preview ? [{ key: 'Cache-Control', value: BRANDLAB_PREVIEW_CACHE_CONTROL }] : []),\n ]\n}\n\nconst STATIC_ASSET_SOURCE = /^\\/((?:[A-Za-z0-9][A-Za-z0-9._-]*\\/){1,4}):([A-Za-z_][A-Za-z0-9_]*)\\*$/\nconst STATIC_ASSET_RESERVED = new Set(['api', '_next', '_vercel'])\nconst STATIC_ASSET_MAX_SOURCES = 10\nconst STATIC_ASSET_MAX_LENGTH = 100\n\nexport interface BrandlabStaticAssetFolder {\n /** The declared source, for example `/images/:path*`. */\n source: string\n /** The public folder's path segments, for example `['images']`. */\n folder: readonly string[]\n /**\n * The emitted rule's source: file names with an extension below the folder,\n * such as `/images/:path(.+\\.[A-Za-z0-9]+)`. The folder itself and\n * extensionless paths, where a page would answer, never match.\n */\n fileSource: string\n}\n\n/**\n * Validates `staticAssetCache`: at most 10 public folders, each written as Next's\n * source `/<folder>/:path*` (one to four literal segments, then one catch-all\n * parameter, at most 100 characters). `/`, `/api`, `/_next` and `/_vercel` are\n * rejected.\n */\nexport function parseBrandlabStaticAssetCache(sources: unknown): BrandlabStaticAssetFolder[] {\n if (!Array.isArray(sources)) {\n throw new Error('staticAssetCache must list public folders such as /images/:path*.')\n }\n if (sources.length > STATIC_ASSET_MAX_SOURCES) {\n throw new Error(`staticAssetCache lists at most ${STATIC_ASSET_MAX_SOURCES} public folders.`)\n }\n const seen = new Set<string>()\n return sources.map((source: unknown, index) => {\n const field = `staticAssetCache[${index}]`\n if (typeof source !== 'string' || source.length > STATIC_ASSET_MAX_LENGTH) {\n throw new Error(`${field} must be a source of at most ${STATIC_ASSET_MAX_LENGTH} characters.`)\n }\n if (STATIC_ASSET_RESERVED.has(source.split('/')[1]?.toLowerCase() ?? '')) {\n throw new Error(`${field} \"${source}\" is a Next or API path, not a public folder.`)\n }\n const match = STATIC_ASSET_SOURCE.exec(source)\n if (!match) {\n throw new Error(\n `${field} \"${source}\" must name a public folder as /<folder>/:path* (literal folder segments, then one catch-all parameter).`,\n )\n }\n const folder = match[1]!.slice(0, -1).split('/')\n const key = folder.join('/').toLowerCase()\n if (seen.has(key)) throw new Error(`${field} repeats the /${folder.join('/')} folder.`)\n seen.add(key)\n return { source, folder, fileSource: `/${folder.join('/')}/:${match[2]}(.+\\\\.[A-Za-z0-9]+)` }\n })\n}\n\nexport interface BrandlabHeaderRuleOptions extends BrandlabSecurityHeaderOptions {\n /**\n * Public folders of static files that never change in place, for example\n * `['/images/:path*', '/fonts/:path*']`: production builds serve their files with\n * `public, max-age=31536000, immutable`. A file replaced under the same name stays\n * cached in browsers for up to a year, so give a changed file a new name.\n */\n staticAssetCache?: readonly string[]\n}\n\nexport function buildBrandlabHeaderRules(options: BrandlabHeaderRuleOptions): BrandlabHeaderRule[] {\n if (!options || typeof options !== 'object') {\n throw new TypeError('Header options are required.')\n }\n const { staticAssetCache, ...security } = options\n const folders = staticAssetCache === undefined ? [] : parseBrandlabStaticAssetCache(staticAssetCache)\n const rules: BrandlabHeaderRule[] = [\n {\n source: '/:path*',\n headers: buildBrandlabSecurityHeaders(security),\n },\n ]\n // A production build outside a Preview; Previews keep private, no-store everywhere.\n const { environment, productionBuild } = options.deployment\n if (productionBuild && environment !== 'preview') {\n for (const folder of folders) {\n rules.push({\n source: folder.fileSource,\n headers: [{ key: 'Cache-Control', value: BRANDLAB_STATIC_ASSET_CACHE_CONTROL }],\n })\n }\n }\n return rules\n}\n"]}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ContactAttributionSummary } from '@brandlab-digital/site-core/contact';
|
|
2
|
-
|
|
2
|
+
import { type BrandlabAnalytics, type PhoneCallPlacement } from './analytics.js';
|
|
3
|
+
export type PhoneCallLocation = PhoneCallPlacement;
|
|
3
4
|
export interface LastPhoneCallEvent {
|
|
4
5
|
key: string;
|
|
5
6
|
at: number;
|
|
@@ -17,6 +18,13 @@ export declare function shouldTrackPhoneCallClick(options: {
|
|
|
17
18
|
export declare function persistInitialLanding(win?: Window): void;
|
|
18
19
|
export declare function getInitialLandingPath(win?: Window): string | undefined;
|
|
19
20
|
export declare function getInitialContactAttribution(win?: Window): ContactAttributionSummary;
|
|
21
|
+
/**
|
|
22
|
+
* Placement of a telephone link: its own `data-phone-location`, then a
|
|
23
|
+
* `<header>` or `<footer>` ancestor, then the nearest ancestor declaring a
|
|
24
|
+
* valid `data-phone-location` (for example a hero or call-to-action section),
|
|
25
|
+
* otherwise `page`. Unrecognised values are ignored, so every tap still
|
|
26
|
+
* produces exactly one event.
|
|
27
|
+
*/
|
|
20
28
|
export declare function getPhoneCallLocation(link: HTMLAnchorElement): PhoneCallLocation;
|
|
21
29
|
export declare function installPhoneClickListener(options: {
|
|
22
30
|
trackPhoneLinkClick: (context: {
|
|
@@ -27,4 +35,16 @@ export declare function installPhoneClickListener(options: {
|
|
|
27
35
|
win?: Window;
|
|
28
36
|
dedupeWindowMs?: number;
|
|
29
37
|
}): () => void;
|
|
38
|
+
/**
|
|
39
|
+
* The whole of `src/instrumentation-client.ts`:
|
|
40
|
+
* `installBrandlabInstrumentation(analytics)`. Stores the privacy-safe landing
|
|
41
|
+
* path and visit source for contact attribution, and installs the single
|
|
42
|
+
* telephone-link listener that sends one `phone_link_click` per tap (rapid
|
|
43
|
+
* repeats of the same link are de-duplicated). Installing twice in one window
|
|
44
|
+
* is a no-op; the returned function removes the listener.
|
|
45
|
+
*/
|
|
46
|
+
export declare function installBrandlabInstrumentation(analytics: Pick<BrandlabAnalytics, 'trackPhoneLinkClick'>, options?: {
|
|
47
|
+
win?: Window;
|
|
48
|
+
dedupeWindowMs?: number;
|
|
49
|
+
}): () => void;
|
|
30
50
|
//# sourceMappingURL=instrumentation-client.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"instrumentation-client.d.ts","sourceRoot":"","sources":["../src/instrumentation-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,yBAAyB,EAC/B,MAAM,qCAAqC,CAAA;
|
|
1
|
+
{"version":3,"file":"instrumentation-client.d.ts","sourceRoot":"","sources":["../src/instrumentation-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,yBAAyB,EAC/B,MAAM,qCAAqC,CAAA;AAC5C,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACxB,MAAM,gBAAgB,CAAA;AAMvB,MAAM,MAAM,iBAAiB,GAAG,kBAAkB,CAAA;AAElD,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAA;IACX,EAAE,EAAE,MAAM,CAAA;CACX;AAED,eAAO,MAAM,8BAA8B,6BAA6B,CAAA;AACxE,eAAO,MAAM,mCAAmC,OAAO,CAAA;AAEvD,wBAAgB,2BAA2B,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,UAe9E;AAED,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,UAEtE;AAED,wBAAgB,yBAAyB,CAAC,OAAO,EAAE;IACjD,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,CAAC,EAAE,kBAAkB,CAAA;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB,WAOA;AAED,wBAAgB,qBAAqB,CAAC,GAAG,GAAE,MAAe,QAgCzD;AAED,wBAAgB,qBAAqB,CAAC,GAAG,GAAE,MAAe,sBAOzD;AAED,wBAAgB,4BAA4B,CAAC,GAAG,GAAE,MAAe,GAAG,yBAAyB,CAkB5F;AASD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,iBAAiB,GAAG,iBAAiB,CAc/E;AAED,wBAAgB,yBAAyB,CAAC,OAAO,EAAE;IACjD,mBAAmB,EAAE,CAAC,OAAO,EAAE;QAC7B,QAAQ,EAAE,MAAM,CAAA;QAChB,QAAQ,EAAE,iBAAiB,CAAA;QAC3B,kBAAkB,CAAC,EAAE,MAAM,CAAA;KAC5B,KAAK,IAAI,CAAA;IACV,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB,cA4CA;AAID;;;;;;;GAOG;AACH,wBAAgB,8BAA8B,CAC5C,SAAS,EAAE,IAAI,CAAC,iBAAiB,EAAE,qBAAqB,CAAC,EACzD,OAAO,GAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAAO,GACtD,MAAM,IAAI,CA2BZ"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isContactVisitSource, } from '@brandlab-digital/site-core/contact';
|
|
2
|
+
import { PHONE_CALL_PLACEMENTS, } from './analytics.js';
|
|
2
3
|
import { classifyContactAttribution, normalizeContactAttributionSummary, } from './contact/attribution.js';
|
|
3
4
|
export const INITIAL_LANDING_STORAGE_PREFIX = 'brandlab.initialLanding.';
|
|
4
5
|
export const DEFAULT_PHONE_CALL_DEDUPE_WINDOW_MS = 1200;
|
|
@@ -85,18 +86,32 @@ export function getInitialContactAttribution(win = window) {
|
|
|
85
86
|
return { initialLandingPath: fallbackPath, visitSource: 'direct-or-unknown' };
|
|
86
87
|
}
|
|
87
88
|
}
|
|
89
|
+
function placementValue(value) {
|
|
90
|
+
const normalized = value?.trim().toLowerCase();
|
|
91
|
+
return PHONE_CALL_PLACEMENTS.includes(normalized)
|
|
92
|
+
? normalized
|
|
93
|
+
: undefined;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Placement of a telephone link: its own `data-phone-location`, then a
|
|
97
|
+
* `<header>` or `<footer>` ancestor, then the nearest ancestor declaring a
|
|
98
|
+
* valid `data-phone-location` (for example a hero or call-to-action section),
|
|
99
|
+
* otherwise `page`. Unrecognised values are ignored, so every tap still
|
|
100
|
+
* produces exactly one event.
|
|
101
|
+
*/
|
|
88
102
|
export function getPhoneCallLocation(link) {
|
|
89
|
-
const
|
|
90
|
-
if (
|
|
91
|
-
return
|
|
103
|
+
const explicit = placementValue(link.getAttribute('data-phone-location'));
|
|
104
|
+
if (explicit)
|
|
105
|
+
return explicit;
|
|
92
106
|
if (link.closest('header'))
|
|
93
107
|
return 'header';
|
|
94
108
|
if (link.closest('footer'))
|
|
95
109
|
return 'footer';
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
110
|
+
for (let ancestor = link.parentElement?.closest('[data-phone-location]'); ancestor; ancestor = ancestor.parentElement?.closest('[data-phone-location]')) {
|
|
111
|
+
const placement = placementValue(ancestor.getAttribute('data-phone-location'));
|
|
112
|
+
if (placement)
|
|
113
|
+
return placement;
|
|
114
|
+
}
|
|
100
115
|
return 'page';
|
|
101
116
|
}
|
|
102
117
|
export function installPhoneClickListener(options) {
|
|
@@ -137,4 +152,44 @@ export function installPhoneClickListener(options) {
|
|
|
137
152
|
win.document.removeEventListener('click', handlePhoneClick, true);
|
|
138
153
|
};
|
|
139
154
|
}
|
|
155
|
+
const installedInstrumentation = new WeakMap();
|
|
156
|
+
/**
|
|
157
|
+
* The whole of `src/instrumentation-client.ts`:
|
|
158
|
+
* `installBrandlabInstrumentation(analytics)`. Stores the privacy-safe landing
|
|
159
|
+
* path and visit source for contact attribution, and installs the single
|
|
160
|
+
* telephone-link listener that sends one `phone_link_click` per tap (rapid
|
|
161
|
+
* repeats of the same link are de-duplicated). Installing twice in one window
|
|
162
|
+
* is a no-op; the returned function removes the listener.
|
|
163
|
+
*/
|
|
164
|
+
export function installBrandlabInstrumentation(analytics, options = {}) {
|
|
165
|
+
if (!analytics || typeof analytics.trackPhoneLinkClick !== 'function') {
|
|
166
|
+
throw new TypeError('installBrandlabInstrumentation requires the defineBrandlabAnalytics binding.');
|
|
167
|
+
}
|
|
168
|
+
const win = options.win ?? (typeof window === 'undefined' ? undefined : window);
|
|
169
|
+
if (!win)
|
|
170
|
+
return () => undefined;
|
|
171
|
+
const existing = installedInstrumentation.get(win);
|
|
172
|
+
if (existing)
|
|
173
|
+
return existing;
|
|
174
|
+
persistInitialLanding(win);
|
|
175
|
+
const removeListener = installPhoneClickListener({
|
|
176
|
+
win,
|
|
177
|
+
...(options.dedupeWindowMs !== undefined ? { dedupeWindowMs: options.dedupeWindowMs } : {}),
|
|
178
|
+
trackPhoneLinkClick: ({ pagePath, location }) => {
|
|
179
|
+
try {
|
|
180
|
+
analytics.trackPhoneLinkClick({ pagePath, placement: location });
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
// Tracking must never block the telephone action.
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
const teardown = () => {
|
|
188
|
+
removeListener();
|
|
189
|
+
if (installedInstrumentation.get(win) === teardown)
|
|
190
|
+
installedInstrumentation.delete(win);
|
|
191
|
+
};
|
|
192
|
+
installedInstrumentation.set(win, teardown);
|
|
193
|
+
return teardown;
|
|
194
|
+
}
|
|
140
195
|
//# sourceMappingURL=instrumentation-client.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"instrumentation-client.js","sourceRoot":"","sources":["../src/instrumentation-client.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n isContactVisitSource,\n type ContactAttributionSummary,\n} from '@brandlab-digital/site-core/contact'\nimport {\n classifyContactAttribution,\n normalizeContactAttributionSummary,\n} from './contact/attribution.js'\n\nexport type PhoneCallLocation =\n | 'header'\n | 'footer'\n | 'hero'\n | 'cta'\n | 'fab'\n | 'contact'\n | 'location'\n | 'page'\n\nexport interface LastPhoneCallEvent {\n key: string\n at: number\n}\n\nexport const INITIAL_LANDING_STORAGE_PREFIX = 'brandlab.initialLanding.'\nexport const DEFAULT_PHONE_CALL_DEDUPE_WINDOW_MS = 1200\n\nexport function normalizeInitialLandingPath(pathname: string | null | undefined) {\n const trimmed = pathname?.trim() || '/'\n if (\n !trimmed.startsWith('/') ||\n trimmed.startsWith('//') ||\n trimmed.includes('\\\\') ||\n /%(?:2f|5c)/i.test(trimmed) ||\n /[\\u0000-\\u001f\\u007f]/.test(trimmed) ||\n trimmed.includes('?') ||\n trimmed.includes('#')\n ) {\n return '/'\n }\n const normalized = trimmed.replace(/\\/{2,}/g, '/')\n return normalized === '/' ? '/' : normalized.replace(/\\/+$/, '')\n}\n\nexport function createPhoneCallDedupeKey(pagePath: string, href: string) {\n return `${pagePath}|${href}`\n}\n\nexport function shouldTrackPhoneCallClick(options: {\n key: string\n now: number\n lastEvent?: LastPhoneCallEvent\n dedupeWindowMs?: number\n}) {\n const windowMs = options.dedupeWindowMs ?? DEFAULT_PHONE_CALL_DEDUPE_WINDOW_MS\n return !(\n options.lastEvent &&\n options.lastEvent.key === options.key &&\n options.now - options.lastEvent.at < windowMs\n )\n}\n\nexport function persistInitialLanding(win: Window = window) {\n try {\n const pathKey = `${INITIAL_LANDING_STORAGE_PREFIX}path`\n const sourceKey = `${INITIAL_LANDING_STORAGE_PREFIX}source`\n const detailKey = `${INITIAL_LANDING_STORAGE_PREFIX}sourceDetail`\n const existingPath = win.sessionStorage.getItem(pathKey)\n const safePath = normalizeInitialLandingPath(existingPath ?? win.location.pathname)\n if (existingPath !== safePath) {\n win.sessionStorage.setItem(pathKey, safePath)\n }\n const existingSource = win.sessionStorage.getItem(sourceKey)\n if (!isContactVisitSource(existingSource)) {\n const attribution = classifyContactAttribution({\n initialLandingPath: safePath,\n search: win.location.search,\n referrer: win.document.referrer,\n siteOrigin: win.location.origin,\n })\n win.sessionStorage.setItem(sourceKey, attribution.visitSource)\n if (attribution.visitSourceDetail) {\n win.sessionStorage.setItem(detailKey, attribution.visitSourceDetail)\n } else {\n win.sessionStorage.removeItem(detailKey)\n }\n }\n // Remove values written by the pre-RC privacy contract. Query strings and\n // referrers are not needed for canonical lead measurement.\n win.sessionStorage.removeItem(`${INITIAL_LANDING_STORAGE_PREFIX}url`)\n win.sessionStorage.removeItem(`${INITIAL_LANDING_STORAGE_PREFIX}referrer`)\n } catch {\n // Storage failures must not affect navigation or lead tracking.\n }\n}\n\nexport function getInitialLandingPath(win: Window = window) {\n try {\n const value = win.sessionStorage.getItem(`${INITIAL_LANDING_STORAGE_PREFIX}path`)\n return value ? normalizeInitialLandingPath(value) : undefined\n } catch {\n return undefined\n }\n}\n\nexport function getInitialContactAttribution(win: Window = window): ContactAttributionSummary {\n const fallbackPath = normalizeInitialLandingPath(win.location.pathname)\n try {\n const initialLandingPath = normalizeInitialLandingPath(\n win.sessionStorage.getItem(`${INITIAL_LANDING_STORAGE_PREFIX}path`) ?? fallbackPath,\n )\n const visitSource = win.sessionStorage.getItem(`${INITIAL_LANDING_STORAGE_PREFIX}source`)\n const visitSourceDetail = win.sessionStorage.getItem(\n `${INITIAL_LANDING_STORAGE_PREFIX}sourceDetail`,\n )\n return normalizeContactAttributionSummary({\n initialLandingPath,\n visitSource: isContactVisitSource(visitSource) ? visitSource : 'direct-or-unknown',\n ...(visitSourceDetail ? { visitSourceDetail } : {}),\n })\n } catch {\n return { initialLandingPath: fallbackPath, visitSource: 'direct-or-unknown' }\n }\n}\n\nexport function getPhoneCallLocation(link: HTMLAnchorElement): PhoneCallLocation {\n const explicitLocation = link.dataset.phoneLocation as PhoneCallLocation | undefined\n\n if (explicitLocation) return explicitLocation\n if (link.closest('header')) return 'header'\n if (link.closest('footer')) return 'footer'\n if (link.closest('[data-phone-location=\"hero\"]')) return 'hero'\n if (link.closest('[data-phone-location=\"cta\"]')) return 'cta'\n\n return 'page'\n}\n\nexport function installPhoneClickListener(options: {\n trackPhoneLinkClick: (context: {\n pagePath: string\n location: PhoneCallLocation\n initialLandingPath?: string\n }) => void\n win?: Window\n dedupeWindowMs?: number\n}) {\n const win = options.win ?? window\n const trackPhoneLinkClick = options.trackPhoneLinkClick\n if (!trackPhoneLinkClick) {\n throw new Error('installPhoneClickListener requires trackPhoneLinkClick.')\n }\n let lastEvent: LastPhoneCallEvent | undefined\n\n const handlePhoneClick = (event: MouseEvent) => {\n const target = event.target\n if (!(target instanceof Element)) return\n\n const link = target.closest<HTMLAnchorElement>('a[href^=\"tel:\"]')\n if (!link) return\n\n const pagePath = win.location.pathname || '/'\n const href = link.getAttribute('href') || link.href\n const key = createPhoneCallDedupeKey(pagePath, href)\n const now = Date.now()\n\n if (\n !shouldTrackPhoneCallClick({\n key,\n now,\n lastEvent,\n dedupeWindowMs: options.dedupeWindowMs,\n })\n ) {\n return\n }\n\n lastEvent = { key, at: now }\n trackPhoneLinkClick({\n pagePath,\n location: getPhoneCallLocation(link),\n initialLandingPath: getInitialLandingPath(win),\n })\n }\n\n win.document.addEventListener('click', handlePhoneClick, true)\n\n return () => {\n win.document.removeEventListener('click', handlePhoneClick, true)\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"instrumentation-client.js","sourceRoot":"","sources":["../src/instrumentation-client.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n isContactVisitSource,\n type ContactAttributionSummary,\n} from '@brandlab-digital/site-core/contact'\nimport {\n PHONE_CALL_PLACEMENTS,\n type BrandlabAnalytics,\n type PhoneCallPlacement,\n} from './analytics.js'\nimport {\n classifyContactAttribution,\n normalizeContactAttributionSummary,\n} from './contact/attribution.js'\n\nexport type PhoneCallLocation = PhoneCallPlacement\n\nexport interface LastPhoneCallEvent {\n key: string\n at: number\n}\n\nexport const INITIAL_LANDING_STORAGE_PREFIX = 'brandlab.initialLanding.'\nexport const DEFAULT_PHONE_CALL_DEDUPE_WINDOW_MS = 1200\n\nexport function normalizeInitialLandingPath(pathname: string | null | undefined) {\n const trimmed = pathname?.trim() || '/'\n if (\n !trimmed.startsWith('/') ||\n trimmed.startsWith('//') ||\n trimmed.includes('\\\\') ||\n /%(?:2f|5c)/i.test(trimmed) ||\n /[\\u0000-\\u001f\\u007f]/.test(trimmed) ||\n trimmed.includes('?') ||\n trimmed.includes('#')\n ) {\n return '/'\n }\n const normalized = trimmed.replace(/\\/{2,}/g, '/')\n return normalized === '/' ? '/' : normalized.replace(/\\/+$/, '')\n}\n\nexport function createPhoneCallDedupeKey(pagePath: string, href: string) {\n return `${pagePath}|${href}`\n}\n\nexport function shouldTrackPhoneCallClick(options: {\n key: string\n now: number\n lastEvent?: LastPhoneCallEvent\n dedupeWindowMs?: number\n}) {\n const windowMs = options.dedupeWindowMs ?? DEFAULT_PHONE_CALL_DEDUPE_WINDOW_MS\n return !(\n options.lastEvent &&\n options.lastEvent.key === options.key &&\n options.now - options.lastEvent.at < windowMs\n )\n}\n\nexport function persistInitialLanding(win: Window = window) {\n try {\n const pathKey = `${INITIAL_LANDING_STORAGE_PREFIX}path`\n const sourceKey = `${INITIAL_LANDING_STORAGE_PREFIX}source`\n const detailKey = `${INITIAL_LANDING_STORAGE_PREFIX}sourceDetail`\n const existingPath = win.sessionStorage.getItem(pathKey)\n const safePath = normalizeInitialLandingPath(existingPath ?? win.location.pathname)\n if (existingPath !== safePath) {\n win.sessionStorage.setItem(pathKey, safePath)\n }\n const existingSource = win.sessionStorage.getItem(sourceKey)\n if (!isContactVisitSource(existingSource)) {\n const attribution = classifyContactAttribution({\n initialLandingPath: safePath,\n search: win.location.search,\n referrer: win.document.referrer,\n siteOrigin: win.location.origin,\n })\n win.sessionStorage.setItem(sourceKey, attribution.visitSource)\n if (attribution.visitSourceDetail) {\n win.sessionStorage.setItem(detailKey, attribution.visitSourceDetail)\n } else {\n win.sessionStorage.removeItem(detailKey)\n }\n }\n // Remove values written by the pre-RC privacy contract. Query strings and\n // referrers are not needed for canonical lead measurement.\n win.sessionStorage.removeItem(`${INITIAL_LANDING_STORAGE_PREFIX}url`)\n win.sessionStorage.removeItem(`${INITIAL_LANDING_STORAGE_PREFIX}referrer`)\n } catch {\n // Storage failures must not affect navigation or lead tracking.\n }\n}\n\nexport function getInitialLandingPath(win: Window = window) {\n try {\n const value = win.sessionStorage.getItem(`${INITIAL_LANDING_STORAGE_PREFIX}path`)\n return value ? normalizeInitialLandingPath(value) : undefined\n } catch {\n return undefined\n }\n}\n\nexport function getInitialContactAttribution(win: Window = window): ContactAttributionSummary {\n const fallbackPath = normalizeInitialLandingPath(win.location.pathname)\n try {\n const initialLandingPath = normalizeInitialLandingPath(\n win.sessionStorage.getItem(`${INITIAL_LANDING_STORAGE_PREFIX}path`) ?? fallbackPath,\n )\n const visitSource = win.sessionStorage.getItem(`${INITIAL_LANDING_STORAGE_PREFIX}source`)\n const visitSourceDetail = win.sessionStorage.getItem(\n `${INITIAL_LANDING_STORAGE_PREFIX}sourceDetail`,\n )\n return normalizeContactAttributionSummary({\n initialLandingPath,\n visitSource: isContactVisitSource(visitSource) ? visitSource : 'direct-or-unknown',\n ...(visitSourceDetail ? { visitSourceDetail } : {}),\n })\n } catch {\n return { initialLandingPath: fallbackPath, visitSource: 'direct-or-unknown' }\n }\n}\n\nfunction placementValue(value: string | null | undefined): PhoneCallLocation | undefined {\n const normalized = value?.trim().toLowerCase()\n return PHONE_CALL_PLACEMENTS.includes(normalized as PhoneCallLocation)\n ? normalized as PhoneCallLocation\n : undefined\n}\n\n/**\n * Placement of a telephone link: its own `data-phone-location`, then a\n * `<header>` or `<footer>` ancestor, then the nearest ancestor declaring a\n * valid `data-phone-location` (for example a hero or call-to-action section),\n * otherwise `page`. Unrecognised values are ignored, so every tap still\n * produces exactly one event.\n */\nexport function getPhoneCallLocation(link: HTMLAnchorElement): PhoneCallLocation {\n const explicit = placementValue(link.getAttribute('data-phone-location'))\n if (explicit) return explicit\n if (link.closest('header')) return 'header'\n if (link.closest('footer')) return 'footer'\n for (\n let ancestor = link.parentElement?.closest('[data-phone-location]');\n ancestor;\n ancestor = ancestor.parentElement?.closest('[data-phone-location]')\n ) {\n const placement = placementValue(ancestor.getAttribute('data-phone-location'))\n if (placement) return placement\n }\n return 'page'\n}\n\nexport function installPhoneClickListener(options: {\n trackPhoneLinkClick: (context: {\n pagePath: string\n location: PhoneCallLocation\n initialLandingPath?: string\n }) => void\n win?: Window\n dedupeWindowMs?: number\n}) {\n const win = options.win ?? window\n const trackPhoneLinkClick = options.trackPhoneLinkClick\n if (!trackPhoneLinkClick) {\n throw new Error('installPhoneClickListener requires trackPhoneLinkClick.')\n }\n let lastEvent: LastPhoneCallEvent | undefined\n\n const handlePhoneClick = (event: MouseEvent) => {\n const target = event.target\n if (!(target instanceof Element)) return\n\n const link = target.closest<HTMLAnchorElement>('a[href^=\"tel:\"]')\n if (!link) return\n\n const pagePath = win.location.pathname || '/'\n const href = link.getAttribute('href') || link.href\n const key = createPhoneCallDedupeKey(pagePath, href)\n const now = Date.now()\n\n if (\n !shouldTrackPhoneCallClick({\n key,\n now,\n lastEvent,\n dedupeWindowMs: options.dedupeWindowMs,\n })\n ) {\n return\n }\n\n lastEvent = { key, at: now }\n trackPhoneLinkClick({\n pagePath,\n location: getPhoneCallLocation(link),\n initialLandingPath: getInitialLandingPath(win),\n })\n }\n\n win.document.addEventListener('click', handlePhoneClick, true)\n\n return () => {\n win.document.removeEventListener('click', handlePhoneClick, true)\n }\n}\n\nconst installedInstrumentation = new WeakMap<Window, () => void>()\n\n/**\n * The whole of `src/instrumentation-client.ts`:\n * `installBrandlabInstrumentation(analytics)`. Stores the privacy-safe landing\n * path and visit source for contact attribution, and installs the single\n * telephone-link listener that sends one `phone_link_click` per tap (rapid\n * repeats of the same link are de-duplicated). Installing twice in one window\n * is a no-op; the returned function removes the listener.\n */\nexport function installBrandlabInstrumentation(\n analytics: Pick<BrandlabAnalytics, 'trackPhoneLinkClick'>,\n options: { win?: Window; dedupeWindowMs?: number } = {},\n): () => void {\n if (!analytics || typeof analytics.trackPhoneLinkClick !== 'function') {\n throw new TypeError('installBrandlabInstrumentation requires the defineBrandlabAnalytics binding.')\n }\n const win = options.win ?? (typeof window === 'undefined' ? undefined : window)\n if (!win) return () => undefined\n const existing = installedInstrumentation.get(win)\n if (existing) return existing\n\n persistInitialLanding(win)\n const removeListener = installPhoneClickListener({\n win,\n ...(options.dedupeWindowMs !== undefined ? { dedupeWindowMs: options.dedupeWindowMs } : {}),\n trackPhoneLinkClick: ({ pagePath, location }) => {\n try {\n analytics.trackPhoneLinkClick({ pagePath, placement: location })\n } catch {\n // Tracking must never block the telephone action.\n }\n },\n })\n const teardown = () => {\n removeListener()\n if (installedInstrumentation.get(win) === teardown) installedInstrumentation.delete(win)\n }\n installedInstrumentation.set(win, teardown)\n return teardown\n}\n"]}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { GtagTransport } from '@brandlab-digital/site-core/analytics';
|
|
2
|
+
export interface LeadTrackingConfig {
|
|
3
|
+
enabled: boolean;
|
|
4
|
+
enhancedConversionsEnabled?: boolean;
|
|
5
|
+
measurementId?: string | null;
|
|
6
|
+
googleAdsConversionId?: string | null;
|
|
7
|
+
googleAdsConversionLabel?: string | null;
|
|
8
|
+
gtag?: GtagTransport;
|
|
9
|
+
}
|
|
10
|
+
export interface LeadPageParams {
|
|
11
|
+
page_template?: string;
|
|
12
|
+
service_slug?: string;
|
|
13
|
+
location_slug?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface LeadAdsParams {
|
|
16
|
+
currency?: string;
|
|
17
|
+
value?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface LeadContext {
|
|
20
|
+
readonly pagePath: string;
|
|
21
|
+
readonly pageParams: LeadPageParams;
|
|
22
|
+
readonly formData: FormData;
|
|
23
|
+
}
|
|
24
|
+
export interface LeadBinding {
|
|
25
|
+
readonly config: LeadTrackingConfig;
|
|
26
|
+
pageParams(pagePath: string): LeadPageParams;
|
|
27
|
+
adsParams(lead: LeadContext): LeadAdsParams;
|
|
28
|
+
}
|
|
29
|
+
export declare function getBrowserGtag(win?: Window | undefined): GtagTransport | undefined;
|
|
30
|
+
export declare function safePagePath(value: string): string;
|
|
31
|
+
export declare function safeSlug(value: string | undefined): string | undefined;
|
|
32
|
+
export declare function registerLeadBinding(binding: object, lead: LeadBinding): void;
|
|
33
|
+
export declare function leadBindingFor(binding: unknown): LeadBinding | undefined;
|
|
34
|
+
//# sourceMappingURL=lead-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lead-registry.d.ts","sourceRoot":"","sources":["../src/lead-registry.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uCAAuC,CAAA;AAG1E,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,OAAO,CAAA;IAChB,0BAA0B,CAAC,EAAE,OAAO,CAAA;IACpC,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrC,wBAAwB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxC,IAAI,CAAC,EAAE,aAAa,CAAA;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,UAAU,EAAE,cAAc,CAAA;IACnC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAA;CAC5B;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAA;IACnC,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,CAAA;IAC5C,SAAS,CAAC,IAAI,EAAE,WAAW,GAAG,aAAa,CAAA;CAC5C;AAED,wBAAgB,cAAc,CAC5B,GAAG,GAAE,MAAM,GAAG,SAA8D,GAC3E,aAAa,GAAG,SAAS,CAG3B;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,UAQzC;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,sBAKjD;AAID,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,QAErE;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,WAAW,GAAG,SAAS,CAExE"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { normalizeBrandlabRoutePath } from '@brandlab-digital/site-core/routes';
|
|
2
|
+
export function getBrowserGtag(win = typeof window === 'undefined' ? undefined : window) {
|
|
3
|
+
const candidate = win?.gtag;
|
|
4
|
+
return typeof candidate === 'function' ? candidate : undefined;
|
|
5
|
+
}
|
|
6
|
+
export function safePagePath(value) {
|
|
7
|
+
const path = value.trim().split(/[?#]/, 1)[0];
|
|
8
|
+
if (!path || path.length > 500)
|
|
9
|
+
return '/';
|
|
10
|
+
try {
|
|
11
|
+
return normalizeBrandlabRoutePath(path);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return '/';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function safeSlug(value) {
|
|
18
|
+
const normalized = value?.trim().toLowerCase();
|
|
19
|
+
return normalized && /^[a-z0-9]+(?:[-_][a-z0-9]+)*$/.test(normalized) && normalized.length <= 80
|
|
20
|
+
? normalized
|
|
21
|
+
: undefined;
|
|
22
|
+
}
|
|
23
|
+
const bindings = new WeakMap();
|
|
24
|
+
export function registerLeadBinding(binding, lead) {
|
|
25
|
+
bindings.set(binding, Object.freeze(lead));
|
|
26
|
+
}
|
|
27
|
+
export function leadBindingFor(binding) {
|
|
28
|
+
return binding && typeof binding === 'object' ? bindings.get(binding) : undefined;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=lead-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lead-registry.js","sourceRoot":"","sources":["../src/lead-registry.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,0BAA0B,EAAE,MAAM,oCAAoC,CAAA;AAkC/E,MAAM,UAAU,cAAc,CAC5B,MAA0B,OAAO,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM;IAE5E,MAAM,SAAS,GAAI,GAAuD,EAAE,IAAI,CAAA;IAChF,OAAO,OAAO,SAAS,KAAK,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAA;AAChE,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAC7C,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,GAAG,GAAG;QAAE,OAAO,GAAG,CAAA;IAC1C,IAAI,CAAC;QACH,OAAO,0BAA0B,CAAC,IAAI,CAAC,CAAA;IACzC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,GAAG,CAAA;IACZ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,KAAyB;IAChD,MAAM,UAAU,GAAG,KAAK,EAAE,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;IAC9C,OAAO,UAAU,IAAI,+BAA+B,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,UAAU,CAAC,MAAM,IAAI,EAAE;QAC9F,CAAC,CAAC,UAAU;QACZ,CAAC,CAAC,SAAS,CAAA;AACf,CAAC;AAED,MAAM,QAAQ,GAAG,IAAI,OAAO,EAAuB,CAAA;AAEnD,MAAM,UAAU,mBAAmB,CAAC,OAAe,EAAE,IAAiB;IACpE,QAAQ,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAA;AAC5C,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,OAAgB;IAC7C,OAAO,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;AACnF,CAAC","sourcesContent":["// Internal module (not in package exports). defineBrandlabAnalytics registers\n// each binding's declared lead reporting here; only useBrandlabContactForm and\n// the contact testing surface read it. It holds data, so pages without a\n// contact form do not bundle the lead-sending code.\nimport type { GtagTransport } from '@brandlab-digital/site-core/analytics'\nimport { normalizeBrandlabRoutePath } from '@brandlab-digital/site-core/routes'\n\nexport interface LeadTrackingConfig {\n enabled: boolean\n enhancedConversionsEnabled?: boolean\n measurementId?: string | null\n googleAdsConversionId?: string | null\n googleAdsConversionLabel?: string | null\n gtag?: GtagTransport\n}\n\nexport interface LeadPageParams {\n page_template?: string\n service_slug?: string\n location_slug?: string\n}\n\nexport interface LeadAdsParams {\n currency?: string\n value?: number\n}\n\nexport interface LeadContext {\n readonly pagePath: string\n readonly pageParams: LeadPageParams\n readonly formData: FormData\n}\n\nexport interface LeadBinding {\n readonly config: LeadTrackingConfig\n pageParams(pagePath: string): LeadPageParams\n adsParams(lead: LeadContext): LeadAdsParams\n}\n\nexport function getBrowserGtag(\n win: Window | undefined = typeof window === 'undefined' ? undefined : window,\n): GtagTransport | undefined {\n const candidate = (win as (Window & { gtag?: GtagTransport }) | undefined)?.gtag\n return typeof candidate === 'function' ? candidate : undefined\n}\n\nexport function safePagePath(value: string) {\n const path = value.trim().split(/[?#]/, 1)[0]\n if (!path || path.length > 500) return '/'\n try {\n return normalizeBrandlabRoutePath(path)\n } catch {\n return '/'\n }\n}\n\nexport function safeSlug(value: string | undefined) {\n const normalized = value?.trim().toLowerCase()\n return normalized && /^[a-z0-9]+(?:[-_][a-z0-9]+)*$/.test(normalized) && normalized.length <= 80\n ? normalized\n : undefined\n}\n\nconst bindings = new WeakMap<object, LeadBinding>()\n\nexport function registerLeadBinding(binding: object, lead: LeadBinding) {\n bindings.set(binding, Object.freeze(lead))\n}\n\nexport function leadBindingFor(binding: unknown): LeadBinding | undefined {\n return binding && typeof binding === 'object' ? bindings.get(binding) : undefined\n}\n"]}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type ContactFormConversionResult } from '@brandlab-digital/site-core/analytics';
|
|
2
|
+
import type { ContactActionResult } from '@brandlab-digital/site-core/contact';
|
|
3
|
+
import type { BrandlabContactFieldRoles } from './contact/shared.js';
|
|
4
|
+
import { type LeadAdsParams, type LeadBinding, type LeadPageParams, type LeadTrackingConfig } from './lead-registry.js';
|
|
5
|
+
export type AcceptedContactResult = Extract<ContactActionResult, {
|
|
6
|
+
ok: true;
|
|
7
|
+
}>;
|
|
8
|
+
export declare function isTrackableContactActionResult(result: ContactActionResult): result is Extract<ContactActionResult, {
|
|
9
|
+
ok: true;
|
|
10
|
+
accepted: true;
|
|
11
|
+
}>;
|
|
12
|
+
/**
|
|
13
|
+
* GA4 contact_form plus the Google Ads form conversion, only after provider
|
|
14
|
+
* acceptance. Email and phone may only come from fields with explicit
|
|
15
|
+
* declarative roles and never enter the GA4 parameter object.
|
|
16
|
+
*/
|
|
17
|
+
export declare function trackDeliveredContactWithEnhancedConversions(result: ContactActionResult, config: LeadTrackingConfig, options: {
|
|
18
|
+
pagePath: string;
|
|
19
|
+
/** Field names with declarative roles; only email and phone roles are read. */
|
|
20
|
+
definition: BrandlabContactFieldRoles;
|
|
21
|
+
formData: FormData;
|
|
22
|
+
/** Live Consent Mode decision for ad_user_data; absence is deny. */
|
|
23
|
+
adUserDataConsent: boolean;
|
|
24
|
+
params?: LeadPageParams;
|
|
25
|
+
adsParams?: LeadAdsParams;
|
|
26
|
+
}): ContactFormConversionResult;
|
|
27
|
+
export interface AcceptedLeadInput {
|
|
28
|
+
/** The form's pagePath, exactly as the hook received it. */
|
|
29
|
+
pagePath: string;
|
|
30
|
+
/** The accepted attempt's fields. */
|
|
31
|
+
formData: FormData;
|
|
32
|
+
fieldRoles: BrandlabContactFieldRoles;
|
|
33
|
+
adUserDataConsent: boolean;
|
|
34
|
+
}
|
|
35
|
+
/** Sends the binding's declared lead for one accepted enquiry. */
|
|
36
|
+
export declare function reportAcceptedLead(lead: LeadBinding, result: AcceptedContactResult, input: AcceptedLeadInput, override?: Pick<LeadTrackingConfig, 'enabled' | 'gtag'>): ContactFormConversionResult;
|
|
37
|
+
//# sourceMappingURL=lead-reporting.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lead-reporting.d.ts","sourceRoot":"","sources":["../src/lead-reporting.ts"],"names":[],"mappings":"AAGA,OAAO,EAIL,KAAK,2BAA2B,EACjC,MAAM,uCAAuC,CAAA;AAC9C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAA;AAC9E,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAA;AACpE,OAAO,EAIL,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACxB,MAAM,oBAAoB,CAAA;AAE3B,MAAM,MAAM,qBAAqB,GAAG,OAAO,CAAC,mBAAmB,EAAE;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,CAAC,CAAA;AA2B9E,wBAAgB,8BAA8B,CAC5C,MAAM,EAAE,mBAAmB,GAC1B,MAAM,IAAI,OAAO,CAAC,mBAAmB,EAAE;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,IAAI,CAAA;CAAE,CAAC,CAEtE;AAaD;;;;GAIG;AACH,wBAAgB,4CAA4C,CAC1D,MAAM,EAAE,mBAAmB,EAC3B,MAAM,EAAE,kBAAkB,EAC1B,OAAO,EAAE;IACP,QAAQ,EAAE,MAAM,CAAA;IAChB,+EAA+E;IAC/E,UAAU,EAAE,yBAAyB,CAAA;IACrC,QAAQ,EAAE,QAAQ,CAAA;IAClB,oEAAoE;IACpE,iBAAiB,EAAE,OAAO,CAAA;IAC1B,MAAM,CAAC,EAAE,cAAc,CAAA;IACvB,SAAS,CAAC,EAAE,aAAa,CAAA;CAC1B,GACA,2BAA2B,CAqB7B;AAED,MAAM,WAAW,iBAAiB;IAChC,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAA;IAChB,qCAAqC;IACrC,QAAQ,EAAE,QAAQ,CAAA;IAClB,UAAU,EAAE,yBAAyB,CAAA;IACrC,iBAAiB,EAAE,OAAO,CAAA;CAC3B;AAED,kEAAkE;AAClE,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,qBAAqB,EAC7B,KAAK,EAAE,iBAAiB,EACxB,QAAQ,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,SAAS,GAAG,MAAM,CAAC,GACtD,2BAA2B,CAU7B"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Internal module (not in package exports): the single accepted-enquiry
|
|
2
|
+
// reporting path, used only by useBrandlabContactForm and the contact testing
|
|
3
|
+
// surface, with the lead reporting a binding declared in lead-registry.
|
|
4
|
+
import { normalizeEnhancedConversionEmail, normalizeEnhancedConversionPhone, sendContactFormConversion, } from '@brandlab-digital/site-core/analytics';
|
|
5
|
+
import { getBrowserGtag, safePagePath, safeSlug, } from './lead-registry.js';
|
|
6
|
+
function sanitizeContactParams(params) {
|
|
7
|
+
return {
|
|
8
|
+
...(safeSlug(params?.page_template)
|
|
9
|
+
? { page_template: safeSlug(params?.page_template) }
|
|
10
|
+
: {}),
|
|
11
|
+
...(safeSlug(params?.service_slug)
|
|
12
|
+
? { service_slug: safeSlug(params?.service_slug) }
|
|
13
|
+
: {}),
|
|
14
|
+
...(safeSlug(params?.location_slug)
|
|
15
|
+
? { location_slug: safeSlug(params?.location_slug) }
|
|
16
|
+
: {}),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function sanitizeAdsParams(params) {
|
|
20
|
+
return {
|
|
21
|
+
...(params?.currency && /^[A-Z]{3}$/.test(params.currency)
|
|
22
|
+
? { currency: params.currency }
|
|
23
|
+
: {}),
|
|
24
|
+
...(typeof params?.value === 'number' && Number.isFinite(params.value) && params.value >= 0
|
|
25
|
+
? { value: params.value }
|
|
26
|
+
: {}),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function isTrackableContactActionResult(result) {
|
|
30
|
+
return result.ok && result.accepted && result.trackConversion;
|
|
31
|
+
}
|
|
32
|
+
function explicitRoleValue(definition, formData, role) {
|
|
33
|
+
const field = definition.fields.find((candidate) => candidate.role === role);
|
|
34
|
+
if (!field)
|
|
35
|
+
return undefined;
|
|
36
|
+
const value = formData.get(field.name);
|
|
37
|
+
return typeof value === 'string' ? value : undefined;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* GA4 contact_form plus the Google Ads form conversion, only after provider
|
|
41
|
+
* acceptance. Email and phone may only come from fields with explicit
|
|
42
|
+
* declarative roles and never enter the GA4 parameter object.
|
|
43
|
+
*/
|
|
44
|
+
export function trackDeliveredContactWithEnhancedConversions(result, config, options) {
|
|
45
|
+
if (!isTrackableContactActionResult(result)) {
|
|
46
|
+
return { ga4Sent: false, enhancedConversionSent: false, adsConversionSent: false };
|
|
47
|
+
}
|
|
48
|
+
const email = normalizeEnhancedConversionEmail(explicitRoleValue(options.definition, options.formData, 'email'));
|
|
49
|
+
const phone = normalizeEnhancedConversionPhone(explicitRoleValue(options.definition, options.formData, 'phone'));
|
|
50
|
+
return sendContactFormConversion({
|
|
51
|
+
...config,
|
|
52
|
+
gtag: config.gtag ?? getBrowserGtag(),
|
|
53
|
+
pagePath: safePagePath(options.pagePath),
|
|
54
|
+
submissionId: result.confirmationId,
|
|
55
|
+
params: sanitizeContactParams(options.params),
|
|
56
|
+
adsParams: sanitizeAdsParams(options.adsParams),
|
|
57
|
+
enhancedConversionsEnabled: config.enhancedConversionsEnabled === true,
|
|
58
|
+
adUserDataConsent: options.adUserDataConsent === true,
|
|
59
|
+
...((email || phone) ? { enhancedConversion: { email, phone } } : {}),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/** Sends the binding's declared lead for one accepted enquiry. */
|
|
63
|
+
export function reportAcceptedLead(lead, result, input, override) {
|
|
64
|
+
const params = lead.pageParams(input.pagePath);
|
|
65
|
+
return trackDeliveredContactWithEnhancedConversions(result, { ...lead.config, ...override }, {
|
|
66
|
+
pagePath: input.pagePath,
|
|
67
|
+
definition: input.fieldRoles,
|
|
68
|
+
formData: input.formData,
|
|
69
|
+
adUserDataConsent: input.adUserDataConsent,
|
|
70
|
+
params,
|
|
71
|
+
adsParams: lead.adsParams({ pagePath: input.pagePath, pageParams: params, formData: input.formData }),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=lead-reporting.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lead-reporting.js","sourceRoot":"","sources":["../src/lead-reporting.ts"],"names":[],"mappings":"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","sourcesContent":["// Internal module (not in package exports): the single accepted-enquiry\n// reporting path, used only by useBrandlabContactForm and the contact testing\n// surface, with the lead reporting a binding declared in lead-registry.\nimport {\n normalizeEnhancedConversionEmail,\n normalizeEnhancedConversionPhone,\n sendContactFormConversion,\n type ContactFormConversionResult,\n} from '@brandlab-digital/site-core/analytics'\nimport type { ContactActionResult } from '@brandlab-digital/site-core/contact'\nimport type { BrandlabContactFieldRoles } from './contact/shared.js'\nimport {\n getBrowserGtag,\n safePagePath,\n safeSlug,\n type LeadAdsParams,\n type LeadBinding,\n type LeadPageParams,\n type LeadTrackingConfig,\n} from './lead-registry.js'\n\nexport type AcceptedContactResult = Extract<ContactActionResult, { ok: true }>\n\nfunction sanitizeContactParams(params: LeadPageParams | undefined) {\n return {\n ...(safeSlug(params?.page_template)\n ? { page_template: safeSlug(params?.page_template) }\n : {}),\n ...(safeSlug(params?.service_slug)\n ? { service_slug: safeSlug(params?.service_slug) }\n : {}),\n ...(safeSlug(params?.location_slug)\n ? { location_slug: safeSlug(params?.location_slug) }\n : {}),\n }\n}\n\nfunction sanitizeAdsParams(params: LeadAdsParams | undefined) {\n return {\n ...(params?.currency && /^[A-Z]{3}$/.test(params.currency)\n ? { currency: params.currency }\n : {}),\n ...(typeof params?.value === 'number' && Number.isFinite(params.value) && params.value >= 0\n ? { value: params.value }\n : {}),\n }\n}\n\nexport function isTrackableContactActionResult(\n result: ContactActionResult,\n): result is Extract<ContactActionResult, { ok: true; accepted: true }> {\n return result.ok && result.accepted && result.trackConversion\n}\n\nfunction explicitRoleValue(\n definition: BrandlabContactFieldRoles,\n formData: FormData,\n role: 'email' | 'phone',\n) {\n const field = definition.fields.find((candidate) => candidate.role === role)\n if (!field) return undefined\n const value = formData.get(field.name)\n return typeof value === 'string' ? value : undefined\n}\n\n/**\n * GA4 contact_form plus the Google Ads form conversion, only after provider\n * acceptance. Email and phone may only come from fields with explicit\n * declarative roles and never enter the GA4 parameter object.\n */\nexport function trackDeliveredContactWithEnhancedConversions(\n result: ContactActionResult,\n config: LeadTrackingConfig,\n options: {\n pagePath: string\n /** Field names with declarative roles; only email and phone roles are read. */\n definition: BrandlabContactFieldRoles\n formData: FormData\n /** Live Consent Mode decision for ad_user_data; absence is deny. */\n adUserDataConsent: boolean\n params?: LeadPageParams\n adsParams?: LeadAdsParams\n },\n): ContactFormConversionResult {\n if (!isTrackableContactActionResult(result)) {\n return { ga4Sent: false, enhancedConversionSent: false, adsConversionSent: false }\n }\n const email = normalizeEnhancedConversionEmail(\n explicitRoleValue(options.definition, options.formData, 'email'),\n )\n const phone = normalizeEnhancedConversionPhone(\n explicitRoleValue(options.definition, options.formData, 'phone'),\n )\n return sendContactFormConversion({\n ...config,\n gtag: config.gtag ?? getBrowserGtag(),\n pagePath: safePagePath(options.pagePath),\n submissionId: result.confirmationId,\n params: sanitizeContactParams(options.params),\n adsParams: sanitizeAdsParams(options.adsParams),\n enhancedConversionsEnabled: config.enhancedConversionsEnabled === true,\n adUserDataConsent: options.adUserDataConsent === true,\n ...((email || phone) ? { enhancedConversion: { email, phone } } : {}),\n })\n}\n\nexport interface AcceptedLeadInput {\n /** The form's pagePath, exactly as the hook received it. */\n pagePath: string\n /** The accepted attempt's fields. */\n formData: FormData\n fieldRoles: BrandlabContactFieldRoles\n adUserDataConsent: boolean\n}\n\n/** Sends the binding's declared lead for one accepted enquiry. */\nexport function reportAcceptedLead(\n lead: LeadBinding,\n result: AcceptedContactResult,\n input: AcceptedLeadInput,\n override?: Pick<LeadTrackingConfig, 'enabled' | 'gtag'>,\n): ContactFormConversionResult {\n const params = lead.pageParams(input.pagePath)\n return trackDeliveredContactWithEnhancedConversions(result, { ...lead.config, ...override }, {\n pagePath: input.pagePath,\n definition: input.fieldRoles,\n formData: input.formData,\n adUserDataConsent: input.adUserDataConsent,\n params,\n adsParams: lead.adsParams({ pagePath: input.pagePath, pageParams: params, formData: input.formData }),\n })\n}\n"]}
|
package/dist/metadata.d.ts
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
import { type BrandlabSeoMetadata
|
|
1
|
+
import { type BrandlabSeoMetadata } from '@brandlab-digital/site-core/metadata';
|
|
2
2
|
import { type BrandlabRouteCatalog } from '@brandlab-digital/site-core/routes';
|
|
3
3
|
import { type BrandlabSeoPage } from '@brandlab-digital/site-core/seo-page';
|
|
4
|
+
import type { BrandlabSite } from '@brandlab-digital/site-core/site';
|
|
4
5
|
export type NextCompatibleMetadata = Omit<BrandlabSeoMetadata, 'canonicalPath' | 'canonicalUrl' | 'resolvedTitle'>;
|
|
5
|
-
export type BrandlabMetadataDefaults = Pick<BuildSeoMetadataInput, 'brandName' | 'siteUrl' | 'locale' | 'titleTemplate' | 'defaultSocialImage'>;
|
|
6
|
-
export type BrandlabPageMetadataInput = Omit<BuildSeoMetadataInput, keyof BrandlabMetadataDefaults>;
|
|
7
|
-
export type BrandlabStrictMetadataDefaults = Pick<BuildBrandlabMetadataInput, 'brandName' | 'siteUrl' | 'locale' | 'titleTemplate' | 'defaultSocialImage'>;
|
|
8
|
-
export type BrandlabStrictPageMetadataInput = Omit<BuildBrandlabMetadataInput, keyof BrandlabStrictMetadataDefaults>;
|
|
9
6
|
export declare function toNextMetadata(metadata: BrandlabSeoMetadata): NextCompatibleMetadata;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
/**
|
|
8
|
+
* The project's one page-metadata binding (`src/lib/seo/metadata.ts`):
|
|
9
|
+
* `export const pageMetadata = createRouteMetadataBuilder(site, routes)`.
|
|
10
|
+
* Canonical path and robots come from the route catalogue; the title mode is
|
|
11
|
+
* explicit per page. Keywords keep their form: a comma-separated string renders
|
|
12
|
+
* exactly as given, a list renders joined with commas, as Next renders them.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createRouteMetadataBuilder(site: BrandlabSite, routes: BrandlabRouteCatalog): (input: BrandlabSeoPage) => NextCompatibleMetadata;
|
|
16
15
|
//# sourceMappingURL=metadata.d.ts.map
|
package/dist/metadata.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,mBAAmB,EACzB,MAAM,sCAAsC,CAAA;AAC7C,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,oCAAoC,CAAA;AAC3C,OAAO,EAAiB,KAAK,eAAe,EAAE,MAAM,sCAAsC,CAAA;AAC1F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAEpE,MAAM,MAAM,sBAAsB,GAAG,IAAI,CACvC,mBAAmB,EACnB,eAAe,GAAG,cAAc,GAAG,eAAe,CACnD,CAAA;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,mBAAmB,GAAG,sBAAsB,CAIpF;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,YAAY,EAClB,MAAM,EAAE,oBAAoB,IAEpB,OAAO,eAAe,KAAG,sBAAsB,CA+BxD"}
|