Skip to main content

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.

Framework Guides

Agentic Content Infrastructure (ACI) works with your existing frontend project. This page covers how to set up each supported framework, from installing the runtime to building your first component.
The aci-learn repository contains a complete Astro starter you can clone and run locally.

Astro

Astro is ACI’s recommended starting point. Its island architecture and static-first approach align naturally with ACI’s pre-compile model.

Setup

# Create a new Astro project or use an existing one
npm create astro@latest my-site
cd my-site

# Add the ACI runtime
npm install @aci/runtime zod

Config File

Create .aci.yaml in your project root:
# .aci.yaml
version: "1"
siteId: "your_site_id"
framework: astro

source:
  root: "./"

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

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

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

rendererProtocol: stdio-json

Component Registry

Define your components with Zod schemas in src/cms/components.ts:
// src/cms/components.ts
import { defineComponent } from '@aci/runtime';
import { z } from 'zod';

const imageSchema = z.object({
  src: z.string().min(1),
  alt: z.string().optional(),
});

export default [
  defineComponent({
    name: 'hero',
    schema: z.object({
      headline: z.string().min(1),
      subtext: z.string().optional(),
      image: imageSchema,
      cta: z.object({
        label: z.string().min(1),
        href: z.string().min(1),
      }).optional(),
    }),
    renderModes: { canStatic: true, canSSR: true, canClientIsland: false },
  }),

  defineComponent({
    name: 'product_card',
    schema: z.object({
      name: z.string().min(1),
      price: z.string().min(1),
      description: z.string().optional(),
      image: imageSchema,
    }),
    renderModes: { canStatic: true, canSSR: true, canClientIsland: true },
    defaultIslandMode: 'client',
  }),
];

Layout Registry

Define your page layouts in src/cms/layouts.ts:
// src/cms/layouts.ts
import { defineLayout } from '@aci/runtime';
import { z } from 'zod';

export default [
  defineLayout({
    name: 'marketing',
    schema: z.object({}),
    regions: ['main'],
  }),

  defineLayout({
    name: 'product',
    schema: z.object({
      showBreadcrumbs: z.boolean().optional(),
    }),
    regions: ['main', 'sidebar'],
  }),
];

Renderer Entry

Create the renderer that ACI calls to produce HTML in src/cms/renderer.ts:
// src/cms/renderer.ts
import { experimental_AstroContainer as AstroContainer } from 'astro/container';
import RenderPage from '../render/RenderPage.astro';
import type { GradialRenderer } from '@aci/runtime';

const renderer: GradialRenderer = {
  async renderPage(request) {
    const container = await AstroContainer.create();
    const html = await container.renderToString(RenderPage, {
      props: {
        input: {
          route: request.requestContext?.url || '/',
          page: request.page,
          siteConfig: request.siteConfig,
        }
      }
    });

    return {
      html,
      status: 200,
      cachePolicy: { scope: 'public', ttl: 60 }
    };
  }
};

export default renderer;

Component Implementation

Create your Astro component that matches the registry schema:
---
// src/components/sections/Hero.astro
interface Props {
  headline: string;
  subtext?: string;
  image: { src: string; alt?: string };
  cta?: { label: string; href: string };
}

const { headline, subtext, image, cta } = Astro.props;
---

<section class="hero py-20 bg-gradient-to-b from-indigo-50 to-white">
  <div class="max-w-6xl mx-auto px-4 grid md:grid-cols-2 gap-12 items-center">
    <div>
      <h1 class="text-5xl font-bold text-gray-900">{headline}</h1>
      {subtext && <p class="mt-4 text-xl text-gray-600">{subtext}</p>}
      {cta && (
        <a href={cta.href} class="mt-8 inline-block px-6 py-3 bg-indigo-600 text-white rounded-lg">
          {cta.label}
        </a>
      )}
    </div>
    <img src={image.src} alt={image.alt || ''} class="rounded-xl shadow-lg" />
  </div>
</section>

Block Registry (Component Mapping)

Map component names to their implementations in src/components/blockRegistry.ts:
// src/components/blockRegistry.ts
import Hero from './sections/Hero.astro';
import ProductCard from './blocks/ProductCard.astro';

export const blockRegistry: Record<string, any> = {
  hero: Hero,
  product_card: ProductCard,
};

Next.js

ACI supports Next.js with both the App Router and Pages Router. The same capsule interface works for SSG and SSR pages.

Setup

npm install @aci/runtime zod

Config

# .aci.yaml
version: "1"
siteId: "your_site_id"
framework: nextjs

source:
  root: "./"

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

capabilities:
  staticRender: true
  ssr: true

Component Example

// src/components/Hero.tsx
interface HeroProps {
  headline: string;
  subtext?: string;
  image: { src: string; alt?: string };
  cta?: { label: string; href: string };
}

