Skip to main content
ACI doesn’t own your frontend. Your website code stays in your repository, written in whatever framework you choose. ACI integrates through a lightweight frontend contract, just three things your project exposes so ACI can build, validate, and render through it: configuration, content contracts, and a renderer entrypoint. Everything else, your build system, your CSS, your testing setup, your deploy scripts, stays exactly as it is.

The Three Parts

Config File

.aci.yaml tells ACI what framework you use, where contract files live, which routes are CMS-managed, and what render capabilities your site supports.

Contracts

Component and layout contracts define content names, Zod schemas, slots, render modes, and optional image slots.

Renderer Entry

ACI calls a GradialRenderer entrypoint with validated content. Your code returns rendered HTML and optional cache metadata.

Config File

.aci.yaml lives in the project root:
version: "1"
siteId: "site_gradial_aci_local_astro_starter"
framework: astro

source:
  root: "./"

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

capabilities:
  staticRender: true
  ssr: true
  ssrIslands: true
  clientIslands: true
  fragmentRender: true

routes:
  cmsManaged: "/[...slug]"
  frameworkOwned:
    - "/api/*"
    - "/_astro/*"

rendererProtocol: stdio-json
Supported framework values are astro, next, sveltekit, and custom.

Component Contracts

Component contracts live in code, but they should stay separate from runtime component implementations. The ACI compiler imports the contract tree to extract schemas and metadata, so contract files must not import Astro, React, Svelte, CSS, browser APIs, or framework runtime modules.
import { defineComponentContract } from '@gradial/aci';
import { z } from 'zod';

export const homeHeroContract = defineComponentContract({
  name: 'home_hero',
  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)
  }),
  renderModes: { canStatic: true, canSSR: true, canClientIsland: false }
});
Export contracts from src/cms/contracts/components/index.ts:
import { homeHeroContract } from './homeHero.contract';
import { ctaBandContract } from './ctaBand.contract';

export default [
  homeHeroContract,
  ctaBandContract
];
The contract serves three purposes:
  1. Validation: ACI validates authored JSON against the component schema. If an editor or agent puts the wrong shape in a field, the compiler catches it before anything ships.
  2. Rendering eligibility: Render modes tell ACI whether a block can be statically rendered, server-rendered, or hydrated as a client island.
  3. Agent guidance: Stable component names and schemas give AI agents a safe, well-described content surface to work against.
Schemas live in code, right next to the components that use them. The developers who build the components also define what content those components accept, so there’s no drift between CMS configuration and what your code actually expects.

Layout Contracts

Layouts declare named slots/regions that pages can fill:
import { defineLayoutContract, slot } from '@gradial/aci';

export default [
  defineLayoutContract({
    name: 'marketing',
    slots: [slot('main', true), slot('footer')]
  })
];
The page document’s layout must match a layout contract, and its regions keys must align with layout slots.

Image and DAM Fields

Use GradialImageSchema for DAM-backed image fields. Components that declare a GradialImageSchema field must also declare an imageSlots contract so ACI knows which derivatives to generate.
import { defineComponentContract, GradialImageSchema } from '@gradial/aci';
import { z } from 'zod';

export const heroContract = defineComponentContract({
  name: 'hero',
  schema: z.object({
    headline: z.string().min(1),
    image: GradialImageSchema
  }),
  renderModes: { canStatic: true, canSSR: true, canClientIsland: false },
  imageSlots: {
    image: {
      formats: ['image/avif', 'image/webp', 'image/jpeg'],
      sizes: '100vw',
      outputs: [
        { aspectRatio: '16:9', widths: [768, 1280, 1920] }
      ]
    }
  }
});
Source content references DAM assets with an asset reference:
{
  "$type": "dam.assetRef",
  "assetId": "ast_site_home_hero",
  "alt": "City skyline at dusk"
}
The content compiler resolves that reference into a render-ready image object in .aci/compiled.

Renderer Entry

The renderer entry exports a GradialRenderer:
import type { GradialRenderer } from '@gradial/aci';

const renderer: GradialRenderer = {
  async renderPage(request) {
    return {
      html: '<!doctype html><html><body>...</body></html>',
      status: 200,
      cachePolicy: { scope: 'public', ttl: 60 }
    };
  }
};

export default renderer;
The renderer receives a fully validated page payload, all references resolved, plus request and release context. Your code turns it into HTML. That’s the entire rendering interface. Framework adapters wrap this in Astro, Next.js, or SvelteKit rendering primitives so you write it the way your framework expects.

Code Capsules

When ACI builds your project, it produces a code capsule, a self-contained artifact that includes your built frontend, registry metadata, static client assets, and renderer entrypoint. The same capsule shape is used for preview and publish. Content can change independently from code, and ACI links a content branch to a code artifact when it needs to render or publish.

Framework Support

Astro

The recommended starting point. Static-first sites, component islands, and framework-native development.

Next.js

SSR-oriented sites through the App Router integration.

SvelteKit

SvelteKit rendering through the same contract shape, no special-casing required.
The content compiler and publish pipeline are framework-agnostic. Framework-specific code lives at the adapter boundary.

What ACI Extracts

ACI extracts a machine-readable registry:
  • Components: names, schemas, render modes, optional image slots
  • Layouts: layout names and slot requirements
  • Routes: CMS-managed and framework-owned route patterns from .aci.yaml
  • Capabilities: static, SSR, island, and fragment support
Keep compile-time contracts small and deterministic. Runtime component mapping, design-system imports, CSS, and browser behavior belong in your framework code, not in contract files.

Next: Framework Guides →