@gradial/aci 0.1.27 → 0.1.29

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 (86) hide show
  1. package/README.md +47 -0
  2. package/bin/aci-darwin-arm64 +0 -0
  3. package/dist/assets/index.d.ts +1 -1
  4. package/dist/astro/index.d.ts +1 -0
  5. package/dist/astro/index.js +7 -0
  6. package/dist/collect/api.d.ts +24 -0
  7. package/dist/collect/api.js +63 -0
  8. package/dist/collect/batcher.d.ts +65 -0
  9. package/dist/collect/batcher.js +127 -0
  10. package/dist/collect/config.d.ts +30 -0
  11. package/dist/collect/config.js +53 -0
  12. package/dist/collect/consent/adapters/c15t.d.ts +2 -0
  13. package/dist/collect/consent/adapters/c15t.js +43 -0
  14. package/dist/collect/consent/adapters/cookiebot.d.ts +2 -0
  15. package/dist/collect/consent/adapters/cookiebot.js +48 -0
  16. package/dist/collect/consent/adapters/onetrust.d.ts +2 -0
  17. package/dist/collect/consent/adapters/onetrust.js +54 -0
  18. package/dist/collect/consent/adapters/trustarc.d.ts +2 -0
  19. package/dist/collect/consent/adapters/trustarc.js +65 -0
  20. package/dist/collect/consent/envelope.d.ts +49 -0
  21. package/dist/collect/consent/envelope.js +100 -0
  22. package/dist/collect/consent/mapping.d.ts +62 -0
  23. package/dist/collect/consent/mapping.js +129 -0
  24. package/dist/collect/declarative.d.ts +35 -0
  25. package/dist/collect/declarative.js +100 -0
  26. package/dist/collect/gapbuffer.d.ts +55 -0
  27. package/dist/collect/gapbuffer.js +134 -0
  28. package/dist/collect/index.d.ts +6 -0
  29. package/dist/collect/index.js +154 -0
  30. package/dist/collect/linkclicks.d.ts +12 -0
  31. package/dist/collect/linkclicks.js +35 -0
  32. package/dist/collect/pageviews.d.ts +22 -0
  33. package/dist/collect/pageviews.js +42 -0
  34. package/dist/collect/transport.d.ts +22 -0
  35. package/dist/collect/transport.js +52 -0
  36. package/dist/collect/ulid.d.ts +6 -0
  37. package/dist/collect/ulid.js +36 -0
  38. package/dist/collect/variationviews.d.ts +31 -0
  39. package/dist/collect/variationviews.js +80 -0
  40. package/dist/collect/webvitals.d.ts +38 -0
  41. package/dist/collect/webvitals.js +99 -0
  42. package/dist/content/index.d.ts +1 -0
  43. package/dist/content/index.js +1 -0
  44. package/dist/content/personalization.d.ts +57 -0
  45. package/dist/content/personalization.js +54 -0
  46. package/dist/content/types.d.ts +3 -0
  47. package/dist/index.d.ts +1 -1
  48. package/dist/next/asset-route.js +16 -3
  49. package/dist/next/collect-script.d.ts +12 -0
  50. package/dist/next/collect-script.js +15 -0
  51. package/dist/next/config.js +1 -0
  52. package/dist/next/index.d.ts +3 -0
  53. package/dist/next/index.js +3 -0
  54. package/dist/next/page.d.ts +2 -0
  55. package/dist/next/page.js +5 -2
  56. package/dist/react/VideoPlayer.d.ts +3 -3
  57. package/dist/react/index.d.ts +4 -4
  58. package/dist/react/index.js +2 -1
  59. package/dist/types/data.d.ts +2 -0
  60. package/dist/types/image.d.ts +37 -11
  61. package/dist/types/image.js +20 -17
  62. package/dist/types/media.d.ts +14 -2
  63. package/dist/types/page.d.ts +2 -0
  64. package/dist/types/video.d.ts +7 -1
  65. package/package.json +6 -1
  66. package/src/types/data.ts +3 -0
  67. package/src/types/image.ts +44 -29
  68. package/src/types/page.ts +2 -0
  69. package/templates/astro/template/package.json.tmpl +1 -1
  70. package/templates/astro/template/src/fixtures/sections.ts +2 -1
  71. package/templates/astro/template/src/layouts/SiteLayout.astro +77 -1
  72. package/templates/astro/template/src/pages/[...slug].astro +3 -2
  73. package/templates/common/.agents/AGENTS.md +5 -0
  74. package/templates/common/.agents/skills/authoring/SKILL.md +31 -0
  75. package/templates/common/.agents/skills/authoring/references/asset-rendering.md +110 -0
  76. package/templates/common/.agents/skills/authoring/references/block-slots.md +140 -0
  77. package/templates/common/.agents/skills/authoring/references/rich-text.md +75 -0
  78. package/templates/common/.agents/skills/authoring/references/sdk-server-functions.md +192 -0
  79. package/templates/common/.agents/skills/authoring/references/type-safety.md +141 -0
  80. package/templates/common/.content/config/site.json +5 -0
  81. package/templates/common/.content/schemas/site.schema.json +15 -0
  82. package/templates/common/README.md.tmpl +15 -0
  83. package/templates/nextjs/template/package.json.tmpl +3 -1
  84. package/templates/nextjs/template/src/app/_components/ConsentManager.tsx +132 -0
  85. package/templates/nextjs/template/src/app/layout.tsx +3 -1
  86. package/templates/nextjs/template/src/components/sections/HomeHero.tsx +1 -1