export function Hero({ headline, subtext, image, cta }: HeroProps) {
  return (
    <section className="hero py-20 bg-gradient-to-b from-indigo-50 to-white">
      <div className="max-w-6xl mx-auto px-4 grid md:grid-cols-2 gap-12 items-center">
        <div>
          <h1 className="text-5xl font-bold text-gray-900">{headline}</h1>
          {subtext && <p className="mt-4 text-xl text-gray-600">{subtext}</p>}
          {cta && (
            <a href={cta.href} className="mt-8 inline-block px-6 py-3 bg-indigo-600 text-white rounded-lg">
              {cta.label}
            </a>
          )}
        </div>
        <img src={image.src} alt={image.alt || ''} className="rounded-xl shadow-lg" />
      </div>
    </section>
  );
}

SvelteKit

ACI supports SvelteKit with static prerendering and server-side rendering.

Setup

npm install @aci/runtime zod

Config

# .aci.yaml
version: "1"
siteId: "your_site_id"
framework: sveltekit

source:
  root: "./"

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

capabilities:
  staticRender: true
  ssr: true

Component Example

<!-- src/lib/components/Hero.svelte -->
<script lang="ts">
  export let headline: string;
  export let subtext: string | undefined = undefined;
  export let image: { src: string; alt?: string };
  export let cta: { label: string; href: string } | undefined = undefined;
</script>

<section class="hero py-20 bg-gradient-to-b from-indigo-50 to-white">
  <div class="max-w-6xl mx-auto px-4 grid md:grid-cols-2 gap-12 items-center">
    <div>
      <h1 class="text-5xl font-bold text-gray-900">{headline}</h1>
      {#if subtext}
        <p class="mt-4 text-xl text-gray-600">{subtext}</p>
      {/if}
      {#if cta}
        <a href={cta.href} class="mt-8 inline-block px-6 py-3 bg-indigo-600 text-white rounded-lg">
          {cta.label}
        </a>
      {/if}
    </div>
    <img src={image.src} alt={image.alt || ''} class="rounded-xl shadow-lg" />
  </div>
</section>

Local Development

All frameworks work with ACI’s local development loop:
# Start the local ACI server (runs in background)
aci serve

# In another terminal, start your framework's dev server
npm run dev

# Or use the combined command
aci dev
This gives you:
  • Hot-reloading preview at localhost:4321 (Astro) or your framework’s default port
  • Content files in .content/ that you can edit directly
  • Instant feedback — edit JSON content, refresh the page, see changes

Content Structure

Content lives in .content/ as JSON files:
.content/
├── config/
│   └── site.json          # Site-wide settings (nav, footer, etc.)
└── pages/
    ├── home/
    │   └── _index.json    # Homepage content
    └── product/
        └── widget/
            └── _index.json # /product/widget page content

Example Page Content

{
  "id": "home",
  "$type": "page",
  "status": "published",
  "layout": "marketing",
  "metadata": {
    "title": "Welcome to My Site",
    "description": "The best site ever built."
  },
  "regions": {
    "main": [
      {
        "id": "hero-1",
        "component": "hero",
        "props": {
          "headline": "Build faster with ACI",
          "subtext": "Content infrastructure that gets out of your way.",
          "image": {
            "src": "https://example.com/hero.jpg",
            "alt": "Hero image"
          },
          "cta": {
            "label": "Get started",
            "href": "/docs"
          }
        }
      }
    ]
  }
}
Edit the JSON, refresh your browser, and see the changes immediately.

Key Concepts

defineComponent

Every component in your registry is defined with defineComponent():
defineComponent({
  name: 'hero',                    // Matches "component" field in content JSON
  schema: z.object({...}),         // Zod schema for validation
  renderModes: {
    canStatic: true,               // Can render at build time
    canSSR: true,                  // Can render on server per-request
    canClientIsland: false,        // Can hydrate on client
  },
  defaultIslandMode: 'static',     // Default if not specified in content
})

defineLayout

Layouts define the structure of pages:
defineLayout({
  name: 'marketing',               // Matches "layout" field in content JSON
  schema: z.object({...}),         // Layout-level props (optional)
  regions: ['main', 'sidebar'],    // Named slots for components
})

GradialRenderer

Your renderer implements the GradialRenderer interface:
interface GradialRenderer {
  renderPage(request: RenderPageRequest): Promise<RenderPageResponse>;
}

interface RenderPageRequest {
  page: PageContent;               // The validated page content
  siteConfig?: SiteConfig;         // Site-wide configuration
  requestContext?: {
    url: string;                   // The request URL
    headers?: Record<string, string>;
  };
}

interface RenderPageResponse {
  html: string;                    // The rendered HTML
  status: number;                  // HTTP status code
  cachePolicy?: {
    scope: 'public' | 'private';
    ttl: number;                   // Seconds
  };
}

Next: CLI Reference →