> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gradial.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Frontend Contract

> Connect ACI content to your site with configuration, typed contracts, and a framework registry.

Your frontend remains your code. Gradial ACI (Agentic Content Infrastructure) connects to it through a small, explicit contract that defines where content belongs, which props each component accepts, and which runtime component renders each block.

That boundary gives content teams flexibility without asking the CMS to understand framework internals. It also gives developers a reliable place to enforce component and layout rules.

## The contract at a glance

<CardGroup cols={2}>
  <Card title="Site configuration" icon="gear">
    `.aci.yaml` identifies the framework, source directories, contract registries, and CMS-managed route.
  </Card>

  <Card title="Component contracts" icon="cubes">
    Zod schemas define stable component IDs and the props content may supply.
  </Card>

  <Card title="Layout contracts" icon="table-layout">
    Layouts define the regions a page can fill and optional fragment defaults.
  </Card>

  <Card title="Runtime registry" icon="code">
    The registry pairs each component contract with the Astro or React component that renders it.
  </Card>
</CardGroup>

## Configure the site

The `.aci.yaml` file lives at the project root. A generated Astro project starts with this shape:

```yaml theme={null}
version: "1"
siteId: "my-site"
domain: "meridian.local"
framework: astro

source:
  root: "./"
  outDir: "dist"
  publicDir: "public"

componentRegistry: ./src/cms/contracts/components/index.ts
layoutRegistry: ./src/cms/contracts/layouts/index.ts

routes:
  cmsManaged: "/[...slug]"

rendererProtocol: stdio-json
```

The generated starter owns the framework integration around this file. In most projects, you only need to change the site identity, domain, source paths, or registry paths.

| Field | Purpose |
| - | - |
| `siteId` | Stable identifier for the site |
| `domain` | Domain associated with the site configuration |
| `framework` | `astro` or `next` |
| `source` | Project root and framework output locations |
| `componentRegistry` | Compile-time list of component contracts |
| `layoutRegistry` | Compile-time list of layout contracts |
| `routes.cmsManaged` | Catch-all route served from ACI content |
| `rendererProtocol` | Starter-managed renderer connection; keep the generated value unless your Gradial team directs otherwise |

<Warning>
  Keep `siteId` stable after the project is connected to an ACI site. Coordinate a change with your Gradial team so local configuration and hosted resources remain aligned.
</Warning>

## Define a component contract

A component contract gives a content block a stable ID and validates its props. Keep the schema in a contract file that does not import your runtime component.

```ts theme={null}
// src/cms/contracts/components/homeHero.contract.ts
import { defineComponentContract, ImageSchema } from '@gradial/aci';
import { z } from 'zod';

export const schema = z.object({
  eyebrow: z.string().optional(),
  headline: z.string().min(1),
  description: z.string().optional(),
  ctaLabel: z.string().min(1),
  ctaHref: z.string().min(1),
  image: ImageSchema.optional(),
});

export type ContentProps = z.infer<typeof schema>;

export const homeHeroContract = defineComponentContract({
  id: 'home_hero',
  props: schema,
  imageSlots: {
    image: {
      formats: ['image/avif', 'image/webp', 'image/jpeg'],
      sizes: '100vw',
      outputs: [
        {
          aspectRatio: '16:9',
          widths: [768, 1280, 1920],
          media: '(min-width: 768px)',
        },
        {
          aspectRatio: '3:4',
          widths: [480, 720, 960],
          media: '(max-width: 767px)',
        },
      ],
    },
  },
});
```

Use IDs that describe the component's role and keep them stable. Content documents refer to `home_hero` directly, so renaming the ID is a content migration rather than a cosmetic code change.

Export the contract from the component contract registry:

```ts theme={null}
// src/cms/contracts/components/index.ts
import { homeHeroContract } from './homeHero.contract';

const componentContracts = [homeHeroContract];

export default componentContracts;
```

## Keep contracts and runtime code separate

