@gradial/aci 0.1.27 → 0.1.28
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.
- package/bin/aci-darwin-arm64 +0 -0
- package/dist/next/asset-route.js +16 -3
- package/dist/next/index.d.ts +2 -0
- package/dist/next/index.js +2 -0
- package/dist/providers/file.js +8 -1
- package/dist/types/data.d.ts +2 -0
- package/dist/types/page.d.ts +2 -0
- package/package.json +6 -1
- package/src/types/data.ts +3 -0
- package/src/types/page.ts +2 -0
- package/templates/astro/template/package.json.tmpl +1 -1
- package/templates/common/.agents/AGENTS.md +5 -0
- package/templates/common/.agents/skills/authoring/SKILL.md +31 -0
- package/templates/common/.agents/skills/authoring/references/asset-rendering.md +110 -0
- package/templates/common/.agents/skills/authoring/references/block-slots.md +140 -0
- package/templates/common/.agents/skills/authoring/references/rich-text.md +75 -0
- package/templates/common/.agents/skills/authoring/references/sdk-server-functions.md +192 -0
- package/templates/common/.agents/skills/authoring/references/type-safety.md +141 -0
- package/templates/nextjs/template/package.json.tmpl +1 -1
package/bin/aci-darwin-arm64
CHANGED
|
Binary file
|
package/dist/next/asset-route.js
CHANGED
|
@@ -8,9 +8,9 @@ export async function GET(_request, { params }) {
|
|
|
8
8
|
if (assetPath === '/') {
|
|
9
9
|
return new Response('asset not found', { status: 404 });
|
|
10
10
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
return
|
|
11
|
+
const local = await localAssetResponse(assetPath);
|
|
12
|
+
if (local) {
|
|
13
|
+
return local;
|
|
14
14
|
}
|
|
15
15
|
try {
|
|
16
16
|
const releaseId = await resolveReleaseId();
|
|
@@ -20,3 +20,16 @@ export async function GET(_request, { params }) {
|
|
|
20
20
|
return new Response('asset not found', { status: 404 });
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
+
async function localAssetResponse(assetPath) {
|
|
24
|
+
const localRoot = process.env.ACI_LOCAL_ASSET_ROOT;
|
|
25
|
+
if (localRoot) {
|
|
26
|
+
const response = await new FileContentProvider(localRoot).fetchRaw(assetPath);
|
|
27
|
+
if (response.ok)
|
|
28
|
+
return response;
|
|
29
|
+
}
|
|
30
|
+
if (process.env.ACI_CONTENT_PROVIDER !== 'file') {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const response = await new FileContentProvider().fetchRaw(assetPath);
|
|
34
|
+
return response.ok ? response : null;
|
|
35
|
+
}
|
package/dist/next/index.d.ts
CHANGED
|
@@ -2,3 +2,5 @@ export { DevRefresh, type DevRefreshProps } from './dev-refresh.js';
|
|
|
2
2
|
export { withAci, type WithAciOptions } from './config.js';
|
|
3
3
|
export { isPreviewMode } from './preview-mode.js';
|
|
4
4
|
export { createPage, type CreatePageOptions, type SlotPropsFn } from './page.js';
|
|
5
|
+
export { getSiteConfig, getFragment, getLayout, getPage, getRenderInput, routeFromNextParams } from './server.js';
|
|
6
|
+
export { PageNotFoundError, FragmentNotFoundError, LayoutNotFoundError } from './server.js';
|
package/dist/next/index.js
CHANGED
|
@@ -2,3 +2,5 @@ export { DevRefresh } from './dev-refresh.js';
|
|
|
2
2
|
export { withAci } from './config.js';
|
|
3
3
|
export { isPreviewMode } from './preview-mode.js';
|
|
4
4
|
export { createPage } from './page.js';
|
|
5
|
+
export { getSiteConfig, getFragment, getLayout, getPage, getRenderInput, routeFromNextParams } from './server.js';
|
|
6
|
+
export { PageNotFoundError, FragmentNotFoundError, LayoutNotFoundError } from './server.js';
|
package/dist/providers/file.js
CHANGED
|
@@ -60,7 +60,14 @@ export class FileContentProvider {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
async manifest() {
|
|
63
|
-
this.#manifest ??= this.readJSON('manifest.json')
|
|
63
|
+
this.#manifest ??= this.readJSON('manifest.json').catch(() => ({
|
|
64
|
+
manifestVersion: '0',
|
|
65
|
+
buildDigest: '',
|
|
66
|
+
siteConfigRef: '',
|
|
67
|
+
routes: {},
|
|
68
|
+
fragments: {},
|
|
69
|
+
layouts: {},
|
|
70
|
+
}));
|
|
64
71
|
return await this.#manifest;
|
|
65
72
|
}
|
|
66
73
|
async fetchRaw(key) {
|
package/dist/types/data.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
export type TaxonomyAssignments = Record<string, string[]>;
|
|
2
3
|
type InferDataProps<TSchema> = TSchema extends z.ZodType<infer T> ? T : Record<string, unknown>;
|
|
3
4
|
export interface DataModelDefinition<TSchema = unknown, TType extends string = string> {
|
|
4
5
|
type: TType;
|
|
@@ -11,6 +12,7 @@ export interface DataModelContract<TSchema = unknown, TType extends string = str
|
|
|
11
12
|
export interface DataEntry<TProps = Record<string, unknown>, TType extends string = string> {
|
|
12
13
|
$type: TType;
|
|
13
14
|
props: TProps;
|
|
15
|
+
taxonomy?: TaxonomyAssignments;
|
|
14
16
|
}
|
|
15
17
|
export type InferDataModelProps<TContract> = TContract extends DataModelContract<infer TSchema, string> ? InferDataProps<TSchema> : Record<string, unknown>;
|
|
16
18
|
export type InferDataModelType<TContract> = TContract extends DataModelContract<unknown, infer TType> ? TType : string;
|
package/dist/types/page.d.ts
CHANGED
|
@@ -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 {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gradial/aci",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.28",
|
|
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>
|
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
|
|
|
@@ -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
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Block Slots Reference
|
|
2
|
+
|
|
3
|
+
## What Block Slots Are
|
|
4
|
+
|
|
5
|
+
Container components (like `container` and `grid_column`) can render
|
|
6
|
+
child blocks. The SDK detects block slots from contracts and injects a
|
|
7
|
+
`renderChildren` callback into the component props at render time.
|
|
8
|
+
|
|
9
|
+
This lets editors nest blocks inside other blocks in content JSON, and
|
|
10
|
+
the SDK resolves the child components through the registry — no
|
|
11
|
+
hardcoded component name matching needed.
|
|
12
|
+
|
|
13
|
+
## Defining Block Slots in Contracts
|
|
14
|
+
|
|
15
|
+
Use `blockRefArray()` from `@gradial/aci` in the contract's Zod schema
|
|
16
|
+
to declare a block slot field. The compiler scans the schema for
|
|
17
|
+
`blockRefArray` and `blockRef` fields and auto-discovers block slots —
|
|
18
|
+
no separate `slots` or `blockSlots` declaration needed.
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { blockRefArray, defineComponentContract } from '@gradial/aci';
|
|
22
|
+
import { z } from 'zod';
|
|
23
|
+
import { calloutContract } from './callout.contract';
|
|
24
|
+
import { headingContract } from './heading.contract';
|
|
25
|
+
|
|
26
|
+
export const schema = z.object({
|
|
27
|
+
theme: z.enum(['auto', 'light', 'dark']).optional(),
|
|
28
|
+
className: z.string().optional(),
|
|
29
|
+
blocks: blockRefArray([calloutContract, headingContract]).optional(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export type ContentProps = z.infer<typeof schema>;
|
|
33
|
+
|
|
34
|
+
export const containerContract = defineComponentContract({
|
|
35
|
+
id: 'container',
|
|
36
|
+
props: schema,
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`blockRefArray()` takes an array of allowed contracts — only those block
|
|
41
|
+
types can be placed in the slot. This flows into the generated JSON
|
|
42
|
+
schema, so the content validator enforces the allowlist at build time.
|
|
43
|
+
|
|
44
|
+
For a single (non-array) block slot, use `blockRef(contracts)` instead:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { blockRef, defineComponentContract } from '@gradial/aci';
|
|
48
|
+
import { headingContract } from './heading.contract';
|
|
49
|
+
|
|
50
|
+
export const schema = z.object({
|
|
51
|
+
heading: blockRef([headingContract]).optional(),
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Using renderChildren in Components
|
|
56
|
+
|
|
57
|
+
The SDK's render system checks `entry.contract.blockSlots` and, if
|
|
58
|
+
present, injects a `renderChildren` callback into the component props.
|
|
59
|
+
This callback resolves child blocks through the registry.
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
import type { BlockRef, ReactElement } from '@gradial/aci';
|
|
63
|
+
import type { RenderChildren } from '@gradial/aci/react';
|
|
64
|
+
import type { ContentProps } from '@/cms/contracts/components/container.contract';
|
|
65
|
+
|
|
66
|
+
export async function ContainerSection({
|
|
67
|
+
theme = 'auto',
|
|
68
|
+
className = '',
|
|
69
|
+
blocks = [],
|
|
70
|
+
renderChildren,
|
|
71
|
+
}: ContentProps & { renderChildren?: RenderChildren<readonly BlockRef[]> }) {
|
|
72
|
+
const rendered = blocks.length && renderChildren
|
|
73
|
+
? await renderChildren(blocks)
|
|
74
|
+
: null;
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<section className={`theme-${theme} py-[var(--spacing-section-y)] ${className}`}>
|
|
78
|
+
<div className="flex flex-col gap-6">
|
|
79
|
+
{rendered}
|
|
80
|
+
</div>
|
|
81
|
+
</section>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The `RenderChildren` type is exported from `@gradial/aci/react`:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
type RenderChildren<T extends readonly BlockRef[]> = (blocks: T) => Promise<ReactElement[]>;
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Rules
|
|
93
|
+
|
|
94
|
+
- **Never hardcode component name matching.** No switch statements on
|
|
95
|
+
`block.component`. Always use `renderChildren` from the SDK — it uses
|
|
96
|
+
the registry to resolve components.
|
|
97
|
+
- **Always use `blockRefArray()` in contracts** to declare block slot
|
|
98
|
+
fields. The compiler auto-discovers them from the Zod schema.
|
|
99
|
+
- **Atomic block contracts must be registered** in the registry with
|
|
100
|
+
their components. `renderChildren` resolves child blocks through the
|
|
101
|
+
same registry used for top-level blocks.
|
|
102
|
+
- **`blockRefArray()` takes an array of allowed contracts** — only
|
|
103
|
+
those block types can be placed in the slot. This is enforced at
|
|
104
|
+
content validation time.
|
|
105
|
+
- **`renderChildren` is optional** — always guard with a conditional
|
|
106
|
+
(`blocks.length && renderChildren ? await renderChildren(blocks) : null`)
|
|
107
|
+
so the component works in isolation (e.g., Storybook).
|
|
108
|
+
- **Nested blocks live inside `props`**, not in a sibling `slots` field.
|
|
109
|
+
The content JSON places child blocks directly in the prop value
|
|
110
|
+
matching the `blockRefArray` field name.
|
|
111
|
+
|
|
112
|
+
## Content JSON Example
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"id": "cta-section",
|
|
117
|
+
"component": "container",
|
|
118
|
+
"props": {
|
|
119
|
+
"theme": "dark",
|
|
120
|
+
"blocks": [
|
|
121
|
+
{
|
|
122
|
+
"id": "primary-cta",
|
|
123
|
+
"component": "callout",
|
|
124
|
+
"props": {
|
|
125
|
+
"headline": "Get started today",
|
|
126
|
+
"body": "Ship content updates in minutes."
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"id": "heading",
|
|
131
|
+
"component": "heading",
|
|
132
|
+
"props": {
|
|
133
|
+
"text": "Why teams choose us",
|
|
134
|
+
"level": 2
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Rich Text Reference
|
|
2
|
+
|
|
3
|
+
## Rules
|
|
4
|
+
|
|
5
|
+
- **NEVER** use `marked`, `markdown-it`, or `dangerouslySetInnerHTML`
|
|
6
|
+
directly in component code.
|
|
7
|
+
- **ALWAYS** use the SDK's `RichText` component from `@gradial/aci/react`.
|
|
8
|
+
|
|
9
|
+
The SDK `RichText` component uses `sanitize-html` with an explicit
|
|
10
|
+
allowlist. It renders markdown via `markdown-it` (with `html: false` to
|
|
11
|
+
prevent raw HTML in markdown) and then sanitizes the output. Links get
|
|
12
|
+
`rel="noopener noreferrer"` automatically.
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
import { RichText } from '@gradial/aci/react';
|
|
18
|
+
|
|
19
|
+
export function ArticleBody({ body }: { body: string }) {
|
|
20
|
+
return <RichText value={body} />;
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The `value` prop accepts:
|
|
25
|
+
|
|
26
|
+
- **`string`** — treated as markdown and rendered to HTML
|
|
27
|
+
- **`RichTextValue`** object — `{ format: 'markdown', markdown: '...' }`
|
|
28
|
+
or `{ format: 'html', html: '...' }`
|
|
29
|
+
|
|
30
|
+
When a string is passed, the component wraps it as
|
|
31
|
+
`{ format: 'markdown', markdown: value }` internally.
|
|
32
|
+
|
|
33
|
+
## In Contracts
|
|
34
|
+
|
|
35
|
+
Use `RichTextValueSchema` for typed rich text fields:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { RichTextValueSchema } from '@gradial/aci';
|
|
39
|
+
import { z } from 'zod';
|
|
40
|
+
|
|
41
|
+
export const schema = z.object({
|
|
42
|
+
body: RichTextValueSchema.optional(),
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Or accept a plain string:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
export const schema = z.object({
|
|
50
|
+
body: z.string().optional(),
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Both forms work with the `RichText` component — it accepts
|
|
55
|
+
`RichTextValue | string`.
|
|
56
|
+
|
|
57
|
+
## Sanitization Policy
|
|
58
|
+
|
|
59
|
+
The SDK allowlist includes:
|
|
60
|
+
|
|
61
|
+
- Standard formatting tags: `p`, `br`, `strong`, `em`, `ul`, `ol`, `li`,
|
|
62
|
+
`code`, `pre`, `blockquote`, `h1`–`h6`, `img`
|
|
63
|
+
- Links (`a`) with `href`, `name`, `target`, `rel` attributes
|
|
64
|
+
- Images (`img`) with `src`, `alt`, `width`, `height`, `loading`
|
|
65
|
+
attributes
|
|
66
|
+
|
|
67
|
+
Raw HTML in markdown input is rejected by `markdown-it`'s `html: false`
|
|
68
|
+
setting. HTML format values are sanitized by `sanitize-html`.
|
|
69
|
+
|
|
70
|
+
## Anti-Patterns
|
|
71
|
+
|
|
72
|
+
- Importing `marked` or `markdown-it` directly — the SDK already
|
|
73
|
+
handles this with proper sanitization
|
|
74
|
+
- Using `dangerouslySetInnerHTML` with unsanitized content
|
|
75
|
+
- Writing a custom markdown-to-HTML pipeline in component code
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# SDK Server Functions Reference
|
|
2
|
+
|
|
3
|
+
## `@gradial/aci/next` Exports
|
|
4
|
+
|
|
5
|
+
The SDK provides server-side functions that handle content resolution
|
|
6
|
+
across preview, local file, and deployed S3 modes. **Always use these
|
|
7
|
+
functions instead of directly importing `FileContentProvider` or
|
|
8
|
+
`S3ContentProvider`.** The SDK handles the resolution chain automatically.
|
|
9
|
+
|
|
10
|
+
### `createPage(registry)`
|
|
11
|
+
|
|
12
|
+
The catch-all route handler for Next.js App Router. Owns param
|
|
13
|
+
normalization, content loading via render input, metadata generation,
|
|
14
|
+
`notFound` handling, layout resolution, and block rendering via the
|
|
15
|
+
registry.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
// src/app/[[...slug]]/page.tsx
|
|
19
|
+
import { createPage } from '@gradial/aci/next';
|
|
20
|
+
import { registry } from '@/cms/registry';
|
|
21
|
+
|
|
22
|
+
const page = createPage(registry);
|
|
23
|
+
export const dynamic = 'force-dynamic';
|
|
24
|
+
export const generateMetadata = page.generateMetadata;
|
|
25
|
+
export default page.default;
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`createPage` loads the page's layout, resolves all slots (filling from
|
|
29
|
+
page regions or layout fragment defaults), and renders blocks via the
|
|
30
|
+
registry. The `main` slot is wrapped in `<main>`; other slots render as
|
|
31
|
+
fragments.
|
|
32
|
+
|
|
33
|
+
### `withAci(nextConfig, options?)`
|
|
34
|
+
|
|
35
|
+
Next.js config wrapper. Sets up asset rewrites (`.gradial-dam` prefix)
|
|
36
|
+
and dev-mode content watch via webpack plugin.
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
// next.config.ts
|
|
40
|
+
import { withAci } from '@gradial/aci/next';
|
|
41
|
+
import type { NextConfig } from 'next';
|
|
42
|
+
|
|
43
|
+
const nextConfig: NextConfig = { reactStrictMode: true };
|
|
44
|
+
export default withAci(nextConfig);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### `getSiteConfig<T>()`
|
|
48
|
+
|
|
49
|
+
Loads the site configuration. Checks the render input header first
|
|
50
|
+
(preview mode), then falls back to file or S3 provider. **MUST be used
|
|
51
|
+
in `layout.tsx` instead of `new FileContentProvider()`.**
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
import { getSiteConfig } from '@gradial/aci/next';
|
|
55
|
+
|
|
56
|
+
const siteConfig = await getSiteConfig<MySiteConfig>();
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Generic: pass your site config interface for typed access:
|
|
60
|
+
`getSiteConfig<SiteConfig>()`.
|
|
61
|
+
|
|
62
|
+
### `getFragment(name)`
|
|
63
|
+
|
|
64
|
+
Loads a fragment by name. Checks render input first (preview mode), then
|
|
65
|
+
provider fallback.
|
|
66
|
+
|
|
67
|
+
### `getLayout(name)`
|
|
68
|
+
|
|
69
|
+
Loads a layout by name. Checks render input first (preview mode), then
|
|
70
|
+
provider fallback.
|
|
71
|
+
|
|
72
|
+
### `getPage(route)`
|
|
73
|
+
|
|
74
|
+
Loads page content for a specific route. Checks render input first
|
|
75
|
+
(preview mode), then provider fallback.
|
|
76
|
+
|
|
77
|
+
### `getRenderInput(route)`
|
|
78
|
+
|
|
79
|
+
Loads the full render input (page + siteConfig + domain + locale). Used
|
|
80
|
+
internally by `createPage`. Available for custom server composition when
|
|
81
|
+
you need the full render context.
|
|
82
|
+
|
|
83
|
+
### `routeFromNextParams(params)`
|
|
84
|
+
|
|
85
|
+
Normalizes Next.js catch-all params (`{ slug?: string[] }`) into a route
|
|
86
|
+
string. Handles `Promise`-wrapped params from Next.js 15+.
|
|
87
|
+
|
|
88
|
+
### `isPreviewMode()`
|
|
89
|
+
|
|
90
|
+
Async function returning the preview release ID string when in preview
|
|
91
|
+
mode, or an empty string when not. Use as a truthy check:
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
const previewRelease = await isPreviewMode();
|
|
95
|
+
if (previewRelease) { /* preview-specific logic */ }
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### `DevRefresh`
|
|
99
|
+
|
|
100
|
+
React component for dev-mode content refresh. Connects to the SDK's
|
|
101
|
+
content watch WebSocket and triggers a page reload when compiled content
|
|
102
|
+
changes. Add to the root layout:
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
import { DevRefresh } from '@gradial/aci/next';
|
|
106
|
+
|
|
107
|
+
// In layout body:
|
|
108
|
+
<DevRefresh />
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Only active in development (`NODE_ENV !== 'production'`). Renders `null`
|
|
112
|
+
in production.
|
|
113
|
+
|
|
114
|
+
### Error Classes
|
|
115
|
+
|
|
116
|
+
| Class | Thrown When |
|
|
117
|
+
|-------|-------------|
|
|
118
|
+
| `PageNotFoundError` | Page not found for a route |
|
|
119
|
+
| `FragmentNotFoundError` | Fragment not found by name |
|
|
120
|
+
| `LayoutNotFoundError` | Layout not found by name |
|
|
121
|
+
|
|
122
|
+
`createPage` catches `PageNotFoundError` and calls `notFound()`
|
|
123
|
+
automatically.
|
|
124
|
+
|
|
125
|
+
## Three-Tier Content Resolution
|
|
126
|
+
|
|
127
|
+
Every SDK server function follows the same resolution chain:
|
|
128
|
+
|
|
129
|
+
### Tier 1: Render Input (Preview Mode)
|
|
130
|
+
|
|
131
|
+
When the Go harness sends a render request, it sets the
|
|
132
|
+
`x-gradial-render-input-id` header. The SDK checks the
|
|
133
|
+
`pendingRenderInputs` in-process map for a matching render input and
|
|
134
|
+
returns page data directly from the Go harness's render payload. This
|
|
135
|
+
is how preview works — the harness pushes content into the Next.js
|
|
136
|
+
process without touching S3 or disk.
|
|
137
|
+
|
|
138
|
+
### Tier 2: File Provider (`ACI_CONTENT_PROVIDER=file`)
|
|
139
|
+
|
|
140
|
+
When the `ACI_CONTENT_PROVIDER` environment variable is set to `file`,
|
|
141
|
+
the SDK reads compiled content from `.aci/compiled/` on disk. This is
|
|
142
|
+
the local development path. Also used as a fallback in preview mode when
|
|
143
|
+
no render input header is present.
|
|
144
|
+
|
|
145
|
+
### Tier 3: S3 Provider (Deployed SSR)
|
|
146
|
+
|
|
147
|
+
When neither render input nor file provider is active, the SDK reads
|
|
148
|
+
from S3. The release ID is resolved from (in order):
|
|
149
|
+
|
|
150
|
+
1. `x-gradial-release-id` header (set by middleware)
|
|
151
|
+
2. `ACI_RELEASE_ID` environment variable
|
|
152
|
+
3. Vercel Edge Config active release pointer
|
|
153
|
+
|
|
154
|
+
## Critical Rule
|
|
155
|
+
|
|
156
|
+
**NEVER import `FileContentProvider` or `S3ContentProvider` directly in
|
|
157
|
+
app code.** Always use `getPage()`, `getSiteConfig()`, `getFragment()`,
|
|
158
|
+
`getLayout()`, or `getRenderInput()` from `@gradial/aci/next`. The SDK
|
|
159
|
+
handles the resolution chain automatically.
|
|
160
|
+
|
|
161
|
+
The only exception is the asset route handler
|
|
162
|
+
(`src/app/gradial/assets/[releaseId]/[...path]/route.ts`), which directly
|
|
163
|
+
uses `FileContentProvider` for local asset serving and
|
|
164
|
+
`getReleaseAssetResponse` for S3 assets.
|
|
165
|
+
|
|
166
|
+
## Layout.tsx Pattern
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
import { DevRefresh, getSiteConfig } from '@gradial/aci/next';
|
|
170
|
+
|
|
171
|
+
export default async function Layout({ children }: { children: ReactNode }) {
|
|
172
|
+
const siteConfig = await getSiteConfig<SiteConfig>().catch(() => null);
|
|
173
|
+
return (
|
|
174
|
+
<html lang="en">
|
|
175
|
+
<body>
|
|
176
|
+
{children}
|
|
177
|
+
<DevRefresh />
|
|
178
|
+
</body>
|
|
179
|
+
</html>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Chrome (Header/Footer) is CMS-managed via layout fragment defaults and
|
|
185
|
+
rendered by `createPage` through the registry. The `layout.tsx` file
|
|
186
|
+
should be minimal — import CSS, optionally load site config for
|
|
187
|
+
metadata/fonts, and render `<DevRefresh />`.
|
|
188
|
+
|
|
189
|
+
For sites that need site config in the layout (e.g., for fonts or
|
|
190
|
+
metadata), use `getSiteConfig<SiteConfig>()` with a `.catch(() => null)`
|
|
191
|
+
fallback so the layout doesn't crash if content isn't available during
|
|
192
|
+
build.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Type Safety Reference
|
|
2
|
+
|
|
3
|
+
## ContentProps from Contracts
|
|
4
|
+
|
|
5
|
+
Contracts export `ContentProps = z.infer<typeof schema>`. Use this type
|
|
6
|
+
in runtime components for prop typing:
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
// src/cms/contracts/components/hero.contract.ts
|
|
10
|
+
import { defineComponentContract, ImageSchema } from '@gradial/aci';
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
|
|
13
|
+
export const schema = z.object({
|
|
14
|
+
headline: z.string().min(1),
|
|
15
|
+
image: ImageSchema,
|
|
16
|
+
ctaLabel: z.string().optional(),
|
|
17
|
+
ctaHref: z.string().optional(),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export type ContentProps = z.infer<typeof schema>;
|
|
21
|
+
|
|
22
|
+
export const heroContract = defineComponentContract({
|
|
23
|
+
id: 'hero',
|
|
24
|
+
props: schema,
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
// src/components/sections/Hero.tsx
|
|
30
|
+
import type { ContentProps } from '@/cms/contracts/components/hero.contract';
|
|
31
|
+
|
|
32
|
+
type HeroProps = ContentProps & { className?: string };
|
|
33
|
+
|
|
34
|
+
export function Hero({ headline, image, ctaLabel, ctaHref, className }: HeroProps) {
|
|
35
|
+
// ...
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Rules
|
|
40
|
+
|
|
41
|
+
### No `as any` casts
|
|
42
|
+
|
|
43
|
+
**NEVER** use `as any` or `as any[]` casts in component code. If the
|
|
44
|
+
type doesn't fit, fix the contract schema.
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
// BAD
|
|
48
|
+
const items = props.items as any[];
|
|
49
|
+
|
|
50
|
+
// GOOD — fix the schema to match the actual shape
|
|
51
|
+
export const schema = z.object({
|
|
52
|
+
items: z.array(z.object({
|
|
53
|
+
title: z.string(),
|
|
54
|
+
description: z.string(),
|
|
55
|
+
})),
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### No passthrough schemas
|
|
60
|
+
|
|
61
|
+
Contracts must define proper schemas. Do not use
|
|
62
|
+
`z.object({}).passthrough()` or `z.record(z.any())`:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
// BAD — hides type errors and allows any content
|
|
66
|
+
export const schema = z.object({}).passthrough();
|
|
67
|
+
export const schema = z.record(z.any());
|
|
68
|
+
|
|
69
|
+
// GOOD — explicit fields
|
|
70
|
+
export const schema = z.object({
|
|
71
|
+
headline: z.string(),
|
|
72
|
+
items: z.array(z.object({ title: z.string() })),
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### CSS custom properties
|
|
77
|
+
|
|
78
|
+
For React `style` props with CSS custom properties, use
|
|
79
|
+
`as React.CSSProperties` (not `as any`):
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
// BAD
|
|
83
|
+
<div style={{ '--accent': 'red' } as any} />
|
|
84
|
+
|
|
85
|
+
// GOOD
|
|
86
|
+
<div style={{ '--accent': 'red' } as React.CSSProperties} />
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Image type
|
|
90
|
+
|
|
91
|
+
Use the `Image` type from `@gradial/aci` for image props. Never
|
|
92
|
+
duplicate the type locally:
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
// BAD — local duplicate
|
|
96
|
+
interface Image { src: string; alt: string; }
|
|
97
|
+
|
|
98
|
+
// GOOD — import from SDK
|
|
99
|
+
import type { Image } from '@gradial/aci';
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Site config
|
|
103
|
+
|
|
104
|
+
Define a `SiteConfig` interface and pass it to `getSiteConfig()`:
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
import { getSiteConfig } from '@gradial/aci/next';
|
|
108
|
+
|
|
109
|
+
interface SiteConfig {
|
|
110
|
+
title: string;
|
|
111
|
+
domain: string;
|
|
112
|
+
defaultLocale: string;
|
|
113
|
+
navigation: NavItem[];
|
|
114
|
+
footer: FooterConfig;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const config = await getSiteConfig<SiteConfig>();
|
|
118
|
+
// config is typed as SiteConfig
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Block slot types
|
|
122
|
+
|
|
123
|
+
Use `RenderChildren` from `@gradial/aci/react` and `BlockRef` from
|
|
124
|
+
`@gradial/aci` for block slot component props:
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
import type { BlockRef } from '@gradial/aci';
|
|
128
|
+
import type { RenderChildren } from '@gradial/aci/react';
|
|
129
|
+
import type { ContentProps } from '@/cms/contracts/components/container.contract';
|
|
130
|
+
|
|
131
|
+
export async function ContainerSection({
|
|
132
|
+
blocks = [],
|
|
133
|
+
renderChildren,
|
|
134
|
+
}: ContentProps & {
|
|
135
|
+
renderChildren?: RenderChildren<readonly BlockRef[]>;
|
|
136
|
+
}) {
|
|
137
|
+
// ...
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
See `references/block-slots.md` for the full block slot pattern.
|