@gradial/aci 0.1.29 → 0.1.31

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.
Binary file
package/bin/aci.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from 'node:child_process';
3
- import { existsSync } from 'node:fs';
3
+ import { chmodSync, existsSync } from 'node:fs';
4
4
  import { cp, mkdir, readdir, readFile, rename, stat, writeFile } from 'node:fs/promises';
5
5
  import { createRequire } from 'node:module';
6
6
  import { basename, dirname, join, relative, resolve } from 'node:path';
@@ -62,6 +62,9 @@ function resolveWorkspaceBinary() {
62
62
  }
63
63
 
64
64
  const binaryPath = resolveBundledBinary() ?? resolvePackagedBinary() ?? resolveWorkspaceBinary();
65
+ if (binaryPath) {
66
+ try { chmodSync(binaryPath, 0o755); } catch {}
67
+ }
65
68
  const fallback = binaryPath ? null : resolveGoFallback();
66
69
 
67
70
  if (!binaryPath && !fallback) {
@@ -1,3 +1,3 @@
1
- export { type Image, type ImageFallback, type ImageSource, type ImageAttrs, ImageSchema, imageAttrs, primaryImageSource, } from '../types/image.js';
1
+ export { type Image, type ImageSource, type ImageAttrs, ImageSchema, imageAttrs, primaryImageSource, } from '../types/image.js';
2
2
  export { type Video, type VideoSource, VideoSchema, VideoSourceSchema, } from '../types/video.js';
3
3
  export { type Asset, AssetSchema, isImage, isVideo, isAsset, } from '../types/media.js';
@@ -5,6 +5,13 @@ export interface RouteEntry {
5
5
  payloadRef: string;
6
6
  digest: string;
7
7
  fragmentRefs?: string[];
8
+ variants?: Record<string, RouteVariantEntry>;
9
+ }
10
+ export interface RouteVariantEntry {
11
+ variantKey: string;
12
+ payloadRef: string;
13
+ digest?: string;
14
+ variantValues?: Record<string, string>;
8
15
  }
9
16
  export interface FragmentEntry {
10
17
  id: string;
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@ export type { Registry, RegistryEntry, AnyComponent } from './registry.js';
5
5
  export { blockRef, blockRefArray } from './types/block-ref.js';
6
6
  export type { BlockRef, BlockRefEntry, BlockRefMeta, BlockRefUnion } from './types/block-ref.js';
7
7
  export type { ComponentContract, ComponentContractID, ComponentContractValidator, ComponentRegistry, ComponentValidationContext, ComponentValidationIssue, InferComponentProps, } from './types/component.js';
8
- export { ImageSchema, imageAttrs, primaryImageSource, type Image, type ImageFallback, type ImageSource, type ImageAttrs, type ImageSlotContract, type SlotOutput, } from './types/image.js';
8
+ export { ImageSchema, imageAttrs, primaryImageSource, type Image, type ImageSource, type ImageAttrs, type ImageSlotContract, type SlotOutput, } from './types/image.js';
9
9
  export { LinkSchema, type Link, type LinkTarget, type LinkResolverContext, resolveLink, resolveHref, assertSafeHref, normalizeLinkRel, } from './types/link.js';
10
10
  export { HeadingSchema, InlineContentSchema, RichTextValueSchema, type Heading, type InlineContent, type RichTextValue, } from './types/rich-text.js';
11
11
  export type { Config, RouteConfig, ExternalDependency, DAMConfig, } from './types/config.js';
@@ -10,6 +10,9 @@ export async function getPage(route = '', config = {}) {
10
10
  if (runtimeInput)
11
11
  return runtimeInput.page;
12
12
  const provider = await resolveProviderWithFallback(config);
13
+ const variantPage = await pageFromFunctionRef(provider);
14
+ if (variantPage !== null)
15
+ return variantPage;
13
16
  return await provider.getPage(route);
14
17
  }
15
18
  export async function getSiteConfig(config = {}) {
@@ -53,7 +56,7 @@ export async function getRenderInput(route = '/', config = {}) {
53
56
  // Cache shared resources (manifest, siteConfig) but not per-page content
54
57
  // Page-level caching is handled by CDN
55
58
  const [page, siteConfig] = await Promise.all([
56
- provider.getPage(route),
59
+ pageFromFunctionRef(provider).then((variantPage) => variantPage ?? provider.getPage(route)),
57
60
  getCachedSiteConfig(provider),
58
61
  ]);
59
62
  return {
@@ -73,6 +76,28 @@ export async function getPageRuntimeRenderInput() {
73
76
  return null;
74
77
  }
75
78
  }
79
+ // The personalization edge proxies framework-origin requests with
80
+ // X-Baremetal-Function-Ref naming the exact page payload its decision
81
+ // selected (base or variant). When present, the SSR render must use that
82
+ // payload — otherwise every visitor gets the base page and edge decisioning
83
+ // is cosmetic. The S3 provider only serves refs the release manifest
84
+ // declares.
85
+ const BAREMETAL_FUNCTION_REF_HEADER = 'x-baremetal-function-ref';
86
+ async function pageFromFunctionRef(provider) {
87
+ if (!(provider instanceof S3ContentProvider))
88
+ return null;
89
+ let ref = '';
90
+ try {
91
+ const current = await headers();
92
+ ref = (current.get(BAREMETAL_FUNCTION_REF_HEADER) || '').trim();
93
+ }
94
+ catch {
95
+ return null;
96
+ }
97
+ if (!ref)
98
+ return null;
99
+ return await provider.getPageByRef(ref);
100
+ }
76
101
  export { isPreviewMode } from './preview-mode.js';
77
102
  export async function routeFromNextParams(params) {
78
103
  if (!params)
@@ -16,6 +16,13 @@ export declare class S3ContentProvider implements ContentProvider {
16
16
  listRoutes(): Promise<RouteEntry[]>;
17
17
  getSiteConfig<T = unknown>(): Promise<T>;
18
18
  getPage<T = unknown>(route: string): Promise<T>;
19
+ /**
20
+ * getPageByRef serves the exact payload the edge selected (the
21
+ * X-Baremetal-Function-Ref contract): base page or variant. Only refs the
22
+ * release manifest declares are served — the header names published
23
+ * content, it cannot address arbitrary keys.
24
+ */
25
+ getPageByRef<T = unknown>(payloadRef: string): Promise<T>;
19
26
  getFragment<T = unknown>(id: string): Promise<T>;
20
27
  getLayout<T = unknown>(name: string): Promise<T>;
21
28
  manifest(): Promise<CompiledManifest>;
@@ -32,6 +32,26 @@ export class S3ContentProvider {
32
32
  throw new PageNotFoundError(route, error);
33
33
  }
34
34
  }
35
+ /**
36
+ * getPageByRef serves the exact payload the edge selected (the
37
+ * X-Baremetal-Function-Ref contract): base page or variant. Only refs the
38
+ * release manifest declares are served — the header names published
39
+ * content, it cannot address arbitrary keys.
40
+ */
41
+ async getPageByRef(payloadRef) {
42
+ const manifest = await this.manifest();
43
+ const known = Object.values(manifest.routes).some((entry) => entry.payloadRef === payloadRef ||
44
+ Object.values(entry.variants ?? {}).some((variant) => variant.payloadRef === payloadRef));
45
+ if (!known) {
46
+ throw new PageNotFoundError(payloadRef);
47
+ }
48
+ try {
49
+ return await this.getJSON(payloadRef);
50
+ }
51
+ catch (error) {
52
+ throw new PageNotFoundError(payloadRef, error);
53
+ }
54
+ }
35
55
  async getFragment(id) {
36
56
  const manifest = await this.manifest();
37
57
  const entry = manifest.fragments[id];
@@ -12,10 +12,9 @@ export function Image({ image, value, ...attrs }) {
12
12
  assetId: '',
13
13
  versionId: '',
14
14
  alt: '',
15
- fallback: { src: '', width: 0, height: 0 },
16
15
  sources: [],
17
16
  };
18
- if (!resolved.fallback.src) {
17
+ if (!resolved.sources[0]?.src) {
19
18
  return createElement('img', {
20
19
  src: '',
21
20
  alt: resolved.alt,
@@ -1,17 +1,13 @@
1
1
  import { z } from 'zod';
2
- export interface ImageFallback {
3
- /** Derivative URL for the base <img>. */
2
+ export interface ImageSource {
3
+ /** Primary derivative URL for this rendition candidate. */
4
4
  src: string;
5
5
  /** Intrinsic width in pixels (0 when the format carries none, e.g. SVG). */
6
- width: number;
6
+ width?: number;
7
7
  /** Intrinsic height in pixels (0 when the format carries none). */
8
- height: number;
9
- /** MIME type hint, e.g. image/webp. */
10
- type?: string;
11
- }
12
- export interface ImageSource {
13
- /** srcset value — a single URL or a width-descriptor candidate list. */
14
- srcset: string;
8
+ height?: number;
9
+ /** Optional width-descriptor rendition candidates. */
10
+ srcset?: string;
15
11
  /** sizes attribute from the slot contract. */
16
12
  sizes?: string;
17
13
  /** Media query scoping this source (art direction). */
@@ -28,8 +24,6 @@ export interface Image {
28
24
  versionId: string;
29
25
  /** Accessible description of the image content. */
30
26
  alt: string;
31
- /** Base <img> rendition. */
32
- fallback: ImageFallback;
33
27
  /** <source> candidates produced for the asset slot. */
34
28
  sources: ImageSource[];
35
29
  }
@@ -43,14 +37,11 @@ export interface SlotOutput {
43
37
  widths: number[];
44
38
  media?: string;
45
39
  }
46
- export declare const ImageFallbackSchema: z.ZodObject<{
47
- src: z.ZodString;
48
- width: z.ZodNumber;
49
- height: z.ZodNumber;
50
- type: z.ZodOptional<z.ZodString>;
51
- }, z.core.$strip>;
52
40
  export declare const ImageSourceSchema: z.ZodObject<{
53
- srcset: z.ZodString;
41
+ src: z.ZodString;
42
+ srcset: z.ZodOptional<z.ZodString>;
43
+ width: z.ZodOptional<z.ZodNumber>;
44
+ height: z.ZodOptional<z.ZodNumber>;
54
45
  sizes: z.ZodOptional<z.ZodString>;
55
46
  media: z.ZodOptional<z.ZodString>;
56
47
  type: z.ZodString;
@@ -60,14 +51,11 @@ export declare const ImageSchema: z.ZodObject<{
60
51
  assetId: z.ZodString;
61
52
  versionId: z.ZodString;
62
53
  alt: z.ZodString;
63
- fallback: z.ZodObject<{
64
- src: z.ZodString;
65
- width: z.ZodNumber;
66
- height: z.ZodNumber;
67
- type: z.ZodOptional<z.ZodString>;
68
- }, z.core.$strip>;
69
54
  sources: z.ZodArray<z.ZodObject<{
70
- srcset: z.ZodString;
55
+ src: z.ZodString;
56
+ srcset: z.ZodOptional<z.ZodString>;
57
+ width: z.ZodOptional<z.ZodNumber>;
58
+ height: z.ZodOptional<z.ZodNumber>;
71
59
  sizes: z.ZodOptional<z.ZodString>;
72
60
  media: z.ZodOptional<z.ZodString>;
73
61
  type: z.ZodString;
@@ -75,9 +63,9 @@ export declare const ImageSchema: z.ZodObject<{
75
63
  }, z.core.$strip>;
76
64
  export type ImageAttrs = Record<string, unknown>;
77
65
  /**
78
- * The primary rendition of an image — its fallback (base <img>) entry.
66
+ * The primary rendition of an image — its first source entry.
79
67
  */
80
- export declare function primaryImageSource(image: Image): ImageFallback;
68
+ export declare function primaryImageSource(image: Image): ImageSource;
81
69
  /**
82
70
  * Computes the full set of <img> attributes for an Image.
83
71
  *
@@ -2,14 +2,11 @@ import { z } from 'zod';
2
2
  // ---------------------------------------------------------------------------
3
3
  // Zod schema
4
4
  // ---------------------------------------------------------------------------
5
- export const ImageFallbackSchema = z.object({
6
- src: z.string().min(1),
7
- width: z.number().int().nonnegative(),
8
- height: z.number().int().nonnegative(),
9
- type: z.string().min(1).optional(),
10
- });
11
5
  export const ImageSourceSchema = z.object({
12
- srcset: z.string().min(1),
6
+ src: z.string().min(1),
7
+ srcset: z.string().min(1).optional(),
8
+ width: z.number().int().nonnegative().optional(),
9
+ height: z.number().int().nonnegative().optional(),
13
10
  sizes: z.string().min(1).optional(),
14
11
  media: z.string().min(1).optional(),
15
12
  type: z.string().min(1),
@@ -19,14 +16,13 @@ export const ImageSchema = z.object({
19
16
  assetId: z.string().min(1),
20
17
  versionId: z.string().min(1),
21
18
  alt: z.string(),
22
- fallback: ImageFallbackSchema,
23
19
  sources: z.array(ImageSourceSchema).min(1),
24
20
  });
25
21
  /**
26
- * The primary rendition of an image — its fallback (base <img>) entry.
22
+ * The primary rendition of an image — its first source entry.
27
23
  */
28
24
  export function primaryImageSource(image) {
29
- return image.fallback;
25
+ return image.sources[0];
30
26
  }
31
27
  /**
32
28
  * Computes the full set of <img> attributes for an Image.
@@ -38,12 +34,13 @@ export function primaryImageSource(image) {
38
34
  * first unscoped source only.
39
35
  */
40
36
  export function imageAttrs(image, overrides = {}) {
41
- const unscoped = image.sources.find((s) => !s.media);
37
+ const primary = image.sources[0];
38
+ const unscoped = image.sources.find((s) => !s.media) ?? primary;
42
39
  return {
43
- src: image.fallback.src,
40
+ src: primary.src,
44
41
  alt: image.alt,
45
- width: image.fallback.width > 0 ? image.fallback.width : undefined,
46
- height: image.fallback.height > 0 ? image.fallback.height : undefined,
42
+ width: primary.width && primary.width > 0 ? primary.width : undefined,
43
+ height: primary.height && primary.height > 0 ? primary.height : undefined,
47
44
  srcSet: unscoped?.srcset || undefined,
48
45
  sizes: unscoped?.sizes || undefined,
49
46
  loading: 'lazy',
@@ -9,14 +9,11 @@ export declare const AssetSchema: z.ZodUnion<readonly [z.ZodObject<{
9
9
  assetId: z.ZodString;
10
10
  versionId: z.ZodString;
11
11
  alt: z.ZodString;
12
- fallback: z.ZodObject<{
13
- src: z.ZodString;
14
- width: z.ZodNumber;
15
- height: z.ZodNumber;
16
- type: z.ZodOptional<z.ZodString>;
17
- }, z.core.$strip>;
18
12
  sources: z.ZodArray<z.ZodObject<{
19
- srcset: z.ZodString;
13
+ src: z.ZodString;
14
+ srcset: z.ZodOptional<z.ZodString>;
15
+ width: z.ZodOptional<z.ZodNumber>;
16
+ height: z.ZodOptional<z.ZodNumber>;
20
17
  sizes: z.ZodOptional<z.ZodString>;
21
18
  media: z.ZodOptional<z.ZodString>;
22
19
  type: z.ZodString;
@@ -35,14 +32,11 @@ export declare const AssetSchema: z.ZodUnion<readonly [z.ZodObject<{
35
32
  assetId: z.ZodString;
36
33
  versionId: z.ZodString;
37
34
  alt: z.ZodString;
38
- fallback: z.ZodObject<{
39
- src: z.ZodString;
40
- width: z.ZodNumber;
41
- height: z.ZodNumber;
42
- type: z.ZodOptional<z.ZodString>;
43
- }, z.core.$strip>;
44
35
  sources: z.ZodArray<z.ZodObject<{
45
- srcset: z.ZodString;
36
+ src: z.ZodString;
37
+ srcset: z.ZodOptional<z.ZodString>;
38
+ width: z.ZodOptional<z.ZodNumber>;
39
+ height: z.ZodOptional<z.ZodNumber>;
46
40
  sizes: z.ZodOptional<z.ZodString>;
47
41
  media: z.ZodOptional<z.ZodString>;
48
42
  type: z.ZodString;
@@ -52,14 +52,11 @@ export declare const VideoSchema: z.ZodObject<{
52
52
  assetId: z.ZodString;
53
53
  versionId: z.ZodString;
54
54
  alt: z.ZodString;
55
- fallback: z.ZodObject<{
56
- src: z.ZodString;
57
- width: z.ZodNumber;
58
- height: z.ZodNumber;
59
- type: z.ZodOptional<z.ZodString>;
60
- }, z.core.$strip>;
61
55
  sources: z.ZodArray<z.ZodObject<{
62
- srcset: z.ZodString;
56
+ src: z.ZodString;
57
+ srcset: z.ZodOptional<z.ZodString>;
58
+ width: z.ZodOptional<z.ZodNumber>;
59
+ height: z.ZodOptional<z.ZodNumber>;
63
60
  sizes: z.ZodOptional<z.ZodString>;
64
61
  media: z.ZodOptional<z.ZodString>;
65
62
  type: z.ZodString;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradial/aci",
3
- "version": "0.1.29",
3
+ "version": "0.1.31",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -85,13 +85,6 @@
85
85
  "default": "./dist/astro/index.js"
86
86
  }
87
87
  },
88
- "scripts": {
89
- "build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json",
90
- "build:cli": "cd ../.. && ./scripts/build-cli.sh",
91
- "generate-starters": "cd ../.. && tsx scripts/generate-starters.ts",
92
- "compile-registry": "tsx src/cli/compile-registry.mjs",
93
- "prepack": "npm run generate-starters && npm run build && npm run build:cli"
94
- },
95
88
  "dependencies": {
96
89
  "markdown-it": "^14.1.0",
97
90
  "sanitize-html": "^2.13.1",
@@ -127,5 +120,11 @@
127
120
  "tsx": "^4.20.0",
128
121
  "typescript": "^5.9.3",
129
122
  "zod": "^4.0.0"
123
+ },
124
+ "scripts": {
125
+ "build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json",
126
+ "build:cli": "cd ../.. && ./scripts/build-cli.sh",
127
+ "generate-starters": "cd ../.. && tsx scripts/generate-starters.ts",
128
+ "compile-registry": "tsx src/cli/compile-registry.mjs"
130
129
  }
131
- }
130
+ }
File without changes
@@ -1,27 +1,21 @@
1
1
  import { z } from 'zod';
2
2
 
3
3
  // ---------------------------------------------------------------------------
4
- // Image fallback + source variants
4
+ // Image source variants
5
5
  //
6
- // The fallback is the base <img> rendition (largest derivative); each source
7
- // entry is one <source> element candidate with a ready-made srcset scoped by
8
- // the optional media/sizes of the slot output that produced it.
6
+ // Each source provides a primary src and may include a ready-made srcset
7
+ // scoped by the optional media/sizes of the slot output that produced it.
9
8
  // ---------------------------------------------------------------------------
10
9
 
11
- export interface ImageFallback {
12
- /** Derivative URL for the base <img>. */
10
+ export interface ImageSource {
11
+ /** Primary derivative URL for this rendition candidate. */
13
12
  src: string;
14
13
  /** Intrinsic width in pixels (0 when the format carries none, e.g. SVG). */
15
- width: number;
14
+ width?: number;
16
15
  /** Intrinsic height in pixels (0 when the format carries none). */
17
- height: number;
18
- /** MIME type hint, e.g. image/webp. */
19
- type?: string;
20
- }
21
-
22
- export interface ImageSource {
23
- /** srcset value — a single URL or a width-descriptor candidate list. */
24
- srcset: string;
16
+ height?: number;
17
+ /** Optional width-descriptor rendition candidates. */
18
+ srcset?: string;
25
19
  /** sizes attribute from the slot contract. */
26
20
  sizes?: string;
27
21
  /** Media query scoping this source (art direction). */
@@ -47,8 +41,6 @@ export interface Image {
47
41
  versionId: string;
48
42
  /** Accessible description of the image content. */
49
43
  alt: string;
50
- /** Base <img> rendition. */
51
- fallback: ImageFallback;
52
44
  /** <source> candidates produced for the asset slot. */
53
45
  sources: ImageSource[];
54
46
  }
@@ -73,15 +65,11 @@ export interface SlotOutput {
73
65
  // Zod schema
74
66
  // ---------------------------------------------------------------------------
75
67
 
76
- export const ImageFallbackSchema = z.object({
77
- src: z.string().min(1),
78
- width: z.number().int().nonnegative(),
79
- height: z.number().int().nonnegative(),
80
- type: z.string().min(1).optional(),
81
- });
82
-
83
68
  export const ImageSourceSchema = z.object({
84
- srcset: z.string().min(1),
69
+ src: z.string().min(1),
70
+ srcset: z.string().min(1).optional(),
71
+ width: z.number().int().nonnegative().optional(),
72
+ height: z.number().int().nonnegative().optional(),
85
73
  sizes: z.string().min(1).optional(),
86
74
  media: z.string().min(1).optional(),
87
75
  type: z.string().min(1),
@@ -92,7 +80,6 @@ export const ImageSchema = z.object({
92
80
  assetId: z.string().min(1),
93
81
  versionId: z.string().min(1),
94
82
  alt: z.string(),
95
- fallback: ImageFallbackSchema,
96
83
  sources: z.array(ImageSourceSchema).min(1),
97
84
  });
98
85
 
@@ -103,10 +90,10 @@ export const ImageSchema = z.object({
103
90
  export type ImageAttrs = Record<string, unknown>;
104
91
 
105
92
  /**
106
- * The primary rendition of an image — its fallback (base <img>) entry.
93
+ * The primary rendition of an image — its first source entry.
107
94
  */
108
- export function primaryImageSource(image: Image): ImageFallback {
109
- return image.fallback;
95
+ export function primaryImageSource(image: Image): ImageSource {
96
+ return image.sources[0];
110
97
  }
111
98
 
112
99
  /**
@@ -119,13 +106,14 @@ export function primaryImageSource(image: Image): ImageFallback {
119
106
  * first unscoped source only.
120
107
  */
121
108
  export function imageAttrs(image: Image, overrides: ImageAttrs = {}): ImageAttrs {
122
- const unscoped = image.sources.find((s) => !s.media);
109
+ const primary = image.sources[0];
110
+ const unscoped = image.sources.find((s) => !s.media) ?? primary;
123
111
 
124
112
  return {
125
- src: image.fallback.src,
113
+ src: primary.src,
126
114
  alt: image.alt,
127
- width: image.fallback.width > 0 ? image.fallback.width : undefined,
128
- height: image.fallback.height > 0 ? image.fallback.height : undefined,
115
+ width: primary.width && primary.width > 0 ? primary.width : undefined,
116
+ height: primary.height && primary.height > 0 ? primary.height : undefined,
129
117
  srcSet: unscoped?.srcset || undefined,
130
118
  sizes: unscoped?.sizes || undefined,
131
119
  loading: 'lazy',
@@ -27,9 +27,8 @@ export const sectionStoryFixtures: SectionStoryFixture[] = [
27
27
  assetId: 'fixture_seattle',
28
28
  versionId: 'v0001',
29
29
  alt: 'Seattle skyline at dusk',
30
- fallback: { src: '/assets/seattle.avif', width: 1920, height: 1280, type: 'image/avif' },
31
30
  sources: [
32
- { srcset: '/assets/seattle.avif', sizes: '100vw', type: 'image/avif' }
31
+ { src: '/assets/seattle.avif', width: 1920, height: 1280, sizes: '100vw', type: 'image/avif' }
33
32
  ]
34
33
  }
35
34
  }
@@ -17,9 +17,7 @@
17
17
  "build:storybook": "storybook build"
18
18
  },
19
19
  "dependencies": {
20
- "@c15t/nextjs": "^2.1.0",
21
20
  "@gradial/aci": "{{aciVersion}}",
22
- "js-yaml": "^4.1.1",
23
21
  "next": "^15.1.6",
24
22
  "react": "^19.0.0",
25
23
  "react-dom": "^19.0.0"
@@ -1,8 +1,6 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import { Inter, Space_Grotesk } from 'next/font/google';
3
3
  import { DevRefresh } from '@gradial/aci/next';
4
- import { ConsentManager } from '@/app/_components/ConsentManager';
5
- import '@c15t/nextjs/styles.css';
6
4
  import '@/design-system/styles.css';
7
5
 
8
6
  const inter = Inter({
@@ -23,7 +21,7 @@ export default function Layout({ children }: { children: ReactNode }) {
23
21
  return (
24
22
  <html lang="en" className={`${inter.variable} ${spaceGrotesk.variable}`}>
25
23
  <body>
26
- <ConsentManager>{children}</ConsentManager>
24
+ {children}
27
25
  <DevRefresh />
28
26
  </body>
29
27
  </html>
@@ -1,132 +0,0 @@
1
- 'use client';
2
-
3
- import {
4
- ConsentBanner,
5
- ConsentDialog,
6
- ConsentManagerProvider,
7
- useConsentManager,
8
- type ConsentManagerProviderProps,
9
- } from '@c15t/nextjs';
10
- import { useEffect, useMemo, type ReactNode } from 'react';
11
-
12
- // Hosted c15t backend (consent.io or self-hosted). Unset = offline mode:
13
- // consent is stored client-side and no network calls are made.
14
- const C15T_BACKEND_URL = process.env.NEXT_PUBLIC_C15T_BACKEND_URL;
15
-
16
- // The CMP's own categories. The mapping from categories to the Gradial
17
- // personalization purpose lives in .content/config/site.json
18
- // (personalization.consentCategories), not here.
19
- const CONSENT_CATEGORIES = ['necessary', 'measurement', 'marketing'] as const;
20
-
21
- // ---------------------------------------------------------------------------
22
- // Gradial collect bridge
23
- //
24
- // The Gradial collect SDK's c15t preset (the site config's
25
- // `personalization.consentPreset: "c15t"`, emitted as
26
- // `data-aci-consent="c15t"` on the collect tag) reads
27
- // `window.c15t = { consents, onConsentChange }`. c15t itself does not
28
- // publish that shape, so this bridge mirrors the store onto it — only after
29
- // a real visitor decision (`consentInfo` set), never for pre-decision
30
- // defaults, so the SDK cannot mistake an untouched banner for a withdrawal.
31
- // ---------------------------------------------------------------------------
32
-
33
- type Consents = Record<string, boolean>;
34
- type ConsentListener = (payload: { consents: Consents }) => void;
35
-
36
- declare global {
37
- interface Window {
38
- c15t?: {
39
- consents?: Consents;
40
- onConsentChange?: (cb: ConsentListener) => void;
41
- };
42
- }
43
- }
44
-
45
- const consentListeners: ConsentListener[] = [];
46
-
47
- function publishAciConsents(consents: Consents): void {
48
- const bridge = (window.c15t ??= {});
49
- bridge.consents = consents;
50
- bridge.onConsentChange ??= (cb: ConsentListener) => {
51
- consentListeners.push(cb);
52
- };
53
- for (const listener of [...consentListeners]) {
54
- listener({ consents });
55
- }
56
- }
57
-
58
- function AciConsentBridge() {
59
- const { consents, consentInfo } = useConsentManager();
60
- useEffect(() => {
61
- if (!consentInfo || !consents) return;
62
- publishAciConsents(consents as Consents);
63
- }, [consents, consentInfo]);
64
- return null;
65
- }
66
-
67
- // ---------------------------------------------------------------------------
68
- // Consent manager
69
- // ---------------------------------------------------------------------------
70
-
71
- function getConsentOptions(): ConsentManagerProviderProps['options'] {
72
- const baseOptions = {
73
- consentCategories: [...CONSENT_CATEGORIES],
74
- legalLinks: {
75
- privacyPolicy: {
76
- href: '/privacy/',
77
- target: '_self' as const,
78
- label: 'Privacy policy',
79
- },
80
- },
81
- };
82
-
83
- if (C15T_BACKEND_URL) {
84
- return {
85
- ...baseOptions,
86
- mode: 'hosted',
87
- backendURL: C15T_BACKEND_URL,
88
- };
89
- }
90
-
91
- return {
92
- ...baseOptions,
93
- mode: 'offline',
94
- offlinePolicy: {
95
- policyPacks: [
96
- {
97
- id: 'default-opt-in',
98
- match: { isDefault: true },
99
- consent: {
100
- model: 'opt-in',
101
- categories: [...CONSENT_CATEGORIES],
102
- },
103
- ui: { mode: 'banner' },
104
- },
105
- ],
106
- },
107
- };
108
- }
109
-
110
- export function ConsentManager({ children }: { children: ReactNode }) {
111
- const options = useMemo(() => getConsentOptions(), []);
112
-
113
- return (
114
- <ConsentManagerProvider options={options}>
115
- {children}
116
- <AciConsentBridge />
117
- <ConsentBanner
118
- title="Cookie preferences"
119
- description="We use cookies to keep the site working, measure performance, and support marketing."
120
- acceptButtonText="Accept all"
121
- rejectButtonText="Reject all"
122
- customizeButtonText="Customize"
123
- legalLinks={['privacyPolicy']}
124
- layout={[['reject', 'accept'], 'customize']}
125
- primaryButton="accept"
126
- scrollLock={false}
127
- hideBranding
128
- />
129
- <ConsentDialog legalLinks={['privacyPolicy']} scrollLock trapFocus hideBranding />
130
- </ConsentManagerProvider>
131
- );
132
- }