@brandlab-digital/site-next 2.0.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.
Files changed (104) 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 +148 -0
  18. package/dist/contact/form-client.d.ts.map +1 -0
  19. package/dist/contact/form-client.js +234 -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-reviews-client.d.ts +12 -1
  45. package/dist/google-reviews-client.d.ts.map +1 -1
  46. package/dist/google-reviews-client.js +38 -41
  47. package/dist/google-reviews-client.js.map +1 -1
  48. package/dist/google-reviews-ui.d.ts +4 -2
  49. package/dist/google-reviews-ui.d.ts.map +1 -1
  50. package/dist/google-reviews-ui.js +10 -9
  51. package/dist/google-reviews-ui.js.map +1 -1
  52. package/dist/google-tag-plan.d.ts +1 -4
  53. package/dist/google-tag-plan.d.ts.map +1 -1
  54. package/dist/google-tag-plan.js +1 -14
  55. package/dist/google-tag-plan.js.map +1 -1
  56. package/dist/google-tag.d.ts +5 -4
  57. package/dist/google-tag.d.ts.map +1 -1
  58. package/dist/google-tag.js +5 -4
  59. package/dist/google-tag.js.map +1 -1
  60. package/dist/headers.d.ts +21 -28
  61. package/dist/headers.d.ts.map +1 -1
  62. package/dist/headers.js +62 -51
  63. package/dist/headers.js.map +1 -1
  64. package/dist/instrumentation-client.d.ts +21 -1
  65. package/dist/instrumentation-client.d.ts.map +1 -1
  66. package/dist/instrumentation-client.js +62 -7
  67. package/dist/instrumentation-client.js.map +1 -1
  68. package/dist/lead-registry.d.ts +34 -0
  69. package/dist/lead-registry.d.ts.map +1 -0
  70. package/dist/lead-registry.js +30 -0
  71. package/dist/lead-registry.js.map +1 -0
  72. package/dist/lead-reporting.d.ts +37 -0
  73. package/dist/lead-reporting.d.ts.map +1 -0
  74. package/dist/lead-reporting.js +74 -0
  75. package/dist/lead-reporting.js.map +1 -0
  76. package/dist/metadata.d.ts +10 -11
  77. package/dist/metadata.d.ts.map +1 -1
  78. package/dist/metadata.js +17 -19
  79. package/dist/metadata.js.map +1 -1
  80. package/dist/next-config.d.ts +102 -0
  81. package/dist/next-config.d.ts.map +1 -0
  82. package/dist/next-config.js +140 -0
  83. package/dist/next-config.js.map +1 -0
  84. package/dist/robots.d.ts +14 -6
  85. package/dist/robots.d.ts.map +1 -1
  86. package/dist/robots.js +26 -17
  87. package/dist/robots.js.map +1 -1
  88. package/dist/sitemaps.d.ts +34 -0
  89. package/dist/sitemaps.d.ts.map +1 -1
  90. package/dist/sitemaps.js +66 -1
  91. package/dist/sitemaps.js.map +1 -1
  92. package/dist/testing/server-only.d.ts +8 -0
  93. package/dist/testing/server-only.d.ts.map +1 -0
  94. package/dist/testing/server-only.js +8 -0
  95. package/dist/testing/server-only.js.map +1 -0
  96. package/dist/ui-client.d.ts +1 -12
  97. package/dist/ui-client.d.ts.map +1 -1
  98. package/dist/ui-client.js +0 -17
  99. package/dist/ui-client.js.map +1 -1
  100. package/dist/ui.d.ts +22 -2
  101. package/dist/ui.d.ts.map +1 -1
  102. package/dist/ui.js +67 -6
  103. package/dist/ui.js.map +1 -1
  104. package/package.json +16 -3
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lead-registry.js","sourceRoot":"","sources":["../src/lead-registry.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,0BAA0B,EAAE,MAAM,oCAAoC,CAAA;AAkC/E,MAAM,UAAU,cAAc,CAC5B,MAA0B,OAAO,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM;IAE5E,MAAM,SAAS,GAAI,GAAuD,EAAE,IAAI,CAAA;IAChF,OAAO,OAAO,SAAS,KAAK,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAA;AAChE,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAC7C,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,GAAG,GAAG;QAAE,OAAO,GAAG,CAAA;IAC1C,IAAI,CAAC;QACH,OAAO,0BAA0B,CAAC,IAAI,CAAC,CAAA;IACzC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,GAAG,CAAA;IACZ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,KAAyB;IAChD,MAAM,UAAU,GAAG,KAAK,EAAE,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;IAC9C,OAAO,UAAU,IAAI,+BAA+B,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,UAAU,CAAC,MAAM,IAAI,EAAE;QAC9F,CAAC,CAAC,UAAU;QACZ,CAAC,CAAC,SAAS,CAAA;AACf,CAAC;AAED,MAAM,QAAQ,GAAG,IAAI,OAAO,EAAuB,CAAA;AAEnD,MAAM,UAAU,mBAAmB,CAAC,OAAe,EAAE,IAAiB;IACpE,QAAQ,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAA;AAC5C,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,OAAgB;IAC7C,OAAO,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;AACnF,CAAC","sourcesContent":["// Internal module (not in package exports). defineBrandlabAnalytics registers\n// each binding's declared lead reporting here; only useBrandlabContactForm and\n// the contact testing surface read it. It holds data, so pages without a\n// contact form do not bundle the lead-sending code.\nimport type { GtagTransport } from '@brandlab-digital/site-core/analytics'\nimport { normalizeBrandlabRoutePath } from '@brandlab-digital/site-core/routes'\n\nexport interface LeadTrackingConfig {\n enabled: boolean\n enhancedConversionsEnabled?: boolean\n measurementId?: string | null\n googleAdsConversionId?: string | null\n googleAdsConversionLabel?: string | null\n gtag?: GtagTransport\n}\n\nexport interface LeadPageParams {\n page_template?: string\n service_slug?: string\n location_slug?: string\n}\n\nexport interface LeadAdsParams {\n currency?: string\n value?: number\n}\n\nexport interface LeadContext {\n readonly pagePath: string\n readonly pageParams: LeadPageParams\n readonly formData: FormData\n}\n\nexport interface LeadBinding {\n readonly config: LeadTrackingConfig\n pageParams(pagePath: string): LeadPageParams\n adsParams(lead: LeadContext): LeadAdsParams\n}\n\nexport function getBrowserGtag(\n win: Window | undefined = typeof window === 'undefined' ? undefined : window,\n): GtagTransport | undefined {\n const candidate = (win as (Window & { gtag?: GtagTransport }) | undefined)?.gtag\n return typeof candidate === 'function' ? candidate : undefined\n}\n\nexport function safePagePath(value: string) {\n const path = value.trim().split(/[?#]/, 1)[0]\n if (!path || path.length > 500) return '/'\n try {\n return normalizeBrandlabRoutePath(path)\n } catch {\n return '/'\n }\n}\n\nexport function safeSlug(value: string | undefined) {\n const normalized = value?.trim().toLowerCase()\n return normalized && /^[a-z0-9]+(?:[-_][a-z0-9]+)*$/.test(normalized) && normalized.length <= 80\n ? normalized\n : undefined\n}\n\nconst bindings = new WeakMap<object, LeadBinding>()\n\nexport function registerLeadBinding(binding: object, lead: LeadBinding) {\n bindings.set(binding, Object.freeze(lead))\n}\n\nexport function leadBindingFor(binding: unknown): LeadBinding | undefined {\n return binding && typeof binding === 'object' ? bindings.get(binding) : undefined\n}\n"]}
@@ -0,0 +1,37 @@
1
+ import { type ContactFormConversionResult } from '@brandlab-digital/site-core/analytics';
2
+ import type { ContactActionResult } from '@brandlab-digital/site-core/contact';
3
+ import type { BrandlabContactFieldRoles } from './contact/shared.js';
4
+ import { type LeadAdsParams, type LeadBinding, type LeadPageParams, type LeadTrackingConfig } from './lead-registry.js';
5
+ export type AcceptedContactResult = Extract<ContactActionResult, {
6
+ ok: true;
7
+ }>;
8
+ export declare function isTrackableContactActionResult(result: ContactActionResult): result is Extract<ContactActionResult, {
9
+ ok: true;
10
+ accepted: true;
11
+ }>;
12
+ /**
13
+ * GA4 contact_form plus the Google Ads form conversion, only after provider
14
+ * acceptance. Email and phone may only come from fields with explicit
15
+ * declarative roles and never enter the GA4 parameter object.
16
+ */
17
+ export declare function trackDeliveredContactWithEnhancedConversions(result: ContactActionResult, config: LeadTrackingConfig, options: {
18
+ pagePath: string;
19
+ /** Field names with declarative roles; only email and phone roles are read. */
20
+ definition: BrandlabContactFieldRoles;
21
+ formData: FormData;
22
+ /** Live Consent Mode decision for ad_user_data; absence is deny. */
23
+ adUserDataConsent: boolean;
24
+ params?: LeadPageParams;
25
+ adsParams?: LeadAdsParams;
26
+ }): ContactFormConversionResult;
27
+ export interface AcceptedLeadInput {
28
+ /** The form's pagePath, exactly as the hook received it. */
29
+ pagePath: string;
30
+ /** The accepted attempt's fields. */
31
+ formData: FormData;
32
+ fieldRoles: BrandlabContactFieldRoles;
33
+ adUserDataConsent: boolean;
34
+ }
35
+ /** Sends the binding's declared lead for one accepted enquiry. */
36
+ export declare function reportAcceptedLead(lead: LeadBinding, result: AcceptedContactResult, input: AcceptedLeadInput, override?: Pick<LeadTrackingConfig, 'enabled' | 'gtag'>): ContactFormConversionResult;
37
+ //# sourceMappingURL=lead-reporting.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lead-reporting.d.ts","sourceRoot":"","sources":["../src/lead-reporting.ts"],"names":[],"mappings":"AAGA,OAAO,EAIL,KAAK,2BAA2B,EACjC,MAAM,uCAAuC,CAAA;AAC9C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAA;AAC9E,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAA;AACpE,OAAO,EAIL,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACxB,MAAM,oBAAoB,CAAA;AAE3B,MAAM,MAAM,qBAAqB,GAAG,OAAO,CAAC,mBAAmB,EAAE;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,CAAC,CAAA;AA2B9E,wBAAgB,8BAA8B,CAC5C,MAAM,EAAE,mBAAmB,GAC1B,MAAM,IAAI,OAAO,CAAC,mBAAmB,EAAE;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,IAAI,CAAA;CAAE,CAAC,CAEtE;AAaD;;;;GAIG;AACH,wBAAgB,4CAA4C,CAC1D,MAAM,EAAE,mBAAmB,EAC3B,MAAM,EAAE,kBAAkB,EAC1B,OAAO,EAAE;IACP,QAAQ,EAAE,MAAM,CAAA;IAChB,+EAA+E;IAC/E,UAAU,EAAE,yBAAyB,CAAA;IACrC,QAAQ,EAAE,QAAQ,CAAA;IAClB,oEAAoE;IACpE,iBAAiB,EAAE,OAAO,CAAA;IAC1B,MAAM,CAAC,EAAE,cAAc,CAAA;IACvB,SAAS,CAAC,EAAE,aAAa,CAAA;CAC1B,GACA,2BAA2B,CAqB7B;AAED,MAAM,WAAW,iBAAiB;IAChC,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAA;IAChB,qCAAqC;IACrC,QAAQ,EAAE,QAAQ,CAAA;IAClB,UAAU,EAAE,yBAAyB,CAAA;IACrC,iBAAiB,EAAE,OAAO,CAAA;CAC3B;AAED,kEAAkE;AAClE,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,qBAAqB,EAC7B,KAAK,EAAE,iBAAiB,EACxB,QAAQ,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,SAAS,GAAG,MAAM,CAAC,GACtD,2BAA2B,CAU7B"}
@@ -0,0 +1,74 @@
1
+ // Internal module (not in package exports): the single accepted-enquiry
2
+ // reporting path, used only by useBrandlabContactForm and the contact testing
3
+ // surface, with the lead reporting a binding declared in lead-registry.
4
+ import { normalizeEnhancedConversionEmail, normalizeEnhancedConversionPhone, sendContactFormConversion, } from '@brandlab-digital/site-core/analytics';
5
+ import { getBrowserGtag, safePagePath, safeSlug, } from './lead-registry.js';
6
+ function sanitizeContactParams(params) {
7
+ return {
8
+ ...(safeSlug(params?.page_template)
9
+ ? { page_template: safeSlug(params?.page_template) }
10
+ : {}),
11
+ ...(safeSlug(params?.service_slug)
12
+ ? { service_slug: safeSlug(params?.service_slug) }
13
+ : {}),
14
+ ...(safeSlug(params?.location_slug)
15
+ ? { location_slug: safeSlug(params?.location_slug) }
16
+ : {}),
17
+ };
18
+ }
19
+ function sanitizeAdsParams(params) {
20
+ return {
21
+ ...(params?.currency && /^[A-Z]{3}$/.test(params.currency)
22
+ ? { currency: params.currency }
23
+ : {}),
24
+ ...(typeof params?.value === 'number' && Number.isFinite(params.value) && params.value >= 0
25
+ ? { value: params.value }
26
+ : {}),
27
+ };
28
+ }
29
+ export function isTrackableContactActionResult(result) {
30
+ return result.ok && result.accepted && result.trackConversion;
31
+ }
32
+ function explicitRoleValue(definition, formData, role) {
33
+ const field = definition.fields.find((candidate) => candidate.role === role);
34
+ if (!field)
35
+ return undefined;
36
+ const value = formData.get(field.name);
37
+ return typeof value === 'string' ? value : undefined;
38
+ }
39
+ /**
40
+ * GA4 contact_form plus the Google Ads form conversion, only after provider
41
+ * acceptance. Email and phone may only come from fields with explicit
42
+ * declarative roles and never enter the GA4 parameter object.
43
+ */
44
+ export function trackDeliveredContactWithEnhancedConversions(result, config, options) {
45
+ if (!isTrackableContactActionResult(result)) {
46
+ return { ga4Sent: false, enhancedConversionSent: false, adsConversionSent: false };
47
+ }
48
+ const email = normalizeEnhancedConversionEmail(explicitRoleValue(options.definition, options.formData, 'email'));
49
+ const phone = normalizeEnhancedConversionPhone(explicitRoleValue(options.definition, options.formData, 'phone'));
50
+ return sendContactFormConversion({
51
+ ...config,
52
+ gtag: config.gtag ?? getBrowserGtag(),
53
+ pagePath: safePagePath(options.pagePath),
54
+ submissionId: result.confirmationId,
55
+ params: sanitizeContactParams(options.params),
56
+ adsParams: sanitizeAdsParams(options.adsParams),
57
+ enhancedConversionsEnabled: config.enhancedConversionsEnabled === true,
58
+ adUserDataConsent: options.adUserDataConsent === true,
59
+ ...((email || phone) ? { enhancedConversion: { email, phone } } : {}),
60
+ });
61
+ }
62
+ /** Sends the binding's declared lead for one accepted enquiry. */
63
+ export function reportAcceptedLead(lead, result, input, override) {
64
+ const params = lead.pageParams(input.pagePath);
65
+ return trackDeliveredContactWithEnhancedConversions(result, { ...lead.config, ...override }, {
66
+ pagePath: input.pagePath,
67
+ definition: input.fieldRoles,
68
+ formData: input.formData,
69
+ adUserDataConsent: input.adUserDataConsent,
70
+ params,
71
+ adsParams: lead.adsParams({ pagePath: input.pagePath, pageParams: params, formData: input.formData }),
72
+ });
73
+ }
74
+ //# sourceMappingURL=lead-reporting.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lead-reporting.js","sourceRoot":"","sources":["../src/lead-reporting.ts"],"names":[],"mappings":"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","sourcesContent":["// Internal module (not in package exports): the single accepted-enquiry\n// reporting path, used only by useBrandlabContactForm and the contact testing\n// surface, with the lead reporting a binding declared in lead-registry.\nimport {\n normalizeEnhancedConversionEmail,\n normalizeEnhancedConversionPhone,\n sendContactFormConversion,\n type ContactFormConversionResult,\n} from '@brandlab-digital/site-core/analytics'\nimport type { ContactActionResult } from '@brandlab-digital/site-core/contact'\nimport type { BrandlabContactFieldRoles } from './contact/shared.js'\nimport {\n getBrowserGtag,\n safePagePath,\n safeSlug,\n type LeadAdsParams,\n type LeadBinding,\n type LeadPageParams,\n type LeadTrackingConfig,\n} from './lead-registry.js'\n\nexport type AcceptedContactResult = Extract<ContactActionResult, { ok: true }>\n\nfunction sanitizeContactParams(params: LeadPageParams | undefined) {\n return {\n ...(safeSlug(params?.page_template)\n ? { page_template: safeSlug(params?.page_template) }\n : {}),\n ...(safeSlug(params?.service_slug)\n ? { service_slug: safeSlug(params?.service_slug) }\n : {}),\n ...(safeSlug(params?.location_slug)\n ? { location_slug: safeSlug(params?.location_slug) }\n : {}),\n }\n}\n\nfunction sanitizeAdsParams(params: LeadAdsParams | undefined) {\n return {\n ...(params?.currency && /^[A-Z]{3}$/.test(params.currency)\n ? { currency: params.currency }\n : {}),\n ...(typeof params?.value === 'number' && Number.isFinite(params.value) && params.value >= 0\n ? { value: params.value }\n : {}),\n }\n}\n\nexport function isTrackableContactActionResult(\n result: ContactActionResult,\n): result is Extract<ContactActionResult, { ok: true; accepted: true }> {\n return result.ok && result.accepted && result.trackConversion\n}\n\nfunction explicitRoleValue(\n definition: BrandlabContactFieldRoles,\n formData: FormData,\n role: 'email' | 'phone',\n) {\n const field = definition.fields.find((candidate) => candidate.role === role)\n if (!field) return undefined\n const value = formData.get(field.name)\n return typeof value === 'string' ? value : undefined\n}\n\n/**\n * GA4 contact_form plus the Google Ads form conversion, only after provider\n * acceptance. Email and phone may only come from fields with explicit\n * declarative roles and never enter the GA4 parameter object.\n */\nexport function trackDeliveredContactWithEnhancedConversions(\n result: ContactActionResult,\n config: LeadTrackingConfig,\n options: {\n pagePath: string\n /** Field names with declarative roles; only email and phone roles are read. */\n definition: BrandlabContactFieldRoles\n formData: FormData\n /** Live Consent Mode decision for ad_user_data; absence is deny. */\n adUserDataConsent: boolean\n params?: LeadPageParams\n adsParams?: LeadAdsParams\n },\n): ContactFormConversionResult {\n if (!isTrackableContactActionResult(result)) {\n return { ga4Sent: false, enhancedConversionSent: false, adsConversionSent: false }\n }\n const email = normalizeEnhancedConversionEmail(\n explicitRoleValue(options.definition, options.formData, 'email'),\n )\n const phone = normalizeEnhancedConversionPhone(\n explicitRoleValue(options.definition, options.formData, 'phone'),\n )\n return sendContactFormConversion({\n ...config,\n gtag: config.gtag ?? getBrowserGtag(),\n pagePath: safePagePath(options.pagePath),\n submissionId: result.confirmationId,\n params: sanitizeContactParams(options.params),\n adsParams: sanitizeAdsParams(options.adsParams),\n enhancedConversionsEnabled: config.enhancedConversionsEnabled === true,\n adUserDataConsent: options.adUserDataConsent === true,\n ...((email || phone) ? { enhancedConversion: { email, phone } } : {}),\n })\n}\n\nexport interface AcceptedLeadInput {\n /** The form's pagePath, exactly as the hook received it. */\n pagePath: string\n /** The accepted attempt's fields. */\n formData: FormData\n fieldRoles: BrandlabContactFieldRoles\n adUserDataConsent: boolean\n}\n\n/** Sends the binding's declared lead for one accepted enquiry. */\nexport function reportAcceptedLead(\n lead: LeadBinding,\n result: AcceptedContactResult,\n input: AcceptedLeadInput,\n override?: Pick<LeadTrackingConfig, 'enabled' | 'gtag'>,\n): ContactFormConversionResult {\n const params = lead.pageParams(input.pagePath)\n return trackDeliveredContactWithEnhancedConversions(result, { ...lead.config, ...override }, {\n pagePath: input.pagePath,\n definition: input.fieldRoles,\n formData: input.formData,\n adUserDataConsent: input.adUserDataConsent,\n params,\n adsParams: lead.adsParams({ pagePath: input.pagePath, pageParams: params, formData: input.formData }),\n })\n}\n"]}
@@ -1,16 +1,15 @@
1
- import { type BrandlabSeoMetadata, type BuildBrandlabMetadataInput, type BuildSeoMetadataInput } from '@brandlab-digital/site-core/metadata';
1
+ import { type BrandlabSeoMetadata } from '@brandlab-digital/site-core/metadata';
2
2
  import { type BrandlabRouteCatalog } from '@brandlab-digital/site-core/routes';
