@brandlab-digital/site-next 2.1.0 → 3.0.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 +148 -0
- package/dist/contact/form-client.d.ts.map +1 -0
- package/dist/contact/form-client.js +234 -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 +21 -28
- package/dist/headers.d.ts.map +1 -1
- package/dist/headers.js +62 -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 +102 -0
- package/dist/next-config.d.ts.map +1 -0
- package/dist/next-config.js +140 -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
|
@@ -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"}
|
package/dist/metadata.js
CHANGED
|
@@ -1,37 +1,35 @@
|
|
|
1
|
-
import { buildBrandlabMetadata,
|
|
1
|
+
import { buildBrandlabMetadata, } from '@brandlab-digital/site-core/metadata';
|
|
2
2
|
import { getRouteById, resolveBrandlabRouteIndexing, } from '@brandlab-digital/site-core/routes';
|
|
3
3
|
import { defineSeoPage } from '@brandlab-digital/site-core/seo-page';
|
|
4
4
|
export function toNextMetadata(metadata) {
|
|
5
5
|
const { canonicalPath: _canonicalPath, canonicalUrl: _canonicalUrl, resolvedTitle: _resolvedTitle, ...next } = metadata;
|
|
6
6
|
return next;
|
|
7
7
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
export function createBrandlabNextMetadataBuilder(defaults) {
|
|
18
|
-
return (page) => buildBrandlabNextMetadata({ ...defaults, ...page });
|
|
19
|
-
}
|
|
20
|
-
/** Builds page metadata from the same route identity used by breadcrumbs and sitemaps. */
|
|
21
|
-
export function createRouteMetadataBuilder(defaults, routes) {
|
|
8
|
+
/**
|
|
9
|
+
* The project's one page-metadata binding (`src/lib/seo/metadata.ts`):
|
|
10
|
+
* `export const pageMetadata = createRouteMetadataBuilder(site, routes)`.
|
|
11
|
+
* Canonical path and robots come from the route catalogue; the title mode is
|
|
12
|
+
* explicit per page. Keywords keep their form: a comma-separated string renders
|
|
13
|
+
* exactly as given, a list renders joined with commas, as Next renders them.
|
|
14
|
+
*/
|
|
15
|
+
export function createRouteMetadataBuilder(site, routes) {
|
|
22
16
|
return (input) => {
|
|
23
17
|
const page = defineSeoPage(input);
|
|
24
18
|
const route = getRouteById(routes, page.routeId);
|
|
25
19
|
if (!route)
|
|
26
20
|
throw new Error(`Unknown metadata route ID "${page.routeId}".`);
|
|
27
|
-
return
|
|
28
|
-
|
|
21
|
+
return toNextMetadata(buildBrandlabMetadata({
|
|
22
|
+
brandName: site.name,
|
|
23
|
+
siteUrl: site.canonicalUrl,
|
|
24
|
+
locale: site.locale,
|
|
25
|
+
titleTemplate: site.titleTemplate,
|
|
26
|
+
defaultSocialImage: site.defaultSocialImage,
|
|
29
27
|
title: page.title,
|
|
30
28
|
description: page.description,
|
|
31
29
|
pathname: route.canonicalPath ?? route.path,
|
|
32
30
|
indexing: resolveBrandlabRouteIndexing(route),
|
|
33
31
|
...(page.titleMode ? { titleMode: page.titleMode } : {}),
|
|
34
|
-
...(page.keywords ? { keywords:
|
|
32
|
+
...(page.keywords !== undefined ? { keywords: page.keywords } : {}),
|
|
35
33
|
...(page.socialImage ? { socialImage: page.socialImage } : {}),
|
|
36
34
|
...(page.article
|
|
37
35
|
? {
|
|
@@ -44,7 +42,7 @@ export function createRouteMetadataBuilder(defaults, routes) {
|
|
|
44
42
|
},
|
|
45
43
|
}
|
|
46
44
|
: {}),
|
|
47
|
-
});
|
|
45
|
+
}));
|
|
48
46
|
};
|
|
49
47
|
}
|
|
50
48
|
//# sourceMappingURL=metadata.js.map
|
package/dist/metadata.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metadata.js","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,
|
|
1
|
+
{"version":3,"file":"metadata.js","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,GAEtB,MAAM,sCAAsC,CAAA;AAC7C,OAAO,EACL,YAAY,EACZ,4BAA4B,GAE7B,MAAM,oCAAoC,CAAA;AAC3C,OAAO,EAAE,aAAa,EAAwB,MAAM,sCAAsC,CAAA;AAQ1F,MAAM,UAAU,cAAc,CAAC,QAA6B;IAC1D,MAAM,EAAE,aAAa,EAAE,cAAc,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,cAAc,EAAE,GAAG,IAAI,EAAE,GAC1G,QAAQ,CAAA;IACV,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,0BAA0B,CACxC,IAAkB,EAClB,MAA4B;IAE5B,OAAO,CAAC,KAAsB,EAA0B,EAAE;QACxD,MAAM,IAAI,GAAG,aAAa,CAAC,KAAK,CAAC,CAAA;QACjC,MAAM,KAAK,GAAG,YAAY,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAA;QAChD,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,KAAK,CAAC,8BAA8B,IAAI,CAAC,OAAO,IAAI,CAAC,CAAA;QAE3E,OAAO,cAAc,CAAC,qBAAqB,CAAC;YAC1C,SAAS,EAAE,IAAI,CAAC,IAAI;YACpB,OAAO,EAAE,IAAI,CAAC,YAAY;YAC1B,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,aAAa,EAAE,IAAI,CAAC,aAAa;YACjC,kBAAkB,EAAE,IAAI,CAAC,kBAAkB;YAC3C,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,QAAQ,EAAE,KAAK,CAAC,aAAa,IAAI,KAAK,CAAC,IAAI;YAC3C,QAAQ,EAAE,4BAA4B,CAAC,KAAK,CAAC;YAC7C,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACxD,GAAG,CAAC,IAAI,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACnE,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC9D,GAAG,CAAC,IAAI,CAAC,OAAO;gBACd,CAAC,CAAC;oBACE,OAAO,EAAE;wBACP,aAAa,EAAE,IAAI,CAAC,OAAO,CAAC,aAAa;wBACzC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY;4BAC3B,CAAC,CAAC,EAAE,YAAY,EAAE,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE;4BAC7C,CAAC,CAAC,EAAE,CAAC;wBACP,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC;qBACnC;iBACF;gBACH,CAAC,CAAC,EAAE,CAAC;SACR,CAAC,CAAC,CAAA;IACL,CAAC,CAAA;AACH,CAAC","sourcesContent":["import {\n buildBrandlabMetadata,\n type BrandlabSeoMetadata,\n} from '@brandlab-digital/site-core/metadata'\nimport {\n getRouteById,\n resolveBrandlabRouteIndexing,\n type BrandlabRouteCatalog,\n} from '@brandlab-digital/site-core/routes'\nimport { defineSeoPage, type BrandlabSeoPage } from '@brandlab-digital/site-core/seo-page'\nimport type { BrandlabSite } from '@brandlab-digital/site-core/site'\n\nexport type NextCompatibleMetadata = Omit<\n BrandlabSeoMetadata,\n 'canonicalPath' | 'canonicalUrl' | 'resolvedTitle'\n>\n\nexport function toNextMetadata(metadata: BrandlabSeoMetadata): NextCompatibleMetadata {\n const { canonicalPath: _canonicalPath, canonicalUrl: _canonicalUrl, resolvedTitle: _resolvedTitle, ...next } =\n metadata\n return next\n}\n\n/**\n * The project's one page-metadata binding (`src/lib/seo/metadata.ts`):\n * `export const pageMetadata = createRouteMetadataBuilder(site, routes)`.\n * Canonical path and robots come from the route catalogue; the title mode is\n * explicit per page. Keywords keep their form: a comma-separated string renders\n * exactly as given, a list renders joined with commas, as Next renders them.\n */\nexport function createRouteMetadataBuilder(\n site: BrandlabSite,\n routes: BrandlabRouteCatalog,\n) {\n return (input: BrandlabSeoPage): NextCompatibleMetadata => {\n const page = defineSeoPage(input)\n const route = getRouteById(routes, page.routeId)\n if (!route) throw new Error(`Unknown metadata route ID \"${page.routeId}\".`)\n\n return toNextMetadata(buildBrandlabMetadata({\n brandName: site.name,\n siteUrl: site.canonicalUrl,\n locale: site.locale,\n titleTemplate: site.titleTemplate,\n defaultSocialImage: site.defaultSocialImage,\n title: page.title,\n description: page.description,\n pathname: route.canonicalPath ?? route.path,\n indexing: resolveBrandlabRouteIndexing(route),\n ...(page.titleMode ? { titleMode: page.titleMode } : {}),\n ...(page.keywords !== undefined ? { keywords: page.keywords } : {}),\n ...(page.socialImage ? { socialImage: page.socialImage } : {}),\n ...(page.article\n ? {\n article: {\n publishedTime: page.article.publishedTime,\n ...(page.article.modifiedTime\n ? { modifiedTime: page.article.modifiedTime }\n : {}),\n authors: [...page.article.authors],\n },\n }\n : {}),\n }))\n }\n}\n"]}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { type BrandlabRedirectTable } from '@brandlab-digital/site-core/redirects';
|
|
2
|
+
import { type BrandlabDeploymentEnvironment, type BrandlabEnvironment, type BrandlabSite } from '@brandlab-digital/site-core/site';
|
|
3
|
+
import { type BrandlabContentSecurityPolicyExtensions, type BrandlabHeader, type BrandlabHeaderRule, type BrandlabHstsExpansion } from './headers.js';
|
|
4
|
+
/** Next redirect records: 308 stays `permanent: true`, 301 stays `statusCode: 301`. */
|
|
5
|
+
export type BrandlabNextRedirect = {
|
|
6
|
+
source: string;
|
|
7
|
+
destination: string;
|
|
8
|
+
has?: {
|
|
9
|
+
type: 'host';
|
|
10
|
+
value: string;
|
|
11
|
+
}[];
|
|
12
|
+
} & ({
|
|
13
|
+
permanent: true;
|
|
14
|
+
statusCode?: never;
|
|
15
|
+
} | {
|
|
16
|
+
statusCode: 301;
|
|
17
|
+
permanent?: never;
|
|
18
|
+
});
|
|
19
|
+
export interface BrandlabNextRemotePattern {
|
|
20
|
+
protocol?: 'http' | 'https';
|
|
21
|
+
hostname: string;
|
|
22
|
+
port?: string;
|
|
23
|
+
pathname?: string;
|
|
24
|
+
search?: string;
|
|
25
|
+
}
|
|
26
|
+
/** The site's own image settings; the output format is always WebP. */
|
|
27
|
+
export interface BrandlabNextImagesOptions {
|
|
28
|
+
deviceSizes?: readonly number[];
|
|
29
|
+
imageSizes?: readonly number[];
|
|
30
|
+
qualities?: readonly number[];
|
|
31
|
+
minimumCacheTTL?: number;
|
|
32
|
+
remotePatterns?: readonly BrandlabNextRemotePattern[];
|
|
33
|
+
}
|
|
34
|
+
export interface BrandlabNextImagesConfig {
|
|
35
|
+
formats: 'image/webp'[];
|
|
36
|
+
deviceSizes?: number[];
|
|
37
|
+
imageSizes?: number[];
|
|
38
|
+
qualities?: number[];
|
|
39
|
+
minimumCacheTTL?: number;
|
|
40
|
+
remotePatterns?: BrandlabNextRemotePattern[];
|
|
41
|
+
}
|
|
42
|
+
export interface BrandlabNextConfig {
|
|
43
|
+
poweredByHeader: false;
|
|
44
|
+
/** Trailing slashes are handled by single-hop redirect rules instead. */
|
|
45
|
+
skipTrailingSlashRedirect: true;
|
|
46
|
+
env: {
|
|
47
|
+
NEXT_PUBLIC_VERCEL_ENV: BrandlabDeploymentEnvironment;
|
|
48
|
+
};
|
|
49
|
+
outputFileTracingIncludes: Record<string, string[]>;
|
|
50
|
+
images: BrandlabNextImagesConfig;
|
|
51
|
+
pageExtensions?: string[];
|
|
52
|
+
headers: () => Promise<BrandlabHeaderRule[]>;
|
|
53
|
+
redirects: () => Promise<BrandlabNextRedirect[]>;
|
|
54
|
+
}
|
|
55
|
+
export interface BrandlabNextConfigOptions {
|
|
56
|
+
site: BrandlabSite;
|
|
57
|
+
/** The src/data/redirects.ts table (defineRedirects). */
|
|
58
|
+
redirects: BrandlabRedirectTable;
|
|
59
|
+
images: BrandlabNextImagesOptions;
|
|
60
|
+
/** Reviewed CSP origins for declared integrations such as Meta Pixel or map embeds. */
|
|
61
|
+
csp?: BrandlabContentSecurityPolicyExtensions;
|
|
62
|
+
hsts?: BrandlabHstsExpansion;
|
|
63
|
+
/** Reviewed exception to the default Permissions-Policy that is already live. */
|
|
64
|
+
permissionsPolicy?: string;
|
|
65
|
+
/** Reviewed response headers that are already live; protected headers cannot be replaced. */
|
|
66
|
+
extraHeaders?: readonly BrandlabHeader[];
|
|
67
|
+
/** Defaults to process.env; tests supply a fixed environment. */
|
|
68
|
+
environment?: BrandlabEnvironment;
|
|
69
|
+
}
|
|
70
|
+
/** Sharp's Linux binaries must travel with the contact route's image sanitiser. */
|
|
71
|
+
export declare const BRANDLAB_CONTACT_OUTPUT_FILE_TRACING: Readonly<{
|
|
72
|
+
'/api/contact': readonly string[];
|
|
73
|
+
}>;
|
|
74
|
+
/**
|
|
75
|
+
* Converts the redirect table to Next redirects: each entry keeps its 301 or
|
|
76
|
+
* 308 status (Next forwards the query string), gains a single-hop
|
|
77
|
+
* trailing-slash variant, then one trailing-slash rule and one permanent
|
|
78
|
+
* canonical-host rule per alias host follow. There is no x-forwarded-proto
|
|
79
|
+
* rule: Vercel's edge already enforces HTTPS.
|
|
80
|
+
*/
|
|
81
|
+
export declare function buildBrandlabRedirects(options: {
|
|
82
|
+
site: BrandlabSite;
|
|
83
|
+
redirects: BrandlabRedirectTable;
|
|
84
|
+
}): BrandlabNextRedirect[];
|
|
85
|
+
/**
|
|
86
|
+
* The whole next.config.ts after its first line
|
|
87
|
+
* `import '@brandlab-digital/site-next/image-runtime'`:
|
|
88
|
+
* `export default defineBrandlabNextConfig({ site, redirects, images })`.
|
|
89
|
+
*
|
|
90
|
+
* Sets poweredByHeader: false, WebP image output, Sharp output tracing for
|
|
91
|
+
* /api/contact, NEXT_PUBLIC_VERCEL_ENV for browser gates, the deployment's
|
|
92
|
+
* header rules and the redirect table. Validation runs when the config loads,
|
|
93
|
+
* so an invalid table or header exception fails the build.
|
|
94
|
+
*/
|
|
95
|
+
export declare function defineBrandlabNextConfig(options: BrandlabNextConfigOptions & {
|
|
96
|
+
mdx?: undefined;
|
|
97
|
+
}): BrandlabNextConfig;
|
|
98
|
+
export declare function defineBrandlabNextConfig<Wrapped>(options: BrandlabNextConfigOptions & {
|
|
99
|
+
/** The @next/mdx wrapper: createMDX({ options: { remarkPlugins: ['remark-gfm'] } }). */
|
|
100
|
+
mdx: (config: BrandlabNextConfig) => Wrapped;
|
|
101
|
+
}): Wrapped;
|
|
102
|
+
//# sourceMappingURL=next-config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"next-config.d.ts","sourceRoot":"","sources":["../src/next-config.ts"],"names":[],"mappings":"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"}
|