@brandlab-digital/site-next 2.1.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +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-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 +21 -28
  53. package/dist/headers.d.ts.map +1 -1
  54. package/dist/headers.js +62 -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 +102 -0
  73. package/dist/next-config.d.ts.map +1 -0
  74. package/dist/next-config.js +140 -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
@@ -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"}
package/dist/robots.js CHANGED
@@ -1,22 +1,31 @@
1
- import { getAbsoluteUrl } from '@brandlab-digital/site-core/metadata';
2
- import { shouldIndexSite } from '@brandlab-digital/site-core/sitemaps';
3
- export function buildRobotsMetadata(options) {
4
- const shouldIndex = shouldIndexSite({
5
- nodeEnv: options.nodeEnv,
6
- vercelEnv: options.vercelEnv,
7
- allowIndexing: options.allowIndexing,
8
- });
9
- if (!shouldIndex) {
1
+ import { resolveBrandlabDeployment, } from '@brandlab-digital/site-core/site';
2
+ /**
3
+ * Crawlers may index any production build except a Vercel Preview: the real
4
+ * production deployment and a local `next start`. `next dev` and every
5
+ * Preview are closed. There is no environment override.
6
+ */
7
+ export function isBrandlabIndexableDeployment(deployment) {
8
+ return deployment.environment === 'production' ||
9
+ (deployment.environment === 'development' && deployment.productionBuild);
10
+ }
11
+ /**
12
+ * `src/app/robots.ts`: `export default createBrandlabRobots(site)`.
13
+ * The deployment is resolved when Next renders robots.txt.
14
+ */
15
+ export function createBrandlabRobots(site, options = {}) {
16
+ const sitemap = `${site.canonicalUrl}/sitemap.xml`;
17
+ return function robots() {
18
+ const deployment = resolveBrandlabDeployment(options.environment ?? process.env);
19
+ if (!isBrandlabIndexableDeployment(deployment)) {
20
+ return { rules: [{ userAgent: '*', disallow: '/' }] };
21
+ }
22
+ // No `host` directive: it is a non-standard robots.txt extension ignored
23
+ // by Google and Bing; canonical-host consolidation is owned by permanent
24
+ // redirects and canonical URLs.
10
25
  return {
11
- rules: [{ userAgent: '*', disallow: '/' }],
26
+ rules: [{ userAgent: '*', allow: '/' }],
27
+ sitemap,
12
28
  };
13
- }
14
- // No `host` directive: it is a non-standard robots.txt extension ignored
15
- // by Google and Bing; canonical-host consolidation is owned by permanent
16
- // edge redirects + canonical URLs (Brandlab Project Standard §5).
17
- return {
18
- rules: [{ userAgent: '*', allow: '/' }],
19
- sitemap: getAbsoluteUrl(options.siteUrl, '/sitemap.xml'),
20
29
  };
21
30
  }
22
31
  //# sourceMappingURL=robots.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"robots.js","sourceRoot":"","sources":["../src/robots.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,sCAAsC,CAAA;AACrE,OAAO,EAAE,eAAe,EAAE,MAAM,sCAAsC,CAAA;AAOtE,MAAM,UAAU,mBAAmB,CAAC,OAKnC;IACC,MAAM,WAAW,GAAG,eAAe,CAAC;QAClC,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,aAAa,EAAE,OAAO,CAAC,aAAa;KACrC,CAAC,CAAA;IAEF,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,OAAO;YACL,KAAK,EAAE,CAAC,EAAE,SAAS,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC;SAC3C,CAAA;IACH,CAAC;IAED,yEAAyE;IACzE,yEAAyE;IACzE,kEAAkE;IAClE,OAAO;QACL,KAAK,EAAE,CAAC,EAAE,SAAS,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC;QACvC,OAAO,EAAE,cAAc,CAAC,OAAO,CAAC,OAAO,EAAE,cAAc,CAAC;KACzD,CAAA;AACH,CAAC","sourcesContent":["import { getAbsoluteUrl } from '@brandlab-digital/site-core/metadata'\nimport { shouldIndexSite } from '@brandlab-digital/site-core/sitemaps'\n\nexport interface BrandlabRobotsMetadata {\n rules: Array<{ userAgent: string; allow?: string; disallow?: string }>\n sitemap?: string\n}\n\nexport function buildRobotsMetadata(options: {\n siteUrl: string\n nodeEnv?: string\n vercelEnv?: string\n allowIndexing?: string\n}): BrandlabRobotsMetadata {\n const shouldIndex = shouldIndexSite({\n nodeEnv: options.nodeEnv,\n vercelEnv: options.vercelEnv,\n allowIndexing: options.allowIndexing,\n })\n\n if (!shouldIndex) {\n return {\n rules: [{ userAgent: '*', disallow: '/' }],\n }\n }\n\n // No `host` directive: it is a non-standard robots.txt extension ignored\n // by Google and Bing; canonical-host consolidation is owned by permanent\n // edge redirects + canonical URLs (Brandlab Project Standard §5).\n return {\n rules: [{ userAgent: '*', allow: '/' }],\n sitemap: getAbsoluteUrl(options.siteUrl, '/sitemap.xml'),\n }\n}\n"]}
1
+ {"version":3,"file":"robots.js","sourceRoot":"","sources":["../src/robots.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,yBAAyB,GAI1B,MAAM,kCAAkC,CAAA;AAOzC;;;;GAIG;AACH,MAAM,UAAU,6BAA6B,CAAC,UAA8B;IAC1E,OAAO,UAAU,CAAC,WAAW,KAAK,YAAY;QAC5C,CAAC,UAAU,CAAC,WAAW,KAAK,aAAa,IAAI,UAAU,CAAC,eAAe,CAAC,CAAA;AAC5E,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAClC,IAAkB,EAClB,UAAiD,EAAE;IAEnD,MAAM,OAAO,GAAG,GAAG,IAAI,CAAC,YAAY,cAAc,CAAA;IAClD,OAAO,SAAS,MAAM;QACpB,MAAM,UAAU,GAAG,yBAAyB,CAAC,OAAO,CAAC,WAAW,IAAI,OAAO,CAAC,GAAG,CAAC,CAAA;QAChF,IAAI,CAAC,6BAA6B,CAAC,UAAU,CAAC,EAAE,CAAC;YAC/C,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,SAAS,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,EAAE,CAAA;QACvD,CAAC;QACD,yEAAyE;QACzE,yEAAyE;QACzE,gCAAgC;QAChC,OAAO;YACL,KAAK,EAAE,CAAC,EAAE,SAAS,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC;YACvC,OAAO;SACR,CAAA;IACH,CAAC,CAAA;AACH,CAAC","sourcesContent":["import {\n resolveBrandlabDeployment,\n type BrandlabDeployment,\n type BrandlabEnvironment,\n type BrandlabSite,\n} from '@brandlab-digital/site-core/site'\n\nexport interface BrandlabRobotsMetadata {\n rules: Array<{ userAgent: string; allow?: string; disallow?: string }>\n sitemap?: string\n}\n\n/**\n * Crawlers may index any production build except a Vercel Preview: the real\n * production deployment and a local `next start`. `next dev` and every\n * Preview are closed. There is no environment override.\n */\nexport function isBrandlabIndexableDeployment(deployment: BrandlabDeployment) {\n return deployment.environment === 'production' ||\n (deployment.environment === 'development' && deployment.productionBuild)\n}\n\n/**\n * `src/app/robots.ts`: `export default createBrandlabRobots(site)`.\n * The deployment is resolved when Next renders robots.txt.\n */\nexport function createBrandlabRobots(\n site: BrandlabSite,\n options: { environment?: BrandlabEnvironment } = {},\n) {\n const sitemap = `${site.canonicalUrl}/sitemap.xml`\n return function robots(): BrandlabRobotsMetadata {\n const deployment = resolveBrandlabDeployment(options.environment ?? process.env)\n if (!isBrandlabIndexableDeployment(deployment)) {\n return { rules: [{ userAgent: '*', disallow: '/' }] }\n }\n // No `host` directive: it is a non-standard robots.txt extension ignored\n // by Google and Bing; canonical-host consolidation is owned by permanent\n // redirects and canonical URLs.\n return {\n rules: [{ userAgent: '*', allow: '/' }],\n sitemap,\n }\n }\n}\n"]}
@@ -1,6 +1,40 @@
1
+ import type { BrandlabRouteCatalog } from '@brandlab-digital/site-core/routes';
2
+ import type { BrandlabSite } from '@brandlab-digital/site-core/site';
1
3
  import { type SitemapEntry } from '@brandlab-digital/site-core/sitemaps';
2
4
  export declare function sitemapXmlResponse(entries: readonly SitemapEntry[]): Response;
3
5
  export declare function sitemapIndexXmlResponse(paths: readonly string[], options: {
4
6
  absoluteUrl: (path: string) => string;
5
7
  }): Response;
8
+ export type BrandlabSitemapMode = 'single' | 'split';
9
+ export interface BrandlabSitemapRouteOptions {
10
+ /** `split` only when brandlab.config declares `sitemap.mode: 'split'`. */
11
+ mode: BrandlabSitemapMode;
12
+ /** Split mode: URLs per child sitemap (default 1000). */
13
+ maxUrlsPerSitemap?: number;
14
+ }
15
+ export interface BrandlabSitemapChunkRouteContext {
16
+ params: Promise<{
17
+ chunk: string;
18
+ }>;
19
+ }
20
+ export interface BrandlabSitemapRoute {
21
+ readonly mode: BrandlabSitemapMode;
22
+ /** `src/app/sitemap.xml/route.ts`: the URL set, or the sitemap index in split mode. */
23
+ GET(): Response;
24
+ /** Child sitemap paths listed by the index; empty in single mode. */
25
+ chunkPaths(): string[];
26
+ /** `src/app/sitemaps/[chunk]/route.ts` (split mode, with `dynamicParams = false`). */
27
+ chunk: {
28
+ generateStaticParams(): {
29
+ chunk: string;
30
+ }[];
31
+ GET(request: Request, context: BrandlabSitemapChunkRouteContext): Promise<Response>;
32
+ };
33
+ }
34
+ /**
35
+ * The route-catalogue sitemap. `lastmod` comes only from each route's
36
+ * `lastSignificantChange`, never from build time. Entries are resolved once,
37
+ * so an invalid catalogue fails the build.
38
+ */
39
+ export declare function createBrandlabSitemapRoute(routes: BrandlabRouteCatalog, site: BrandlabSite, options?: BrandlabSitemapRouteOptions): BrandlabSitemapRoute;
6
40
  //# sourceMappingURL=sitemaps.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sitemaps.d.ts","sourceRoot":"","sources":["../src/sitemaps.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,YAAY,EAClB,MAAM,sCAAsC,CAAA;AAE7C,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,GAAG,QAAQ,CAI7E;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,OAAO,EAAE;IAAE,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;CAAE,GACjD,QAAQ,CAIV"}
1
+ {"version":3,"file":"sitemaps.d.ts","sourceRoot":"","sources":["../src/sitemaps.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAqB,MAAM,oCAAoC,CAAA;AACjG,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AACpE,OAAO,EAOL,KAAK,YAAY,EAClB,MAAM,sCAAsC,CAAA;AAE7C,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,GAAG,QAAQ,CAI7E;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,OAAO,EAAE;IAAE,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;CAAE,GACjD,QAAQ,CAIV;AAED,MAAM,MAAM,mBAAmB,GAAG,QAAQ,GAAG,OAAO,CAAA;AAEpD,MAAM,WAAW,2BAA2B;IAC1C,0EAA0E;IAC1E,IAAI,EAAE,mBAAmB,CAAA;IACzB,yDAAyD;IACzD,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED,MAAM,WAAW,gCAAgC;IAC/C,MAAM,EAAE,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACnC;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAA;IAClC,uFAAuF;IACvF,GAAG,IAAI,QAAQ,CAAA;IACf,qEAAqE;IACrE,UAAU,IAAI,MAAM,EAAE,CAAA;IACtB,sFAAsF;IACtF,KAAK,EAAE;QACL,oBAAoB,IAAI;YAAE,KAAK,EAAE,MAAM,CAAA;SAAE,EAAE,CAAA;QAC3C,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,gCAAgC,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;KACpF,CAAA;CACF;AAiCD;;;;GAIG;AACH,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,oBAAoB,EAC5B,IAAI,EAAE,YAAY,EAClB,OAAO,GAAE,2BAAgD,GACxD,oBAAoB,CAsCtB"}
package/dist/sitemaps.js CHANGED
@@ -1,4 +1,5 @@
1
- import { getSitemapXmlHeaders, serializeSitemap, serializeSitemapIndex, } from '@brandlab-digital/site-core/sitemaps';
1
+ import { getAbsoluteUrl } from '@brandlab-digital/site-core/metadata';
2
+ import { buildSitemapEntries, createSitemapChunks, getSitemapXmlHeaders, serializeSitemap, serializeSitemapIndex, } from '@brandlab-digital/site-core/sitemaps';
2
3
  export function sitemapXmlResponse(entries) {
3
4
  return new Response(serializeSitemap(entries), {
4
5
  headers: getSitemapXmlHeaders(),
@@ -9,4 +10,68 @@ export function sitemapIndexXmlResponse(paths, options) {
9
10
  headers: getSitemapXmlHeaders(),
10
11
  });
11
12
  }
13
+ const SPLIT_SITEMAP_FAMILIES = [
14
+ { name: 'services', kinds: ['service'] },
15
+ { name: 'locations', kinds: ['location'] },
16
+ { name: 'service-locations', kinds: ['service-location'] },
17
+ { name: 'blog', kinds: ['blog-post'] },
18
+ { name: 'projects', kinds: ['project'] },
19
+ ];
20
+ const SPLIT_SITEMAP_PREFIX = '/sitemaps';
21
+ const DEFAULT_SPLIT_MAX_URLS = 1_000;
22
+ function splitSitemapChunks(routes, siteUrl, maxUrlsPerSitemap) {
23
+ const familyKinds = new Set(SPLIT_SITEMAP_FAMILIES.flatMap((family) => family.kinds));
24
+ const families = [
25
+ { name: 'core', routes: routes.filter((route) => !familyKinds.has(route.kind)) },
26
+ ...SPLIT_SITEMAP_FAMILIES.map((family) => ({
27
+ name: family.name,
28
+ routes: routes.filter((route) => family.kinds.includes(route.kind)),
29
+ })),
30
+ ];
31
+ return families.flatMap((family) => createSitemapChunks(buildSitemapEntries(family.routes, { siteUrl }), {
32
+ pathPrefix: `${SPLIT_SITEMAP_PREFIX}/${family.name}`,
33
+ maxUrlsPerSitemap,
34
+ }));
35
+ }
36
+ /**
37
+ * The route-catalogue sitemap. `lastmod` comes only from each route's
38
+ * `lastSignificantChange`, never from build time. Entries are resolved once,
39
+ * so an invalid catalogue fails the build.
40
+ */
41
+ export function createBrandlabSitemapRoute(routes, site, options = { mode: 'single' }) {
42
+ if (options.mode !== 'single' && options.mode !== 'split') {
43
+ throw new Error('Sitemap mode must be "single" or "split".');
44
+ }
45
+ if (options.mode === 'single' && options.maxUrlsPerSitemap !== undefined) {
46
+ throw new Error('maxUrlsPerSitemap applies only to split sitemaps.');
47
+ }
48
+ const entries = buildSitemapEntries(routes, { siteUrl: site.canonicalUrl });
49
+ const chunks = options.mode === 'split'
50
+ ? splitSitemapChunks(routes, site.canonicalUrl, options.maxUrlsPerSitemap ?? DEFAULT_SPLIT_MAX_URLS)
51
+ : [];
52
+ const chunkByFileName = new Map(chunks.map((chunk) => [chunk.path.slice(SPLIT_SITEMAP_PREFIX.length + 1), chunk]));
53
+ const absoluteUrl = (path) => getAbsoluteUrl(site.canonicalUrl, path);
54
+ return Object.freeze({
55
+ mode: options.mode,
56
+ GET() {
57
+ return options.mode === 'split'
58
+ ? sitemapIndexXmlResponse(chunks.map((chunk) => chunk.path), { absoluteUrl })
59
+ : sitemapXmlResponse(entries);
60
+ },
61
+ chunkPaths() {
62
+ return chunks.map((chunk) => chunk.path);
63
+ },
64
+ chunk: Object.freeze({
65
+ generateStaticParams() {
66
+ return [...chunkByFileName.keys()].map((chunk) => ({ chunk }));
67
+ },
68
+ async GET(_request, context) {
69
+ const found = chunkByFileName.get((await context.params).chunk);
70
+ return found
71
+ ? sitemapXmlResponse(found.entries)
72
+ : new Response('Not found', { status: 404 });
73
+ },
74
+ }),
75
+ });
76
+ }
12
77
  //# sourceMappingURL=sitemaps.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sitemaps.js","sourceRoot":"","sources":["../src/sitemaps.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,qBAAqB,GAEtB,MAAM,sCAAsC,CAAA;AAE7C,MAAM,UAAU,kBAAkB,CAAC,OAAgC;IACjE,OAAO,IAAI,QAAQ,CAAC,gBAAgB,CAAC,OAAO,CAAC,EAAE;QAC7C,OAAO,EAAE,oBAAoB,EAAE;KAChC,CAAC,CAAA;AACJ,CAAC;AAED,MAAM,UAAU,uBAAuB,CACrC,KAAwB,EACxB,OAAkD;IAElD,OAAO,IAAI,QAAQ,CAAC,qBAAqB,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE;QACzD,OAAO,EAAE,oBAAoB,EAAE;KAChC,CAAC,CAAA;AACJ,CAAC","sourcesContent":["import {\n getSitemapXmlHeaders,\n serializeSitemap,\n serializeSitemapIndex,\n type SitemapEntry,\n} from '@brandlab-digital/site-core/sitemaps'\n\nexport function sitemapXmlResponse(entries: readonly SitemapEntry[]): Response {\n return new Response(serializeSitemap(entries), {\n headers: getSitemapXmlHeaders(),\n })\n}\n\nexport function sitemapIndexXmlResponse(\n paths: readonly string[],\n options: { absoluteUrl: (path: string) => string },\n): Response {\n return new Response(serializeSitemapIndex(paths, options), {\n headers: getSitemapXmlHeaders(),\n })\n}\n"]}
1
+ {"version":3,"file":"sitemaps.js","sourceRoot":"","sources":["../src/sitemaps.ts"],"names":[],"mappings":"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","sourcesContent":["import { getAbsoluteUrl } from '@brandlab-digital/site-core/metadata'\nimport type { BrandlabRouteCatalog, BrandlabRouteKind } from '@brandlab-digital/site-core/routes'\nimport type { BrandlabSite } from '@brandlab-digital/site-core/site'\nimport {\n buildSitemapEntries,\n createSitemapChunks,\n getSitemapXmlHeaders,\n serializeSitemap,\n serializeSitemapIndex,\n type SitemapChunk,\n type SitemapEntry,\n} from '@brandlab-digital/site-core/sitemaps'\n\nexport function sitemapXmlResponse(entries: readonly SitemapEntry[]): Response {\n return new Response(serializeSitemap(entries), {\n headers: getSitemapXmlHeaders(),\n })\n}\n\nexport function sitemapIndexXmlResponse(\n paths: readonly string[],\n options: { absoluteUrl: (path: string) => string },\n): Response {\n return new Response(serializeSitemapIndex(paths, options), {\n headers: getSitemapXmlHeaders(),\n })\n}\n\nexport type BrandlabSitemapMode = 'single' | 'split'\n\nexport interface BrandlabSitemapRouteOptions {\n /** `split` only when brandlab.config declares `sitemap.mode: 'split'`. */\n mode: BrandlabSitemapMode\n /** Split mode: URLs per child sitemap (default 1000). */\n maxUrlsPerSitemap?: number\n}\n\nexport interface BrandlabSitemapChunkRouteContext {\n params: Promise<{ chunk: string }>\n}\n\nexport interface BrandlabSitemapRoute {\n readonly mode: BrandlabSitemapMode\n /** `src/app/sitemap.xml/route.ts`: the URL set, or the sitemap index in split mode. */\n GET(): Response\n /** Child sitemap paths listed by the index; empty in single mode. */\n chunkPaths(): string[]\n /** `src/app/sitemaps/[chunk]/route.ts` (split mode, with `dynamicParams = false`). */\n chunk: {\n generateStaticParams(): { chunk: string }[]\n GET(request: Request, context: BrandlabSitemapChunkRouteContext): Promise<Response>\n }\n}\n\nconst SPLIT_SITEMAP_FAMILIES: readonly { name: string; kinds: readonly BrandlabRouteKind[] }[] = [\n { name: 'services', kinds: ['service'] },\n { name: 'locations', kinds: ['location'] },\n { name: 'service-locations', kinds: ['service-location'] },\n { name: 'blog', kinds: ['blog-post'] },\n { name: 'projects', kinds: ['project'] },\n]\nconst SPLIT_SITEMAP_PREFIX = '/sitemaps'\nconst DEFAULT_SPLIT_MAX_URLS = 1_000\n\nfunction splitSitemapChunks(\n routes: BrandlabRouteCatalog,\n siteUrl: string,\n maxUrlsPerSitemap: number,\n): SitemapChunk[] {\n const familyKinds = new Set(SPLIT_SITEMAP_FAMILIES.flatMap((family) => family.kinds))\n const families = [\n { name: 'core', routes: routes.filter((route) => !familyKinds.has(route.kind)) },\n ...SPLIT_SITEMAP_FAMILIES.map((family) => ({\n name: family.name,\n routes: routes.filter((route) => family.kinds.includes(route.kind)),\n })),\n ]\n return families.flatMap((family) =>\n createSitemapChunks(buildSitemapEntries(family.routes, { siteUrl }), {\n pathPrefix: `${SPLIT_SITEMAP_PREFIX}/${family.name}`,\n maxUrlsPerSitemap,\n }),\n )\n}\n\n/**\n * The route-catalogue sitemap. `lastmod` comes only from each route's\n * `lastSignificantChange`, never from build time. Entries are resolved once,\n * so an invalid catalogue fails the build.\n */\nexport function createBrandlabSitemapRoute(\n routes: BrandlabRouteCatalog,\n site: BrandlabSite,\n options: BrandlabSitemapRouteOptions = { mode: 'single' },\n): BrandlabSitemapRoute {\n if (options.mode !== 'single' && options.mode !== 'split') {\n throw new Error('Sitemap mode must be \"single\" or \"split\".')\n }\n if (options.mode === 'single' && options.maxUrlsPerSitemap !== undefined) {\n throw new Error('maxUrlsPerSitemap applies only to split sitemaps.')\n }\n const entries = buildSitemapEntries(routes, { siteUrl: site.canonicalUrl })\n const chunks = options.mode === 'split'\n ? splitSitemapChunks(routes, site.canonicalUrl, options.maxUrlsPerSitemap ?? DEFAULT_SPLIT_MAX_URLS)\n : []\n const chunkByFileName = new Map(\n chunks.map((chunk) => [chunk.path.slice(SPLIT_SITEMAP_PREFIX.length + 1), chunk]),\n )\n const absoluteUrl = (path: string) => getAbsoluteUrl(site.canonicalUrl, path)\n\n return Object.freeze({\n mode: options.mode,\n GET() {\n return options.mode === 'split'\n ? sitemapIndexXmlResponse(chunks.map((chunk) => chunk.path), { absoluteUrl })\n : sitemapXmlResponse(entries)\n },\n chunkPaths() {\n return chunks.map((chunk) => chunk.path)\n },\n chunk: Object.freeze({\n generateStaticParams() {\n return [...chunkByFileName.keys()].map((chunk) => ({ chunk }))\n },\n async GET(_request: Request, context: BrandlabSitemapChunkRouteContext) {\n const found = chunkByFileName.get((await context.params).chunk)\n return found\n ? sitemapXmlResponse(found.entries)\n : new Response('Not found', { status: 404 })\n },\n }),\n })\n}\n"]}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Empty stand-in for the `server-only` guard in Vitest, where server modules
3
+ * run outside Next's react-server condition. Alias it in vitest.config.ts:
4
+ * `resolve: { alias: { 'server-only': '@brandlab-digital/site-next/testing/server-only' } }`.
5
+ * Never import it from application code.
6
+ */
7
+ export {};
8
+ //# sourceMappingURL=server-only.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-only.d.ts","sourceRoot":"","sources":["../../src/testing/server-only.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,CAAA"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Empty stand-in for the `server-only` guard in Vitest, where server modules
3
+ * run outside Next's react-server condition. Alias it in vitest.config.ts:
4
+ * `resolve: { alias: { 'server-only': '@brandlab-digital/site-next/testing/server-only' } }`.
5
+ * Never import it from application code.
6
+ */
7
+ export {};
8
+ //# sourceMappingURL=server-only.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-only.js","sourceRoot":"","sources":["../../src/testing/server-only.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,CAAA","sourcesContent":["/**\n * Empty stand-in for the `server-only` guard in Vitest, where server modules\n * run outside Next's react-server condition. Alias it in vitest.config.ts:\n * `resolve: { alias: { 'server-only': '@brandlab-digital/site-next/testing/server-only' } }`.\n * Never import it from application code.\n */\nexport {}\n"]}
@@ -1,5 +1,4 @@
1
- import { type AnchorHTMLAttributes, type DialogHTMLAttributes, type ReactNode, type RefObject } from 'react';
2
- import { type PhoneCallPlacement } from './analytics.js';
1
+ import { type DialogHTMLAttributes, type ReactNode, type RefObject } from 'react';
3
2
  export interface HeadlessDialogProps extends Omit<DialogHTMLAttributes<HTMLDialogElement>, 'open' | 'onClose'> {
4
3
  open: boolean;
5
4
  onClose: () => void;
@@ -10,16 +9,6 @@ export interface HeadlessDialogProps extends Omit<DialogHTMLAttributes<HTMLDialo
10
9
  children: ReactNode;
11
10
  }
12
11
  export declare function HeadlessDialog({ open, onClose, labelledBy, describedBy, initialFocusRef, returnFocusRef, children, ...props }: HeadlessDialogProps): import("react/jsx-runtime").JSX.Element;
13
- export interface TrackedTelephoneLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href' | 'onClick'> {
14
- phoneE164: string;
15
- placement: PhoneCallPlacement;
16
- onTrack: (input: {
17
- event: 'phone_link_click';
18
- placement: PhoneCallPlacement;
19
- }) => void;
20
- children: ReactNode;
21
- }
22
- export declare function TrackedTelephoneLink({ phoneE164, placement, onTrack, children, ...props }: TrackedTelephoneLinkProps): import("react/jsx-runtime").JSX.Element;
23
12
  export interface GalleryLightboxItem {
24
13
  id: string;
25
14
  alt: string;
@@ -1 +1 @@
1
- {"version":3,"file":"ui-client.d.ts","sourceRoot":"","sources":["../src/ui-client.tsx"],"names":[],"mappings":"AAEA,OAAO,EAIL,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EAEzB,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,OAAO,CAAA;AACd,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,gBAAgB,CAAA;AAEvB,MAAM,WAAW,mBACf,SAAQ,IAAI,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzE,IAAI,EAAE,OAAO,CAAA;IACb,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,eAAe,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAA;IAC/C,cAAc,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAA;IAC9C,QAAQ,EAAE,SAAS,CAAA;CACpB;AAED,wBAAgB,cAAc,CAAC,EAC7B,IAAI,EACJ,OAAO,EACP,UAAU,EACV,WAAW,EACX,eAAe,EACf,cAAc,EACd,QAAQ,EACR,GAAG,KAAK,EACT,EAAE,mBAAmB,2CA2CrB;AAED,MAAM,WAAW,yBACf,SAAQ,IAAI,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzE,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,kBAAkB,CAAA;IAC7B,OAAO,EAAE,CAAC,KAAK,EAAE;QAAE,KAAK,EAAE,kBAAkB,CAAC;QAAC,SAAS,EAAE,kBAAkB,CAAA;KAAE,KAAK,IAAI,CAAA;IACtF,QAAQ,EAAE,SAAS,CAAA;CACpB;AAED,wBAAgB,oBAAoB,CAAC,EACnC,SAAS,EACT,SAAS,EACT,OAAO,EACP,QAAQ,EACR,GAAG,KAAK,EACT,EAAE,yBAAyB,2CAsB3B;AAED,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAA;IACV,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,oBAAoB,CAAC,IAAI,SAAS,mBAAmB;IACpE,IAAI,EAAE,OAAO,CAAA;IACb,KAAK,EAAE,SAAS,IAAI,EAAE,CAAA;IACtB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,SAAS,CAAA;IAChB,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,UAAU,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,SAAS,CAAA;IACrC,aAAa,EAAE,MAAM,CAAA;IACrB,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,eAAe,CAAC,IAAI,SAAS,mBAAmB,EAAE,EAChE,IAAI,EACJ,KAAK,EACL,YAAgB,EAChB,OAAO,EACP,KAAK,EACL,OAAO,EACP,UAAU,EACV,aAAa,EACb,SAAS,EACT,UAAU,GACX,EAAE,oBAAoB,CAAC,IAAI,CAAC,2CAgD5B;AAED,wBAAgB,uBAAuB,YAUtC;AAED,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,OAAO,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,0EAA0E;AAC1E,wBAAgB,kBAAkB,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,uBAAuB,QAuBxF"}
1
+ {"version":3,"file":"ui-client.d.ts","sourceRoot":"","sources":["../src/ui-client.tsx"],"names":[],"mappings":"AAEA,OAAO,EAIL,KAAK,oBAAoB,EAEzB,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,OAAO,CAAA;AAEd,MAAM,WAAW,mBACf,SAAQ,IAAI,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzE,IAAI,EAAE,OAAO,CAAA;IACb,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,eAAe,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAA;IAC/C,cAAc,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAA;IAC9C,QAAQ,EAAE,SAAS,CAAA;CACpB;AAED,wBAAgB,cAAc,CAAC,EAC7B,IAAI,EACJ,OAAO,EACP,UAAU,EACV,WAAW,EACX,eAAe,EACf,cAAc,EACd,QAAQ,EACR,GAAG,KAAK,EACT,EAAE,mBAAmB,2CA2CrB;AAED,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAA;IACV,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,oBAAoB,CAAC,IAAI,SAAS,mBAAmB;IACpE,IAAI,EAAE,OAAO,CAAA;IACb,KAAK,EAAE,SAAS,IAAI,EAAE,CAAA;IACtB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,SAAS,CAAA;IAChB,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,UAAU,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,SAAS,CAAA;IACrC,aAAa,EAAE,MAAM,CAAA;IACrB,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,eAAe,CAAC,IAAI,SAAS,mBAAmB,EAAE,EAChE,IAAI,EACJ,KAAK,EACL,YAAgB,EAChB,OAAO,EACP,KAAK,EACL,OAAO,EACP,UAAU,EACV,aAAa,EACb,SAAS,EACT,UAAU,GACX,EAAE,oBAAoB,CAAC,IAAI,CAAC,2CAgD5B;AAED,wBAAgB,uBAAuB,YAUtC;AAED,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,OAAO,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,0EAA0E;AAC1E,wBAAgB,kBAAkB,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,uBAAuB,QAuBxF"}
package/dist/ui-client.js CHANGED
@@ -1,7 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useRef, useState, } from 'react';
4
- import { PHONE_CALL_PLACEMENTS, } from './analytics.js';
5
4
  export function HeadlessDialog({ open, onClose, labelledBy, describedBy, initialFocusRef, returnFocusRef, children, ...props }) {
6
5
  const dialogRef = useRef(null);
7
6
  const previouslyFocused = useRef(null);
@@ -37,22 +36,6 @@ export function HeadlessDialog({ open, onClose, labelledBy, describedBy, initial
37
36
  onClose();
38
37
  }, onClose: onClose, ref: dialogRef, children: children }));
39
38
  }
40
- export function TrackedTelephoneLink({ phoneE164, placement, onTrack, children, ...props }) {
41
- if (!/^\+[1-9]\d{7,14}$/.test(phoneE164)) {
42
- throw new Error('TrackedTelephoneLink requires an E.164 phone number.');
43
- }
44
- if (!PHONE_CALL_PLACEMENTS.includes(placement)) {
45
- throw new Error('TrackedTelephoneLink requires a canonical phone-call placement.');
46
- }
47
- return (_jsx("a", { ...props, href: `tel:${phoneE164}`, onClick: () => {
48
- try {
49
- onTrack({ event: 'phone_link_click', placement });
50
- }
51
- catch {
52
- // Tracking must never block the telephone action.
53
- }
54
- }, children: children }));
55
- }
56
39
  export function GalleryLightbox({ open, items, initialIndex = 0, titleId, title, onClose, renderItem, previousLabel, nextLabel, closeLabel, }) {
57
40
  const [index, setIndex] = useState(initialIndex);
58
41
  const closeRef = useRef(null);
@@ -1 +1 @@
1
- {"version":3,"file":"ui-client.js","sourceRoot":"","sources":["../src/ui-client.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,OAAO,EACL,SAAS,EACT,MAAM,EACN,QAAQ,GAMT,MAAM,OAAO,CAAA;AACd,OAAO,EACL,qBAAqB,GAEtB,MAAM,gBAAgB,CAAA;AAavB,MAAM,UAAU,cAAc,CAAC,EAC7B,IAAI,EACJ,OAAO,EACP,UAAU,EACV,WAAW,EACX,eAAe,EACf,cAAc,EACd,QAAQ,EACR,GAAG,KAAK,EACY;IACpB,MAAM,SAAS,GAAG,MAAM,CAAoB,IAAI,CAAC,CAAA;IACjD,MAAM,iBAAiB,GAAG,MAAM,CAAqB,IAAI,CAAC,CAAA;IAE1D,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,CAAA;QAChC,IAAI,CAAC,MAAM;YAAE,OAAM;QACnB,IAAI,IAAI,EAAE,CAAC;YACT,iBAAiB,CAAC,OAAO,GAAG,QAAQ,CAAC,aAAa,YAAY,WAAW;gBACvE,CAAC,CAAC,QAAQ,CAAC,aAAa;gBACxB,CAAC,CAAC,IAAI,CAAA;YACR,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;gBACjB,IAAI,OAAO,MAAM,CAAC,SAAS,KAAK,UAAU;oBAAE,MAAM,CAAC,SAAS,EAAE,CAAA;;oBACzD,MAAM,CAAC,YAAY,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;YACtC,CAAC;YACD,MAAM,WAAW,GAAG,eAAe,EAAE,OAAO,IAAI,MAAM,CAAA;YACtD,cAAc,CAAC,GAAG,EAAE,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC,CAAA;YACzC,OAAM;QACR,CAAC;QACD,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;YAChB,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,UAAU;gBAAE,MAAM,CAAC,KAAK,EAAE,CAAA;;gBACjD,MAAM,CAAC,eAAe,CAAC,MAAM,CAAC,CAAA;QACrC,CAAC;QACD,MAAM,YAAY,GAAG,cAAc,EAAE,OAAO,IAAI,iBAAiB,CAAC,OAAO,CAAA;QACzE,cAAc,CAAC,GAAG,EAAE,CAAC,YAAY,EAAE,KAAK,EAAE,CAAC,CAAA;IAC7C,CAAC,EAAE,CAAC,eAAe,EAAE,IAAI,EAAE,cAAc,CAAC,CAAC,CAAA;IAE3C,OAAO,CACL,oBACM,KAAK,sBACS,WAAW,qBACZ,UAAU,gBAChB,MAAM,EACjB,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE;YAClB,KAAK,CAAC,cAAc,EAAE,CAAA;YACtB,OAAO,EAAE,CAAA;QACX,CAAC,EACD,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,SAAS,YAEb,QAAQ,GACF,CACV,CAAA;AACH,CAAC;AAUD,MAAM,UAAU,oBAAoB,CAAC,EACnC,SAAS,EACT,SAAS,EACT,OAAO,EACP,QAAQ,EACR,GAAG,KAAK,EACkB;IAC1B,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;QACzC,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAA;IACzE,CAAC;IACD,IAAI,CAAC,qBAAqB,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;QAC/C,MAAM,IAAI,KAAK,CAAC,iEAAiE,CAAC,CAAA;IACpF,CAAC;IACD,OAAO,CACL,eACM,KAAK,EACT,IAAI,EAAE,OAAO,SAAS,EAAE,EACxB,OAAO,EAAE,GAAG,EAAE;YACZ,IAAI,CAAC;gBACH,OAAO,CAAC,EAAE,KAAK,EAAE,kBAAkB,EAAE,SAAS,EAAE,CAAC,CAAA;YACnD,CAAC;YAAC,MAAM,CAAC;gBACP,kDAAkD;YACpD,CAAC;QACH,CAAC,YAEA,QAAQ,GACP,CACL,CAAA;AACH,CAAC;AAoBD,MAAM,UAAU,eAAe,CAAmC,EAChE,IAAI,EACJ,KAAK,EACL,YAAY,GAAG,CAAC,EAChB,OAAO,EACP,KAAK,EACL,OAAO,EACP,UAAU,EACV,aAAa,EACb,SAAS,EACT,UAAU,GACiB;IAC3B,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,YAAY,CAAC,CAAA;IAChD,MAAM,QAAQ,GAAG,MAAM,CAAoB,IAAI,CAAC,CAAA;IAEhD,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,IAAI;YAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;IACxF,CAAC,EAAE,CAAC,YAAY,EAAE,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAA;IAEtC,MAAM,IAAI,GAAG,CAAC,MAAc,EAAE,EAAE;QAC9B,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;YAAE,OAAM;QAC5B,QAAQ,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,GAAG,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,CAAA;IACzE,CAAC,CAAA;IACD,MAAM,SAAS,GAAG,CAAC,KAAuC,EAAE,EAAE;QAC5D,IAAI,KAAK,CAAC,GAAG,KAAK,WAAW,EAAE,CAAC;YAC9B,KAAK,CAAC,cAAc,EAAE,CAAA;YACtB,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;QACV,CAAC;aAAM,IAAI,KAAK,CAAC,GAAG,KAAK,YAAY,EAAE,CAAC;YACtC,KAAK,CAAC,cAAc,EAAE,CAAA;YACtB,IAAI,CAAC,CAAC,CAAC,CAAA;QACT,CAAC;IACH,CAAC,CAAA;IACD,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,CAAA;IAEzB,OAAO,CACL,MAAC,cAAc,IACb,eAAe,EAAE,QAAQ,EACzB,UAAU,EAAE,OAAO,EACnB,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,SAAS,EACpB,IAAI,EAAE,IAAI,aAEV,aAAI,EAAE,EAAE,OAAO,YAAG,KAAK,GAAM,EAC7B,+BAAoB,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAC,QAAQ,YAC3E,UAAU,GACJ,EACR,IAAI,CAAC,CAAC,CAAC,6BAAuB,UAAU,CAAC,IAAI,CAAC,EAAC,+BAAa,IAAI,CAAC,GAAG,GAAc,KAA9D,IAAI,CAAC,EAAE,CAAgE,CAAC,CAAC,CAAC,IAAI,EAClG,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAClB,kCAAsB,OAAO,aAC3B,+BAAoB,aAAa,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAC,QAAQ,YACtE,aAAa,GACP,EACT,+BAAoB,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAC,QAAQ,YACjE,SAAS,GACH,IACL,CACP,CAAC,CAAC,CAAC,IAAI,IACO,CAClB,CAAA;AACH,CAAC;AAED,MAAM,UAAU,uBAAuB;IACrC,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IAC7C,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,kCAAkC,CAAC,CAAA;QACnE,MAAM,MAAM,GAAG,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;QAC9C,MAAM,EAAE,CAAA;QACR,KAAK,CAAC,gBAAgB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAA;QACxC,OAAO,GAAG,EAAE,CAAC,KAAK,CAAC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAA;IAC1D,CAAC,EAAE,EAAE,CAAC,CAAA;IACN,OAAO,OAAO,CAAA;AAChB,CAAC;AAQD,0EAA0E;AAC1E,MAAM,UAAU,kBAAkB,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAA2B;IACvF,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,MAAM;YAAE,OAAM;QACnB,IAAI,SAAS,GAAG,KAAK,CAAA;QACrB,cAAc,CAAC,GAAG,EAAE;YAClB,IAAI,SAAS;gBAAE,OAAM;YACrB,MAAM,IAAI,GAAG,QAAQ,CAAC,cAAc,CAAC,MAAM,CAAC,CAAA;YAC5C,IAAI,CAAC,CAAC,IAAI,YAAY,eAAe,CAAC;gBAAE,OAAM;YAC9C,MAAM,OAAO,GAAG,QAAQ,CAAC,cAAc,CAAC,SAAS,CAAC,CAAA;YAClD,MAAM,MAAM,GAAG,OAAO,YAAY,WAAW,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;gBACrE,CAAC,CAAC,OAAO;gBACT,CAAC,CAAC,IAAI,CAAC,aAAa,CAAc;oBAC9B,4CAA4C;oBAC5C,6CAA6C;oBAC7C,+CAA+C;oBAC/C,6CAA6C;oBAC7C,6DAA6D;iBAC9D,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;YAChB,MAAM,EAAE,KAAK,EAAE,CAAA;QACjB,CAAC,CAAC,CAAA;QACF,OAAO,GAAG,EAAE,GAAG,SAAS,GAAG,IAAI,CAAA,CAAC,CAAC,CAAA;IACnC,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,CAAA;IAC/B,OAAO,IAAI,CAAA;AACb,CAAC","sourcesContent":["'use client'\n\nimport {\n useEffect,\n useRef,\n useState,\n type AnchorHTMLAttributes,\n type DialogHTMLAttributes,\n type KeyboardEvent,\n type ReactNode,\n type RefObject,\n} from 'react'\nimport {\n PHONE_CALL_PLACEMENTS,\n type PhoneCallPlacement,\n} from './analytics.js'\n\nexport interface HeadlessDialogProps\n extends Omit<DialogHTMLAttributes<HTMLDialogElement>, 'open' | 'onClose'> {\n open: boolean\n onClose: () => void\n labelledBy: string\n describedBy?: string\n initialFocusRef?: RefObject<HTMLElement | null>\n returnFocusRef?: RefObject<HTMLElement | null>\n children: ReactNode\n}\n\nexport function HeadlessDialog({\n open,\n onClose,\n labelledBy,\n describedBy,\n initialFocusRef,\n returnFocusRef,\n children,\n ...props\n}: HeadlessDialogProps) {\n const dialogRef = useRef<HTMLDialogElement>(null)\n const previouslyFocused = useRef<HTMLElement | null>(null)\n\n useEffect(() => {\n const dialog = dialogRef.current\n if (!dialog) return\n if (open) {\n previouslyFocused.current = document.activeElement instanceof HTMLElement\n ? document.activeElement\n : null\n if (!dialog.open) {\n if (typeof dialog.showModal === 'function') dialog.showModal()\n else dialog.setAttribute('open', '')\n }\n const focusTarget = initialFocusRef?.current ?? dialog\n queueMicrotask(() => focusTarget.focus())\n return\n }\n if (dialog.open) {\n if (typeof dialog.close === 'function') dialog.close()\n else dialog.removeAttribute('open')\n }\n const returnTarget = returnFocusRef?.current ?? previouslyFocused.current\n queueMicrotask(() => returnTarget?.focus())\n }, [initialFocusRef, open, returnFocusRef])\n\n return (\n <dialog\n {...props}\n aria-describedby={describedBy}\n aria-labelledby={labelledBy}\n aria-modal=\"true\"\n onCancel={(event) => {\n event.preventDefault()\n onClose()\n }}\n onClose={onClose}\n ref={dialogRef}\n >\n {children}\n </dialog>\n )\n}\n\nexport interface TrackedTelephoneLinkProps\n extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href' | 'onClick'> {\n phoneE164: string\n placement: PhoneCallPlacement\n onTrack: (input: { event: 'phone_link_click'; placement: PhoneCallPlacement }) => void\n children: ReactNode\n}\n\nexport function TrackedTelephoneLink({\n phoneE164,\n placement,\n onTrack,\n children,\n ...props\n}: TrackedTelephoneLinkProps) {\n if (!/^\\+[1-9]\\d{7,14}$/.test(phoneE164)) {\n throw new Error('TrackedTelephoneLink requires an E.164 phone number.')\n }\n if (!PHONE_CALL_PLACEMENTS.includes(placement)) {\n throw new Error('TrackedTelephoneLink requires a canonical phone-call placement.')\n }\n return (\n <a\n {...props}\n href={`tel:${phoneE164}`}\n onClick={() => {\n try {\n onTrack({ event: 'phone_link_click', placement })\n } catch {\n // Tracking must never block the telephone action.\n }\n }}\n >\n {children}\n </a>\n )\n}\n\nexport interface GalleryLightboxItem {\n id: string\n alt: string\n}\n\nexport interface GalleryLightboxProps<Item extends GalleryLightboxItem> {\n open: boolean\n items: readonly Item[]\n initialIndex?: number\n titleId: string\n title: ReactNode\n onClose: () => void\n renderItem: (item: Item) => ReactNode\n previousLabel: string\n nextLabel: string\n closeLabel: string\n}\n\nexport function GalleryLightbox<Item extends GalleryLightboxItem>({\n open,\n items,\n initialIndex = 0,\n titleId,\n title,\n onClose,\n renderItem,\n previousLabel,\n nextLabel,\n closeLabel,\n}: GalleryLightboxProps<Item>) {\n const [index, setIndex] = useState(initialIndex)\n const closeRef = useRef<HTMLButtonElement>(null)\n\n useEffect(() => {\n if (open) setIndex(Math.min(Math.max(initialIndex, 0), Math.max(items.length - 1, 0)))\n }, [initialIndex, items.length, open])\n\n const move = (offset: number) => {\n if (items.length < 2) return\n setIndex((current) => (current + offset + items.length) % items.length)\n }\n const onKeyDown = (event: KeyboardEvent<HTMLDialogElement>) => {\n if (event.key === 'ArrowLeft') {\n event.preventDefault()\n move(-1)\n } else if (event.key === 'ArrowRight') {\n event.preventDefault()\n move(1)\n }\n }\n const item = items[index]\n\n return (\n <HeadlessDialog\n initialFocusRef={closeRef}\n labelledBy={titleId}\n onClose={onClose}\n onKeyDown={onKeyDown}\n open={open}\n >\n <h2 id={titleId}>{title}</h2>\n <button aria-label={closeLabel} onClick={onClose} ref={closeRef} type=\"button\">\n {closeLabel}\n </button>\n {item ? <figure key={item.id}>{renderItem(item)}<figcaption>{item.alt}</figcaption></figure> : null}\n {items.length > 1 ? (\n <nav aria-labelledby={titleId}>\n <button aria-label={previousLabel} onClick={() => move(-1)} type=\"button\">\n {previousLabel}\n </button>\n <button aria-label={nextLabel} onClick={() => move(1)} type=\"button\">\n {nextLabel}\n </button>\n </nav>\n ) : null}\n </HeadlessDialog>\n )\n}\n\nexport function usePrefersReducedMotion() {\n const [reduced, setReduced] = useState(false)\n useEffect(() => {\n const query = window.matchMedia('(prefers-reduced-motion: reduce)')\n const update = () => setReduced(query.matches)\n update()\n query.addEventListener('change', update)\n return () => query.removeEventListener('change', update)\n }, [])\n return reduced\n}\n\nexport interface HeadlessErrorFocusProps {\n active: boolean\n formId: string\n summaryId: string\n}\n\n/** Focuses the error summary, falling back to the first invalid field. */\nexport function HeadlessErrorFocus({ active, formId, summaryId }: HeadlessErrorFocusProps) {\n useEffect(() => {\n if (!active) return\n let cancelled = false\n queueMicrotask(() => {\n if (cancelled) return\n const form = document.getElementById(formId)\n if (!(form instanceof HTMLFormElement)) return\n const summary = document.getElementById(summaryId)\n const target = summary instanceof HTMLElement && form.contains(summary)\n ? summary\n : form.querySelector<HTMLElement>([\n 'input[aria-invalid=\"true\"]:not([disabled])',\n 'select[aria-invalid=\"true\"]:not([disabled])',\n 'textarea[aria-invalid=\"true\"]:not([disabled])',\n 'button[aria-invalid=\"true\"]:not([disabled])',\n '[aria-invalid=\"true\"][tabindex]:not([aria-disabled=\"true\"])',\n ].join(','))\n target?.focus()\n })\n return () => { cancelled = true }\n }, [active, formId, summaryId])\n return null\n}\n"]}
1
+ {"version":3,"file":"ui-client.js","sourceRoot":"","sources":["../src/ui-client.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client'\n\nimport {\n useEffect,\n useRef,\n useState,\n type DialogHTMLAttributes,\n type KeyboardEvent,\n type ReactNode,\n type RefObject,\n} from 'react'\n\nexport interface HeadlessDialogProps\n extends Omit<DialogHTMLAttributes<HTMLDialogElement>, 'open' | 'onClose'> {\n open: boolean\n onClose: () => void\n labelledBy: string\n describedBy?: string\n initialFocusRef?: RefObject<HTMLElement | null>\n returnFocusRef?: RefObject<HTMLElement | null>\n children: ReactNode\n}\n\nexport function HeadlessDialog({\n open,\n onClose,\n labelledBy,\n describedBy,\n initialFocusRef,\n returnFocusRef,\n children,\n ...props\n}: HeadlessDialogProps) {\n const dialogRef = useRef<HTMLDialogElement>(null)\n const previouslyFocused = useRef<HTMLElement | null>(null)\n\n useEffect(() => {\n const dialog = dialogRef.current\n if (!dialog) return\n if (open) {\n previouslyFocused.current = document.activeElement instanceof HTMLElement\n ? document.activeElement\n : null\n if (!dialog.open) {\n if (typeof dialog.showModal === 'function') dialog.showModal()\n else dialog.setAttribute('open', '')\n }\n const focusTarget = initialFocusRef?.current ?? dialog\n queueMicrotask(() => focusTarget.focus())\n return\n }\n if (dialog.open) {\n if (typeof dialog.close === 'function') dialog.close()\n else dialog.removeAttribute('open')\n }\n const returnTarget = returnFocusRef?.current ?? previouslyFocused.current\n queueMicrotask(() => returnTarget?.focus())\n }, [initialFocusRef, open, returnFocusRef])\n\n return (\n <dialog\n {...props}\n aria-describedby={describedBy}\n aria-labelledby={labelledBy}\n aria-modal=\"true\"\n onCancel={(event) => {\n event.preventDefault()\n onClose()\n }}\n onClose={onClose}\n ref={dialogRef}\n >\n {children}\n </dialog>\n )\n}\n\nexport interface GalleryLightboxItem {\n id: string\n alt: string\n}\n\nexport interface GalleryLightboxProps<Item extends GalleryLightboxItem> {\n open: boolean\n items: readonly Item[]\n initialIndex?: number\n titleId: string\n title: ReactNode\n onClose: () => void\n renderItem: (item: Item) => ReactNode\n previousLabel: string\n nextLabel: string\n closeLabel: string\n}\n\nexport function GalleryLightbox<Item extends GalleryLightboxItem>({\n open,\n items,\n initialIndex = 0,\n titleId,\n title,\n onClose,\n renderItem,\n previousLabel,\n nextLabel,\n closeLabel,\n}: GalleryLightboxProps<Item>) {\n const [index, setIndex] = useState(initialIndex)\n const closeRef = useRef<HTMLButtonElement>(null)\n\n useEffect(() => {\n if (open) setIndex(Math.min(Math.max(initialIndex, 0), Math.max(items.length - 1, 0)))\n }, [initialIndex, items.length, open])\n\n const move = (offset: number) => {\n if (items.length < 2) return\n setIndex((current) => (current + offset + items.length) % items.length)\n }\n const onKeyDown = (event: KeyboardEvent<HTMLDialogElement>) => {\n if (event.key === 'ArrowLeft') {\n event.preventDefault()\n move(-1)\n } else if (event.key === 'ArrowRight') {\n event.preventDefault()\n move(1)\n }\n }\n const item = items[index]\n\n return (\n <HeadlessDialog\n initialFocusRef={closeRef}\n labelledBy={titleId}\n onClose={onClose}\n onKeyDown={onKeyDown}\n open={open}\n >\n <h2 id={titleId}>{title}</h2>\n <button aria-label={closeLabel} onClick={onClose} ref={closeRef} type=\"button\">\n {closeLabel}\n </button>\n {item ? <figure key={item.id}>{renderItem(item)}<figcaption>{item.alt}</figcaption></figure> : null}\n {items.length > 1 ? (\n <nav aria-labelledby={titleId}>\n <button aria-label={previousLabel} onClick={() => move(-1)} type=\"button\">\n {previousLabel}\n </button>\n <button aria-label={nextLabel} onClick={() => move(1)} type=\"button\">\n {nextLabel}\n </button>\n </nav>\n ) : null}\n </HeadlessDialog>\n )\n}\n\nexport function usePrefersReducedMotion() {\n const [reduced, setReduced] = useState(false)\n useEffect(() => {\n const query = window.matchMedia('(prefers-reduced-motion: reduce)')\n const update = () => setReduced(query.matches)\n update()\n query.addEventListener('change', update)\n return () => query.removeEventListener('change', update)\n }, [])\n return reduced\n}\n\nexport interface HeadlessErrorFocusProps {\n active: boolean\n formId: string\n summaryId: string\n}\n\n/** Focuses the error summary, falling back to the first invalid field. */\nexport function HeadlessErrorFocus({ active, formId, summaryId }: HeadlessErrorFocusProps) {\n useEffect(() => {\n if (!active) return\n let cancelled = false\n queueMicrotask(() => {\n if (cancelled) return\n const form = document.getElementById(formId)\n if (!(form instanceof HTMLFormElement)) return\n const summary = document.getElementById(summaryId)\n const target = summary instanceof HTMLElement && form.contains(summary)\n ? summary\n : form.querySelector<HTMLElement>([\n 'input[aria-invalid=\"true\"]:not([disabled])',\n 'select[aria-invalid=\"true\"]:not([disabled])',\n 'textarea[aria-invalid=\"true\"]:not([disabled])',\n 'button[aria-invalid=\"true\"]:not([disabled])',\n '[aria-invalid=\"true\"][tabindex]:not([aria-disabled=\"true\"])',\n ].join(','))\n target?.focus()\n })\n return () => { cancelled = true }\n }, [active, formId, summaryId])\n return null\n}\n"]}
package/dist/ui.d.ts CHANGED
@@ -74,8 +74,28 @@ export type SafeRichTextNode = {
74
74
  } | {
75
75
  type: 'line-break';
76
76
  };
77
- export interface SafeRichTextProps extends HTMLAttributes<HTMLDivElement> {
77
+ export type SafeRichTextElement = 'div' | 'p' | 'span';
78
+ export interface SafeRichTextProps extends HTMLAttributes<HTMLElement> {
78
79
  nodes: readonly SafeRichTextNode[];
80
+ /**
81
+ * The wrapper element. `div` (default) holds block content: paragraphs and
82
+ * lists. `p` and `span` hold inline content only (text, emphasis, links and
83
+ * line breaks), so the output has no block element and can sit where only
84
+ * phrasing content is allowed, such as inside a button; a block node under
85
+ * them throws.
86
+ */
87
+ as?: SafeRichTextElement;
88
+ /** Class for every link, rendered after `href`. */
89
+ linkClassName?: string;
79
90
  }
80
- export declare function SafeRichText({ nodes, ...props }: SafeRichTextProps): import("react/jsx-runtime").JSX.Element;
91
+ export declare function SafeRichText({ nodes, as: Element, linkClassName, ...props }: SafeRichTextProps): import("react/jsx-runtime").JSX.Element;
92
+ /**
93
+ * Inline rich-text markup as nodes, for content written as short HTML
94
+ * strings: `<strong>` and `<b>` become strong text, `<a href>` becomes a link
95
+ * when its href passes sanitizeRichTextHref (otherwise its text stays as plain
96
+ * text), every other tag is dropped and text is kept exactly as written
97
+ * (React escapes it). Nothing is ever rendered as HTML. Strong text holds text
98
+ * only; markup nested inside it throws.
99
+ */
100
+ export declare function parseInlineRichText(markup: string): SafeRichTextNode[];
81
101
  //# sourceMappingURL=ui.d.ts.map