3
3
  import { type BrandlabSeoPage } from '@brandlab-digital/site-core/seo-page';
4
+ import type { BrandlabSite } from '@brandlab-digital/site-core/site';
4
5
  export type NextCompatibleMetadata = Omit<BrandlabSeoMetadata, 'canonicalPath' | 'canonicalUrl' | 'resolvedTitle'>;
5
- export type BrandlabMetadataDefaults = Pick<BuildSeoMetadataInput, 'brandName' | 'siteUrl' | 'locale' | 'titleTemplate' | 'defaultSocialImage'>;
6
- export type BrandlabPageMetadataInput = Omit<BuildSeoMetadataInput, keyof BrandlabMetadataDefaults>;
7
- export type BrandlabStrictMetadataDefaults = Pick<BuildBrandlabMetadataInput, 'brandName' | 'siteUrl' | 'locale' | 'titleTemplate' | 'defaultSocialImage'>;
8
- export type BrandlabStrictPageMetadataInput = Omit<BuildBrandlabMetadataInput, keyof BrandlabStrictMetadataDefaults>;
9
6
  export declare function toNextMetadata(metadata: BrandlabSeoMetadata): NextCompatibleMetadata;
10
- export declare function buildNextMetadata(input: BuildSeoMetadataInput): NextCompatibleMetadata;
11
- export declare function buildBrandlabNextMetadata(input: BuildBrandlabMetadataInput): NextCompatibleMetadata;
12
- export declare function createNextMetadataBuilder(defaults: BrandlabMetadataDefaults): (page: BrandlabPageMetadataInput) => NextCompatibleMetadata;
13
- export declare function createBrandlabNextMetadataBuilder(defaults: BrandlabStrictMetadataDefaults): (page: BrandlabStrictPageMetadataInput) => NextCompatibleMetadata;
14
- /** Builds page metadata from the same route identity used by breadcrumbs and sitemaps. */
15
- export declare function createRouteMetadataBuilder(defaults: BrandlabStrictMetadataDefaults, routes: BrandlabRouteCatalog): (input: BrandlabSeoPage) => NextCompatibleMetadata;
7
+ /**
8
+ * The project's one page-metadata binding (`src/lib/seo/metadata.ts`):
9
+ * `export const pageMetadata = createRouteMetadataBuilder(site, routes)`.
10
+ * Canonical path and robots come from the route catalogue; the title mode is
11
+ * explicit per page. Keywords keep their form: a comma-separated string renders
12
+ * exactly as given, a list renders joined with commas, as Next renders them.
13
+ */
14
+ export declare function createRouteMetadataBuilder(site: BrandlabSite, routes: BrandlabRouteCatalog): (input: BrandlabSeoPage) => NextCompatibleMetadata;
16
15
  //# sourceMappingURL=metadata.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,mBAAmB,EACxB,KAAK,0BAA0B,EAC/B,KAAK,qBAAqB,EAC3B,MAAM,sCAAsC,CAAA;AAC7C,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,oCAAoC,CAAA;AAC3C,OAAO,EAAiB,KAAK,eAAe,EAAE,MAAM,sCAAsC,CAAA;AAE1F,MAAM,MAAM,sBAAsB,GAAG,IAAI,CACvC,mBAAmB,EACnB,eAAe,GAAG,cAAc,GAAG,eAAe,CACnD,CAAA;AAED,MAAM,MAAM,wBAAwB,GAAG,IAAI,CACzC,qBAAqB,EACrB,WAAW,GAAG,SAAS,GAAG,QAAQ,GAAG,eAAe,GAAG,oBAAoB,CAC5E,CAAA;AAED,MAAM,MAAM,yBAAyB,GAAG,IAAI,CAAC,qBAAqB,EAAE,MAAM,wBAAwB,CAAC,CAAA;AAEnG,MAAM,MAAM,8BAA8B,GAAG,IAAI,CAC/C,0BAA0B,EAC1B,WAAW,GAAG,SAAS,GAAG,QAAQ,GAAG,eAAe,GAAG,oBAAoB,CAC5E,CAAA;AAED,MAAM,MAAM,+BAA+B,GAAG,IAAI,CAChD,0BAA0B,EAC1B,MAAM,8BAA8B,CACrC,CAAA;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,mBAAmB,GAAG,sBAAsB,CAIpF;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,qBAAqB,GAAG,sBAAsB,CAEtF;AAED,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,0BAA0B,GAChC,sBAAsB,CAExB;AAED,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,wBAAwB,IAClE,MAAM,yBAAyB,4BACxC;AAED,wBAAgB,iCAAiC,CAAC,QAAQ,EAAE,8BAA8B,IAChF,MAAM,+BAA+B,4BAE9C;AAED,0FAA0F;AAC1F,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,8BAA8B,EACxC,MAAM,EAAE,oBAAoB,IAEpB,OAAO,eAAe,KAAG,sBAAsB,CA2BxD"}
