@brandlab-digital/site-core 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 +0 -11
- package/dist/analytics.d.ts.map +1 -1
- package/dist/analytics.js +16 -8
- package/dist/analytics.js.map +1 -1
- package/dist/blog.d.ts +11 -0
- package/dist/blog.d.ts.map +1 -1
- package/dist/blog.js +27 -0
- package/dist/blog.js.map +1 -1
- package/dist/business.d.ts +13 -0
- package/dist/business.d.ts.map +1 -1
- package/dist/business.js +19 -8
- package/dist/business.js.map +1 -1
- package/dist/locations.d.ts +89 -2
- package/dist/locations.d.ts.map +1 -1
- package/dist/locations.js +290 -2
- package/dist/locations.js.map +1 -1
- package/dist/metadata.d.ts +18 -5
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +36 -15
- package/dist/metadata.js.map +1 -1
- package/dist/project.d.ts +0 -8
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +21 -28
- package/dist/project.js.map +1 -1
- package/dist/redirects.d.ts +40 -0
- package/dist/redirects.d.ts.map +1 -0
- package/dist/redirects.js +286 -0
- package/dist/redirects.js.map +1 -0
- package/dist/schema.js +1 -1
- package/dist/schema.js.map +1 -1
- package/dist/seo-page.d.ts +3 -2
- package/dist/seo-page.d.ts.map +1 -1
- package/dist/seo-page.js +3 -9
- package/dist/seo-page.js.map +1 -1
- package/dist/site.d.ts +60 -0
- package/dist/site.d.ts.map +1 -0
- package/dist/site.js +116 -0
- package/dist/site.js.map +1 -0
- package/dist/sitemaps.d.ts +5 -6
- package/dist/sitemaps.d.ts.map +1 -1
- package/dist/sitemaps.js +38 -48
- package/dist/sitemaps.js.map +1 -1
- package/package.json +11 -1
package/dist/analytics.d.ts
CHANGED
|
@@ -4,12 +4,6 @@ export declare const BRANDLAB_LEAD_DIAGNOSTIC_EVENTS: readonly ["lead_form_start
|
|
|
4
4
|
export type CanonicalLeadEvent = (typeof CANONICAL_LEAD_EVENTS)[number];
|
|
5
5
|
export type CanonicalPhoneInteractionEvent = (typeof CANONICAL_PHONE_INTERACTION_EVENTS)[number];
|
|
6
6
|
export type BrandlabLeadDiagnosticEvent = (typeof BRANDLAB_LEAD_DIAGNOSTIC_EVENTS)[number];
|
|
7
|
-
/**
|
|
8
|
-
* Analytics and other production-only browser behaviour must use the actual
|
|
9
|
-
* Vercel deployment environment. Vercel preview builds also use
|
|
10
|
-
* NODE_ENV=production, so NODE_ENV is not a safe production gate.
|
|
11
|
-
*/
|
|
12
|
-
export declare function isBrandlabProductionDeployment(environment: string | null | undefined): environment is "production";
|
|
13
7
|
export type BrandlabAnalyticsParameterPolicy = {
|
|
14
8
|
key: string;
|
|
15
9
|
type: 'boolean';
|
|
@@ -46,11 +40,6 @@ export interface BrandlabAnalyticsContract {
|
|
|
46
40
|
mode: 'consent-required';
|
|
47
41
|
};
|
|
48
42
|
};
|
|
49
|
-
/** Privacy-safe GA4 diagnostics. These never become canonical lead events. */
|
|
50
|
-
leadDiagnostics?: {
|
|
51
|
-
enabled: true;
|
|
52
|
-
};
|
|
53
|
-
webVitals: boolean;
|
|
54
43
|
additionalEvents?: readonly BrandlabAnalyticsEventContract[];
|
|
55
44
|
}
|
|
56
45
|
export type GtagTransport = (...args: ['event', string, Record<string, unknown>] | ['set', string, Record<string, unknown>] | ['config', string, Record<string, unknown>?] | ['js', Date]) => void;
|
package/dist/analytics.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../src/analytics.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,qBAAqB,2BAA4B,CAAA;AAC9D,eAAO,MAAM,kCAAkC,+BAAgC,CAAA;AAC/E,eAAO,MAAM,+BAA+B,mEAIlC,CAAA;AAEV,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAA;AACvE,MAAM,MAAM,8BAA8B,GACxC,CAAC,OAAO,kCAAkC,CAAC,CAAC,MAAM,CAAC,CAAA;AACrD,MAAM,MAAM,2BAA2B,GACrC,CAAC,OAAO,+BAA+B,CAAC,CAAC,MAAM,CAAC,CAAA;AAElD
|
|
1
|
+
{"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../src/analytics.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,qBAAqB,2BAA4B,CAAA;AAC9D,eAAO,MAAM,kCAAkC,+BAAgC,CAAA;AAC/E,eAAO,MAAM,+BAA+B,mEAIlC,CAAA;AAEV,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAA;AACvE,MAAM,MAAM,8BAA8B,GACxC,CAAC,OAAO,kCAAkC,CAAC,CAAC,MAAM,CAAC,CAAA;AACrD,MAAM,MAAM,2BAA2B,GACrC,CAAC,OAAO,+BAA+B,CAAC,CAAC,MAAM,CAAC,CAAA;AAElD,MAAM,MAAM,gCAAgC,GACxC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GAChC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAClE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GACxD;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAA;AAEjC,MAAM,WAAW,8BAA8B;IAC7C,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,SAAS,gCAAgC,EAAE,CAAA;CACxD;AAED,MAAM,WAAW,yBAAyB;IACxC,yFAAyF;IACzF,SAAS,EAAE,MAAM,CAAA;IACjB,gBAAgB,EAAE,MAAM,CAAA;IACxB,SAAS,CAAC,EAAE;QACV,eAAe,EAAE,MAAM,CAAA;QACvB,yBAAyB,EAAE,MAAM,CAAA;QACjC,6EAA6E;QAC7E,YAAY,CAAC,EAAE;YACb,kBAAkB,EAAE,MAAM,CAAA;SAC3B,CAAA;QACD,mFAAmF;QACnF,mBAAmB,CAAC,EAAE;YAAE,IAAI,EAAE,kBAAkB,CAAA;SAAE,CAAA;KACnD,CAAA;IACD,gBAAgB,CAAC,EAAE,SAAS,8BAA8B,EAAE,CAAA;CAC7D;AACD,MAAM,MAAM,aAAa,GAAG,CAAC,GAAG,IAAI,EAChC,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAC1C,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GACxC,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,GAC5C,CAAC,IAAI,EAAE,IAAI,CAAC,KACX,IAAI,CAAA;AAET,MAAM,WAAW,0BAA0B;IACzC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACtB;AAED,MAAM,WAAW,mCAAoC,SAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAClF,KAAK,EAAE,MAAM,CAAA;IACb,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,2BAA2B;IAC1C,OAAO,EAAE,OAAO,CAAA;IAChB,sBAAsB,EAAE,OAAO,CAAA;IAC/B,iBAAiB,EAAE,OAAO,CAAA;CAC3B;AA6DD,wBAAgB,uBAAuB,CAAC,KAAK,CAAC,CAAC,SAAS,yBAAyB,EAAE,QAAQ,EAAE,CAAC,GAAG,CAAC,CAyGjG;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,kBAAkB,CAE/E;AAED,wBAAgB,gCAAgC,CAC9C,KAAK,EAAE,MAAM,GACZ,KAAK,IAAI,8BAA8B,CAIzC;AAED,wBAAgB,8BAA8B,CAAC,YAAY,EAAE,MAAM,UAGlE;AAMD,wBAAgB,gCAAgC,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,sBAgBhF;AAED,wBAAgB,gCAAgC,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,sBAShF;AAED,wBAAgB,+BAA+B,CAC7C,QAAQ,EAAE,0BAA0B,GAAG,IAAI,GAAG,SAAS,GACtD,mCAAmC,GAAG,SAAS,CAcjD;AAED,wBAAgB,yBAAyB,CAAC,OAAO,EAAE;IACjD,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAChC,sBASA;AAED,wBAAgB,6BAA6B,CAAC,OAAO,EAAE;IACrD,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAChC,sBAEA;AAED,wBAAgB,0BAA0B,CAAC,OAAO,EAAE;IAClD,OAAO,EAAE,OAAO,CAAA;IAChB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,IAAI,CAAC,EAAE,aAAa,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAChC,WAWA;AAED,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,2BAmC3E;AA2BD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE;IACjD,OAAO,EAAE,OAAO,CAAA;IAChB,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;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,kBAAkB,CAAC,EAAE,0BAA0B,GAAG,IAAI,CAAA;IACtD,0BAA0B,CAAC,EAAE,OAAO,CAAA;IACpC,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACpC,GAAG,2BAA2B,CA0D9B"}
|
package/dist/analytics.js
CHANGED
|
@@ -7,14 +7,6 @@ export const BRANDLAB_LEAD_DIAGNOSTIC_EVENTS = [
|
|
|
7
7
|
'lead_form_error',
|
|
8
8
|
'lead_cta_click',
|
|
9
9
|
];
|
|
10
|
-
/**
|
|
11
|
-
* Analytics and other production-only browser behaviour must use the actual
|
|
12
|
-
* Vercel deployment environment. Vercel preview builds also use
|
|
13
|
-
* NODE_ENV=production, so NODE_ENV is not a safe production gate.
|
|
14
|
-
*/
|
|
15
|
-
export function isBrandlabProductionDeployment(environment) {
|
|
16
|
-
return environment === 'production';
|
|
17
|
-
}
|
|
18
10
|
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
19
11
|
const GMAIL_DOMAINS = new Set(['gmail.com', 'googlemail.com']);
|
|
20
12
|
const AUSTRALIAN_E164_DIGITS = /^61[23478]\d{8}$/;
|
|
@@ -52,15 +44,31 @@ function looksLikePersonalIdentifier(value) {
|
|
|
52
44
|
/^[a-f0-9]{24,}$/.test(normalized) ||
|
|
53
45
|
/\b\d+\s+[a-z][a-z -]+\s+(?:street|st|road|rd|avenue|ave|lane|ln|drive|dr|place|pl|court|ct)\b/.test(normalized));
|
|
54
46
|
}
|
|
47
|
+
const CONTRACT_KEYS = ['projectId', 'measurementIdEnv', 'googleAds', 'additionalEvents'];
|
|
48
|
+
const GOOGLE_ADS_KEYS = ['conversionIdEnv', 'contactConversionLabelEnv', 'websiteCalls', 'enhancedConversions'];
|
|
49
|
+
function assertContractKeys(value, allowed, field) {
|
|
50
|
+
const unsupported = Object.keys(value).filter((key) => !allowed.includes(key));
|
|
51
|
+
if (unsupported.length > 0) {
|
|
52
|
+
throw new Error(`${field} contains unsupported keys: ${unsupported.join(', ')}.`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
55
|
function assertAnalyticsEnvKey(value, field) {
|
|
56
56
|
if (!ANALYTICS_ENV_KEY_PATTERN.test(value)) {
|
|
57
57
|
throw new Error(`${field} must be an environment variable key.`);
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
export function defineAnalyticsContract(contract) {
|
|
61
|
+
if (!contract || typeof contract !== 'object' || Array.isArray(contract)) {
|
|
62
|
+
throw new Error('Analytics contract must be an object.');
|
|
63
|
+
}
|
|
64
|
+
// Retired 3.0 flags fail here instead of being silently ignored: web vitals
|
|
65
|
+
// were removed (page speed comes from Search Console) and the privacy-safe
|
|
66
|
+
// lead diagnostics are always on.
|
|
67
|
+
assertContractKeys(contract, CONTRACT_KEYS, 'Analytics contract');
|
|
61
68
|
assertBrandlabCanonicalEntityId(contract.projectId);
|
|
62
69
|
assertAnalyticsEnvKey(contract.measurementIdEnv, 'measurementIdEnv');
|
|
63
70
|
if (contract.googleAds) {
|
|
71
|
+
assertContractKeys(contract.googleAds, GOOGLE_ADS_KEYS, 'googleAds');
|
|
64
72
|
assertAnalyticsEnvKey(contract.googleAds.conversionIdEnv, 'googleAds.conversionIdEnv');
|
|
65
73
|
assertAnalyticsEnvKey(contract.googleAds.contactConversionLabelEnv, 'googleAds.contactConversionLabelEnv');
|
|
66
74
|
if (contract.googleAds.websiteCalls) {
|
package/dist/analytics.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analytics.js","sourceRoot":"","sources":["../src/analytics.ts"],"names":[],"mappings":"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","sourcesContent":["import { normalizeBrandlabRoutePath } from './routes.js'\nimport { assertBrandlabCanonicalEntityId } from './entity-id.js'\n\nexport const CANONICAL_LEAD_EVENTS = ['contact_form'] as const\nexport const CANONICAL_PHONE_INTERACTION_EVENTS = ['phone_link_click'] as const\nexport const BRANDLAB_LEAD_DIAGNOSTIC_EVENTS = [\n 'lead_form_start',\n 'lead_form_error',\n 'lead_cta_click',\n] as const\n\nexport type CanonicalLeadEvent = (typeof CANONICAL_LEAD_EVENTS)[number]\nexport type CanonicalPhoneInteractionEvent =\n (typeof CANONICAL_PHONE_INTERACTION_EVENTS)[number]\nexport type BrandlabLeadDiagnosticEvent =\n (typeof BRANDLAB_LEAD_DIAGNOSTIC_EVENTS)[number]\n\n/**\n * Analytics and other production-only browser behaviour must use the actual\n * Vercel deployment environment. Vercel preview builds also use\n * NODE_ENV=production, so NODE_ENV is not a safe production gate.\n */\nexport function isBrandlabProductionDeployment(\n environment: string | null | undefined,\n) {\n return environment === 'production'\n}\n\nexport type BrandlabAnalyticsParameterPolicy =\n | { key: string; type: 'boolean' }\n | { key: string; type: 'count'; minimum?: number; maximum?: number }\n | { key: string; type: 'enum'; values: readonly string[] }\n | { key: string; type: 'slug' }\n\nexport interface BrandlabAnalyticsEventContract {\n name: string\n parameters: readonly BrandlabAnalyticsParameterPolicy[]\n}\n\nexport interface BrandlabAnalyticsContract {\n /** Canonical entity binding prevents analytics config being reused by the wrong site. */\n projectId: string\n measurementIdEnv: string\n googleAds?: {\n conversionIdEnv: string\n contactConversionLabelEnv: string\n /** Google Ads forwarding-number label for genuine calls from the website. */\n websiteCalls?: {\n conversionLabelEnv: string\n }\n /** Enhanced Conversions require both this project opt-in and live user consent. */\n enhancedConversions?: { mode: 'consent-required' }\n }\n /** Privacy-safe GA4 diagnostics. These never become canonical lead events. */\n leadDiagnostics?: { enabled: true }\n webVitals: boolean\n additionalEvents?: readonly BrandlabAnalyticsEventContract[]\n}\nexport type GtagTransport = (...args:\n | ['event', string, Record<string, unknown>]\n | ['set', string, Record<string, unknown>]\n | ['config', string, Record<string, unknown>?]\n | ['js', Date]\n) => void\n\nexport interface EnhancedConversionUserData {\n email?: string | null\n phone?: string | null\n}\n\nexport interface GoogleTagEnhancedConversionUserData extends Record<string, unknown> {\n email: string\n phone_number?: string\n}\n\nexport interface ContactFormConversionResult {\n ga4Sent: boolean\n enhancedConversionSent: boolean\n adsConversionSent: boolean\n}\n\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\nconst GMAIL_DOMAINS = new Set(['gmail.com', 'googlemail.com'])\nconst AUSTRALIAN_E164_DIGITS = /^61[23478]\\d{8}$/\nconst GA4_CONTACT_FORM_PARAM_KEYS = new Set([\n 'event_category',\n 'location_slug',\n 'page_path',\n 'page_template',\n 'service_slug',\n 'submission_id',\n])\nconst GOOGLE_ADS_CONVERSION_PARAM_KEYS = new Set([\n 'currency',\n 'transaction_id',\n 'value',\n])\nconst ANALYTICS_ENV_KEY_PATTERN = /^[A-Z][A-Z0-9_]*$/\nconst ANALYTICS_EVENT_PATTERN = /^[a-z][a-z0-9_]{0,39}$/\nconst PROHIBITED_ANALYTICS_PARAM_TOKENS = [\n 'address',\n 'email',\n 'first_name',\n 'last_name',\n 'message',\n 'name',\n 'phone',\n] as const\nconst PRIVACY_SAFE_SUBMISSION_ID_PATTERN = /^[a-f0-9]{64}$/\n\nfunction looksLikePersonalIdentifier(value: string) {\n const normalized = value.trim().toLowerCase()\n const digits = normalized.replace(/\\D/g, '')\n return (\n normalized.includes('@') ||\n digits.length >= 8 ||\n /^\\d{4}$/.test(normalized) ||\n /^[a-f0-9]{24,}$/.test(normalized) ||\n /\\b\\d+\\s+[a-z][a-z -]+\\s+(?:street|st|road|rd|avenue|ave|lane|ln|drive|dr|place|pl|court|ct)\\b/.test(\n normalized,\n )\n )\n}\n\nfunction assertAnalyticsEnvKey(value: string, field: string) {\n if (!ANALYTICS_ENV_KEY_PATTERN.test(value)) {\n throw new Error(`${field} must be an environment variable key.`)\n }\n}\n\nexport function defineAnalyticsContract<const T extends BrandlabAnalyticsContract>(contract: T): T {\n assertBrandlabCanonicalEntityId(contract.projectId)\n assertAnalyticsEnvKey(contract.measurementIdEnv, 'measurementIdEnv')\n if (contract.googleAds) {\n assertAnalyticsEnvKey(contract.googleAds.conversionIdEnv, 'googleAds.conversionIdEnv')\n assertAnalyticsEnvKey(\n contract.googleAds.contactConversionLabelEnv,\n 'googleAds.contactConversionLabelEnv',\n )\n if (contract.googleAds.websiteCalls) {\n assertAnalyticsEnvKey(\n contract.googleAds.websiteCalls.conversionLabelEnv,\n 'googleAds.websiteCalls.conversionLabelEnv',\n )\n }\n if (\n contract.googleAds.enhancedConversions !== undefined &&\n contract.googleAds.enhancedConversions.mode !== 'consent-required'\n ) {\n throw new Error('googleAds.enhancedConversions must require consent.')\n }\n }\n\n const events = contract.additionalEvents ?? []\n const duplicate = events.find((event, index) =>\n events.slice(0, index).some((candidate) => candidate.name === event.name),\n )\n if (duplicate) throw new Error(`Analytics contract contains duplicate event \"${duplicate.name}\".`)\n\n const reservedDiagnostic = events.find((event) =>\n BRANDLAB_LEAD_DIAGNOSTIC_EVENTS.includes(event.name as BrandlabLeadDiagnosticEvent) ||\n CANONICAL_PHONE_INTERACTION_EVENTS.includes(\n event.name as CanonicalPhoneInteractionEvent,\n ),\n )\n if (reservedDiagnostic) {\n throw new Error(\n `Shared lead diagnostic event \"${reservedDiagnostic.name}\" must not be redefined.`,\n )\n }\n\n events.forEach((event, eventIndex) => {\n if (!ANALYTICS_EVENT_PATTERN.test(event.name)) {\n throw new Error(`additionalEvents[${eventIndex}].name is invalid.`)\n }\n if (isCanonicalLeadEvent(event.name)) {\n throw new Error(`Canonical lead event \"${event.name}\" must not be redefined.`)\n }\n if (event.parameters.length === 0 || event.parameters.length > 20) {\n throw new Error(`additionalEvents[${eventIndex}].parameters must contain 1 to 20 policies.`)\n }\n const duplicateParameter = event.parameters.find((parameter, index) =>\n event.parameters.slice(0, index).some((candidate) => candidate.key === parameter.key),\n )\n if (duplicateParameter) {\n throw new Error(`Analytics event \"${event.name}\" contains duplicate parameter \"${duplicateParameter.key}\".`)\n }\n event.parameters.forEach((parameter, keyIndex) => {\n const key = parameter.key\n if (!ANALYTICS_EVENT_PATTERN.test(key)) {\n throw new Error(`additionalEvents[${eventIndex}].parameters[${keyIndex}].key is invalid.`)\n }\n const normalized = key.toLowerCase()\n if (PROHIBITED_ANALYTICS_PARAM_TOKENS.some((token) => normalized.includes(token))) {\n throw new Error(`Analytics parameter \"${key}\" may contain personal information.`)\n }\n if (parameter.type === 'enum') {\n if (\n parameter.values.length === 0 ||\n parameter.values.length > 50 ||\n new Set(parameter.values).size !== parameter.values.length ||\n parameter.values.some(\n (value) =>\n !value.trim() ||\n value.length > 80 ||\n looksLikePersonalIdentifier(value),\n )\n ) {\n throw new Error(`Analytics enum parameter \"${key}\" has an unsafe value policy.`)\n }\n } else if (parameter.type === 'count') {\n const minimum = parameter.minimum ?? 0\n const maximum = parameter.maximum ?? 100_000\n if (\n !Number.isInteger(minimum) ||\n !Number.isInteger(maximum) ||\n minimum < 0 ||\n maximum < minimum ||\n maximum > 1_000_000\n ) {\n throw new Error(`Analytics count parameter \"${key}\" has an invalid range.`)\n }\n }\n })\n })\n\n return contract\n}\n\nexport function isCanonicalLeadEvent(value: string): value is CanonicalLeadEvent {\n return CANONICAL_LEAD_EVENTS.includes(value as CanonicalLeadEvent)\n}\n\nexport function isCanonicalPhoneInteractionEvent(\n value: string,\n): value is CanonicalPhoneInteractionEvent {\n return CANONICAL_PHONE_INTERACTION_EVENTS.includes(\n value as CanonicalPhoneInteractionEvent,\n )\n}\n\nexport function normalizeGoogleAdsConversionId(conversionId: string) {\n const trimmed = conversionId.trim()\n return trimmed.startsWith('AW-') ? trimmed : `AW-${trimmed}`\n}\n\nfunction isGoogleAdsConversionLabel(value: string) {\n return /^[A-Za-z0-9_-]{1,100}$/.test(value)\n}\n\nexport function normalizeEnhancedConversionEmail(email: string | null | undefined) {\n const trimmed = email?.trim().toLowerCase()\n if (!trimmed || !EMAIL_PATTERN.test(trimmed)) {\n return undefined\n }\n\n const [localPart, domain] = trimmed.split('@')\n if (!localPart || !domain) {\n return undefined\n }\n\n const normalizedLocalPart = GMAIL_DOMAINS.has(domain)\n ? localPart.replace(/\\./g, '')\n : localPart\n\n return `${normalizedLocalPart}@${domain}`\n}\n\nexport function normalizeEnhancedConversionPhone(phone: string | null | undefined) {\n const digits = phone?.replace(/[^\\d]/g, '') ?? ''\n if (!digits) {\n return undefined\n }\n\n const normalizedDigits = digits.startsWith('0') ? `61${digits.slice(1)}` : digits\n\n return AUSTRALIAN_E164_DIGITS.test(normalizedDigits) ? `+${normalizedDigits}` : undefined\n}\n\nexport function buildEnhancedConversionUserData(\n userData: EnhancedConversionUserData | null | undefined,\n): GoogleTagEnhancedConversionUserData | undefined {\n const email = normalizeEnhancedConversionEmail(userData?.email)\n if (!email) {\n return undefined\n }\n\n const phoneNumber = normalizeEnhancedConversionPhone(userData?.phone)\n\n return phoneNumber\n ? {\n email,\n phone_number: phoneNumber,\n }\n : { email }\n}\n\nexport function getGoogleAdsContactSendTo(options: {\n conversionId?: string | null\n conversionLabel?: string | null\n}) {\n const conversionId = options.conversionId?.trim()\n const conversionLabel = options.conversionLabel?.trim()\n\n if (!conversionId || !conversionLabel || !isGoogleAdsConversionLabel(conversionLabel)) {\n return undefined\n }\n\n return `${normalizeGoogleAdsConversionId(conversionId)}/${conversionLabel}`\n}\n\nexport function getGoogleAdsWebsiteCallSendTo(options: {\n conversionId?: string | null\n conversionLabel?: string | null\n}) {\n return getGoogleAdsContactSendTo(options)\n}\n\nexport function sendBrandlabAnalyticsEvent(options: {\n enabled: boolean\n measurementId?: string | null\n gtag?: GtagTransport\n eventName: string\n params: Record<string, unknown>\n}) {\n if (!options.enabled || !options.measurementId || typeof options.gtag !== 'function') {\n return false\n }\n\n options.gtag('event', options.eventName, {\n ...options.params,\n send_to: options.measurementId,\n })\n\n return true\n}\n\nexport function sanitizeContactFormGa4Params(params: Record<string, unknown>) {\n return Object.entries(params).reduce<Record<string, unknown>>((sanitized, [key, value]) => {\n const normalizedKey = key.toLowerCase()\n if (!GA4_CONTACT_FORM_PARAM_KEYS.has(normalizedKey)) return sanitized\n if (normalizedKey === 'event_category') {\n if (value === 'lead') sanitized.event_category = 'lead'\n return sanitized\n }\n if (normalizedKey === 'page_path') {\n if (typeof value !== 'string') return sanitized\n const path = value.trim().split(/[?#]/, 1)[0]\n if (!path || path.length > 500) return sanitized\n try {\n sanitized.page_path = normalizeBrandlabRoutePath(path)\n } catch {\n // Invalid or encoded path separators are not analytics dimensions.\n }\n return sanitized\n }\n if (normalizedKey === 'submission_id') {\n if (typeof value === 'string' && PRIVACY_SAFE_SUBMISSION_ID_PATTERN.test(value)) {\n sanitized.submission_id = value\n }\n return sanitized\n }\n if (\n typeof value === 'string' &&\n /^[a-z0-9]+(?:[-_][a-z0-9]+)*$/.test(value) &&\n value.length <= 80\n ) {\n sanitized[normalizedKey] = value\n }\n\n return sanitized\n }, {})\n}\n\nfunction sanitizeGoogleAdsConversionParams(params: Record<string, unknown>) {\n return Object.entries(params).reduce<Record<string, unknown>>((sanitized, [key, value]) => {\n const normalizedKey = key.toLowerCase()\n if (!GOOGLE_ADS_CONVERSION_PARAM_KEYS.has(normalizedKey)) return sanitized\n if (normalizedKey === 'currency' && typeof value === 'string' && /^[A-Z]{3}$/.test(value)) {\n sanitized.currency = value\n } else if (\n normalizedKey === 'value' &&\n typeof value === 'number' &&\n Number.isFinite(value) &&\n value >= 0\n ) {\n sanitized.value = value\n } else if (\n normalizedKey === 'transaction_id' &&\n typeof value === 'string' &&\n PRIVACY_SAFE_SUBMISSION_ID_PATTERN.test(value)\n ) {\n sanitized.transaction_id = value\n }\n\n return sanitized\n }, {})\n}\n\nexport function sendContactFormConversion(options: {\n enabled: boolean\n measurementId?: string | null\n googleAdsConversionId?: string | null\n googleAdsConversionLabel?: string | null\n gtag?: GtagTransport\n pagePath: string\n submissionId?: string | null\n enhancedConversion?: EnhancedConversionUserData | null\n enhancedConversionsEnabled?: boolean\n adUserDataConsent?: boolean\n params?: Record<string, unknown>\n adsParams?: Record<string, unknown>\n}): ContactFormConversionResult {\n const submissionId = options.submissionId?.trim().toLowerCase()\n const privacySafeSubmissionId =\n submissionId && PRIVACY_SAFE_SUBMISSION_ID_PATTERN.test(submissionId)\n ? submissionId\n : undefined\n const baseParams = sanitizeContactFormGa4Params({\n ...options.params,\n page_path: options.pagePath,\n ...(privacySafeSubmissionId ? { submission_id: privacySafeSubmissionId } : {}),\n event_category: 'lead',\n })\n const ga4Sent = sendBrandlabAnalyticsEvent({\n enabled: options.enabled,\n measurementId: options.measurementId,\n gtag: options.gtag,\n eventName: 'contact_form',\n params: baseParams,\n })\n const sendTo = getGoogleAdsContactSendTo({\n conversionId: options.googleAdsConversionId,\n conversionLabel: options.googleAdsConversionLabel,\n })\n const userData =\n options.enhancedConversionsEnabled && options.adUserDataConsent\n ? buildEnhancedConversionUserData(options.enhancedConversion)\n : undefined\n const canSendAdsConversion =\n options.enabled &&\n typeof options.gtag === 'function' &&\n Boolean(sendTo)\n\n if (!canSendAdsConversion || !sendTo || typeof options.gtag !== 'function') {\n return {\n ga4Sent,\n enhancedConversionSent: false,\n adsConversionSent: false,\n }\n }\n\n if (userData) {\n options.gtag('set', 'user_data', userData)\n }\n try {\n options.gtag('event', 'conversion', {\n ...sanitizeGoogleAdsConversionParams(options.adsParams ?? {}),\n send_to: sendTo,\n ...(privacySafeSubmissionId ? { transaction_id: privacySafeSubmissionId } : {}),\n })\n } finally {\n if (userData) options.gtag('set', 'user_data', {})\n }\n\n return {\n ga4Sent,\n enhancedConversionSent: Boolean(userData),\n adsConversionSent: true,\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"analytics.js","sourceRoot":"","sources":["../src/analytics.ts"],"names":[],"mappings":"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","sourcesContent":["import { normalizeBrandlabRoutePath } from './routes.js'\nimport { assertBrandlabCanonicalEntityId } from './entity-id.js'\n\nexport const CANONICAL_LEAD_EVENTS = ['contact_form'] as const\nexport const CANONICAL_PHONE_INTERACTION_EVENTS = ['phone_link_click'] as const\nexport const BRANDLAB_LEAD_DIAGNOSTIC_EVENTS = [\n 'lead_form_start',\n 'lead_form_error',\n 'lead_cta_click',\n] as const\n\nexport type CanonicalLeadEvent = (typeof CANONICAL_LEAD_EVENTS)[number]\nexport type CanonicalPhoneInteractionEvent =\n (typeof CANONICAL_PHONE_INTERACTION_EVENTS)[number]\nexport type BrandlabLeadDiagnosticEvent =\n (typeof BRANDLAB_LEAD_DIAGNOSTIC_EVENTS)[number]\n\nexport type BrandlabAnalyticsParameterPolicy =\n | { key: string; type: 'boolean' }\n | { key: string; type: 'count'; minimum?: number; maximum?: number }\n | { key: string; type: 'enum'; values: readonly string[] }\n | { key: string; type: 'slug' }\n\nexport interface BrandlabAnalyticsEventContract {\n name: string\n parameters: readonly BrandlabAnalyticsParameterPolicy[]\n}\n\nexport interface BrandlabAnalyticsContract {\n /** Canonical entity binding prevents analytics config being reused by the wrong site. */\n projectId: string\n measurementIdEnv: string\n googleAds?: {\n conversionIdEnv: string\n contactConversionLabelEnv: string\n /** Google Ads forwarding-number label for genuine calls from the website. */\n websiteCalls?: {\n conversionLabelEnv: string\n }\n /** Enhanced Conversions require both this project opt-in and live user consent. */\n enhancedConversions?: { mode: 'consent-required' }\n }\n additionalEvents?: readonly BrandlabAnalyticsEventContract[]\n}\nexport type GtagTransport = (...args:\n | ['event', string, Record<string, unknown>]\n | ['set', string, Record<string, unknown>]\n | ['config', string, Record<string, unknown>?]\n | ['js', Date]\n) => void\n\nexport interface EnhancedConversionUserData {\n email?: string | null\n phone?: string | null\n}\n\nexport interface GoogleTagEnhancedConversionUserData extends Record<string, unknown> {\n email: string\n phone_number?: string\n}\n\nexport interface ContactFormConversionResult {\n ga4Sent: boolean\n enhancedConversionSent: boolean\n adsConversionSent: boolean\n}\n\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\nconst GMAIL_DOMAINS = new Set(['gmail.com', 'googlemail.com'])\nconst AUSTRALIAN_E164_DIGITS = /^61[23478]\\d{8}$/\nconst GA4_CONTACT_FORM_PARAM_KEYS = new Set([\n 'event_category',\n 'location_slug',\n 'page_path',\n 'page_template',\n 'service_slug',\n 'submission_id',\n])\nconst GOOGLE_ADS_CONVERSION_PARAM_KEYS = new Set([\n 'currency',\n 'transaction_id',\n 'value',\n])\nconst ANALYTICS_ENV_KEY_PATTERN = /^[A-Z][A-Z0-9_]*$/\nconst ANALYTICS_EVENT_PATTERN = /^[a-z][a-z0-9_]{0,39}$/\nconst PROHIBITED_ANALYTICS_PARAM_TOKENS = [\n 'address',\n 'email',\n 'first_name',\n 'last_name',\n 'message',\n 'name',\n 'phone',\n] as const\nconst PRIVACY_SAFE_SUBMISSION_ID_PATTERN = /^[a-f0-9]{64}$/\n\nfunction looksLikePersonalIdentifier(value: string) {\n const normalized = value.trim().toLowerCase()\n const digits = normalized.replace(/\\D/g, '')\n return (\n normalized.includes('@') ||\n digits.length >= 8 ||\n /^\\d{4}$/.test(normalized) ||\n /^[a-f0-9]{24,}$/.test(normalized) ||\n /\\b\\d+\\s+[a-z][a-z -]+\\s+(?:street|st|road|rd|avenue|ave|lane|ln|drive|dr|place|pl|court|ct)\\b/.test(\n normalized,\n )\n )\n}\n\nconst CONTRACT_KEYS = ['projectId', 'measurementIdEnv', 'googleAds', 'additionalEvents']\nconst GOOGLE_ADS_KEYS = ['conversionIdEnv', 'contactConversionLabelEnv', 'websiteCalls', 'enhancedConversions']\n\nfunction assertContractKeys(value: object, allowed: readonly string[], field: string) {\n const unsupported = Object.keys(value).filter((key) => !allowed.includes(key))\n if (unsupported.length > 0) {\n throw new Error(`${field} contains unsupported keys: ${unsupported.join(', ')}.`)\n }\n}\n\nfunction assertAnalyticsEnvKey(value: string, field: string) {\n if (!ANALYTICS_ENV_KEY_PATTERN.test(value)) {\n throw new Error(`${field} must be an environment variable key.`)\n }\n}\n\nexport function defineAnalyticsContract<const T extends BrandlabAnalyticsContract>(contract: T): T {\n if (!contract || typeof contract !== 'object' || Array.isArray(contract)) {\n throw new Error('Analytics contract must be an object.')\n }\n // Retired 3.0 flags fail here instead of being silently ignored: web vitals\n // were removed (page speed comes from Search Console) and the privacy-safe\n // lead diagnostics are always on.\n assertContractKeys(contract, CONTRACT_KEYS, 'Analytics contract')\n assertBrandlabCanonicalEntityId(contract.projectId)\n assertAnalyticsEnvKey(contract.measurementIdEnv, 'measurementIdEnv')\n if (contract.googleAds) {\n assertContractKeys(contract.googleAds, GOOGLE_ADS_KEYS, 'googleAds')\n assertAnalyticsEnvKey(contract.googleAds.conversionIdEnv, 'googleAds.conversionIdEnv')\n assertAnalyticsEnvKey(\n contract.googleAds.contactConversionLabelEnv,\n 'googleAds.contactConversionLabelEnv',\n )\n if (contract.googleAds.websiteCalls) {\n assertAnalyticsEnvKey(\n contract.googleAds.websiteCalls.conversionLabelEnv,\n 'googleAds.websiteCalls.conversionLabelEnv',\n )\n }\n if (\n contract.googleAds.enhancedConversions !== undefined &&\n contract.googleAds.enhancedConversions.mode !== 'consent-required'\n ) {\n throw new Error('googleAds.enhancedConversions must require consent.')\n }\n }\n\n const events = contract.additionalEvents ?? []\n const duplicate = events.find((event, index) =>\n events.slice(0, index).some((candidate) => candidate.name === event.name),\n )\n if (duplicate) throw new Error(`Analytics contract contains duplicate event \"${duplicate.name}\".`)\n\n const reservedDiagnostic = events.find((event) =>\n BRANDLAB_LEAD_DIAGNOSTIC_EVENTS.includes(event.name as BrandlabLeadDiagnosticEvent) ||\n CANONICAL_PHONE_INTERACTION_EVENTS.includes(\n event.name as CanonicalPhoneInteractionEvent,\n ),\n )\n if (reservedDiagnostic) {\n throw new Error(\n `Shared lead diagnostic event \"${reservedDiagnostic.name}\" must not be redefined.`,\n )\n }\n\n events.forEach((event, eventIndex) => {\n if (!ANALYTICS_EVENT_PATTERN.test(event.name)) {\n throw new Error(`additionalEvents[${eventIndex}].name is invalid.`)\n }\n if (isCanonicalLeadEvent(event.name)) {\n throw new Error(`Canonical lead event \"${event.name}\" must not be redefined.`)\n }\n if (event.parameters.length === 0 || event.parameters.length > 20) {\n throw new Error(`additionalEvents[${eventIndex}].parameters must contain 1 to 20 policies.`)\n }\n const duplicateParameter = event.parameters.find((parameter, index) =>\n event.parameters.slice(0, index).some((candidate) => candidate.key === parameter.key),\n )\n if (duplicateParameter) {\n throw new Error(`Analytics event \"${event.name}\" contains duplicate parameter \"${duplicateParameter.key}\".`)\n }\n event.parameters.forEach((parameter, keyIndex) => {\n const key = parameter.key\n if (!ANALYTICS_EVENT_PATTERN.test(key)) {\n throw new Error(`additionalEvents[${eventIndex}].parameters[${keyIndex}].key is invalid.`)\n }\n const normalized = key.toLowerCase()\n if (PROHIBITED_ANALYTICS_PARAM_TOKENS.some((token) => normalized.includes(token))) {\n throw new Error(`Analytics parameter \"${key}\" may contain personal information.`)\n }\n if (parameter.type === 'enum') {\n if (\n parameter.values.length === 0 ||\n parameter.values.length > 50 ||\n new Set(parameter.values).size !== parameter.values.length ||\n parameter.values.some(\n (value) =>\n !value.trim() ||\n value.length > 80 ||\n looksLikePersonalIdentifier(value),\n )\n ) {\n throw new Error(`Analytics enum parameter \"${key}\" has an unsafe value policy.`)\n }\n } else if (parameter.type === 'count') {\n const minimum = parameter.minimum ?? 0\n const maximum = parameter.maximum ?? 100_000\n if (\n !Number.isInteger(minimum) ||\n !Number.isInteger(maximum) ||\n minimum < 0 ||\n maximum < minimum ||\n maximum > 1_000_000\n ) {\n throw new Error(`Analytics count parameter \"${key}\" has an invalid range.`)\n }\n }\n })\n })\n\n return contract\n}\n\nexport function isCanonicalLeadEvent(value: string): value is CanonicalLeadEvent {\n return CANONICAL_LEAD_EVENTS.includes(value as CanonicalLeadEvent)\n}\n\nexport function isCanonicalPhoneInteractionEvent(\n value: string,\n): value is CanonicalPhoneInteractionEvent {\n return CANONICAL_PHONE_INTERACTION_EVENTS.includes(\n value as CanonicalPhoneInteractionEvent,\n )\n}\n\nexport function normalizeGoogleAdsConversionId(conversionId: string) {\n const trimmed = conversionId.trim()\n return trimmed.startsWith('AW-') ? trimmed : `AW-${trimmed}`\n}\n\nfunction isGoogleAdsConversionLabel(value: string) {\n return /^[A-Za-z0-9_-]{1,100}$/.test(value)\n}\n\nexport function normalizeEnhancedConversionEmail(email: string | null | undefined) {\n const trimmed = email?.trim().toLowerCase()\n if (!trimmed || !EMAIL_PATTERN.test(trimmed)) {\n return undefined\n }\n\n const [localPart, domain] = trimmed.split('@')\n if (!localPart || !domain) {\n return undefined\n }\n\n const normalizedLocalPart = GMAIL_DOMAINS.has(domain)\n ? localPart.replace(/\\./g, '')\n : localPart\n\n return `${normalizedLocalPart}@${domain}`\n}\n\nexport function normalizeEnhancedConversionPhone(phone: string | null | undefined) {\n const digits = phone?.replace(/[^\\d]/g, '') ?? ''\n if (!digits) {\n return undefined\n }\n\n const normalizedDigits = digits.startsWith('0') ? `61${digits.slice(1)}` : digits\n\n return AUSTRALIAN_E164_DIGITS.test(normalizedDigits) ? `+${normalizedDigits}` : undefined\n}\n\nexport function buildEnhancedConversionUserData(\n userData: EnhancedConversionUserData | null | undefined,\n): GoogleTagEnhancedConversionUserData | undefined {\n const email = normalizeEnhancedConversionEmail(userData?.email)\n if (!email) {\n return undefined\n }\n\n const phoneNumber = normalizeEnhancedConversionPhone(userData?.phone)\n\n return phoneNumber\n ? {\n email,\n phone_number: phoneNumber,\n }\n : { email }\n}\n\nexport function getGoogleAdsContactSendTo(options: {\n conversionId?: string | null\n conversionLabel?: string | null\n}) {\n const conversionId = options.conversionId?.trim()\n const conversionLabel = options.conversionLabel?.trim()\n\n if (!conversionId || !conversionLabel || !isGoogleAdsConversionLabel(conversionLabel)) {\n return undefined\n }\n\n return `${normalizeGoogleAdsConversionId(conversionId)}/${conversionLabel}`\n}\n\nexport function getGoogleAdsWebsiteCallSendTo(options: {\n conversionId?: string | null\n conversionLabel?: string | null\n}) {\n return getGoogleAdsContactSendTo(options)\n}\n\nexport function sendBrandlabAnalyticsEvent(options: {\n enabled: boolean\n measurementId?: string | null\n gtag?: GtagTransport\n eventName: string\n params: Record<string, unknown>\n}) {\n if (!options.enabled || !options.measurementId || typeof options.gtag !== 'function') {\n return false\n }\n\n options.gtag('event', options.eventName, {\n ...options.params,\n send_to: options.measurementId,\n })\n\n return true\n}\n\nexport function sanitizeContactFormGa4Params(params: Record<string, unknown>) {\n return Object.entries(params).reduce<Record<string, unknown>>((sanitized, [key, value]) => {\n const normalizedKey = key.toLowerCase()\n if (!GA4_CONTACT_FORM_PARAM_KEYS.has(normalizedKey)) return sanitized\n if (normalizedKey === 'event_category') {\n if (value === 'lead') sanitized.event_category = 'lead'\n return sanitized\n }\n if (normalizedKey === 'page_path') {\n if (typeof value !== 'string') return sanitized\n const path = value.trim().split(/[?#]/, 1)[0]\n if (!path || path.length > 500) return sanitized\n try {\n sanitized.page_path = normalizeBrandlabRoutePath(path)\n } catch {\n // Invalid or encoded path separators are not analytics dimensions.\n }\n return sanitized\n }\n if (normalizedKey === 'submission_id') {\n if (typeof value === 'string' && PRIVACY_SAFE_SUBMISSION_ID_PATTERN.test(value)) {\n sanitized.submission_id = value\n }\n return sanitized\n }\n if (\n typeof value === 'string' &&\n /^[a-z0-9]+(?:[-_][a-z0-9]+)*$/.test(value) &&\n value.length <= 80\n ) {\n sanitized[normalizedKey] = value\n }\n\n return sanitized\n }, {})\n}\n\nfunction sanitizeGoogleAdsConversionParams(params: Record<string, unknown>) {\n return Object.entries(params).reduce<Record<string, unknown>>((sanitized, [key, value]) => {\n const normalizedKey = key.toLowerCase()\n if (!GOOGLE_ADS_CONVERSION_PARAM_KEYS.has(normalizedKey)) return sanitized\n if (normalizedKey === 'currency' && typeof value === 'string' && /^[A-Z]{3}$/.test(value)) {\n sanitized.currency = value\n } else if (\n normalizedKey === 'value' &&\n typeof value === 'number' &&\n Number.isFinite(value) &&\n value >= 0\n ) {\n sanitized.value = value\n } else if (\n normalizedKey === 'transaction_id' &&\n typeof value === 'string' &&\n PRIVACY_SAFE_SUBMISSION_ID_PATTERN.test(value)\n ) {\n sanitized.transaction_id = value\n }\n\n return sanitized\n }, {})\n}\n\nexport function sendContactFormConversion(options: {\n enabled: boolean\n measurementId?: string | null\n googleAdsConversionId?: string | null\n googleAdsConversionLabel?: string | null\n gtag?: GtagTransport\n pagePath: string\n submissionId?: string | null\n enhancedConversion?: EnhancedConversionUserData | null\n enhancedConversionsEnabled?: boolean\n adUserDataConsent?: boolean\n params?: Record<string, unknown>\n adsParams?: Record<string, unknown>\n}): ContactFormConversionResult {\n const submissionId = options.submissionId?.trim().toLowerCase()\n const privacySafeSubmissionId =\n submissionId && PRIVACY_SAFE_SUBMISSION_ID_PATTERN.test(submissionId)\n ? submissionId\n : undefined\n const baseParams = sanitizeContactFormGa4Params({\n ...options.params,\n page_path: options.pagePath,\n ...(privacySafeSubmissionId ? { submission_id: privacySafeSubmissionId } : {}),\n event_category: 'lead',\n })\n const ga4Sent = sendBrandlabAnalyticsEvent({\n enabled: options.enabled,\n measurementId: options.measurementId,\n gtag: options.gtag,\n eventName: 'contact_form',\n params: baseParams,\n })\n const sendTo = getGoogleAdsContactSendTo({\n conversionId: options.googleAdsConversionId,\n conversionLabel: options.googleAdsConversionLabel,\n })\n const userData =\n options.enhancedConversionsEnabled && options.adUserDataConsent\n ? buildEnhancedConversionUserData(options.enhancedConversion)\n : undefined\n const canSendAdsConversion =\n options.enabled &&\n typeof options.gtag === 'function' &&\n Boolean(sendTo)\n\n if (!canSendAdsConversion || !sendTo || typeof options.gtag !== 'function') {\n return {\n ga4Sent,\n enhancedConversionSent: false,\n adsConversionSent: false,\n }\n }\n\n if (userData) {\n options.gtag('set', 'user_data', userData)\n }\n try {\n options.gtag('event', 'conversion', {\n ...sanitizeGoogleAdsConversionParams(options.adsParams ?? {}),\n send_to: sendTo,\n ...(privacySafeSubmissionId ? { transaction_id: privacySafeSubmissionId } : {}),\n })\n } finally {\n if (userData) options.gtag('set', 'user_data', {})\n }\n\n return {\n ga4Sent,\n enhancedConversionSent: Boolean(userData),\n adsConversionSent: true,\n }\n}\n"]}
|
package/dist/blog.d.ts
CHANGED
|
@@ -39,4 +39,15 @@ export declare function getPublishedBlogPosts(posts: readonly BrandlabBlogPost[]
|
|
|
39
39
|
status: "published";
|
|
40
40
|
publishedAt: string;
|
|
41
41
|
})[];
|
|
42
|
+
/**
|
|
43
|
+
* Heading anchor slug used by the fleet's MDX heading IDs: lower-case, drop
|
|
44
|
+
* characters other than letters, digits, underscores, spaces and hyphens, then
|
|
45
|
+
* hyphenate whitespace. Headings without usable text become `section`.
|
|
46
|
+
*/
|
|
47
|
+
export declare function slugifyHeading(text: string): string;
|
|
48
|
+
/**
|
|
49
|
+
* Returns a per-document slugger: create one per rendered article so repeated
|
|
50
|
+
* heading text receives `-2`, `-3` suffixes in document order.
|
|
51
|
+
*/
|
|
52
|
+
export declare function createHeadingSlugger(): (text: string) => string;
|
|
42
53
|
//# sourceMappingURL=blog.d.ts.map
|
package/dist/blog.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blog.d.ts","sourceRoot":"","sources":["../src/blog.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAA;AAE9D,MAAM,MAAM,mBAAmB,GAAG,KAAK,GAAG,YAAY,CAAA;AACtD,MAAM,MAAM,sBAAsB,GAAG,OAAO,GAAG,WAAW,CAAA;AAE1D,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,mBAAmB,CAAA;IAC5B,MAAM,EAAE,sBAAsB,CAAA;IAC9B,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,0FAA0F;IAC1F,GAAG,CAAC,EAAE;QACJ,KAAK,EAAE,MAAM,CAAA;QACb,SAAS,CAAC,EAAE,yBAAyB,CAAA;KACtC,CAAA;IACD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,SAAS,kBAAkB,EAAE,CAAA;IACtC,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC9B,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACxB,KAAK,CAAC,EAAE;QACN,GAAG,EAAE,MAAM,CAAA;QACX,GAAG,EAAE,MAAM,CAAA;KACZ,CAAA;IACD,UAAU,EAAE,MAAM,CAAA;CACnB;AAoBD,wBAAgB,cAAc,CAAC,KAAK,CAAC,CAAC,SAAS,gBAAgB,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,CA0F3E;AAcD,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,gBAAgB,EACtB,GAAG,GAAE,IAAiB,GACrB,IAAI,IAAI,gBAAgB,GAAG;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAMzE;AAED,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,SAAS,gBAAgB,EAAE,EAClC,GAAG,GAAE,IAAiB;YAGiC,WAAW;iBAAe,MAAM;KAIxF"}
|
|
1
|
+
{"version":3,"file":"blog.d.ts","sourceRoot":"","sources":["../src/blog.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAA;AAE9D,MAAM,MAAM,mBAAmB,GAAG,KAAK,GAAG,YAAY,CAAA;AACtD,MAAM,MAAM,sBAAsB,GAAG,OAAO,GAAG,WAAW,CAAA;AAE1D,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,mBAAmB,CAAA;IAC5B,MAAM,EAAE,sBAAsB,CAAA;IAC9B,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,0FAA0F;IAC1F,GAAG,CAAC,EAAE;QACJ,KAAK,EAAE,MAAM,CAAA;QACb,SAAS,CAAC,EAAE,yBAAyB,CAAA;KACtC,CAAA;IACD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,SAAS,kBAAkB,EAAE,CAAA;IACtC,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC9B,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACxB,KAAK,CAAC,EAAE;QACN,GAAG,EAAE,MAAM,CAAA;QACX,GAAG,EAAE,MAAM,CAAA;KACZ,CAAA;IACD,UAAU,EAAE,MAAM,CAAA;CACnB;AAoBD,wBAAgB,cAAc,CAAC,KAAK,CAAC,CAAC,SAAS,gBAAgB,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,CA0F3E;AAcD,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,gBAAgB,EACtB,GAAG,GAAE,IAAiB,GACrB,IAAI,IAAI,gBAAgB,GAAG;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAMzE;AAED,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,SAAS,gBAAgB,EAAE,EAClC,GAAG,GAAE,IAAiB;YAGiC,WAAW;iBAAe,MAAM;KAIxF;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CASnD;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,KAG1B,MAAM,MAAM,KAAG,MAAM,CAQ9B"}
|
package/dist/blog.js
CHANGED
|
@@ -123,4 +123,31 @@ export function getPublishedBlogPosts(posts, now = new Date()) {
|
|
|
123
123
|
.filter((post) => isPublishedBlogPost(post, now))
|
|
124
124
|
.sort((left, right) => Date.parse(right.publishedAt) - Date.parse(left.publishedAt));
|
|
125
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* Heading anchor slug used by the fleet's MDX heading IDs: lower-case, drop
|
|
128
|
+
* characters other than letters, digits, underscores, spaces and hyphens, then
|
|
129
|
+
* hyphenate whitespace. Headings without usable text become `section`.
|
|
130
|
+
*/
|
|
131
|
+
export function slugifyHeading(text) {
|
|
132
|
+
const slug = text
|
|
133
|
+
.toLowerCase()
|
|
134
|
+
.replace(/[^\w\s-]/g, '')
|
|
135
|
+
.replace(/\s+/g, '-')
|
|
136
|
+
.replace(/-+/g, '-')
|
|
137
|
+
.trim();
|
|
138
|
+
return slug.length > 0 ? slug : 'section';
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Returns a per-document slugger: create one per rendered article so repeated
|
|
142
|
+
* heading text receives `-2`, `-3` suffixes in document order.
|
|
143
|
+
*/
|
|
144
|
+
export function createHeadingSlugger() {
|
|
145
|
+
const slugCounts = new Map();
|
|
146
|
+
return (text) => {
|
|
147
|
+
const baseSlug = slugifyHeading(text);
|
|
148
|
+
const count = (slugCounts.get(baseSlug) ?? 0) + 1;
|
|
149
|
+
slugCounts.set(baseSlug, count);
|
|
150
|
+
return count === 1 ? baseSlug : `${baseSlug}-${count}`;
|
|
151
|
+
};
|
|
152
|
+
}
|
|
126
153
|
//# sourceMappingURL=blog.js.map
|
package/dist/blog.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blog.js","sourceRoot":"","sources":["../src/blog.ts"],"names":[],"mappings":"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","sourcesContent":["import { normalizeBrandlabRoutePath } from './routes.js'\nimport type { BrandlabMetadataTitleMode } from './metadata.js'\n\nexport type BrandlabBlogAdapter = 'mdx' | 'structured'\nexport type BrandlabBlogPostStatus = 'draft' | 'published'\n\nexport interface BrandlabBlogAuthor {\n id: string\n name: string\n profileUrl?: string\n}\n\nexport interface BrandlabBlogPost {\n id: string\n slug: string\n path: string\n adapter: BrandlabBlogAdapter\n status: BrandlabBlogPostStatus\n headline: string\n description: string\n /** Optional reviewed search title. The visible and schema headline remains `headline`. */\n seo?: {\n title: string\n titleMode?: BrandlabMetadataTitleMode\n }\n publishedAt?: string\n modifiedAt?: string\n authors: readonly BrandlabBlogAuthor[]\n categories?: readonly string[]\n tags?: readonly string[]\n image?: {\n url: string\n alt: string\n }\n contentRef: string\n}\n\nfunction assertNonEmpty(value: string, field: string) {\n if (!value.trim()) throw new Error(`${field} cannot be empty.`)\n}\n\nfunction parseDate(value: string | undefined, field: string) {\n if (!value) return undefined\n const timestamp = Date.parse(value)\n if (Number.isNaN(timestamp)) throw new Error(`${field} must be a valid date.`)\n return timestamp\n}\n\nconst BLOG_ADAPTERS = new Set<BrandlabBlogAdapter>(['mdx', 'structured'])\nconst BLOG_POST_STATUSES = new Set<BrandlabBlogPostStatus>(['draft', 'published'])\nconst BLOG_METADATA_TITLE_MODES = new Set<BrandlabMetadataTitleMode>([\n 'templated',\n 'absolute',\n])\n\nexport function defineBlogPost<const T extends BrandlabBlogPost>(post: T): T {\n assertNonEmpty(post.id, 'id')\n assertNonEmpty(post.slug, 'slug')\n assertNonEmpty(post.path, 'path')\n assertNonEmpty(post.headline, 'headline')\n assertNonEmpty(post.description, 'description')\n assertNonEmpty(post.contentRef, 'contentRef')\n\n if (post.seo) {\n if (\n typeof post.seo.title !== 'string' ||\n !post.seo.title.trim() ||\n post.seo.title.trim() !== post.seo.title ||\n post.seo.title.length > 160 ||\n /[\\u0000-\\u001f\\u007f]/.test(post.seo.title)\n ) {\n throw new Error('seo.title must be bounded, trimmed, single-line text.')\n }\n if (\n post.seo.titleMode !== undefined &&\n !BLOG_METADATA_TITLE_MODES.has(post.seo.titleMode)\n ) {\n throw new Error('seo.titleMode must be \"templated\" or \"absolute\".')\n }\n }\n\n if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(post.id)) {\n throw new Error('id must be lowercase kebab-case.')\n }\n if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(post.slug)) {\n throw new Error('slug must be lowercase kebab-case.')\n }\n if (!BLOG_ADAPTERS.has(post.adapter)) {\n throw new Error('adapter must be \"mdx\" or \"structured\".')\n }\n if (!BLOG_POST_STATUSES.has(post.status)) {\n throw new Error('status must be \"draft\" or \"published\".')\n }\n try {\n normalizeBrandlabRoutePath(post.path)\n } catch {\n throw new Error('path must be root-relative without a query string or hash.')\n }\n if (post.path.split('/').filter(Boolean).at(-1) !== post.slug) {\n throw new Error('path must end with the canonical post slug.')\n }\n if (post.authors.length === 0) throw new Error('authors cannot be empty.')\n const authorIds = post.authors.map((author) => author.id)\n if (new Set(authorIds).size !== authorIds.length) {\n throw new Error('authors contains duplicate IDs.')\n }\n post.authors.forEach((author, index) => {\n assertNonEmpty(author.id, `authors[${index}].id`)\n assertNonEmpty(author.name, `authors[${index}].name`)\n if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(author.id)) {\n throw new Error(`authors[${index}].id must be lowercase kebab-case.`)\n }\n if (author.profileUrl) assertAbsoluteHttpsUrl(author.profileUrl, `authors[${index}].profileUrl`)\n })\n post.categories?.forEach((category, index) =>\n assertNonEmpty(category, `categories[${index}]`),\n )\n post.tags?.forEach((tag, index) => assertNonEmpty(tag, `tags[${index}]`))\n if (post.categories && new Set(post.categories).size !== post.categories.length) {\n throw new Error('categories contains duplicates.')\n }\n if (post.tags && new Set(post.tags).size !== post.tags.length) {\n throw new Error('tags contains duplicates.')\n }\n\n const publishedAt = parseDate(post.publishedAt, 'publishedAt')\n const modifiedAt = parseDate(post.modifiedAt, 'modifiedAt')\n if (post.status === 'published' && publishedAt === undefined) {\n throw new Error('A published post requires publishedAt.')\n }\n if (publishedAt !== undefined && modifiedAt !== undefined && modifiedAt < publishedAt) {\n throw new Error('modifiedAt cannot be earlier than publishedAt.')\n }\n\n if (post.image) {\n assertNonEmpty(post.image.url, 'image.url')\n assertNonEmpty(post.image.alt, 'image.alt')\n if (post.image.url.startsWith('/')) {\n normalizeBrandlabRoutePath(post.image.url)\n } else {\n assertAbsoluteHttpsUrl(post.image.url, 'image.url')\n }\n }\n\n return post\n}\n\nfunction assertAbsoluteHttpsUrl(value: string, field: string) {\n let url: URL\n try {\n url = new URL(value)\n } catch {\n throw new Error(`${field} must be an absolute HTTPS URL.`)\n }\n if (url.protocol !== 'https:' || url.username || url.password) {\n throw new Error(`${field} must be an absolute HTTPS URL.`)\n }\n}\n\nexport function isPublishedBlogPost(\n post: BrandlabBlogPost,\n now: Date = new Date(),\n): post is BrandlabBlogPost & { status: 'published'; publishedAt: string } {\n return (\n post.status === 'published' &&\n Boolean(post.publishedAt) &&\n Date.parse(post.publishedAt ?? '') <= now.getTime()\n )\n}\n\nexport function getPublishedBlogPosts(\n posts: readonly BrandlabBlogPost[],\n now: Date = new Date(),\n) {\n return posts\n .filter((post): post is BrandlabBlogPost & { status: 'published'; publishedAt: string } =>\n isPublishedBlogPost(post, now),\n )\n .sort((left, right) => Date.parse(right.publishedAt) - Date.parse(left.publishedAt))\n}\n"]}
|
|
1
|
+
{"version":3,"file":"blog.js","sourceRoot":"","sources":["../src/blog.ts"],"names":[],"mappings":"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","sourcesContent":["import { normalizeBrandlabRoutePath } from './routes.js'\nimport type { BrandlabMetadataTitleMode } from './metadata.js'\n\nexport type BrandlabBlogAdapter = 'mdx' | 'structured'\nexport type BrandlabBlogPostStatus = 'draft' | 'published'\n\nexport interface BrandlabBlogAuthor {\n id: string\n name: string\n profileUrl?: string\n}\n\nexport interface BrandlabBlogPost {\n id: string\n slug: string\n path: string\n adapter: BrandlabBlogAdapter\n status: BrandlabBlogPostStatus\n headline: string\n description: string\n /** Optional reviewed search title. The visible and schema headline remains `headline`. */\n seo?: {\n title: string\n titleMode?: BrandlabMetadataTitleMode\n }\n publishedAt?: string\n modifiedAt?: string\n authors: readonly BrandlabBlogAuthor[]\n categories?: readonly string[]\n tags?: readonly string[]\n image?: {\n url: string\n alt: string\n }\n contentRef: string\n}\n\nfunction assertNonEmpty(value: string, field: string) {\n if (!value.trim()) throw new Error(`${field} cannot be empty.`)\n}\n\nfunction parseDate(value: string | undefined, field: string) {\n if (!value) return undefined\n const timestamp = Date.parse(value)\n if (Number.isNaN(timestamp)) throw new Error(`${field} must be a valid date.`)\n return timestamp\n}\n\nconst BLOG_ADAPTERS = new Set<BrandlabBlogAdapter>(['mdx', 'structured'])\nconst BLOG_POST_STATUSES = new Set<BrandlabBlogPostStatus>(['draft', 'published'])\nconst BLOG_METADATA_TITLE_MODES = new Set<BrandlabMetadataTitleMode>([\n 'templated',\n 'absolute',\n])\n\nexport function defineBlogPost<const T extends BrandlabBlogPost>(post: T): T {\n assertNonEmpty(post.id, 'id')\n assertNonEmpty(post.slug, 'slug')\n assertNonEmpty(post.path, 'path')\n assertNonEmpty(post.headline, 'headline')\n assertNonEmpty(post.description, 'description')\n assertNonEmpty(post.contentRef, 'contentRef')\n\n if (post.seo) {\n if (\n typeof post.seo.title !== 'string' ||\n !post.seo.title.trim() ||\n post.seo.title.trim() !== post.seo.title ||\n post.seo.title.length > 160 ||\n /[\\u0000-\\u001f\\u007f]/.test(post.seo.title)\n ) {\n throw new Error('seo.title must be bounded, trimmed, single-line text.')\n }\n if (\n post.seo.titleMode !== undefined &&\n !BLOG_METADATA_TITLE_MODES.has(post.seo.titleMode)\n ) {\n throw new Error('seo.titleMode must be \"templated\" or \"absolute\".')\n }\n }\n\n if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(post.id)) {\n throw new Error('id must be lowercase kebab-case.')\n }\n if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(post.slug)) {\n throw new Error('slug must be lowercase kebab-case.')\n }\n if (!BLOG_ADAPTERS.has(post.adapter)) {\n throw new Error('adapter must be \"mdx\" or \"structured\".')\n }\n if (!BLOG_POST_STATUSES.has(post.status)) {\n throw new Error('status must be \"draft\" or \"published\".')\n }\n try {\n normalizeBrandlabRoutePath(post.path)\n } catch {\n throw new Error('path must be root-relative without a query string or hash.')\n }\n if (post.path.split('/').filter(Boolean).at(-1) !== post.slug) {\n throw new Error('path must end with the canonical post slug.')\n }\n if (post.authors.length === 0) throw new Error('authors cannot be empty.')\n const authorIds = post.authors.map((author) => author.id)\n if (new Set(authorIds).size !== authorIds.length) {\n throw new Error('authors contains duplicate IDs.')\n }\n post.authors.forEach((author, index) => {\n assertNonEmpty(author.id, `authors[${index}].id`)\n assertNonEmpty(author.name, `authors[${index}].name`)\n if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(author.id)) {\n throw new Error(`authors[${index}].id must be lowercase kebab-case.`)\n }\n if (author.profileUrl) assertAbsoluteHttpsUrl(author.profileUrl, `authors[${index}].profileUrl`)\n })\n post.categories?.forEach((category, index) =>\n assertNonEmpty(category, `categories[${index}]`),\n )\n post.tags?.forEach((tag, index) => assertNonEmpty(tag, `tags[${index}]`))\n if (post.categories && new Set(post.categories).size !== post.categories.length) {\n throw new Error('categories contains duplicates.')\n }\n if (post.tags && new Set(post.tags).size !== post.tags.length) {\n throw new Error('tags contains duplicates.')\n }\n\n const publishedAt = parseDate(post.publishedAt, 'publishedAt')\n const modifiedAt = parseDate(post.modifiedAt, 'modifiedAt')\n if (post.status === 'published' && publishedAt === undefined) {\n throw new Error('A published post requires publishedAt.')\n }\n if (publishedAt !== undefined && modifiedAt !== undefined && modifiedAt < publishedAt) {\n throw new Error('modifiedAt cannot be earlier than publishedAt.')\n }\n\n if (post.image) {\n assertNonEmpty(post.image.url, 'image.url')\n assertNonEmpty(post.image.alt, 'image.alt')\n if (post.image.url.startsWith('/')) {\n normalizeBrandlabRoutePath(post.image.url)\n } else {\n assertAbsoluteHttpsUrl(post.image.url, 'image.url')\n }\n }\n\n return post\n}\n\nfunction assertAbsoluteHttpsUrl(value: string, field: string) {\n let url: URL\n try {\n url = new URL(value)\n } catch {\n throw new Error(`${field} must be an absolute HTTPS URL.`)\n }\n if (url.protocol !== 'https:' || url.username || url.password) {\n throw new Error(`${field} must be an absolute HTTPS URL.`)\n }\n}\n\nexport function isPublishedBlogPost(\n post: BrandlabBlogPost,\n now: Date = new Date(),\n): post is BrandlabBlogPost & { status: 'published'; publishedAt: string } {\n return (\n post.status === 'published' &&\n Boolean(post.publishedAt) &&\n Date.parse(post.publishedAt ?? '') <= now.getTime()\n )\n}\n\nexport function getPublishedBlogPosts(\n posts: readonly BrandlabBlogPost[],\n now: Date = new Date(),\n) {\n return posts\n .filter((post): post is BrandlabBlogPost & { status: 'published'; publishedAt: string } =>\n isPublishedBlogPost(post, now),\n )\n .sort((left, right) => Date.parse(right.publishedAt) - Date.parse(left.publishedAt))\n}\n\n/**\n * Heading anchor slug used by the fleet's MDX heading IDs: lower-case, drop\n * characters other than letters, digits, underscores, spaces and hyphens, then\n * hyphenate whitespace. Headings without usable text become `section`.\n */\nexport function slugifyHeading(text: string): string {\n const slug = text\n .toLowerCase()\n .replace(/[^\\w\\s-]/g, '')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .trim()\n\n return slug.length > 0 ? slug : 'section'\n}\n\n/**\n * Returns a per-document slugger: create one per rendered article so repeated\n * heading text receives `-2`, `-3` suffixes in document order.\n */\nexport function createHeadingSlugger() {\n const slugCounts = new Map<string, number>()\n\n return (text: string): string => {\n const baseSlug = slugifyHeading(text)\n const count = (slugCounts.get(baseSlug) ?? 0) + 1\n\n slugCounts.set(baseSlug, count)\n\n return count === 1 ? baseSlug : `${baseSlug}-${count}`\n }\n}\n"]}
|
package/dist/business.d.ts
CHANGED
|
@@ -22,7 +22,12 @@ export type BrandlabFact<T> = VerifiedFact<T> | {
|
|
|
22
22
|
reason: string;
|
|
23
23
|
};
|
|
24
24
|
export interface BrandlabPhoneNumber {
|
|
25
|
+
/** As the business shows it, for example "02 9000 1234", "0412 345 678" or "1300 329 600". */
|
|
25
26
|
display: string;
|
|
27
|
+
/**
|
|
28
|
+
* E.164 form: "+61290001234". A 13, 1300 or 1800 number keeps its national
|
|
29
|
+
* digits after +61 ("+611300329600", "+61131234").
|
|
30
|
+
*/
|
|
26
31
|
e164: string;
|
|
27
32
|
}
|
|
28
33
|
export interface BrandlabPostalAddress {
|
|
@@ -92,7 +97,15 @@ export interface BrandlabDerivedBusinessFacts {
|
|
|
92
97
|
canonicalDomain: string;
|
|
93
98
|
phone?: BrandlabPhoneNumber & {
|
|
94
99
|
digits: string;
|
|
100
|
+
/**
|
|
101
|
+
* An Australian 13, 1300 or 1800 number: callable only within Australia
|
|
102
|
+
* and only as dialled nationally.
|
|
103
|
+
*/
|
|
104
|
+
nonGeographic: boolean;
|
|
105
|
+
/** `tel:+61…` for landline and mobile numbers; the national digits (`tel:1300329600`) for a non-geographic number. */
|
|
95
106
|
telHref: string;
|
|
107
|
+
/** Schema.org `telephone`: E.164 for landline and mobile numbers, the displayed number for a non-geographic one. */
|
|
108
|
+
schemaTelephone: string;
|
|
96
109
|
};
|
|
97
110
|
email?: string;
|
|
98
111
|
publicAddress?: BrandlabPostalAddress;
|
package/dist/business.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"business.d.ts","sourceRoot":"","sources":["../src/business.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"business.d.ts","sourceRoot":"","sources":["../src/business.ts"],"names":[],"mappings":"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"}
|
package/dist/business.js
CHANGED
|
@@ -189,6 +189,13 @@ function assertAddress(address) {
|
|
|
189
189
|
throw new Error('location.address.value.postalCode must be a four-digit Australian postcode.');
|
|
190
190
|
}
|
|
191
191
|
}
|
|
192
|
+
/** Landline and mobile numbers: +61 then a 2, 3, 4, 7 or 8 area or mobile prefix. */
|
|
193
|
+
const AUSTRALIAN_GEOGRAPHIC_E164 = /^\+61[23478]\d{8}$/;
|
|
194
|
+
/** Non-geographic numbers: 13 xx xx, 1300 xxx xxx and 1800 xxx xxx. */
|
|
195
|
+
const AUSTRALIAN_NON_GEOGRAPHIC_E164 = /^\+61(?:1[38]00\d{6}|13(?!00)\d{4})$/;
|
|
196
|
+
function isNonGeographicPhone(e164) {
|
|
197
|
+
return AUSTRALIAN_NON_GEOGRAPHIC_E164.test(e164);
|
|
198
|
+
}
|
|
192
199
|
function assertPhone(phone) {
|
|
193
200
|
assertExactKeys(phone, ['display', 'e164'], 'contact.phone.value');
|
|
194
201
|
assertText(phone.display, 'contact.phone.value.display');
|
|
@@ -197,15 +204,16 @@ function assertPhone(phone) {
|
|
|
197
204
|
!/^[+()\d\s.-]+$/.test(phone.display)) {
|
|
198
205
|
throw new Error('contact.phone.value.display must be a bounded Australian phone number.');
|
|
199
206
|
}
|
|
200
|
-
|
|
201
|
-
|
|
207
|
+
const nonGeographic = isNonGeographicPhone(phone.e164);
|
|
208
|
+
if (!nonGeographic && !AUSTRALIAN_GEOGRAPHIC_E164.test(phone.e164)) {
|
|
209
|
+
throw new Error('contact.phone.value.e164 must be a valid Australian E.164 number: a landline or mobile, or a 13, 1300 or 1800 number.');
|
|
202
210
|
}
|
|
211
|
+
const national = phone.e164.slice(3);
|
|
203
212
|
const displayDigits = phone.display.replace(/\D/g, '');
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
? `
|
|
207
|
-
|
|
208
|
-
if (comparableDisplayDigits !== e164Digits) {
|
|
213
|
+
// A landline or mobile shows its trunk 0 or +61; a non-geographic number is shown as dialled.
|
|
214
|
+
const sameNumber = displayDigits === `61${national}` ||
|
|
215
|
+
displayDigits === (nonGeographic ? national : `0${national}`);
|
|
216
|
+
if (!sameNumber) {
|
|
209
217
|
throw new Error('contact.phone.value.display must identify the same number as e164.');
|
|
210
218
|
}
|
|
211
219
|
}
|
|
@@ -372,7 +380,10 @@ export function deriveBusinessFacts(business) {
|
|
|
372
380
|
phone: {
|
|
373
381
|
...phone,
|
|
374
382
|
digits: phone.e164.replace(/\D/g, ''),
|
|
375
|
-
|
|
383
|
+
nonGeographic: isNonGeographicPhone(phone.e164),
|
|
384
|
+
// 13, 1300 and 1800 numbers cannot be dialled in international form.
|
|
385
|
+
telHref: isNonGeographicPhone(phone.e164) ? `tel:${phone.e164.slice(3)}` : `tel:${phone.e164}`,
|
|
386
|
+
schemaTelephone: isNonGeographicPhone(phone.e164) ? phone.display : phone.e164,
|
|
376
387
|
},
|
|
377
388
|
}
|
|
378
389
|
: {}),
|