@brandlab-digital/site-next 2.1.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/dist/analytics.d.ts +111 -52
  2. package/dist/analytics.d.ts.map +1 -1
  3. package/dist/analytics.js +281 -153
  4. package/dist/analytics.js.map +1 -1
  5. package/dist/blog.d.ts +4 -2
  6. package/dist/blog.d.ts.map +1 -1
  7. package/dist/blog.js +7 -7
  8. package/dist/blog.js.map +1 -1
  9. package/dist/contact/client-entry.d.ts +2 -0
  10. package/dist/contact/client-entry.d.ts.map +1 -1
  11. package/dist/contact/client-entry.js +1 -0
  12. package/dist/contact/client-entry.js.map +1 -1
  13. package/dist/contact/client.d.ts +12 -22
  14. package/dist/contact/client.d.ts.map +1 -1
  15. package/dist/contact/client.js +28 -21
  16. package/dist/contact/client.js.map +1 -1
  17. package/dist/contact/form-client.d.ts +159 -0
  18. package/dist/contact/form-client.d.ts.map +1 -0
  19. package/dist/contact/form-client.js +257 -0
  20. package/dist/contact/form-client.js.map +1 -0
  21. package/dist/contact/index.d.ts +1 -1
  22. package/dist/contact/index.d.ts.map +1 -1
  23. package/dist/contact/index.js +1 -1
  24. package/dist/contact/index.js.map +1 -1
  25. package/dist/contact/route-handler.d.ts +32 -0
  26. package/dist/contact/route-handler.d.ts.map +1 -0
  27. package/dist/contact/route-handler.js +44 -0
  28. package/dist/contact/route-handler.js.map +1 -0
  29. package/dist/contact/server-entry.d.ts +1 -0
  30. package/dist/contact/server-entry.d.ts.map +1 -1
  31. package/dist/contact/server-entry.js +1 -0
  32. package/dist/contact/server-entry.js.map +1 -1
  33. package/dist/contact/server.d.ts +5 -0
  34. package/dist/contact/server.d.ts.map +1 -1
  35. package/dist/contact/server.js +13 -7
  36. package/dist/contact/server.js.map +1 -1
  37. package/dist/contact/shared.d.ts +4 -0
  38. package/dist/contact/shared.d.ts.map +1 -1
  39. package/dist/contact/shared.js.map +1 -1
  40. package/dist/contact/testing.d.ts +29 -0
  41. package/dist/contact/testing.d.ts.map +1 -1
  42. package/dist/contact/testing.js +27 -0
  43. package/dist/contact/testing.js.map +1 -1
  44. package/dist/google-tag-plan.d.ts +1 -4
  45. package/dist/google-tag-plan.d.ts.map +1 -1
  46. package/dist/google-tag-plan.js +1 -14
  47. package/dist/google-tag-plan.js.map +1 -1
  48. package/dist/google-tag.d.ts +5 -4
  49. package/dist/google-tag.d.ts.map +1 -1
  50. package/dist/google-tag.js +5 -4
  51. package/dist/google-tag.js.map +1 -1
  52. package/dist/headers.d.ts +54 -27
  53. package/dist/headers.d.ts.map +1 -1
  54. package/dist/headers.js +122 -51
  55. package/dist/headers.js.map +1 -1
  56. package/dist/instrumentation-client.d.ts +21 -1
  57. package/dist/instrumentation-client.d.ts.map +1 -1
  58. package/dist/instrumentation-client.js +62 -7
  59. package/dist/instrumentation-client.js.map +1 -1
  60. package/dist/lead-registry.d.ts +34 -0
  61. package/dist/lead-registry.d.ts.map +1 -0
  62. package/dist/lead-registry.js +30 -0
  63. package/dist/lead-registry.js.map +1 -0
  64. package/dist/lead-reporting.d.ts +37 -0
  65. package/dist/lead-reporting.d.ts.map +1 -0
  66. package/dist/lead-reporting.js +74 -0
  67. package/dist/lead-reporting.js.map +1 -0
  68. package/dist/metadata.d.ts +10 -11
  69. package/dist/metadata.d.ts.map +1 -1
  70. package/dist/metadata.js +17 -19
  71. package/dist/metadata.js.map +1 -1
  72. package/dist/next-config.d.ts +113 -0
  73. package/dist/next-config.d.ts.map +1 -0
  74. package/dist/next-config.js +213 -0
  75. package/dist/next-config.js.map +1 -0
  76. package/dist/robots.d.ts +14 -6
  77. package/dist/robots.d.ts.map +1 -1
  78. package/dist/robots.js +26 -17
  79. package/dist/robots.js.map +1 -1
  80. package/dist/sitemaps.d.ts +34 -0
  81. package/dist/sitemaps.d.ts.map +1 -1
  82. package/dist/sitemaps.js +66 -1
  83. package/dist/sitemaps.js.map +1 -1
  84. package/dist/testing/server-only.d.ts +8 -0
  85. package/dist/testing/server-only.d.ts.map +1 -0
  86. package/dist/testing/server-only.js +8 -0
  87. package/dist/testing/server-only.js.map +1 -0
  88. package/dist/ui-client.d.ts +1 -12
  89. package/dist/ui-client.d.ts.map +1 -1
  90. package/dist/ui-client.js +0 -17
  91. package/dist/ui-client.js.map +1 -1
  92. package/dist/ui.d.ts +22 -2
  93. package/dist/ui.d.ts.map +1 -1
  94. package/dist/ui.js +67 -6
  95. package/dist/ui.js.map +1 -1
  96. package/package.json +16 -3
@@ -1,2 +1,29 @@
1
+ import { defineContactContract } from '@brandlab-digital/site-core/contact';
2
+ import { leadBindingFor } from '../lead-registry.js';
3
+ import { reportAcceptedLead } from '../lead-reporting.js';
4
+ import { CONTACT_CONFIRMATION_ID_PATTERN } from './shared.js';
1
5
  export { buildContactEmailSubject, escapeContactEmailHtml, renderContactEmail, sanitizeContactEmailSubject, } from './email.js';
6
+ /**
7
+ * Records exactly what useBrandlabContactForm sends for one accepted enquiry
8
+ * on the production deployment, without a browser: the GA4 contact_form event
9
+ * and, when the contract declares Google Ads, the form conversion. Use it to
10
+ * pin a site's declared lead reporting in its own tests.
11
+ */
12
+ export function recordBrandlabContactLead(input) {
13
+ const lead = leadBindingFor(input.analytics);
14
+ if (!lead)
15
+ throw new TypeError('analytics must be the binding returned by defineBrandlabAnalytics.');
16
+ if (!CONTACT_CONFIRMATION_ID_PATTERN.test(input.confirmationId)) {
17
+ throw new TypeError('confirmationId must be 64 lowercase hexadecimal characters.');
18
+ }
19
+ const contract = defineContactContract(input.contract);
20
+ const calls = [];
21
+ const outcome = reportAcceptedLead(lead, { ok: true, accepted: true, trackConversion: true, confirmationId: input.confirmationId }, {
22
+ pagePath: input.pagePath,
23
+ formData: input.formData,
24
+ fieldRoles: { fields: contract.fields.map((field) => ({ name: field.key, role: field.role })) },
25
+ adUserDataConsent: input.adUserDataConsent === true,
26
+ }, { enabled: true, gtag: (...args) => void calls.push(args) });
27
+ return { calls, ...outcome };
28
+ }
2
29
  //# sourceMappingURL=testing.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"testing.js","sourceRoot":"","sources":["../../src/contact/testing.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,wBAAwB,EACxB,sBAAsB,EACtB,kBAAkB,EAClB,2BAA2B,GAG5B,MAAM,YAAY,CAAA","sourcesContent":["export {\n buildContactEmailSubject,\n escapeContactEmailHtml,\n renderContactEmail,\n sanitizeContactEmailSubject,\n type RenderContactEmailInput,\n type RenderedContactEmail,\n} from './email.js'\n"]}