1
+ {"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,mBAAmB,EACzB,MAAM,sCAAsC,CAAA;AAC7C,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,oCAAoC,CAAA;AAC3C,OAAO,EAAiB,KAAK,eAAe,EAAE,MAAM,sCAAsC,CAAA;AAC1F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAEpE,MAAM,MAAM,sBAAsB,GAAG,IAAI,CACvC,mBAAmB,EACnB,eAAe,GAAG,cAAc,GAAG,eAAe,CACnD,CAAA;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,mBAAmB,GAAG,sBAAsB,CAIpF;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,YAAY,EAClB,MAAM,EAAE,oBAAoB,IAEpB,OAAO,eAAe,KAAG,sBAAsB,CA+BxD"}
package/dist/metadata.js CHANGED
@@ -1,37 +1,35 @@
1
- import { buildBrandlabMetadata, buildSeoMetadata, } from '@brandlab-digital/site-core/metadata';
1
+ import { buildBrandlabMetadata, } from '@brandlab-digital/site-core/metadata';
2
2
  import { getRouteById, resolveBrandlabRouteIndexing, } from '@brandlab-digital/site-core/routes';
3
3
  import { defineSeoPage } from '@brandlab-digital/site-core/seo-page';
4
4
  export function toNextMetadata(metadata) {
5
5
  const { canonicalPath: _canonicalPath, canonicalUrl: _canonicalUrl, resolvedTitle: _resolvedTitle, ...next } = metadata;
6
6
  return next;
7
7
  }