Contract extraction runs without loading your application runtime. That makes validation predictable across local development, preview, and publish.

| Location | May import | Avoid importing |
| - | - | - |
| `src/cms/contracts/**` | `@gradial/aci`, Zod, shared schema helpers | Astro or React components, CSS, browser APIs |
| `src/components/**` | Framework code, design-system code, CSS, browser behavior | Nothing is restricted by contract extraction |
| `src/cms/registry.ts` | Contracts and runtime components | Side effects unrelated to registration |

This separation does not duplicate your prop model. Export the inferred TypeScript type from the contract and use it in the runtime component.

```astro theme={null}
---
// src/components/sections/HomeHero.astro
import type { ContentProps } from '../../cms/contracts/components/homeHero.contract';

const { eyebrow, headline, description, ctaLabel, ctaHref } = Astro.props as ContentProps;
---

<section>
  {eyebrow && <p>{eyebrow}</p>}
  <h1>{headline}</h1>
  {description && <p>{description}</p>}
  <a href={ctaHref}>{ctaLabel}</a>
</section>
```

## Register the runtime component

The runtime registry pairs each contract with its implementation:

```ts theme={null}
// src/cms/registry.ts
import { createRegistry } from '@gradial/aci';
import { homeHeroContract } from './contracts/components/homeHero.contract';
import HomeHero from '../components/sections/HomeHero.astro';

export const registry = createRegistry([
  [homeHeroContract, HomeHero],
]);
```

The contract registry and runtime registry serve different jobs:

* `src/cms/contracts/components/index.ts` exposes schemas for compilation and validation.
* `src/cms/registry.ts` tells the framework which component to render after content has passed validation.

## Define a layout contract

Layouts name the regions available to a page. Mark a slot as required by passing `true` as the second argument to `slot`.

```ts theme={null}
// src/cms/contracts/layouts/index.ts
import {
  defineLayoutContract,
  fragmentDefault,
  slot,
} from '@gradial/aci';

const layoutContracts = [
  defineLayoutContract({
    name: 'default',
    slots: [
      slot('header', false),
      slot('main', true),
      slot('footer', false),
    ],
    defaults: {
      header: [fragmentDefault('navbar')],
      footer: [fragmentDefault('site-footer')],
    },
  }),
];

export default layoutContracts;
```

Here, every page must provide a `main` region. If a page does not provide `header` or `footer`, ACI uses the named fragments as defaults.

## Author content against the contract

A page names the layout, then places registered component blocks in its regions:

```json theme={null}
{
  "$type": "page",
  "id": "home",
  "status": "published",
  "layout": "default",
  "metadata": {
    "title": "Welcome",
    "description": "The home page for our site."
  },
  "regions": {
    "main": [
      {
        "$type": "component",
        "id": "hero",
        "component": "home_hero",
        "props": {
          "eyebrow": "Welcome",
          "headline": "A clear path from content to experience.",
          "description": "Every block is checked before it reaches your site.",
          "ctaLabel": "Learn more",
          "ctaHref": "/about/"
        }
      }
    ]
  }
}
```

During compilation, ACI checks that:

* the layout exists;
* region names match layout slots;
* required regions are present;
* component IDs exist in the contract registry;
* props satisfy the component's Zod schema; and
* referenced fragments and assets can be resolved.

Run the same checks locally:

```bash theme={null}
npm run content:compile
npm run typecheck
npm test
```

## Evolve a contract safely

Adding an optional prop is usually backward compatible. Making a field required, narrowing allowed values, or renaming a component ID can invalidate existing content.

For a breaking change:

1. Update the contract and runtime component together.
2. Update every affected page, fragment, and overlay.
3. Compile the full content tree.
4. Run the project's type and conformance checks.
5. Preview the change before it is approved and published.

This keeps the frontend contract useful as a release boundary, not just a TypeScript convenience.

***

*Next: [Framework guides →](/aci/developers/framework-guides)*


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.