@@ -9,10 +9,16 @@ 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
- sources: z.ZodArray<z.ZodObject<{
12
+ fallback: z.ZodObject<{
13
13
  src: z.ZodString;
14
14
  width: z.ZodNumber;
15
15
  height: z.ZodNumber;
16
+ type: z.ZodOptional<z.ZodString>;
17
+ }, z.core.$strip>;
18
+ sources: z.ZodArray<z.ZodObject<{
19
+ srcset: z.ZodString;
20
+ sizes: z.ZodOptional<z.ZodString>;
21
+ media: z.ZodOptional<z.ZodString>;
16
22
  type: z.ZodString;
17
23
  }, z.core.$strip>>;
18
24
  }, z.core.$strip>, z.ZodObject<{
@@ -29,10 +35,16 @@ export declare const AssetSchema: z.ZodUnion<readonly [z.ZodObject<{
29
35
  assetId: z.ZodString;
30
36
  versionId: z.ZodString;
31
37
  alt: z.ZodString;
32
- sources: z.ZodArray<z.ZodObject<{
38
+ fallback: z.ZodObject<{
33
39
  src: z.ZodString;
34
40
  width: z.ZodNumber;
35
41
  height: z.ZodNumber;
42
+ type: z.ZodOptional<z.ZodString>;
43
+ }, z.core.$strip>;
44
+ sources: z.ZodArray<z.ZodObject<{
45
+ srcset: z.ZodString;
46
+ sizes: z.ZodOptional<z.ZodString>;
47
+ media: z.ZodOptional<z.ZodString>;
36
48
  type: z.ZodString;
37
49
  }, z.core.$strip>>;
38
50
  }, z.core.$strip>>;
@@ -1,9 +1,11 @@
1
1
  import type { VaryDimension } from './render-mode.js';
2
+ import type { TaxonomyAssignments } from './data.js';
2
3
  export interface PageDocument {
3
4
  path: string;
4
5
  layout: string;
5
6
  locale: string;
6
7
  metadata: PageMetadata;
8
+ taxonomy?: TaxonomyAssignments;
7
9
  regions: Record<string, RegionNode>;
8
10
  }
9
11
  export interface PageMetadata {
@@ -52,10 +52,16 @@ export declare const VideoSchema: z.ZodObject<{
52
52
  assetId: z.ZodString;
53
53
  versionId: z.ZodString;
54
54
  alt: z.ZodString;
55
- sources: z.ZodArray<z.ZodObject<{
55
+ fallback: z.ZodObject<{
56
56
  src: z.ZodString;
57
57
  width: z.ZodNumber;
58
58
  height: z.ZodNumber;
59
+ type: z.ZodOptional<z.ZodString>;
60
+ }, z.core.$strip>;
61
+ sources: z.ZodArray<z.ZodObject<{
62
+ srcset: z.ZodString;
63
+ sizes: z.ZodOptional<z.ZodString>;
64
+ media: z.ZodOptional<z.ZodString>;
59
65
  type: z.ZodString;
60
66
  }, z.core.$strip>>;
61
67
  }, z.core.$strip>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradial/aci",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -74,6 +74,11 @@
74
74
  "types": "./dist/next/dev-refresh.d.ts",
75
75
  "import": "./dist/next/dev-refresh.js"
76
76
  },
77
+ "./next/server": {
78
+ "types": "./dist/next/server.d.ts",
79
+ "import": "./dist/next/server.js",
80
+ "default": "./dist/next/server.js"
81
+ },
77
82
  "./astro": {
78
83
  "types": "./dist/astro/index.d.ts",
79
84
  "import": "./dist/astro/index.js",
package/src/types/data.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { z } from 'zod';
2
2
 
3
+ export type TaxonomyAssignments = Record<string, string[]>;
4
+
3
5
  type InferDataProps<TSchema> = TSchema extends z.ZodType<infer T>
4
6
  ? T
5
7
  : Record<string, unknown>;
@@ -19,6 +21,7 @@ export interface DataModelContract<TSchema = unknown, TType extends string = str
19
21
  export interface DataEntry<TProps = Record<string, unknown>, TType extends string = string> {
20
22
  $type: TType;
21
23
  props: TProps;
24
+ taxonomy?: TaxonomyAssignments;
22
25
  }
23
26
 
24
27
  export type InferDataModelProps<TContract> = TContract extends DataModelContract<infer TSchema, string>
@@ -1,20 +1,32 @@
1
1
  import { z } from 'zod';
2
2
 
3
3
  // ---------------------------------------------------------------------------
4
- // Image source variant (responsive derivative)
4
+ // Image fallback + source variants
5
5
  //
6
- // Each entry is one concrete derivative produced by the compiler for the asset
7
- // slot. Renderers pick the primary variant and build a srcset from the list.
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.
8
9
  // ---------------------------------------------------------------------------
9
10
 
10
- export interface ImageSource {
11
- /** Derivative URL. */
11
+ export interface ImageFallback {
12
+ /** Derivative URL for the base <img>. */
12
13
  src: string;
13
- /** Intrinsic width in pixels. */
14
+ /** Intrinsic width in pixels (0 when the format carries none, e.g. SVG). */
14
15
  width: number;
15
- /** Intrinsic height in pixels. */
16
+ /** Intrinsic height in pixels (0 when the format carries none). */
16
17
  height: number;
17
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;
25
+ /** sizes attribute from the slot contract. */
26
+ sizes?: string;
27
+ /** Media query scoping this source (art direction). */
28
+ media?: string;
29
+ /** MIME type hint, e.g. image/webp. */
18
30
  type: string;
19
31
  }
20
32
 
@@ -35,7 +47,9 @@ export interface Image {
35
47
  versionId: string;
36
48
  /** Accessible description of the image content. */
37
49
  alt: string;
38
- /** Responsive derivative variants produced for the asset slot. */
50
+ /** Base <img> rendition. */
51
+ fallback: ImageFallback;
52
+ /** <source> candidates produced for the asset slot. */
39
53
  sources: ImageSource[];
40
54
  }
41
55
 
@@ -59,10 +73,17 @@ export interface SlotOutput {
59
73
  // Zod schema
60
74
  // ---------------------------------------------------------------------------
61
75
 
62
- export const ImageSourceSchema = z.object({
76
+ export const ImageFallbackSchema = z.object({
63
77
  src: z.string().min(1),
64
78
  width: z.number().int().nonnegative(),
65
79
  height: z.number().int().nonnegative(),
80
+ type: z.string().min(1).optional(),
81
+ });
82
+
83
+ export const ImageSourceSchema = z.object({
84
+ srcset: z.string().min(1),
85
+ sizes: z.string().min(1).optional(),
86
+ media: z.string().min(1).optional(),
66
87
  type: z.string().min(1),
67
88
  });
68
89
 
@@ -71,6 +92,7 @@ export const ImageSchema = z.object({
71
92
  assetId: z.string().min(1),
72
93
  versionId: z.string().min(1),
73
94
  alt: z.string(),
95
+ fallback: ImageFallbackSchema,
74
96
  sources: z.array(ImageSourceSchema).min(1),
75
97
  });
76
98
 
@@ -81,38 +103,31 @@ export const ImageSchema = z.object({
81
103
  export type ImageAttrs = Record<string, unknown>;
82
104
 
83
105
  /**
84
- * Picks the primary variant from an image's sources.
85
- * Uses the largest width; ties fall back to the first entry.
106
+ * The primary rendition of an image — its fallback (base <img>) entry.
86
107
  */
87
- export function primaryImageSource(image: Image): ImageSource {
88
- return image.sources.reduce((largest, source) =>
89
- source.width > largest.width ? source : largest,
90
- image.sources[0],
91
- );
108
+ export function primaryImageSource(image: Image): ImageFallback {
109
+ return image.fallback;
92
110
  }
93
111
 
94
112
  /**
95
113
  * Computes the full set of <img> attributes for an Image.
96
114
  *
97
115
  * Framework components (React <Picture>, Astro <img>, Svelte <img>) call this
98
- * and spread the result into their native element. No HTML string serialization.
116
+ * and spread the result into their native element. No HTML string
117
+ * serialization. Media-scoped sources need a <picture> wrapper and are not
118
+ * representable as plain <img> attributes, so srcset/sizes are taken from the
119
+ * first unscoped source only.
99
120
  */
100
121
  export function imageAttrs(image: Image, overrides: ImageAttrs = {}): ImageAttrs {
101
- const primary = primaryImageSource(image);
102
- const srcset = image.sources
103
- .map((s) => `${s.src} ${s.width}w`)
104
- .join(', ');
105
- const sizes = image.sources
106
- .map((s) => `(max-width: ${s.width}px) ${s.width}px`)
107
- .join(', ');
122
+ const unscoped = image.sources.find((s) => !s.media);
108
123
 
109
124
  return {
110
- src: primary.src,
125
+ src: image.fallback.src,
111
126
  alt: image.alt,
112
- width: primary.width > 0 ? primary.width : undefined,
113
- height: primary.height > 0 ? primary.height : undefined,
114
- srcSet: srcset || undefined,
115
- sizes: sizes || undefined,
127
+ width: image.fallback.width > 0 ? image.fallback.width : undefined,
128
+ height: image.fallback.height > 0 ? image.fallback.height : undefined,
129
+ srcSet: unscoped?.srcset || undefined,
130
+ sizes: unscoped?.sizes || undefined,
116
131
  loading: 'lazy',
117
132
  decoding: 'async',
118
133
  ...overrides,
package/src/types/page.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  import type { VaryDimension } from './render-mode.js';
2
+ import type { TaxonomyAssignments } from './data.js';
2
3
 
3
4
  export interface PageDocument {
4
5
  path: string;
5
6
  layout: string;
6
7
  locale: string;
7
8
  metadata: PageMetadata;
9
+ taxonomy?: TaxonomyAssignments;
8
10
  regions: Record<string, RegionNode>;
9
11
  }
10
12
 
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "dependencies": {
19
19
  "@astrojs/node": "^10.1.4",
20
- "@gradial/aci": "0.1.23",
20
+ "@gradial/aci": "{{aciVersion}}",
21
21
  "astro": "^6.1.9",
22
22
  "zod": "^4.0.0"
23
23
  },
@@ -27,8 +27,9 @@ 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' },
30
31
  sources: [
31
- { src: '/assets/seattle.avif', width: 1920, height: 1280, type: 'image/avif' }
32
+ { srcset: '/assets/seattle.avif', sizes: '100vw', type: 'image/avif' }
32
33
  ]
33
34
  }
34
35
  }
@@ -6,9 +6,17 @@ interface Props {
6
6
  description: string;
7
7
  canonical?: string;
8
8
  siteName?: string;
9
+ /**
10
+ * Pre-rendered Gradial collect script tag (collectScriptTagHTML from
11
+ * '@gradial/aci/astro'). Empty string / undefined = personalization
12
+ * disabled, no tag. Emitted here because the Astro integration has no
13
+ * attribute-bearing HTML-injection seam (injectScript only injects script
14
+ * content).
15
+ */
16
+ collectTag?: string;
9
17
  }
10
18
 
11
- const { title, description, canonical, siteName = 'Gradial' } = Astro.props;
19
+ const { title, description, canonical, siteName = 'Gradial', collectTag } = Astro.props;
12
20
  ---
13
21
 
14
22
  <!doctype html>
@@ -23,8 +31,76 @@ const { title, description, canonical, siteName = 'Gradial' } = Astro.props;
23
31
  <meta property="og:title" content={title} />
24
32
  <meta property="og:description" content={description} />
25
33
  <meta property="og:site_name" content={siteName} />
34
+ {collectTag ? <Fragment set:html={collectTag} /> : null}
26
35
  </head>
27
36
  <body>
28
37
  <slot />
38
+ <script>
39
+ // Gradial collect ↔ c15t bridge. The collect SDK's c15t preset
40
+ // (data-aci-consent="c15t" on the collect tag) reads
41
+ // window.c15t = { consents, onConsentChange }. c15t itself publishes
42
+ // its zustand store as window.c15tStore instead, so this bridge
43
+ // mirrors any c15t store on the page into the shape the SDK reads.
44
+ //
45
+ // c15t has no Astro UI package — add a consent banner via a React
46
+ // island (@astrojs/react + @c15t/react) or any setup built on
47
+ // c15t's createConsentManagerStore; this bridge picks it up either
48
+ // way. Without a c15t setup the poll gives up silently and only the
49
+ // JS consent API (aci('consent', …) under the manual preset) writes
50
+ // envelopes.
51
+ //
52
+ // Consents are mirrored only after a real visitor decision
53
+ // (consentInfo set), never for pre-decision defaults, so the SDK
54
+ // cannot mistake an untouched banner for a withdrawal.
55
+ type Consents = Record<string, boolean>;
56
+ type ConsentListener = (payload: { consents: Consents }) => void;
57
+ interface ConsentStoreState {
58
+ consents?: Consents;
59
+ consentInfo?: unknown;
60
+ }
61
+ interface ConsentStoreLike {
62
+ getState(): ConsentStoreState;
63
+ subscribe(listener: (state: ConsentStoreState) => void): () => void;
64
+ }
65
+
66
+ const listeners: ConsentListener[] = [];
67
+
68
+ function publish(consents: Consents): void {
69
+ const w = window as typeof window & {
70
+ c15t?: { consents?: Consents; onConsentChange?: (cb: ConsentListener) => void };
71
+ };
72
+ const bridge = (w.c15t ??= {});
73
+ bridge.consents = consents;
74
+ bridge.onConsentChange ??= (cb: ConsentListener) => {
75
+ listeners.push(cb);
76
+ };
77
+ for (const listener of [...listeners]) {
78
+ listener({ consents });
79
+ }
80
+ }
81
+
82
+ function onState(state: ConsentStoreState): void {
83
+ if (state.consentInfo && state.consents) publish(state.consents);
84
+ }
85
+
86
+ function connect(store: ConsentStoreLike): void {
87
+ onState(store.getState());
88
+ store.subscribe(onState);
89
+ }
90
+
91
+ // Bounded poll for the store global — same schedule the collect SDK
92
+ // uses for window.c15t (250 ms for up to 10 s, then give up silently).
93
+ let remaining = 40;
94
+ function poll(): void {
95
+ const store = (window as { c15tStore?: ConsentStoreLike }).c15tStore;
96
+ if (store) {
97
+ connect(store);
98
+ return;
99
+ }
100
+ remaining -= 1;
101
+ if (remaining > 0) setTimeout(poll, 250);
102
+ }
103
+ poll();
104
+ </script>
29
105
  </body>
30
106
  </html>
@@ -1,5 +1,5 @@
1
1
  ---
2
- import { getGradialStaticPaths, getPageRuntimeRenderInput, createRenderChildren } from '@gradial/aci/astro';
2
+ import { getGradialStaticPaths, getPageRuntimeRenderInput, createRenderChildren, collectScriptTagHTML } from '@gradial/aci/astro';
3
3
  import { normalizeRoute, routeMetadataForContent, resolveSlots } from '@gradial/aci/content';
4
4
  import { FileContentProvider } from '@gradial/aci/providers/file';
5
5
  import type { KernelPage, KernelSiteConfig, LayoutEntry, FragmentData } from '@gradial/aci/content';
@@ -36,9 +36,10 @@ const resolvedSlots = await resolveSlots(
36
36
  );
37
37
 
38
38
  const renderChildren = createRenderChildren(registry);
39
+ const collectTag = collectScriptTagHTML(siteConfig);
39
40
  ---
40
41
 
41
- <SiteLayout title={meta.title} description={meta.description} canonical={meta.canonical} siteName={meta.siteName}>
42
+ <SiteLayout title={meta.title} description={meta.description} canonical={meta.canonical} siteName={meta.siteName} collectTag={collectTag}>
42
43
  {layout.slots.map((slot) => {
43
44
  const blocks = resolvedSlots[slot.name];
44
45
  if (!blocks?.length) return null;
@@ -22,6 +22,11 @@ Skills live in `.agents/skills/`. Each has a `SKILL.md` entry point and
22
22
  - Registry lives in `src/cms/registry.ts` (`createRegistry` with `[contract, component]` tuples).
23
23
  - Compiled output goes to `.aci/compiled/` (generated, not checked in).
24
24
  - Framework integration via `withAci()` in the framework config.
25
+ - Next.js server functions from `@gradial/aci/next` — use `getSiteConfig()`, `getPage()`, `getFragment()` instead of importing `FileContentProvider` or `S3ContentProvider` directly.
26
+ - Rich text from `@gradial/aci/react` — use `RichText` component, never raw `marked`/`dangerouslySetInnerHTML`.
27
+ - Images from `@gradial/aci` — use `imageAttrs()` or SDK components, never manual `srcSet` construction.
28
+ - Block slots use `blockRefArray()` in contracts and `renderChildren` in components — never hardcoded component name matching.
29
+ - No `as any` casts — use `ContentProps` from contracts.
25
30
  - Supported frameworks: Astro and Next.js (SvelteKit removed).
26
31
 
27
32
  ## Commands
@@ -588,3 +588,34 @@ chrome-specific override scopes, and token creation workflow.
588
588
  Read `references/repo-structure.md` for the full annotated directory
589
589
  layout, framework-specific paths, naming conventions, and generated
590
590
  files list.
591
+
592
+ ## Deep Dive: SDK Server Functions
593
+
594
+ Read `references/sdk-server-functions.md` for `@gradial/aci/next`
595
+ server-side functions (`createPage`, `getSiteConfig`, `getFragment`,
596
+ etc.), the three-tier content resolution chain, and the critical rule
597
+ against importing `FileContentProvider` or `S3ContentProvider`
598
+ directly in app code.
599
+
600
+ ## Deep Dive: Block Slots
601
+
602
+ Read `references/block-slots.md` for `blockRefArray()` in contracts,
603
+ `renderChildren` in components, and the rules for nested block
604
+ rendering without hardcoded component name matching.
605
+
606
+ ## Deep Dive: Rich Text
607
+
608
+ Read `references/rich-text.md` for the SDK `RichText` component from
609
+ `@gradial/aci/react`, sanitization policy, and why you should never
610
+ use `marked` or `dangerouslySetInnerHTML` directly.
611
+
612
+ ## Deep Dive: Asset Rendering
613
+
614
+ Read `references/asset-rendering.md` for `imageAttrs()`, `Picture`,
615
+ `Image`, `Media` components, and CSS background image patterns.
616
+
617
+ ## Deep Dive: Type Safety
618
+
619
+ Read `references/type-safety.md` for `ContentProps` from contracts,
620
+ `as any` prohibition, CSS custom property typing, site config
621
+ generics, and block slot type imports.
@@ -0,0 +1,110 @@
1
+ # Asset Rendering Reference
2
+
3
+ ## Rules
4
+
5
+ - **NEVER** construct `<img>` tags manually with `src`/`srcSet` from
6
+ image sources.
7
+ - **ALWAYS** use the SDK's `imageAttrs()` function from `@gradial/aci`
8
+ or the `Image`/`Picture` component from `@gradial/aci/react`.
9
+ - **NEVER** duplicate the `Image` type locally — always import from
10
+ `@gradial/aci`.
11
+
12
+ ## SDK Exports
13
+
14
+ ### From `@gradial/aci`
15
+
16
+ | Export | Type | Purpose |
17
+ |--------|------|---------|
18
+ | `Image` | type | The DAM-backed image type (`$type`, `assetId`, `versionId`, `alt`, `sources`) |
19
+ | `ImageSource` | type | A single responsive derivative (`src`, `width`, `height`, `type`) |
20
+ | `imageAttrs(image, overrides?)` | function | Returns a complete attributes object for `<img>` |
21
+ | `primaryImageSource(image)` | function | Picks the largest-width source from an image |
22
+ | `ImageSchema` | Zod schema | Schema for image fields in contracts |
23
+
24
+ ### From `@gradial/aci/react`
25
+
26
+ | Export | Type | Purpose |
27
+ |--------|------|---------|
28
+ | `Image` | component | Renders an `Image` as `<img>` with computed attrs |
29
+ | `Picture` | component | Renders an `Image` as responsive `<img>` (delegates to `imageAttrs()`) |
30
+ | `Media` | component | Renders media assets (images, videos) with type detection |
31
+
32
+ ## imageAttrs()
33
+
34
+ `imageAttrs(image, overrides?)` returns a complete attributes object
35
+ including `src`, `alt`, `width`, `height`, `srcSet`, `sizes`, `loading`,
36
+ and `decoding`. Spread the result into a native `<img>` element:
37
+
38
+ ```tsx
39
+ import { imageAttrs, type Image } from '@gradial/aci';
40
+
41
+ export function HeroImage({ image }: { image: Image }) {
42
+ return <img {...imageAttrs(image)} />;
43
+ }
44
+ ```
45
+
46
+ Override specific attributes by passing a second argument:
47
+
48
+ ```tsx
49
+ <img {...imageAttrs(image, { loading: 'eager' })} />
50
+ ```
51
+
52
+ ## SDK Components
53
+
54
+ ```tsx
55
+ import { Picture, Image } from '@gradial/aci/react';
56
+
57
+ // Picture: delegates to imageAttrs() internally
58
+ <Picture image={image} />
59
+
60
+ // Image: same behavior, accepts image or value prop
61
+ <Image image={image} />
62
+ ```
63
+
64
+ Both components call `imageAttrs()` internally and spread the result
65
+ into a native `<img>` element.
66
+
67
+ ## CSS Background Images
68
+
69
+ For CSS background images, use `primaryImageSource()` to get the URL:
70
+
71
+ ```tsx
72
+ import { primaryImageSource } from '@gradial/aci';
73
+
74
+ export function HeroBackground({ image }: { image: Image }) {
75
+ const src = primaryImageSource(image).src;
76
+ return (
77
+ <div style={{ backgroundImage: `url(${src})` } as React.CSSProperties} />
78
+ );
79
+ }
80
+ ```
81
+
82
+ ## In Contracts
83
+
84
+ Use `ImageSchema` for DAM-managed image fields:
85
+
86
+ ```ts
87
+ import { ImageSchema, defineComponentContract } from '@gradial/aci';
88
+ import { z } from 'zod';
89
+
90
+ export const schema = z.object({
91
+ image: ImageSchema,
92
+ cards: z.array(z.object({
93
+ image: ImageSchema.optional(),
94
+ })),
95
+ });
96
+ ```
97
+
98
+ Declare `imageSlots` matching every `ImageSchema` field, including
99
+ nested dot paths like `cards.image`. See
100
+ `references/image-assets.md` for full image slot configuration.
101
+
102
+ ## Anti-Patterns
103
+
104
+ - Manually building `srcSet` strings from `image.sources` — use
105
+ `imageAttrs()` instead
106
+ - Constructing `<img src={image.sources[0].src} />` — use `imageAttrs()`
107
+ or SDK components
108
+ - Duplicating the `Image` type locally — import from `@gradial/aci`
109
+ - Ignoring `width`/`height` — `imageAttrs()` includes them to prevent
110
+ layout shift