8
- export function buildNextMetadata(input) {
9
- return toNextMetadata(buildSeoMetadata(input));
10
- }
11
- export function buildBrandlabNextMetadata(input) {
12
- return toNextMetadata(buildBrandlabMetadata(input));
13
- }
14
- export function createNextMetadataBuilder(defaults) {
15
- return (page) => buildNextMetadata({ ...defaults, ...page });
16
- }
17
- export function createBrandlabNextMetadataBuilder(defaults) {
18
- return (page) => buildBrandlabNextMetadata({ ...defaults, ...page });
19
- }
20
- /** Builds page metadata from the same route identity used by breadcrumbs and sitemaps. */
21
- export function createRouteMetadataBuilder(defaults, routes) {
8
+ /**
9
+ * The project's one page-metadata binding (`src/lib/seo/metadata.ts`):
10
+ * `export const pageMetadata = createRouteMetadataBuilder(site, routes)`.
11
+ * Canonical path and robots come from the route catalogue; the title mode is
12
+ * explicit per page. Keywords keep their form: a comma-separated string renders
13
+ * exactly as given, a list renders joined with commas, as Next renders them.
14
+ */
15
+ export function createRouteMetadataBuilder(site, routes) {
22
16
  return (input) => {
23
17
  const page = defineSeoPage(input);
24
18
  const route = getRouteById(routes, page.routeId);
25
19
  if (!route)
26
20
  throw new Error(`Unknown metadata route ID "${page.routeId}".`);
27
- return buildBrandlabNextMetadata({
28
- ...defaults,
21
+ return toNextMetadata(buildBrandlabMetadata({
22
+ brandName: site.name,
23
+ siteUrl: site.canonicalUrl,
24
+ locale: site.locale,
25
+ titleTemplate: site.titleTemplate,
26
+ defaultSocialImage: site.defaultSocialImage,
29
27
  title: page.title,
30
28
  description: page.description,
31
29
  pathname: route.canonicalPath ?? route.path,
32
30
  indexing: resolveBrandlabRouteIndexing(route),
33
31
  ...(page.titleMode ? { titleMode: page.titleMode } : {}),
34
- ...(page.keywords ? { keywords: [...page.keywords] } : {}),
32
+ ...(page.keywords !== undefined ? { keywords: page.keywords } : {}),
35
33
  ...(page.socialImage ? { socialImage: page.socialImage } : {}),
36
34
  ...(page.article
37
35
  ? {
@@ -44,7 +42,7 @@ export function createRouteMetadataBuilder(defaults, routes) {
44
42
  },
45
43
  }
46
44
  : {}),
47
- });
45
+ }));
48
46
  };
49
47
  }
50
48
  //# sourceMappingURL=metadata.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"metadata.js","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,gBAAgB,GAIjB,MAAM,sCAAsC,CAAA;AAC7C,OAAO,EACL,YAAY,EACZ,4BAA4B,GAE7B,MAAM,oCAAoC,CAAA;AAC3C,OAAO,EAAE,aAAa,EAAwB,MAAM,sCAAsC,CAAA;AAwB1F,MAAM,UAAU,cAAc,CAAC,QAA6B;IAC1D,MAAM,EAAE,aAAa,EAAE,cAAc,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,cAAc,EAAE,GAAG,IAAI,EAAE,GAC1G,QAAQ,CAAA;IACV,OAAO,IAAI,CAAA;AACb,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,KAA4B;IAC5D,OAAO,cAAc,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAAA;AAChD,CAAC;AAED,MAAM,UAAU,yBAAyB,CACvC,KAAiC;IAEjC,OAAO,cAAc,CAAC,qBAAqB,CAAC,KAAK,CAAC,CAAC,CAAA;AACrD,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,QAAkC;IAC1E,OAAO,CAAC,IAA+B,EAAE,EAAE,CAAC,iBAAiB,CAAC,EAAE,GAAG,QAAQ,EAAE,GAAG,IAAI,EAAE,CAAC,CAAA;AACzF,CAAC;AAED,MAAM,UAAU,iCAAiC,CAAC,QAAwC;IACxF,OAAO,CAAC,IAAqC,EAAE,EAAE,CAC/C,yBAAyB,CAAC,EAAE,GAAG,QAAQ,EAAE,GAAG,IAAI,EAAE,CAAC,CAAA;AACvD,CAAC;AAED,0FAA0F;AAC1F,MAAM,UAAU,0BAA0B,CACxC,QAAwC,EACxC,MAA4B;IAE5B,OAAO,CAAC,KAAsB,EAA0B,EAAE;QACxD,MAAM,IAAI,GAAG,aAAa,CAAC,KAAK,CAAC,CAAA;QACjC,MAAM,KAAK,GAAG,YAAY,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAA;QAChD,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,KAAK,CAAC,8BAA8B,IAAI,CAAC,OAAO,IAAI,CAAC,CAAA;QAE3E,OAAO,yBAAyB,CAAC;YAC/B,GAAG,QAAQ;YACX,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,QAAQ,EAAE,KAAK,CAAC,aAAa,IAAI,KAAK,CAAC,IAAI;YAC3C,QAAQ,EAAE,4BAA4B,CAAC,KAAK,CAAC;YAC7C,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACxD,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC1D,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC9D,GAAG,CAAC,IAAI,CAAC,OAAO;gBACd,CAAC,CAAC;oBACE,OAAO,EAAE;wBACP,aAAa,EAAE,IAAI,CAAC,OAAO,CAAC,aAAa;wBACzC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY;4BAC3B,CAAC,CAAC,EAAE,YAAY,EAAE,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE;4BAC7C,CAAC,CAAC,EAAE,CAAC;wBACP,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC;qBACnC;iBACF;gBACH,CAAC,CAAC,EAAE,CAAC;SACR,CAAC,CAAA;IACJ,CAAC,CAAA;AACH,CAAC","sourcesContent":["import {\n buildBrandlabMetadata,\n buildSeoMetadata,\n type BrandlabSeoMetadata,\n type BuildBrandlabMetadataInput,\n type BuildSeoMetadataInput,\n} from '@brandlab-digital/site-core/metadata'\nimport {\n getRouteById,\n resolveBrandlabRouteIndexing,\n type BrandlabRouteCatalog,\n} from '@brandlab-digital/site-core/routes'\nimport { defineSeoPage, type BrandlabSeoPage } from '@brandlab-digital/site-core/seo-page'\n\nexport type NextCompatibleMetadata = Omit<\n BrandlabSeoMetadata,\n 'canonicalPath' | 'canonicalUrl' | 'resolvedTitle'\n>\n\nexport type BrandlabMetadataDefaults = Pick<\n BuildSeoMetadataInput,\n 'brandName' | 'siteUrl' | 'locale' | 'titleTemplate' | 'defaultSocialImage'\n>\n\nexport type BrandlabPageMetadataInput = Omit<BuildSeoMetadataInput, keyof BrandlabMetadataDefaults>\n\nexport type BrandlabStrictMetadataDefaults = Pick<\n BuildBrandlabMetadataInput,\n 'brandName' | 'siteUrl' | 'locale' | 'titleTemplate' | 'defaultSocialImage'\n>\n\nexport type BrandlabStrictPageMetadataInput = Omit<\n BuildBrandlabMetadataInput,\n keyof BrandlabStrictMetadataDefaults\n>\n\nexport function toNextMetadata(metadata: BrandlabSeoMetadata): NextCompatibleMetadata {\n const { canonicalPath: _canonicalPath, canonicalUrl: _canonicalUrl, resolvedTitle: _resolvedTitle, ...next } =\n metadata\n return next\n}\n\nexport function buildNextMetadata(input: BuildSeoMetadataInput): NextCompatibleMetadata {\n return toNextMetadata(buildSeoMetadata(input))\n}\n\nexport function buildBrandlabNextMetadata(\n input: BuildBrandlabMetadataInput,\n): NextCompatibleMetadata {\n return toNextMetadata(buildBrandlabMetadata(input))\n}\n\nexport function createNextMetadataBuilder(defaults: BrandlabMetadataDefaults) {\n return (page: BrandlabPageMetadataInput) => buildNextMetadata({ ...defaults, ...page })\n}\n\nexport function createBrandlabNextMetadataBuilder(defaults: BrandlabStrictMetadataDefaults) {\n return (page: BrandlabStrictPageMetadataInput) =>\n buildBrandlabNextMetadata({ ...defaults, ...page })\n}\n\n/** Builds page metadata from the same route identity used by breadcrumbs and sitemaps. */\nexport function createRouteMetadataBuilder(\n defaults: BrandlabStrictMetadataDefaults,\n routes: BrandlabRouteCatalog,\n) {\n return (input: BrandlabSeoPage): NextCompatibleMetadata => {\n const page = defineSeoPage(input)\n const route = getRouteById(routes, page.routeId)\n if (!route) throw new Error(`Unknown metadata route ID \"${page.routeId}\".`)\n\n return buildBrandlabNextMetadata({\n ...defaults,\n title: page.title,\n description: page.description,\n pathname: route.canonicalPath ?? route.path,\n indexing: resolveBrandlabRouteIndexing(route),\n ...(page.titleMode ? { titleMode: page.titleMode } : {}),\n ...(page.keywords ? { keywords: [...page.keywords] } : {}),\n ...(page.socialImage ? { socialImage: page.socialImage } : {}),\n ...(page.article\n ? {\n article: {\n publishedTime: page.article.publishedTime,\n ...(page.article.modifiedTime\n ? { modifiedTime: page.article.modifiedTime }\n : {}),\n authors: [...page.article.authors],\n },\n }\n : {}),\n })\n }\n}\n"]}
1
+ {"version":3,"file":"metadata.js","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,GAEtB,MAAM,sCAAsC,CAAA;AAC7C,OAAO,EACL,YAAY,EACZ,4BAA4B,GAE7B,MAAM,oCAAoC,CAAA;AAC3C,OAAO,EAAE,aAAa,EAAwB,MAAM,sCAAsC,CAAA;AAQ1F,MAAM,UAAU,cAAc,CAAC,QAA6B;IAC1D,MAAM,EAAE,aAAa,EAAE,cAAc,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,cAAc,EAAE,GAAG,IAAI,EAAE,GAC1G,QAAQ,CAAA;IACV,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,0BAA0B,CACxC,IAAkB,EAClB,MAA4B;IAE5B,OAAO,CAAC,KAAsB,EAA0B,EAAE;QACxD,MAAM,IAAI,GAAG,aAAa,CAAC,KAAK,CAAC,CAAA;QACjC,MAAM,KAAK,GAAG,YAAY,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAA;QAChD,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,KAAK,CAAC,8BAA8B,IAAI,CAAC,OAAO,IAAI,CAAC,CAAA;QAE3E,OAAO,cAAc,CAAC,qBAAqB,CAAC;YAC1C,SAAS,EAAE,IAAI,CAAC,IAAI;YACpB,OAAO,EAAE,IAAI,CAAC,YAAY;YAC1B,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,aAAa,EAAE,IAAI,CAAC,aAAa;YACjC,kBAAkB,EAAE,IAAI,CAAC,kBAAkB;YAC3C,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,QAAQ,EAAE,KAAK,CAAC,aAAa,IAAI,KAAK,CAAC,IAAI;YAC3C,QAAQ,EAAE,4BAA4B,CAAC,KAAK,CAAC;YAC7C,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACxD,GAAG,CAAC,IAAI,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACnE,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC9D,GAAG,CAAC,IAAI,CAAC,OAAO;gBACd,CAAC,CAAC;oBACE,OAAO,EAAE;wBACP,aAAa,EAAE,IAAI,CAAC,OAAO,CAAC,aAAa;wBACzC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY;4BAC3B,CAAC,CAAC,EAAE,YAAY,EAAE,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE;4BAC7C,CAAC,CAAC,EAAE,CAAC;wBACP,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC;qBACnC;iBACF;gBACH,CAAC,CAAC,EAAE,CAAC;SACR,CAAC,CAAC,CAAA;IACL,CAAC,CAAA;AACH,CAAC","sourcesContent":["import {\n buildBrandlabMetadata,\n type BrandlabSeoMetadata,\n} from '@brandlab-digital/site-core/metadata'\nimport {\n getRouteById,\n resolveBrandlabRouteIndexing,\n type BrandlabRouteCatalog,\n} from '@brandlab-digital/site-core/routes'\nimport { defineSeoPage, type BrandlabSeoPage } from '@brandlab-digital/site-core/seo-page'\nimport type { BrandlabSite } from '@brandlab-digital/site-core/site'\n\nexport type NextCompatibleMetadata = Omit<\n BrandlabSeoMetadata,\n 'canonicalPath' | 'canonicalUrl' | 'resolvedTitle'\n>\n\nexport function toNextMetadata(metadata: BrandlabSeoMetadata): NextCompatibleMetadata {\n const { canonicalPath: _canonicalPath, canonicalUrl: _canonicalUrl, resolvedTitle: _resolvedTitle, ...next } =\n metadata\n return next\n}\n\n/**\n * The project's one page-metadata binding (`src/lib/seo/metadata.ts`):\n * `export const pageMetadata = createRouteMetadataBuilder(site, routes)`.\n * Canonical path and robots come from the route catalogue; the title mode is\n * explicit per page. Keywords keep their form: a comma-separated string renders\n * exactly as given, a list renders joined with commas, as Next renders them.\n */\nexport function createRouteMetadataBuilder(\n site: BrandlabSite,\n routes: BrandlabRouteCatalog,\n) {\n return (input: BrandlabSeoPage): NextCompatibleMetadata => {\n const page = defineSeoPage(input)\n const route = getRouteById(routes, page.routeId)\n if (!route) throw new Error(`Unknown metadata route ID \"${page.routeId}\".`)\n\n return toNextMetadata(buildBrandlabMetadata({\n brandName: site.name,\n siteUrl: site.canonicalUrl,\n locale: site.locale,\n titleTemplate: site.titleTemplate,\n defaultSocialImage: site.defaultSocialImage,\n title: page.title,\n description: page.description,\n pathname: route.canonicalPath ?? route.path,\n indexing: resolveBrandlabRouteIndexing(route),\n ...(page.titleMode ? { titleMode: page.titleMode } : {}),\n ...(page.keywords !== undefined ? { keywords: page.keywords } : {}),\n ...(page.socialImage ? { socialImage: page.socialImage } : {}),\n ...(page.article\n ? {\n article: {\n publishedTime: page.article.publishedTime,\n ...(page.article.modifiedTime\n ? { modifiedTime: page.article.modifiedTime }\n : {}),\n authors: [...page.article.authors],\n },\n }\n : {}),\n }))\n }\n}\n"]}
@@ -0,0 +1,102 @@
1
+ import { type BrandlabRedirectTable } from '@brandlab-digital/site-core/redirects';
2
+ import { type BrandlabDeploymentEnvironment, type BrandlabEnvironment, type BrandlabSite } from '@brandlab-digital/site-core/site';
3
+ import { type BrandlabContentSecurityPolicyExtensions, type BrandlabHeader, type BrandlabHeaderRule, type BrandlabHstsExpansion } from './headers.js';
4
+ /** Next redirect records: 308 stays `permanent: true`, 301 stays `statusCode: 301`. */
5
+ export type BrandlabNextRedirect = {
6
+ source: string;
7
+ destination: string;
8
+ has?: {
9
+ type: 'host';
10
+ value: string;
11
+ }[];
12
+ } & ({
13
+ permanent: true;
14
+ statusCode?: never;
15
+ } | {
16
+ statusCode: 301;
17
+ permanent?: never;
18
+ });
19
+ export interface BrandlabNextRemotePattern {
20
+ protocol?: 'http' | 'https';
21
+ hostname: string;
22
+ port?: string;
23
+ pathname?: string;
24
+ search?: string;
25
+ }
26
+ /** The site's own image settings; the output format is always WebP. */
27
+ export interface BrandlabNextImagesOptions {
28
+ deviceSizes?: readonly number[];
29
+ imageSizes?: readonly number[];
30
+ qualities?: readonly number[];
31
+ minimumCacheTTL?: number;
32
+ remotePatterns?: readonly BrandlabNextRemotePattern[];
33
+ }
34
+ export interface BrandlabNextImagesConfig {
35
+ formats: 'image/webp'[];
36
+ deviceSizes?: number[];
37
+ imageSizes?: number[];
38
+ qualities?: number[];
39
+ minimumCacheTTL?: number;
40
+ remotePatterns?: BrandlabNextRemotePattern[];
41
+ }
42
+ export interface BrandlabNextConfig {
43
+ poweredByHeader: false;
44
+ /** Trailing slashes are handled by single-hop redirect rules instead. */
45
+ skipTrailingSlashRedirect: true;
46
+ env: {
47
+ NEXT_PUBLIC_VERCEL_ENV: BrandlabDeploymentEnvironment;
48
+ };
49
+ outputFileTracingIncludes: Record<string, string[]>;
50
+ images: BrandlabNextImagesConfig;
51
+ pageExtensions?: string[];
52
+ headers: () => Promise<BrandlabHeaderRule[]>;
53
+ redirects: () => Promise<BrandlabNextRedirect[]>;
54
+ }
55
+ export interface BrandlabNextConfigOptions {
56
+ site: BrandlabSite;
57
+ /** The src/data/redirects.ts table (defineRedirects). */
58
+ redirects: BrandlabRedirectTable;
59
+ images: BrandlabNextImagesOptions;
60
+ /** Reviewed CSP origins for declared integrations such as Meta Pixel or map embeds. */
61
+ csp?: BrandlabContentSecurityPolicyExtensions;
62
+ hsts?: BrandlabHstsExpansion;
63
+ /** Reviewed exception to the default Permissions-Policy that is already live. */
64
+ permissionsPolicy?: string;
65
+ /** Reviewed response headers that are already live; protected headers cannot be replaced. */
66
+ extraHeaders?: readonly BrandlabHeader[];
67
+ /** Defaults to process.env; tests supply a fixed environment. */
68
+ environment?: BrandlabEnvironment;
69
+ }
70
+ /** Sharp's Linux binaries must travel with the contact route's image sanitiser. */
71
+ export declare const BRANDLAB_CONTACT_OUTPUT_FILE_TRACING: Readonly<{
72
+ '/api/contact': readonly string[];
73
+ }>;
74
+ /**
75
+ * Converts the redirect table to Next redirects: each entry keeps its 301 or
76
+ * 308 status (Next forwards the query string), gains a single-hop
77
+ * trailing-slash variant, then one trailing-slash rule and one permanent
78
+ * canonical-host rule per alias host follow. There is no x-forwarded-proto
79
+ * rule: Vercel's edge already enforces HTTPS.
80
+ */
81
+ export declare function buildBrandlabRedirects(options: {
82
+ site: BrandlabSite;
83
+ redirects: BrandlabRedirectTable;
84
+ }): BrandlabNextRedirect[];
85
+ /**
86
+ * The whole next.config.ts after its first line
87
+ * `import '@brandlab-digital/site-next/image-runtime'`:
88
+ * `export default defineBrandlabNextConfig({ site, redirects, images })`.
89
+ *
90
+ * Sets poweredByHeader: false, WebP image output, Sharp output tracing for
91
+ * /api/contact, NEXT_PUBLIC_VERCEL_ENV for browser gates, the deployment's
92
+ * header rules and the redirect table. Validation runs when the config loads,
93
+ * so an invalid table or header exception fails the build.
94
+ */
95
+ export declare function defineBrandlabNextConfig(options: BrandlabNextConfigOptions & {
96
+ mdx?: undefined;
97
+ }): BrandlabNextConfig;
98
+ export declare function defineBrandlabNextConfig<Wrapped>(options: BrandlabNextConfigOptions & {
99
+ /** The @next/mdx wrapper: createMDX({ options: { remarkPlugins: ['remark-gfm'] } }). */
100
+ mdx: (config: BrandlabNextConfig) => Wrapped;
101
+ }): Wrapped;
102
+ //# sourceMappingURL=next-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"next-config.d.ts","sourceRoot":"","sources":["../src/next-config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,140 @@
1
+ import { defineRedirects } from '@brandlab-digital/site-core/redirects';
2
+ import { resolveBrandlabDeployment, } from '@brandlab-digital/site-core/site';
3
+ import { buildBrandlabHeaderRules, } from './headers.js';
4
+ /** Sharp's Linux binaries must travel with the contact route's image sanitiser. */
5
+ export const BRANDLAB_CONTACT_OUTPUT_FILE_TRACING = Object.freeze({
6
+ '/api/contact': Object.freeze([
7
+ 'node_modules/sharp/**/*',
8
+ 'node_modules/@img/sharp-linux-x64/**/*',
9
+ 'node_modules/@img/sharp-libvips-linux-x64/**/*',
10
+ ]),
11
+ });
12
+ const MDX_PAGE_EXTENSIONS = ['tsx', 'ts', 'jsx', 'js', 'mdx', 'md'];
13
+ const OPTION_KEYS = new Set([
14
+ 'site',
15
+ 'redirects',
16
+ 'images',
17
+ 'csp',
18
+ 'hsts',
19
+ 'permissionsPolicy',
20
+ 'extraHeaders',
21
+ 'mdx',
22
+ 'environment',
23
+ ]);
24
+ const IMAGE_KEYS = new Set(['deviceSizes', 'imageSizes', 'qualities', 'minimumCacheTTL', 'remotePatterns']);
25
+ function assertKnownKeys(value, allowed, field, hint = '') {
26
+ const unsupported = Object.keys(value).filter((key) => !allowed.has(key));
27
+ if (unsupported.length > 0) {
28
+ throw new Error(`${field} contains unsupported keys: ${unsupported.join(', ')}.${hint}`);
29
+ }
30
+ }
31
+ function positiveIntegers(values, field) {
32
+ if (values === undefined)
33
+ return undefined;
34
+ if (!Array.isArray(values) ||
35
+ values.length === 0 ||
36
+ values.some((value) => !Number.isSafeInteger(value) || value < 1)) {
37
+ throw new Error(`images.${field} must list positive integers.`);
38
+ }
39
+ return [...values];
40
+ }
41
+ function buildImagesConfig(images) {
42
+ if (!images || typeof images !== 'object' || Array.isArray(images)) {
43
+ throw new Error('images must declare the site image settings ({} for Next defaults).');
44
+ }
45
+ assertKnownKeys(images, IMAGE_KEYS, 'images', ' The output format is always image/webp.');
46
+ const deviceSizes = positiveIntegers(images.deviceSizes, 'deviceSizes');
47
+ const imageSizes = positiveIntegers(images.imageSizes, 'imageSizes');
48
+ const qualities = positiveIntegers(images.qualities, 'qualities');
49
+ if (qualities?.some((quality) => quality > 100)) {
50
+ throw new Error('images.qualities must be between 1 and 100.');
51
+ }
52
+ if (images.minimumCacheTTL !== undefined &&
53
+ (!Number.isSafeInteger(images.minimumCacheTTL) || images.minimumCacheTTL < 0)) {
54
+ throw new Error('images.minimumCacheTTL must be a non-negative integer of seconds.');
55
+ }
56
+ if (images.remotePatterns !== undefined && !Array.isArray(images.remotePatterns)) {
57
+ throw new Error('images.remotePatterns must be an array.');
58
+ }
59
+ return {
60
+ formats: ['image/webp'],
61
+ ...(deviceSizes ? { deviceSizes } : {}),
62
+ ...(imageSizes ? { imageSizes } : {}),
63
+ ...(qualities ? { qualities } : {}),
64
+ ...(images.minimumCacheTTL !== undefined ? { minimumCacheTTL: images.minimumCacheTTL } : {}),
65
+ ...(images.remotePatterns
66
+ ? { remotePatterns: images.remotePatterns.map((pattern) => ({ ...pattern })) }
67
+ : {}),
68
+ };
69
+ }
70
+ /**
71
+ * Converts the redirect table to Next redirects: each entry keeps its 301 or
72
+ * 308 status (Next forwards the query string), gains a single-hop
73
+ * trailing-slash variant, then one trailing-slash rule and one permanent
74
+ * canonical-host rule per alias host follow. There is no x-forwarded-proto
75
+ * rule: Vercel's edge already enforces HTTPS.
76
+ */
77
+ export function buildBrandlabRedirects(options) {
78
+ const table = defineRedirects(options.redirects);
79
+ const entries = table.flatMap((redirect) => {
80
+ const status = redirect.status === 301
81
+ ? { statusCode: 301 }
82
+ : { permanent: true };
83
+ const has = redirect.host ? { has: [{ type: 'host', value: redirect.host }] } : {};
84
+ const record = (source) => ({
85
+ source,
86
+ destination: redirect.destination,
87
+ ...status,
88
+ ...has,
89
+ });
90
+ return redirect.source === '/'
91
+ ? [record(redirect.source)]
92
+ : [record(redirect.source), record(`${redirect.source}/`)];
93
+ });
94
+ return [
95
+ ...entries,
96
+ { source: '/:path+/', destination: '/:path+', permanent: true },
97
+ ...options.site.aliasHosts.map((host) => ({
98
+ source: '/:path*',
99
+ has: [{ type: 'host', value: host }],
100
+ destination: `${options.site.canonicalUrl}/:path*`,
101
+ permanent: true,
102
+ })),
103
+ ];
104
+ }
105
+ export function defineBrandlabNextConfig(options) {
106
+ if (!options || typeof options !== 'object') {
107
+ throw new TypeError('defineBrandlabNextConfig requires the site, redirects and images.');
108
+ }
109
+ assertKnownKeys(options, OPTION_KEYS, 'next.config options');
110
+ if (!options.site?.canonicalUrl || !Array.isArray(options.site.aliasHosts)) {
111
+ throw new Error('site must come from defineBrandlabSite.');
112
+ }
113
+ if (options.mdx !== undefined && typeof options.mdx !== 'function') {
114
+ throw new TypeError('mdx must be the @next/mdx config wrapper.');
115
+ }
116
+ const deployment = resolveBrandlabDeployment(options.environment ?? process.env);
117
+ const headerRules = buildBrandlabHeaderRules({
118
+ deployment,
119
+ ...(options.csp ? { contentSecurityPolicyExtensions: options.csp } : {}),
120
+ ...(options.hsts ? { hsts: options.hsts } : {}),
121
+ ...(options.permissionsPolicy ? { permissionsPolicy: options.permissionsPolicy } : {}),
122
+ ...(options.extraHeaders ? { extraHeaders: options.extraHeaders } : {}),
123
+ });
124
+ const redirects = buildBrandlabRedirects({ site: options.site, redirects: options.redirects });
125
+ const config = {
126
+ poweredByHeader: false,
127
+ skipTrailingSlashRedirect: true,
128
+ env: { NEXT_PUBLIC_VERCEL_ENV: deployment.environment },
129
+ outputFileTracingIncludes: Object.fromEntries(Object.entries(BRANDLAB_CONTACT_OUTPUT_FILE_TRACING).map(([route, globs]) => [route, [...globs]])),
130
+ images: buildImagesConfig(options.images),
131
+ ...(options.mdx ? { pageExtensions: [...MDX_PAGE_EXTENSIONS] } : {}),
132
+ headers: async () => headerRules.map((rule) => ({ source: rule.source, headers: rule.headers.map((header) => ({ ...header })) })),
133
+ redirects: async () => redirects.map((redirect) => ({
134
+ ...redirect,
135
+ ...(redirect.has ? { has: redirect.has.map((condition) => ({ ...condition })) } : {}),
136
+ })),
137
+ };
138
+ return options.mdx ? options.mdx(config) : config;
139
+ }
140
+ //# sourceMappingURL=next-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"next-config.js","sourceRoot":"","sources":["../src/next-config.ts"],"names":[],"mappings":"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","sourcesContent":["import { defineRedirects, type BrandlabRedirectTable } from '@brandlab-digital/site-core/redirects'\nimport {\n resolveBrandlabDeployment,\n type BrandlabDeploymentEnvironment,\n type BrandlabEnvironment,\n type BrandlabSite,\n} from '@brandlab-digital/site-core/site'\nimport {\n buildBrandlabHeaderRules,\n type BrandlabContentSecurityPolicyExtensions,\n type BrandlabHeader,\n type BrandlabHeaderRule,\n type BrandlabHstsExpansion,\n} from './headers.js'\n\n/** Next redirect records: 308 stays `permanent: true`, 301 stays `statusCode: 301`. */\nexport type BrandlabNextRedirect = {\n source: string\n destination: string\n has?: { type: 'host'; value: string }[]\n} & ({ permanent: true; statusCode?: never } | { statusCode: 301; permanent?: never })\n\nexport interface BrandlabNextRemotePattern {\n protocol?: 'http' | 'https'\n hostname: string\n port?: string\n pathname?: string\n search?: string\n}\n\n/** The site's own image settings; the output format is always WebP. */\nexport interface BrandlabNextImagesOptions {\n deviceSizes?: readonly number[]\n imageSizes?: readonly number[]\n qualities?: readonly number[]\n minimumCacheTTL?: number\n remotePatterns?: readonly BrandlabNextRemotePattern[]\n}\n\nexport interface BrandlabNextImagesConfig {\n formats: 'image/webp'[]\n deviceSizes?: number[]\n imageSizes?: number[]\n qualities?: number[]\n minimumCacheTTL?: number\n remotePatterns?: BrandlabNextRemotePattern[]\n}\n\nexport interface BrandlabNextConfig {\n poweredByHeader: false\n /** Trailing slashes are handled by single-hop redirect rules instead. */\n skipTrailingSlashRedirect: true\n env: { NEXT_PUBLIC_VERCEL_ENV: BrandlabDeploymentEnvironment }\n outputFileTracingIncludes: Record<string, string[]>\n images: BrandlabNextImagesConfig\n pageExtensions?: string[]\n headers: () => Promise<BrandlabHeaderRule[]>\n redirects: () => Promise<BrandlabNextRedirect[]>\n}\n\nexport interface BrandlabNextConfigOptions {\n site: BrandlabSite\n /** The src/data/redirects.ts table (defineRedirects). */\n redirects: BrandlabRedirectTable\n images: BrandlabNextImagesOptions\n /** Reviewed CSP origins for declared integrations such as Meta Pixel or map embeds. */\n csp?: BrandlabContentSecurityPolicyExtensions\n hsts?: BrandlabHstsExpansion\n /** Reviewed exception to the default Permissions-Policy that is already live. */\n permissionsPolicy?: string\n /** Reviewed response headers that are already live; protected headers cannot be replaced. */\n extraHeaders?: readonly BrandlabHeader[]\n /** Defaults to process.env; tests supply a fixed environment. */\n environment?: BrandlabEnvironment\n}\n\n/** Sharp's Linux binaries must travel with the contact route's image sanitiser. */\nexport const BRANDLAB_CONTACT_OUTPUT_FILE_TRACING = Object.freeze({\n '/api/contact': Object.freeze([\n 'node_modules/sharp/**/*',\n 'node_modules/@img/sharp-linux-x64/**/*',\n 'node_modules/@img/sharp-libvips-linux-x64/**/*',\n ]),\n})\n\nconst MDX_PAGE_EXTENSIONS = ['tsx', 'ts', 'jsx', 'js', 'mdx', 'md']\nconst OPTION_KEYS = new Set([\n 'site',\n 'redirects',\n 'images',\n 'csp',\n 'hsts',\n 'permissionsPolicy',\n 'extraHeaders',\n 'mdx',\n 'environment',\n])\nconst IMAGE_KEYS = new Set(['deviceSizes', 'imageSizes', 'qualities', 'minimumCacheTTL', 'remotePatterns'])\n\nfunction assertKnownKeys(value: object, allowed: ReadonlySet<string>, field: string, hint = '') {\n const unsupported = Object.keys(value).filter((key) => !allowed.has(key))\n if (unsupported.length > 0) {\n throw new Error(`${field} contains unsupported keys: ${unsupported.join(', ')}.${hint}`)\n }\n}\n\nfunction positiveIntegers(values: readonly number[] | undefined, field: string) {\n if (values === undefined) return undefined\n if (\n !Array.isArray(values) ||\n values.length === 0 ||\n values.some((value) => !Number.isSafeInteger(value) || value < 1)\n ) {\n throw new Error(`images.${field} must list positive integers.`)\n }\n return [...values]\n}\n\nfunction buildImagesConfig(images: BrandlabNextImagesOptions): BrandlabNextImagesConfig {\n if (!images || typeof images !== 'object' || Array.isArray(images)) {\n throw new Error('images must declare the site image settings ({} for Next defaults).')\n }\n assertKnownKeys(images, IMAGE_KEYS, 'images', ' The output format is always image/webp.')\n const deviceSizes = positiveIntegers(images.deviceSizes, 'deviceSizes')\n const imageSizes = positiveIntegers(images.imageSizes, 'imageSizes')\n const qualities = positiveIntegers(images.qualities, 'qualities')\n if (qualities?.some((quality) => quality > 100)) {\n throw new Error('images.qualities must be between 1 and 100.')\n }\n if (\n images.minimumCacheTTL !== undefined &&\n (!Number.isSafeInteger(images.minimumCacheTTL) || images.minimumCacheTTL < 0)\n ) {\n throw new Error('images.minimumCacheTTL must be a non-negative integer of seconds.')\n }\n if (images.remotePatterns !== undefined && !Array.isArray(images.remotePatterns)) {\n throw new Error('images.remotePatterns must be an array.')\n }\n return {\n formats: ['image/webp'],\n ...(deviceSizes ? { deviceSizes } : {}),\n ...(imageSizes ? { imageSizes } : {}),\n ...(qualities ? { qualities } : {}),\n ...(images.minimumCacheTTL !== undefined ? { minimumCacheTTL: images.minimumCacheTTL } : {}),\n ...(images.remotePatterns\n ? { remotePatterns: images.remotePatterns.map((pattern) => ({ ...pattern })) }\n : {}),\n }\n}\n\n/**\n * Converts the redirect table to Next redirects: each entry keeps its 301 or\n * 308 status (Next forwards the query string), gains a single-hop\n * trailing-slash variant, then one trailing-slash rule and one permanent\n * canonical-host rule per alias host follow. There is no x-forwarded-proto\n * rule: Vercel's edge already enforces HTTPS.\n */\nexport function buildBrandlabRedirects(options: {\n site: BrandlabSite\n redirects: BrandlabRedirectTable\n}): BrandlabNextRedirect[] {\n const table = defineRedirects(options.redirects)\n const entries = table.flatMap((redirect): BrandlabNextRedirect[] => {\n const status = redirect.status === 301\n ? { statusCode: 301 as const }\n : { permanent: true as const }\n const has = redirect.host ? { has: [{ type: 'host' as const, value: redirect.host }] } : {}\n const record = (source: string): BrandlabNextRedirect => ({\n source,\n destination: redirect.destination,\n ...status,\n ...has,\n })\n return redirect.source === '/'\n ? [record(redirect.source)]\n : [record(redirect.source), record(`${redirect.source}/`)]\n })\n return [\n ...entries,\n { source: '/:path+/', destination: '/:path+', permanent: true },\n ...options.site.aliasHosts.map((host): BrandlabNextRedirect => ({\n source: '/:path*',\n has: [{ type: 'host', value: host }],\n destination: `${options.site.canonicalUrl}/:path*`,\n permanent: true,\n })),\n ]\n}\n\n/**\n * The whole next.config.ts after its first line\n * `import '@brandlab-digital/site-next/image-runtime'`:\n * `export default defineBrandlabNextConfig({ site, redirects, images })`.\n *\n * Sets poweredByHeader: false, WebP image output, Sharp output tracing for\n * /api/contact, NEXT_PUBLIC_VERCEL_ENV for browser gates, the deployment's\n * header rules and the redirect table. Validation runs when the config loads,\n * so an invalid table or header exception fails the build.\n */\nexport function defineBrandlabNextConfig(\n options: BrandlabNextConfigOptions & { mdx?: undefined },\n): BrandlabNextConfig\nexport function defineBrandlabNextConfig<Wrapped>(\n options: BrandlabNextConfigOptions & {\n /** The @next/mdx wrapper: createMDX({ options: { remarkPlugins: ['remark-gfm'] } }). */\n mdx: (config: BrandlabNextConfig) => Wrapped\n },\n): Wrapped\nexport function defineBrandlabNextConfig(\n options: BrandlabNextConfigOptions & { mdx?: (config: BrandlabNextConfig) => unknown },\n): unknown {\n if (!options || typeof options !== 'object') {\n throw new TypeError('defineBrandlabNextConfig requires the site, redirects and images.')\n }\n assertKnownKeys(options, OPTION_KEYS, 'next.config options')\n if (!options.site?.canonicalUrl || !Array.isArray(options.site.aliasHosts)) {\n throw new Error('site must come from defineBrandlabSite.')\n }\n if (options.mdx !== undefined && typeof options.mdx !== 'function') {\n throw new TypeError('mdx must be the @next/mdx config wrapper.')\n }\n const deployment = resolveBrandlabDeployment(options.environment ?? process.env)\n const headerRules = buildBrandlabHeaderRules({\n deployment,\n ...(options.csp ? { contentSecurityPolicyExtensions: options.csp } : {}),\n ...(options.hsts ? { hsts: options.hsts } : {}),\n ...(options.permissionsPolicy ? { permissionsPolicy: options.permissionsPolicy } : {}),\n ...(options.extraHeaders ? { extraHeaders: options.extraHeaders } : {}),\n })\n const redirects = buildBrandlabRedirects({ site: options.site, redirects: options.redirects })\n\n const config: BrandlabNextConfig = {\n poweredByHeader: false,\n skipTrailingSlashRedirect: true,\n env: { NEXT_PUBLIC_VERCEL_ENV: deployment.environment },\n outputFileTracingIncludes: Object.fromEntries(\n Object.entries(BRANDLAB_CONTACT_OUTPUT_FILE_TRACING).map(([route, globs]) => [route, [...globs]]),\n ),\n images: buildImagesConfig(options.images),\n ...(options.mdx ? { pageExtensions: [...MDX_PAGE_EXTENSIONS] } : {}),\n headers: async () =>\n headerRules.map((rule) => ({ source: rule.source, headers: rule.headers.map((header) => ({ ...header })) })),\n redirects: async () =>\n redirects.map((redirect) => ({\n ...redirect,\n ...(redirect.has ? { has: redirect.has.map((condition) => ({ ...condition })) } : {}),\n })),\n }\n return options.mdx ? options.mdx(config) : config\n}\n"]}
package/dist/robots.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type BrandlabDeployment, type BrandlabEnvironment, type BrandlabSite } from '@brandlab-digital/site-core/site';
1
2
  export interface BrandlabRobotsMetadata {
2
3
  rules: Array<{
3
4
  userAgent: string;
@@ -6,10 +7,17 @@ export interface BrandlabRobotsMetadata {
6
7
  }>;
7
8
  sitemap?: string;
8
9
  }
9
- export declare function buildRobotsMetadata(options: {
10
- siteUrl: string;
11
- nodeEnv?: string;
12
- vercelEnv?: string;
13
- allowIndexing?: string;
14
- }): BrandlabRobotsMetadata;
10
+ /**
11
+ * Crawlers may index any production build except a Vercel Preview: the real
12
+ * production deployment and a local `next start`. `next dev` and every
13
+ * Preview are closed. There is no environment override.
14
+ */
15
+ export declare function isBrandlabIndexableDeployment(deployment: BrandlabDeployment): boolean;
16
+ /**
17
+ * `src/app/robots.ts`: `export default createBrandlabRobots(site)`.
18
+ * The deployment is resolved when Next renders robots.txt.
19
+ */
20
+ export declare function createBrandlabRobots(site: BrandlabSite, options?: {
21
+ environment?: BrandlabEnvironment;
22
+ }): () => BrandlabRobotsMetadata;
15
23
  //# sourceMappingURL=robots.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"robots.d.ts","sourceRoot":"","sources":["../src/robots.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,KAAK,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACtE,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE;IAC3C,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB,GAAG,sBAAsB,CAoBzB"}
1
+ {"version":3,"file":"robots.d.ts","sourceRoot":"","sources":["../src/robots.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,EACxB,KAAK,YAAY,EAClB,MAAM,kCAAkC,CAAA;AAEzC,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,KAAK,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACtE,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED;;;;GAIG;AACH,wBAAgB,6BAA6B,CAAC,UAAU,EAAE,kBAAkB,WAG3E;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,YAAY,EAClB,OAAO,GAAE;IAAE,WAAW,CAAC,EAAE,mBAAmB,CAAA;CAAO,SAGzB,sBAAsB,CAajD"}