1
+ {"version":3,"file":"testing.js","sourceRoot":"","sources":["../../src/contact/testing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAgC,MAAM,qCAAqC,CAAA;AAEzG,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AACzD,OAAO,EAAE,+BAA+B,EAAE,MAAM,aAAa,CAAA;AAE7D,OAAO,EACL,wBAAwB,EACxB,sBAAsB,EACtB,kBAAkB,EAClB,2BAA2B,GAG5B,MAAM,YAAY,CAAA;AAwBnB;;;;;GAKG;AACH,MAAM,UAAU,yBAAyB,CAAC,KAAqC;IAC7E,MAAM,IAAI,GAAG,cAAc,CAAC,KAAK,CAAC,SAAS,CAAC,CAAA;IAC5C,IAAI,CAAC,IAAI;QAAE,MAAM,IAAI,SAAS,CAAC,oEAAoE,CAAC,CAAA;IACpG,IAAI,CAAC,+BAA+B,CAAC,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,EAAE,CAAC;QAChE,MAAM,IAAI,SAAS,CAAC,6DAA6D,CAAC,CAAA;IACpF,CAAC;IACD,MAAM,QAAQ,GAAG,qBAAqB,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAA;IACtD,MAAM,KAAK,GAAgB,EAAE,CAAA;IAC7B,MAAM,OAAO,GAAG,kBAAkB,CAChC,IAAI,EACJ,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,cAAc,EAAE,KAAK,CAAC,cAAc,EAAE,EACzF;QACE,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,UAAU,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE;QAC/F,iBAAiB,EAAE,KAAK,CAAC,iBAAiB,KAAK,IAAI;KACpD,EACD,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,GAAG,IAAe,EAAE,EAAE,CAAC,KAAK,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CACvE,CAAA;IACD,OAAO,EAAE,KAAK,EAAE,GAAG,OAAO,EAAE,CAAA;AAC9B,CAAC","sourcesContent":["import { defineContactContract, type BrandlabContactContract } from '@brandlab-digital/site-core/contact'\nimport type { BrandlabAnalytics } from '../analytics.js'\nimport { leadBindingFor } from '../lead-registry.js'\nimport { reportAcceptedLead } from '../lead-reporting.js'\nimport { CONTACT_CONFIRMATION_ID_PATTERN } from './shared.js'\n\nexport {\n buildContactEmailSubject,\n escapeContactEmailHtml,\n renderContactEmail,\n sanitizeContactEmailSubject,\n type RenderContactEmailInput,\n type RenderedContactEmail,\n} from './email.js'\n\nexport interface RecordBrandlabContactLeadInput {\n /** The site's defineBrandlabAnalytics binding. */\n analytics: BrandlabAnalytics\n /** The site's contact contract (its email and phone roles feed Enhanced Conversions). */\n contract: BrandlabContactContract\n /** The form's pagePath, as useBrandlabContactForm receives it. */\n pagePath: string\n /** The accepted enquiry's fields, including any that onSubmitStart adds. */\n formData: FormData\n /** The provider confirmation ID (64 lowercase hex characters). */\n confirmationId: string\n adUserDataConsent?: boolean\n}\n\nexport interface RecordedBrandlabContactLead {\n /** Every gtag call, in order, that the accepted enquiry sends on the production deployment. */\n calls: unknown[][]\n ga4Sent: boolean\n enhancedConversionSent: boolean\n adsConversionSent: boolean\n}\n\n/**\n * Records exactly what useBrandlabContactForm sends for one accepted enquiry\n * on the production deployment, without a browser: the GA4 contact_form event\n * and, when the contract declares Google Ads, the form conversion. Use it to\n * pin a site's declared lead reporting in its own tests.\n */\nexport function recordBrandlabContactLead(input: RecordBrandlabContactLeadInput): RecordedBrandlabContactLead {\n const lead = leadBindingFor(input.analytics)\n if (!lead) throw new TypeError('analytics must be the binding returned by defineBrandlabAnalytics.')\n if (!CONTACT_CONFIRMATION_ID_PATTERN.test(input.confirmationId)) {\n throw new TypeError('confirmationId must be 64 lowercase hexadecimal characters.')\n }\n const contract = defineContactContract(input.contract)\n const calls: unknown[][] = []\n const outcome = reportAcceptedLead(\n lead,\n { ok: true, accepted: true, trackConversion: true, confirmationId: input.confirmationId },\n {\n pagePath: input.pagePath,\n formData: input.formData,\n fieldRoles: { fields: contract.fields.map((field) => ({ name: field.key, role: field.role })) },\n adUserDataConsent: input.adUserDataConsent === true,\n },\n { enabled: true, gtag: (...args: unknown[]) => void calls.push(args) },\n )\n return { calls, ...outcome }\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { type BrandlabAnalyticsContract } from '@brandlab-digital/site-core/analytics';
1
+ /** Supplied by `defineBrandlabAnalytics(...).googleTag`. */
2
2
  export interface BrandlabGoogleTagConfig {
3
3
  enabled: boolean;
4
4
  measurementId?: string | null;
@@ -12,8 +12,5 @@ export interface BrandlabGoogleTagPlan {
12
12
  destinations: readonly string[];
13
13
  websiteCallDestination: string | null;
14
14
  }
15
- export declare function resolveBrandlabGoogleTagConfig(contract: BrandlabAnalyticsContract, environment: Readonly<Record<string, string | undefined>>, options: {
16
- enabled: boolean;
17
- }): BrandlabGoogleTagConfig;
18
15
  export declare function createBrandlabGoogleTagPlan(config: BrandlabGoogleTagConfig, phoneNumber?: string | null): BrandlabGoogleTagPlan | null;
19
16
  //# sourceMappingURL=google-tag-plan.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"google-tag-plan.d.ts","sourceRoot":"","sources":["../src/google-tag-plan.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,yBAAyB,EAC/B,MAAM,uCAAuC,CAAA;AAE9C,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,OAAO,CAAA;IAChB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrC,mCAAmC,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACpD;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,EAAE,SAAS,MAAM,EAAE,CAAA;IAC/B,sBAAsB,EAAE,MAAM,GAAG,IAAI,CAAA;CACtC;AAKD,wBAAgB,8BAA8B,CAC5C,QAAQ,EAAE,yBAAyB,EACnC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,EACzD,OAAO,EAAE;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,GAC5B,uBAAuB,CAYzB;AAED,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,uBAAuB,EAC/B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,GAC1B,qBAAqB,GAAG,IAAI,CA2C9B"}
1
+ {"version":3,"file":"google-tag-plan.d.ts","sourceRoot":"","sources":["../src/google-tag-plan.ts"],"names":[],"mappings":"AAEA,4DAA4D;AAC5D,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,OAAO,CAAA;IAChB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrC,mCAAmC,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACpD;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,EAAE,SAAS,MAAM,EAAE,CAAA;IAC/B,sBAAsB,EAAE,MAAM,GAAG,IAAI,CAAA;CACtC;AAKD,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,uBAAuB,EAC/B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,GAC1B,qBAAqB,GAAG,IAAI,CA2C9B"}
@@ -1,19 +1,6 @@
1
- import { defineAnalyticsContract, getGoogleAdsWebsiteCallSendTo, } from '@brandlab-digital/site-core/analytics';
1
+ import { getGoogleAdsWebsiteCallSendTo } from '@brandlab-digital/site-core/analytics';
2
2
  const GA4_MEASUREMENT_ID_PATTERN = /^G-[A-Z0-9]{4,20}$/i;
3
3
  const GOOGLE_ADS_CONVERSION_ID_PATTERN = /^(?:AW-)?\d{6,20}$/;
4
- export function resolveBrandlabGoogleTagConfig(contract, environment, options) {
5
- defineAnalyticsContract(contract);
6
- return {
7
- enabled: options.enabled,
8
- measurementId: environment[contract.measurementIdEnv],
9
- googleAdsConversionId: contract.googleAds
10
- ? environment[contract.googleAds.conversionIdEnv]
11
- : undefined,
12
- googleAdsWebsiteCallConversionLabel: contract.googleAds?.websiteCalls
13
- ? environment[contract.googleAds.websiteCalls.conversionLabelEnv]
14
- : undefined,
15
- };
16
- }
17
4
  export function createBrandlabGoogleTagPlan(config, phoneNumber) {
18
5
  if (!config.enabled)
19
6
  return null;
@@ -1 +1 @@
1
- {"version":3,"file":"google-tag-plan.js","sourceRoot":"","sources":["../src/google-tag-plan.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n defineAnalyticsContract,\n getGoogleAdsWebsiteCallSendTo,\n type BrandlabAnalyticsContract,\n} from '@brandlab-digital/site-core/analytics'\n\nexport interface BrandlabGoogleTagConfig {\n enabled: boolean\n measurementId?: string | null\n googleAdsConversionId?: string | null\n googleAdsWebsiteCallConversionLabel?: string | null\n}\n\nexport interface BrandlabGoogleTagPlan {\n loaderId: string\n source: string\n inlineScript: string\n destinations: readonly string[]\n websiteCallDestination: string | null\n}\n\nconst GA4_MEASUREMENT_ID_PATTERN = /^G-[A-Z0-9]{4,20}$/i\nconst GOOGLE_ADS_CONVERSION_ID_PATTERN = /^(?:AW-)?\\d{6,20}$/\n\nexport function resolveBrandlabGoogleTagConfig(\n contract: BrandlabAnalyticsContract,\n environment: Readonly<Record<string, string | undefined>>,\n options: { enabled: boolean },\n): BrandlabGoogleTagConfig {\n defineAnalyticsContract(contract)\n return {\n enabled: options.enabled,\n measurementId: environment[contract.measurementIdEnv],\n googleAdsConversionId: contract.googleAds\n ? environment[contract.googleAds.conversionIdEnv]\n : undefined,\n googleAdsWebsiteCallConversionLabel: contract.googleAds?.websiteCalls\n ? environment[contract.googleAds.websiteCalls.conversionLabelEnv]\n : undefined,\n }\n}\n\nexport function createBrandlabGoogleTagPlan(\n config: BrandlabGoogleTagConfig,\n phoneNumber?: string | null,\n): BrandlabGoogleTagPlan | null {\n if (!config.enabled) return null\n const measurementId = validMeasurementId(config.measurementId)\n const googleAdsConversionId = validGoogleAdsConversionId(config.googleAdsConversionId)\n const loaderId = measurementId ?? googleAdsConversionId\n if (!loaderId) return null\n\n const destinations = [...new Set([\n ...(measurementId ? [measurementId] : []),\n ...(googleAdsConversionId ? [googleAdsConversionId] : []),\n ])]\n const displayPhone = safeDisplayedBusinessPhone(phoneNumber)\n const websiteCallDestination = displayPhone\n ? getGoogleAdsWebsiteCallSendTo({\n conversionId: googleAdsConversionId,\n conversionLabel: config.googleAdsWebsiteCallConversionLabel,\n }) ?? null\n : null\n const configCommands = destinations.map(\n (destination) => `window.gtag('config', ${safeJson(destination)});`,\n )\n if (websiteCallDestination && displayPhone) {\n configCommands.push(\n `window.gtag('config', ${safeJson(websiteCallDestination)}, ${safeJson({\n phone_conversion_number: displayPhone,\n })});`,\n )\n }\n\n return {\n loaderId,\n source: `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(loaderId)}`,\n inlineScript: [\n 'window.dataLayer = window.dataLayer || [];',\n 'window.gtag = window.gtag || function(){window.dataLayer.push(arguments);};',\n \"window.gtag('consent', 'default', {ad_user_data:'denied',ad_personalization:'denied'});\",\n \"window.gtag('set', 'user_data', {});\",\n \"window.gtag('js', new Date());\",\n ...configCommands,\n ].join('\\n'),\n destinations,\n websiteCallDestination,\n }\n}\n\nfunction validMeasurementId(value: string | null | undefined) {\n const normalized = value?.trim()\n return normalized && GA4_MEASUREMENT_ID_PATTERN.test(normalized)\n ? normalized.toUpperCase()\n : undefined\n}\n\nfunction validGoogleAdsConversionId(value: string | null | undefined) {\n const normalized = value?.trim()\n if (!normalized || !GOOGLE_ADS_CONVERSION_ID_PATTERN.test(normalized)) return undefined\n return normalized.startsWith('AW-') ? normalized : `AW-${normalized}`\n}\n\nfunction safeDisplayedBusinessPhone(value: string | null | undefined) {\n const normalized = value?.trim()\n const digits = normalized?.replace(/\\D/g, '') ?? ''\n return normalized &&\n normalized.length <= 32 &&\n digits.length >= 6 &&\n /^[0-9+().\\s-]+$/.test(normalized)\n ? normalized\n : undefined\n}\n\nfunction safeJson(value: unknown) {\n return JSON.stringify(value)\n .replace(/</g, '\\\\u003c')\n .replace(/>/g, '\\\\u003e')\n .replace(/&/g, '\\\\u0026')\n .replace(/\\u2028/g, '\\\\u2028')\n .replace(/\\u2029/g, '\\\\u2029')\n}\n"]}
1
+ {"version":3,"file":"google-tag-plan.js","sourceRoot":"","sources":["../src/google-tag-plan.ts"],"names":[],"mappings":"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","sourcesContent":["import { getGoogleAdsWebsiteCallSendTo } from '@brandlab-digital/site-core/analytics'\n\n/** Supplied by `defineBrandlabAnalytics(...).googleTag`. */\nexport interface BrandlabGoogleTagConfig {\n enabled: boolean\n measurementId?: string | null\n googleAdsConversionId?: string | null\n googleAdsWebsiteCallConversionLabel?: string | null\n}\n\nexport interface BrandlabGoogleTagPlan {\n loaderId: string\n source: string\n inlineScript: string\n destinations: readonly string[]\n websiteCallDestination: string | null\n}\n\nconst GA4_MEASUREMENT_ID_PATTERN = /^G-[A-Z0-9]{4,20}$/i\nconst GOOGLE_ADS_CONVERSION_ID_PATTERN = /^(?:AW-)?\\d{6,20}$/\n\nexport function createBrandlabGoogleTagPlan(\n config: BrandlabGoogleTagConfig,\n phoneNumber?: string | null,\n): BrandlabGoogleTagPlan | null {\n if (!config.enabled) return null\n const measurementId = validMeasurementId(config.measurementId)\n const googleAdsConversionId = validGoogleAdsConversionId(config.googleAdsConversionId)\n const loaderId = measurementId ?? googleAdsConversionId\n if (!loaderId) return null\n\n const destinations = [...new Set([\n ...(measurementId ? [measurementId] : []),\n ...(googleAdsConversionId ? [googleAdsConversionId] : []),\n ])]\n const displayPhone = safeDisplayedBusinessPhone(phoneNumber)\n const websiteCallDestination = displayPhone\n ? getGoogleAdsWebsiteCallSendTo({\n conversionId: googleAdsConversionId,\n conversionLabel: config.googleAdsWebsiteCallConversionLabel,\n }) ?? null\n : null\n const configCommands = destinations.map(\n (destination) => `window.gtag('config', ${safeJson(destination)});`,\n )\n if (websiteCallDestination && displayPhone) {\n configCommands.push(\n `window.gtag('config', ${safeJson(websiteCallDestination)}, ${safeJson({\n phone_conversion_number: displayPhone,\n })});`,\n )\n }\n\n return {\n loaderId,\n source: `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(loaderId)}`,\n inlineScript: [\n 'window.dataLayer = window.dataLayer || [];',\n 'window.gtag = window.gtag || function(){window.dataLayer.push(arguments);};',\n \"window.gtag('consent', 'default', {ad_user_data:'denied',ad_personalization:'denied'});\",\n \"window.gtag('set', 'user_data', {});\",\n \"window.gtag('js', new Date());\",\n ...configCommands,\n ].join('\\n'),\n destinations,\n websiteCallDestination,\n }\n}\n\nfunction validMeasurementId(value: string | null | undefined) {\n const normalized = value?.trim()\n return normalized && GA4_MEASUREMENT_ID_PATTERN.test(normalized)\n ? normalized.toUpperCase()\n : undefined\n}\n\nfunction validGoogleAdsConversionId(value: string | null | undefined) {\n const normalized = value?.trim()\n if (!normalized || !GOOGLE_ADS_CONVERSION_ID_PATTERN.test(normalized)) return undefined\n return normalized.startsWith('AW-') ? normalized : `AW-${normalized}`\n}\n\nfunction safeDisplayedBusinessPhone(value: string | null | undefined) {\n const normalized = value?.trim()\n const digits = normalized?.replace(/\\D/g, '') ?? ''\n return normalized &&\n normalized.length <= 32 &&\n digits.length >= 6 &&\n /^[0-9+().\\s-]+$/.test(normalized)\n ? normalized\n : undefined\n}\n\nfunction safeJson(value: unknown) {\n return JSON.stringify(value)\n .replace(/</g, '\\\\u003c')\n .replace(/>/g, '\\\\u003e')\n .replace(/&/g, '\\\\u0026')\n .replace(/\\u2028/g, '\\\\u2028')\n .replace(/\\u2029/g, '\\\\u2029')\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  import { type BrandlabGoogleTagConfig } from './google-tag-plan.js';
2
- export { createBrandlabGoogleTagPlan, resolveBrandlabGoogleTagConfig, } from './google-tag-plan.js';
2
+ export { createBrandlabGoogleTagPlan } from './google-tag-plan.js';
3
3
  export type { BrandlabGoogleTagConfig, BrandlabGoogleTagPlan, } from './google-tag-plan.js';
4
4
  export interface BrandlabGoogleTagProps extends BrandlabGoogleTagConfig {
5
5
  /** Must match the verified public number exactly as it is rendered on the page. */
@@ -9,9 +9,10 @@ export interface BrandlabGoogleTagProps extends BrandlabGoogleTagConfig {
9
9
  /**
10
10
  * Shared Google tag and website-call forwarding-number snippet.
11
11
  *
12
- * Render this exactly once from the root layout. The enabled flag remains the
13
- * project consent/production gate; account-side conversion settings remain
14
- * outside website runtime.
12
+ * Render this exactly once from the root layout:
13
+ * `<BrandlabGoogleTag {...analytics.googleTag} phoneNumber={displayedPhone} />`.
14
+ * `analytics.googleTag.enabled` is true only on the production deployment;
15
+ * account-side conversion settings remain outside website runtime.
15
16
  */
16
17
  export declare function BrandlabGoogleTag(props: BrandlabGoogleTagProps): import("react/jsx-runtime").JSX.Element | null;
17
18
  //# sourceMappingURL=google-tag.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"google-tag.d.ts","sourceRoot":"","sources":["../src/google-tag.tsx"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,uBAAuB,EAC7B,MAAM,sBAAsB,CAAA;AAE7B,OAAO,EACL,2BAA2B,EAC3B,8BAA8B,GAC/B,MAAM,sBAAsB,CAAA;AAC7B,YAAY,EACV,uBAAuB,EACvB,qBAAqB,GACtB,MAAM,sBAAsB,CAAA;AAE7B,MAAM,WAAW,sBAAuB,SAAQ,uBAAuB;IACrE,mFAAmF;IACnF,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,kDAqB9D"}
1
+ {"version":3,"file":"google-tag.d.ts","sourceRoot":"","sources":["../src/google-tag.tsx"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,uBAAuB,EAC7B,MAAM,sBAAsB,CAAA;AAE7B,OAAO,EAAE,2BAA2B,EAAE,MAAM,sBAAsB,CAAA;AAClE,YAAY,EACV,uBAAuB,EACvB,qBAAqB,GACtB,MAAM,sBAAsB,CAAA;AAE7B,MAAM,WAAW,sBAAuB,SAAQ,uBAAuB;IACrE,mFAAmF;IACnF,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,kDAqB9D"}
@@ -1,13 +1,14 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import Script from 'next/script';
3
3
  import { createBrandlabGoogleTagPlan, } from './google-tag-plan.js';
4
- export { createBrandlabGoogleTagPlan, resolveBrandlabGoogleTagConfig, } from './google-tag-plan.js';
4
+ export { createBrandlabGoogleTagPlan } from './google-tag-plan.js';
5
5
  /**
6
6
  * Shared Google tag and website-call forwarding-number snippet.
7
7
  *
8
- * Render this exactly once from the root layout. The enabled flag remains the
9
- * project consent/production gate; account-side conversion settings remain
10
- * outside website runtime.
8
+ * Render this exactly once from the root layout:
9
+ * `<BrandlabGoogleTag {...analytics.googleTag} phoneNumber={displayedPhone} />`.
10
+ * `analytics.googleTag.enabled` is true only on the production deployment;
11
+ * account-side conversion settings remain outside website runtime.
11
12
  */
12
13
  export function BrandlabGoogleTag(props) {
13
14
  const plan = createBrandlabGoogleTagPlan(props, props.phoneNumber);
@@ -1 +1 @@
1
- {"version":3,"file":"google-tag.js","sourceRoot":"","sources":["../src/google-tag.tsx"],"names":[],"mappings":";AAAA,OAAO,MAAM,MAAM,aAAa,CAAA;AAChC,OAAO,EACL,2BAA2B,GAE5B,MAAM,sBAAsB,CAAA;AAE7B,OAAO,EACL,2BAA2B,EAC3B,8BAA8B,GAC/B,MAAM,sBAAsB,CAAA;AAY7B;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAA6B;IAC7D,MAAM,IAAI,GAAG,2BAA2B,CAAC,KAAK,EAAE,KAAK,CAAC,WAAW,CAAC,CAAA;IAClE,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAA;IACtB,OAAO,CACL,8BACE,KAAC,MAAM,IACL,EAAE,EAAC,4BAA4B,8BACN,QAAQ,EACjC,GAAG,EAAE,IAAI,CAAC,MAAM,EAChB,QAAQ,EAAC,kBAAkB,EAC3B,KAAK,EAAE,KAAK,CAAC,KAAK,GAClB,EACF,KAAC,MAAM,IACL,EAAE,EAAC,4BAA4B,8BACN,QAAQ,EACjC,QAAQ,EAAC,kBAAkB,EAC3B,KAAK,EAAE,KAAK,CAAC,KAAK,EAClB,uBAAuB,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,YAAY,EAAE,GACtD,IACD,CACJ,CAAA;AACH,CAAC","sourcesContent":["import Script from 'next/script'\nimport {\n createBrandlabGoogleTagPlan,\n type BrandlabGoogleTagConfig,\n} from './google-tag-plan.js'\n\nexport {\n createBrandlabGoogleTagPlan,\n resolveBrandlabGoogleTagConfig,\n} from './google-tag-plan.js'\nexport type {\n BrandlabGoogleTagConfig,\n BrandlabGoogleTagPlan,\n} from './google-tag-plan.js'\n\nexport interface BrandlabGoogleTagProps extends BrandlabGoogleTagConfig {\n /** Must match the verified public number exactly as it is rendered on the page. */\n phoneNumber?: string | null\n nonce?: string\n}\n\n/**\n * Shared Google tag and website-call forwarding-number snippet.\n *\n * Render this exactly once from the root layout. The enabled flag remains the\n * project consent/production gate; account-side conversion settings remain\n * outside website runtime.\n */\nexport function BrandlabGoogleTag(props: BrandlabGoogleTagProps) {\n const plan = createBrandlabGoogleTagPlan(props, props.phoneNumber)\n if (!plan) return null\n return (\n <>\n <Script\n id=\"brandlab-google-tag-loader\"\n data-brandlab-google-tag=\"loader\"\n src={plan.source}\n strategy=\"afterInteractive\"\n nonce={props.nonce}\n />\n <Script\n id=\"brandlab-google-tag-config\"\n data-brandlab-google-tag=\"config\"\n strategy=\"afterInteractive\"\n nonce={props.nonce}\n dangerouslySetInnerHTML={{ __html: plan.inlineScript }}\n />\n </>\n )\n}\n"]}
1
+ {"version":3,"file":"google-tag.js","sourceRoot":"","sources":["../src/google-tag.tsx"],"names":[],"mappings":";AAAA,OAAO,MAAM,MAAM,aAAa,CAAA;AAChC,OAAO,EACL,2BAA2B,GAE5B,MAAM,sBAAsB,CAAA;AAE7B,OAAO,EAAE,2BAA2B,EAAE,MAAM,sBAAsB,CAAA;AAYlE;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAA6B;IAC7D,MAAM,IAAI,GAAG,2BAA2B,CAAC,KAAK,EAAE,KAAK,CAAC,WAAW,CAAC,CAAA;IAClE,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAA;IACtB,OAAO,CACL,8BACE,KAAC,MAAM,IACL,EAAE,EAAC,4BAA4B,8BACN,QAAQ,EACjC,GAAG,EAAE,IAAI,CAAC,MAAM,EAChB,QAAQ,EAAC,kBAAkB,EAC3B,KAAK,EAAE,KAAK,CAAC,KAAK,GAClB,EACF,KAAC,MAAM,IACL,EAAE,EAAC,4BAA4B,8BACN,QAAQ,EACjC,QAAQ,EAAC,kBAAkB,EAC3B,KAAK,EAAE,KAAK,CAAC,KAAK,EAClB,uBAAuB,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,YAAY,EAAE,GACtD,IACD,CACJ,CAAA;AACH,CAAC","sourcesContent":["import Script from 'next/script'\nimport {\n createBrandlabGoogleTagPlan,\n type BrandlabGoogleTagConfig,\n} from './google-tag-plan.js'\n\nexport { createBrandlabGoogleTagPlan } from './google-tag-plan.js'\nexport type {\n BrandlabGoogleTagConfig,\n BrandlabGoogleTagPlan,\n} from './google-tag-plan.js'\n\nexport interface BrandlabGoogleTagProps extends BrandlabGoogleTagConfig {\n /** Must match the verified public number exactly as it is rendered on the page. */\n phoneNumber?: string | null\n nonce?: string\n}\n\n/**\n * Shared Google tag and website-call forwarding-number snippet.\n *\n * Render this exactly once from the root layout:\n * `<BrandlabGoogleTag {...analytics.googleTag} phoneNumber={displayedPhone} />`.\n * `analytics.googleTag.enabled` is true only on the production deployment;\n * account-side conversion settings remain outside website runtime.\n */\nexport function BrandlabGoogleTag(props: BrandlabGoogleTagProps) {\n const plan = createBrandlabGoogleTagPlan(props, props.phoneNumber)\n if (!plan) return null\n return (\n <>\n <Script\n id=\"brandlab-google-tag-loader\"\n data-brandlab-google-tag=\"loader\"\n src={plan.source}\n strategy=\"afterInteractive\"\n nonce={props.nonce}\n />\n <Script\n id=\"brandlab-google-tag-config\"\n data-brandlab-google-tag=\"config\"\n strategy=\"afterInteractive\"\n nonce={props.nonce}\n dangerouslySetInnerHTML={{ __html: plan.inlineScript }}\n />\n </>\n )\n}\n"]}
package/dist/headers.d.ts CHANGED
@@ -1,20 +1,22 @@
1
1
  /**
2
- * Brandlab security/response headers (Brandlab Project Standard §2).
2
+ * Brandlab security/response headers: one canonical header set for every
3
+ * client site, emitted by `defineBrandlabNextConfig`.
3
4
  *
4
- * One canonical header set for every client site, composed into
5
- * `next.config.ts`:
5
+ * The deployment decides everything environment-specific:
6
+ * - a production build (`next build`, on Vercel or locally) enforces the
7
+ * production Content Security Policy and sends HSTS;
8
+ * - `next dev` sends a report-only development policy without HSTS;
9
+ * - a Vercel Preview additionally sends `noindex, nofollow, noarchive` and
10
+ * `private, no-store`, so previews never reach search results or caches.
6
11
  *
7
- * async headers() {
8
- * return buildBrandlabHeaderRules({
9
- * isProduction,
10
- * profile: 'next-local-service',
11
- * })
12
- * }
12
+ * The production X-Robots-Tag carries snippet-display controls only; per-page
13
+ * indexation stays owned by Metadata.robots.
13
14
  *
14
- * The X-Robots-Tag here carries snippet-display controls only; per-page
15
- * indexation stays owned by Metadata.robots (noindex pages override via
16
- * their <meta name="robots">).
15
+ * Declared public folders of static files (`staticAssetCache`) are served with
16
+ * `public, max-age=31536000, immutable` by production builds; Previews and
17
+ * `next dev` never cache them.
17
18
  */
19
+ import type { BrandlabDeployment } from '@brandlab-digital/site-core/site';
18
20
  export interface BrandlabHeader {
19
21
  key: string;
20
22
  value: string;
@@ -23,11 +25,13 @@ export interface BrandlabHeaderRule {
23
25
  source: string;
24
26
  headers: BrandlabHeader[];
25
27
  }
26
- export declare const BRANDLAB_NEXT_HEADER_PROFILES: readonly ["next-local-service", "next-root-level"];
27
- export type BrandlabNextHeaderProfile = (typeof BRANDLAB_NEXT_HEADER_PROFILES)[number];
28
28
  export declare const BRANDLAB_DEFAULT_PERMISSIONS_POLICY = "camera=(), microphone=(), geolocation=()";
29
29
  export declare const BRANDLAB_DEFAULT_X_ROBOTS_TAG = "max-snippet:-1, max-image-preview:large, max-video-preview:-1";
30
+ export declare const BRANDLAB_PREVIEW_X_ROBOTS_TAG = "noindex, nofollow, noarchive";
31
+ export declare const BRANDLAB_PREVIEW_CACHE_CONTROL = "private, no-store, max-age=0";
30
32
  export declare const BRANDLAB_HSTS_VALUE = "max-age=31536000";
33
+ /** Cache-Control for the declared static public folders on production builds. */
34
+ export declare const BRANDLAB_STATIC_ASSET_CACHE_CONTROL = "public, max-age=31536000, immutable";
31
35
  export interface BrandlabHstsExpansion {
32
36
  includeSubDomains: true;
33
37
  preload?: true;
@@ -39,26 +43,49 @@ export interface BrandlabContentSecurityPolicyExtensions {
39
43
  connectSrc?: readonly string[];
40
44
  frameSrc?: readonly string[];
41
45
  }
42
- export type BrandlabContentSecurityPolicyMode = 'enforce' | 'report-only';
43
46
  export interface BrandlabSecurityHeaderOptions {
44
- isProduction: boolean;
45
- profile: BrandlabNextHeaderProfile;
46
- permissionsPolicy?: string;
47
- /** Pass null to omit the snippet-control header entirely. */
48
- xRobotsTag?: string | null;
49
- extraHeaders?: readonly BrandlabHeader[];
47
+ /** From resolveBrandlabDeployment(process.env). */
48
+ deployment: BrandlabDeployment;
50
49
  hsts?: BrandlabHstsExpansion;
51
- contentSecurityPolicyMode?: BrandlabContentSecurityPolicyMode;
52
50
  /** Reviewed HTTPS origins needed by an approved project integration. */
53
51
  contentSecurityPolicyExtensions?: BrandlabContentSecurityPolicyExtensions;
52
+ /** Reviewed project exception, for example `geolocation=(self)` for a location feature. */
53
+ permissionsPolicy?: string;
54
+ /** Reviewed project headers; they cannot replace a protected header. */
55
+ extraHeaders?: readonly BrandlabHeader[];
54
56
  }
55
57
  export declare function buildBrandlabContentSecurityPolicy(options: {
56
- isProduction: boolean;
57
- profile: BrandlabNextHeaderProfile;
58
+ deployment: BrandlabDeployment;
58
59
  contentSecurityPolicyExtensions?: BrandlabContentSecurityPolicyExtensions;
59
60
  }): string;
60
61
  export declare function buildBrandlabSecurityHeaders(options: BrandlabSecurityHeaderOptions): BrandlabHeader[];
61
- export declare function buildBrandlabHeaderRules(options: BrandlabSecurityHeaderOptions & {
62
- source?: string;
63
- }): BrandlabHeaderRule[];
62
+ export interface BrandlabStaticAssetFolder {
63
+ /** The declared source, for example `/images/:path*`. */
64
+ source: string;
65
+ /** The public folder's path segments, for example `['images']`. */
66
+ folder: readonly string[];
67
+ /**
68
+ * The emitted rule's source: file names with an extension below the folder,
69
+ * such as `/images/:path(.+\.[A-Za-z0-9]+)`. The folder itself and
70
+ * extensionless paths, where a page would answer, never match.
71
+ */
72
+ fileSource: string;
73
+ }
74
+ /**
75
+ * Validates `staticAssetCache`: at most 10 public folders, each written as Next's
76
+ * source `/<folder>/:path*` (one to four literal segments, then one catch-all
77
+ * parameter, at most 100 characters). `/`, `/api`, `/_next` and `/_vercel` are
78
+ * rejected.
79
+ */
80
+ export declare function parseBrandlabStaticAssetCache(sources: unknown): BrandlabStaticAssetFolder[];
81
+ export interface BrandlabHeaderRuleOptions extends BrandlabSecurityHeaderOptions {
82
+ /**
83
+ * Public folders of static files that never change in place, for example
84
+ * `['/images/:path*', '/fonts/:path*']`: production builds serve their files with
85
+ * `public, max-age=31536000, immutable`. A file replaced under the same name stays
86
+ * cached in browsers for up to a year, so give a changed file a new name.
87
+ */
88
+ staticAssetCache?: readonly string[];
89
+ }
90
+ export declare function buildBrandlabHeaderRules(options: BrandlabHeaderRuleOptions): BrandlabHeaderRule[];
64
91
  //# sourceMappingURL=headers.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"headers.d.ts","sourceRoot":"","sources":["../src/headers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,cAAc,EAAE,CAAA;CAC1B;AAED,eAAO,MAAM,6BAA6B,oDAGhC,CAAA;AAEV,MAAM,MAAM,yBAAyB,GAAG,CAAC,OAAO,6BAA6B,CAAC,CAAC,MAAM,CAAC,CAAA;AAEtF,eAAO,MAAM,mCAAmC,6CACJ,CAAA;AAE5C,eAAO,MAAM,6BAA6B,kEACuB,CAAA;AAEjE,eAAO,MAAM,mBAAmB,qBACZ,CAAA;AAEpB,MAAM,WAAW,qBAAqB;IACpC,iBAAiB,EAAE,IAAI,CAAA;IACvB,OAAO,CAAC,EAAE,IAAI,CAAA;IACd,mEAAmE;IACnE,iBAAiB,EAAE,MAAM,CAAA;CAC1B;AAcD,MAAM,WAAW,uCAAuC;IACtD,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC7B,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC9B,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAC7B;AAED,MAAM,MAAM,iCAAiC,GAAG,SAAS,GAAG,aAAa,CAAA;AAEzE,MAAM,WAAW,6BAA6B;IAC5C,YAAY,EAAE,OAAO,CAAA;IACrB,OAAO,EAAE,yBAAyB,CAAA;IAClC,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,6DAA6D;IAC7D,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,YAAY,CAAC,EAAE,SAAS,cAAc,EAAE,CAAA;IACxC,IAAI,CAAC,EAAE,qBAAqB,CAAA;IAC5B,yBAAyB,CAAC,EAAE,iCAAiC,CAAA;IAC7D,wEAAwE;IACxE,+BAA+B,CAAC,EAAE,uCAAuC,CAAA;CAC1E;AAED,wBAAgB,kCAAkC,CAAC,OAAO,EAAE;IAC1D,YAAY,EAAE,OAAO,CAAA;IACrB,OAAO,EAAE,yBAAyB,CAAA;IAClC,+BAA+B,CAAC,EAAE,uCAAuC,CAAA;CAC1E,UAwDA;AAyCD,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,6BAA6B,GACrC,cAAc,EAAE,CAmDlB;AAED,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,6BAA6B,GAAG;IAChF,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,GAAG,kBAAkB,EAAE,CAWvB"}
1
+ {"version":3,"file":"headers.d.ts","sourceRoot":"","sources":["../src/headers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EACV,kBAAkB,EAEnB,MAAM,kCAAkC,CAAA;AAEzC,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,cAAc,EAAE,CAAA;CAC1B;AAED,eAAO,MAAM,mCAAmC,6CACJ,CAAA;AAE5C,eAAO,MAAM,6BAA6B,kEACuB,CAAA;AAEjE,eAAO,MAAM,6BAA6B,iCAAiC,CAAA;AAE3E,eAAO,MAAM,8BAA8B,iCAAiC,CAAA;AAE5E,eAAO,MAAM,mBAAmB,qBACZ,CAAA;AAEpB,iFAAiF;AACjF,eAAO,MAAM,mCAAmC,wCAAwC,CAAA;AAExF,MAAM,WAAW,qBAAqB;IACpC,iBAAiB,EAAE,IAAI,CAAA;IACvB,OAAO,CAAC,EAAE,IAAI,CAAA;IACd,mEAAmE;IACnE,iBAAiB,EAAE,MAAM,CAAA;CAC1B;AAoBD,MAAM,WAAW,uCAAuC;IACtD,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC7B,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC9B,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAC7B;AAED,MAAM,WAAW,6BAA6B;IAC5C,mDAAmD;IACnD,UAAU,EAAE,kBAAkB,CAAA;IAC9B,IAAI,CAAC,EAAE,qBAAqB,CAAA;IAC5B,wEAAwE;IACxE,+BAA+B,CAAC,EAAE,uCAAuC,CAAA;IACzE,2FAA2F;IAC3F,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,wEAAwE;IACxE,YAAY,CAAC,EAAE,SAAS,cAAc,EAAE,CAAA;CACzC;AAcD,wBAAgB,kCAAkC,CAAC,OAAO,EAAE;IAC1D,UAAU,EAAE,kBAAkB,CAAA;IAC9B,+BAA+B,CAAC,EAAE,uCAAuC,CAAA;CAC1E,UAsDA;AAiDD,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,6BAA6B,GACrC,cAAc,EAAE,CAmDlB;AAOD,MAAM,WAAW,yBAAyB;IACxC,yDAAyD;IACzD,MAAM,EAAE,MAAM,CAAA;IACd,mEAAmE;IACnE,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;IACzB;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAA;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,OAAO,GAAG,yBAAyB,EAAE,CA4B3F;AAED,MAAM,WAAW,yBAA0B,SAAQ,6BAA6B;IAC9E;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CACrC;AAED,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,yBAAyB,GAAG,kBAAkB,EAAE,CAuBjG"}
package/dist/headers.js CHANGED
@@ -1,29 +1,31 @@
1
1
  /**
2
- * Brandlab security/response headers (Brandlab Project Standard §2).
2
+ * Brandlab security/response headers: one canonical header set for every
3
+ * client site, emitted by `defineBrandlabNextConfig`.
3
4
  *
4
- * One canonical header set for every client site, composed into
5
- * `next.config.ts`:
5
+ * The deployment decides everything environment-specific:
6
+ * - a production build (`next build`, on Vercel or locally) enforces the
7
+ * production Content Security Policy and sends HSTS;
8
+ * - `next dev` sends a report-only development policy without HSTS;
9
+ * - a Vercel Preview additionally sends `noindex, nofollow, noarchive` and
10
+ * `private, no-store`, so previews never reach search results or caches.
6
11
  *
7
- * async headers() {
8
- * return buildBrandlabHeaderRules({
9
- * isProduction,
10
- * profile: 'next-local-service',
11
- * })
12
- * }
12
+ * The production X-Robots-Tag carries snippet-display controls only; per-page
13
+ * indexation stays owned by Metadata.robots.
13
14
  *
14
- * The X-Robots-Tag here carries snippet-display controls only; per-page
15
- * indexation stays owned by Metadata.robots (noindex pages override via
16
- * their <meta name="robots">).
15
+ * Declared public folders of static files (`staticAssetCache`) are served with
16
+ * `public, max-age=31536000, immutable` by production builds; Previews and
17
+ * `next dev` never cache them.
17
18
  */
18
- export const BRANDLAB_NEXT_HEADER_PROFILES = [
19
- 'next-local-service',
20
- 'next-root-level',
21
- ];
22
19
  export const BRANDLAB_DEFAULT_PERMISSIONS_POLICY = 'camera=(), microphone=(), geolocation=()';
23
20
  export const BRANDLAB_DEFAULT_X_ROBOTS_TAG = 'max-snippet:-1, max-image-preview:large, max-video-preview:-1';
21
+ export const BRANDLAB_PREVIEW_X_ROBOTS_TAG = 'noindex, nofollow, noarchive';
22
+ export const BRANDLAB_PREVIEW_CACHE_CONTROL = 'private, no-store, max-age=0';
24
23
  export const BRANDLAB_HSTS_VALUE = 'max-age=31536000';
24
+ /** Cache-Control for the declared static public folders on production builds. */
25
+ export const BRANDLAB_STATIC_ASSET_CACHE_CONTROL = 'public, max-age=31536000, immutable';
25
26
  const HEADER_NAME_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
26
27
  const PROTECTED_HEADER_NAMES = new Set([
28
+ 'cache-control',
27
29
  'content-security-policy',
28
30
  'content-security-policy-report-only',
29
31
  'x-frame-options',
@@ -33,11 +35,23 @@ const PROTECTED_HEADER_NAMES = new Set([
33
35
  'x-robots-tag',
34
36
  'strict-transport-security',
35
37
  ]);
36
- export function buildBrandlabContentSecurityPolicy(options) {
37
- const profile = options.profile;
38
- if (!BRANDLAB_NEXT_HEADER_PROFILES.includes(profile)) {
39
- throw new Error(`Unsupported Brandlab Next header profile "${profile}".`);
38
+ const DEPLOYMENT_ENVIRONMENTS = new Set([
39
+ 'production',
40
+ 'preview',
41
+ 'development',
42
+ ]);
43
+ function assertDeployment(deployment) {
44
+ const candidate = deployment;
45
+ if (!candidate ||
46
+ typeof candidate !== 'object' ||
47
+ !DEPLOYMENT_ENVIRONMENTS.has(candidate.environment) ||
48
+ typeof candidate.productionBuild !== 'boolean') {
49
+ throw new Error('Header rules require the deployment from resolveBrandlabDeployment(process.env).');
40
50
  }
51
+ }
52
+ export function buildBrandlabContentSecurityPolicy(options) {
53
+ assertDeployment(options.deployment);
54
+ const productionBuild = options.deployment.productionBuild;
41
55
  const scriptSources = [
42
56
  "'self'",
43
57
  "'unsafe-inline'",
@@ -49,7 +63,7 @@ export function buildBrandlabContentSecurityPolicy(options) {
49
63
  'https://googleads.g.doubleclick.net',
50
64
  'https://challenges.cloudflare.com',
51
65
  ...normalizeCspExtensions(options.contentSecurityPolicyExtensions?.scriptSrc, 'scriptSrc'),
52
- ...(options.isProduction ? [] : ["'unsafe-eval'", 'http://localhost:*']),
66
+ ...(productionBuild ? [] : ["'unsafe-eval'", 'http://localhost:*']),
53
67
  ];
54
68
  const connectSources = [
55
69
  "'self'",
@@ -66,7 +80,7 @@ export function buildBrandlabContentSecurityPolicy(options) {
66
80
  'https://*.analytics.google.com',
67
81
  'https://challenges.cloudflare.com',
68
82
  ...normalizeCspExtensions(options.contentSecurityPolicyExtensions?.connectSrc, 'connectSrc'),
69
- ...(options.isProduction ? [] : ['http://localhost:*', 'ws://localhost:*']),
83
+ ...(productionBuild ? [] : ['http://localhost:*', 'ws://localhost:*']),
70
84
  ];
71
85
  const directives = [
72
86
  "default-src 'self'",
@@ -82,7 +96,7 @@ export function buildBrandlabContentSecurityPolicy(options) {
82
96
  `frame-src 'self' https://challenges.cloudflare.com https://www.googletagmanager.com https://www.google.com ${normalizeCspExtensions(options.contentSecurityPolicyExtensions?.frameSrc, 'frameSrc').join(' ')}`.trim(),
83
97
  "manifest-src 'self'",
84
98
  "worker-src 'self' blob:",
85
- ...(options.isProduction ? ['upgrade-insecure-requests'] : []),
99
+ ...(productionBuild ? ['upgrade-insecure-requests'] : []),
86
100
  ];
87
101
  return directives.join('; ');
88
102
  }
@@ -120,15 +134,25 @@ function validateExtraHeaders(headers) {
120
134
  assertHeaderValue(header.value, `extraHeaders[${index}].value`);
121
135
  });
122
136
  }
137
+ const HEADER_OPTION_KEYS = new Set([
138
+ 'deployment',
139
+ 'hsts',
140
+ 'contentSecurityPolicyExtensions',
141
+ 'permissionsPolicy',
142
+ 'extraHeaders',
143
+ ]);
123
144
  export function buildBrandlabSecurityHeaders(options) {
124
- if (typeof options.isProduction !== 'boolean') {
125
- throw new Error('isProduction must be boolean.');
145
+ if (!options || typeof options !== 'object') {
146
+ throw new TypeError('Header options are required.');
147
+ }
148
+ const unsupported = Object.keys(options).filter((key) => !HEADER_OPTION_KEYS.has(key));
149
+ if (unsupported.length > 0) {
150
+ throw new Error(`Header options contain unsupported keys: ${unsupported.join(', ')}. The deployment decides CSP, HSTS and X-Robots-Tag.`);
126
151
  }
127
- const xRobotsTag = options.xRobotsTag === undefined ? BRANDLAB_DEFAULT_X_ROBOTS_TAG : options.xRobotsTag;
152
+ assertDeployment(options.deployment);
153
+ const { environment, productionBuild } = options.deployment;
128
154
  const permissionsPolicy = options.permissionsPolicy ?? BRANDLAB_DEFAULT_PERMISSIONS_POLICY;
129
155
  assertHeaderValue(permissionsPolicy, 'permissionsPolicy');
130
- if (xRobotsTag !== null)
131
- assertHeaderValue(xRobotsTag, 'xRobotsTag');
132
156
  validateExtraHeaders(options.extraHeaders ?? []);
133
157
  if (options.hsts) {
134
158
  assertHeaderValue(options.hsts.readinessEvidence, 'hsts.readinessEvidence');
@@ -139,19 +163,7 @@ export function buildBrandlabSecurityHeaders(options) {
139
163
  const hstsValue = options.hsts
140
164
  ? `${BRANDLAB_HSTS_VALUE}; includeSubDomains${options.hsts.preload ? '; preload' : ''}`
141
165
  : BRANDLAB_HSTS_VALUE;
142
- const cspMode = options.contentSecurityPolicyMode ?? (options.isProduction ? 'enforce' : 'report-only');
143
- if (!new Set(['enforce', 'report-only']).has(cspMode)) {
144
- throw new Error(`Unsupported contentSecurityPolicyMode "${cspMode}".`);
145
- }
146
- if (options.isProduction && cspMode !== 'enforce') {
147
- throw new Error('Production Content Security Policy must use enforce mode.');
148
- }
149
- const cspHeader = [{
150
- key: cspMode === 'enforce'
151
- ? 'Content-Security-Policy'
152
- : 'Content-Security-Policy-Report-Only',
153
- value: buildBrandlabContentSecurityPolicy(options),
154
- }];
166
+ const preview = environment === 'preview';
155
167
  return [
156
168
  { key: 'X-Frame-Options', value: 'DENY' },
157
169
  { key: 'X-Content-Type-Options', value: 'nosniff' },
@@ -160,24 +172,83 @@ export function buildBrandlabSecurityHeaders(options) {
160
172
  key: 'Permissions-Policy',
161
173
  value: permissionsPolicy,
162
174
  },
163
- ...(xRobotsTag ? [{ key: 'X-Robots-Tag', value: xRobotsTag }] : []),
164
- ...(options.isProduction
175
+ {
176
+ key: 'X-Robots-Tag',
177
+ value: preview ? BRANDLAB_PREVIEW_X_ROBOTS_TAG : BRANDLAB_DEFAULT_X_ROBOTS_TAG,
178
+ },
179
+ ...(productionBuild
165
180
  ? [{ key: 'Strict-Transport-Security', value: hstsValue }]
166
181
  : []),
167
- ...cspHeader,
168
- ...(options.extraHeaders ?? []),
182
+ {
183
+ key: productionBuild
184
+ ? 'Content-Security-Policy'
185
+ : 'Content-Security-Policy-Report-Only',
186
+ value: buildBrandlabContentSecurityPolicy(options),
187
+ },
188
+ ...(options.extraHeaders ?? []).map((header) => ({ ...header })),
189
+ ...(preview ? [{ key: 'Cache-Control', value: BRANDLAB_PREVIEW_CACHE_CONTROL }] : []),
169
190
  ];
170
191
  }
192
+ const STATIC_ASSET_SOURCE = /^\/((?:[A-Za-z0-9][A-Za-z0-9._-]*\/){1,4}):([A-Za-z_][A-Za-z0-9_]*)\*$/;
193
+ const STATIC_ASSET_RESERVED = new Set(['api', '_next', '_vercel']);
194
+ const STATIC_ASSET_MAX_SOURCES = 10;
195
+ const STATIC_ASSET_MAX_LENGTH = 100;
196
+ /**
197
+ * Validates `staticAssetCache`: at most 10 public folders, each written as Next's
198
+ * source `/<folder>/:path*` (one to four literal segments, then one catch-all
199
+ * parameter, at most 100 characters). `/`, `/api`, `/_next` and `/_vercel` are
200
+ * rejected.
201
+ */
202
+ export function parseBrandlabStaticAssetCache(sources) {
203
+ if (!Array.isArray(sources)) {
204
+ throw new Error('staticAssetCache must list public folders such as /images/:path*.');
205
+ }
206
+ if (sources.length > STATIC_ASSET_MAX_SOURCES) {
207
+ throw new Error(`staticAssetCache lists at most ${STATIC_ASSET_MAX_SOURCES} public folders.`);
208
+ }
209
+ const seen = new Set();
210
+ return sources.map((source, index) => {
211
+ const field = `staticAssetCache[${index}]`;
212
+ if (typeof source !== 'string' || source.length > STATIC_ASSET_MAX_LENGTH) {
213
+ throw new Error(`${field} must be a source of at most ${STATIC_ASSET_MAX_LENGTH} characters.`);
214
+ }
215
+ if (STATIC_ASSET_RESERVED.has(source.split('/')[1]?.toLowerCase() ?? '')) {
216
+ throw new Error(`${field} "${source}" is a Next or API path, not a public folder.`);
217
+ }
218
+ const match = STATIC_ASSET_SOURCE.exec(source);
219
+ if (!match) {
220
+ throw new Error(`${field} "${source}" must name a public folder as /<folder>/:path* (literal folder segments, then one catch-all parameter).`);
221
+ }
222
+ const folder = match[1].slice(0, -1).split('/');
223
+ const key = folder.join('/').toLowerCase();
224
+ if (seen.has(key))
225
+ throw new Error(`${field} repeats the /${folder.join('/')} folder.`);
226
+ seen.add(key);
227
+ return { source, folder, fileSource: `/${folder.join('/')}/:${match[2]}(.+\\.[A-Za-z0-9]+)` };
228
+ });
229
+ }
171
230
  export function buildBrandlabHeaderRules(options) {
172
- const source = options.source ?? '/:path*';
173
- if (!source.startsWith('/') || /[\u0000-\u001F\u007F]/.test(source)) {
174
- throw new Error('Header-rule source must be a root-relative single-line pattern.');
231
+ if (!options || typeof options !== 'object') {
232
+ throw new TypeError('Header options are required.');
175
233
  }
176
- return [
234
+ const { staticAssetCache, ...security } = options;
235
+ const folders = staticAssetCache === undefined ? [] : parseBrandlabStaticAssetCache(staticAssetCache);
236
+ const rules = [
177
237
  {
178
- source,
179
- headers: buildBrandlabSecurityHeaders(options),
238
+ source: '/:path*',
239
+ headers: buildBrandlabSecurityHeaders(security),
180
240
  },
181
241
  ];
242
+ // A production build outside a Preview; Previews keep private, no-store everywhere.
243
+ const { environment, productionBuild } = options.deployment;
244
+ if (productionBuild && environment !== 'preview') {
245
+ for (const folder of folders) {
246
+ rules.push({
247
+ source: folder.fileSource,
248
+ headers: [{ key: 'Cache-Control', value: BRANDLAB_STATIC_ASSET_CACHE_CONTROL }],
249
+ });
250
+ }
251
+ }
252
+ return rules;
182
253
  }
183
254
  //# sourceMappingURL=headers.js.map