The CWA is in heavy development
The CWA is still in alpha and not ready for production - some code and implementations are likely to change. If you would like to try out the CWA, please enjoy what we have provided and feel free to provide feedback, or get involved on GitHub.
DraftGuides

Your First Component

Build a complete CWA component from scratch — the PHP entity, the Vue display template, and the admin editing tab.

A complete component has three parts:

  1. The PHP entity — defines the data stored in the API
  2. The Vue display component — how it looks on the front end
  3. The admin tab — the editing form shown in the inline CMS

We'll build a Title component — a simple heading with a text field.


Step 1: The PHP Entity

Create api/src/Entity/Title.php:

<?php

namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Silverback\ApiComponentsBundle\Annotation as Silverback;
use Silverback\ApiComponentsBundle\Entity\Core\AbstractComponent;
use Silverback\ApiComponentsBundle\Entity\Utility\PublishableTrait;
use Symfony\Component\Validator\Constraints as Assert;

#[Silverback\Publishable]
#[ApiResource(mercure: true)]
#[ORM\Entity]
class Title extends AbstractComponent
{
    use PublishableTrait;

    #[Assert\NotBlank(groups: ['Title:published'])]
    #[ORM\Column(type: 'text', nullable: true)]
    public ?string $title = null;
}

What each part does:

  • AbstractComponent — gives it an IRI, UUID, uiComponent field, and ComponentGroup support
  • #[Silverback\Publishable] + PublishableTrait — draft/publish workflow
  • #[ApiResource(mercure: true)] — creates REST endpoints and broadcasts real-time updates on change
  • #[ORM\Entity] — Doctrine entity (needs a database table)
  • Assert\NotBlank(groups: ['Title:published']) — only required when publishing, not when saving a draft

Run the migration:

bin/console doctrine:migrations:diff
bin/console doctrine:migrations:migrate

Step 2: The Vue Display Component

Create app/cwa/components/Title/Title.vue:

<template>
  <h1 class="text-4xl font-bold">
    {{ resource?.data?.title || 'No Title' }}
  </h1>
</template>

<script setup lang="ts">
import { toRef } from 'vue'
import type { IriProp } from '#cwa/composables/cwa-resource'
import { useCwaResource } from '#imports'

const props = defineProps<IriProp>()

const { getResource, exposeMeta } = useCwaResource(toRef(props, 'iri'))
const resource = getResource()

defineExpose(exposeMeta)
</script>

The required pattern explained

defineProps<IriProp>() — every display component receives an iri prop. The module passes this when it renders the component.

useCwaResource(toRef(props, 'iri')) — subscribes to the resource at that IRI. Returns getResource (a function that returns a Ref<Resource>) and exposeMeta (metadata the admin panel needs).

const resource = getResource() — the resource ref. It's undefined while loading, null if not found, and an object when ready.

defineExpose(exposeMeta)required. Without this the admin manager panel can't find and select this component. Never omit it.

resource?.data?.title — access your fields on resource.value.data. The ?. guards against the loading state.


Step 3: The Admin Tab

Create app/cwa/components/Title/admin/Title.vue:

<template>
  <CwaUiFormLabelWrapper label="Title">
    <CwaUiFormInput v-model="titleModel.model.value" />
  </CwaUiFormLabelWrapper>
</template>

<script setup lang="ts">
import { useCwaResourceManagerTab, useCwaResourceModel } from '#imports'

const { exposeMeta, iri } = useCwaResourceManagerTab({ name: 'Title' })
const titleModel = useCwaResourceModel<string>(iri, 'title')

defineExpose(exposeMeta)
</script>

How this works

useCwaResourceManagerTab({ name: 'Title' }) — registers this file as a tab in the manager panel. The tab label is 'Title'. Returns { exposeMeta, iri } where iri is a ref to the selected component's IRI.

useCwaResourceModel<string>(iri, 'title') — creates a two-way binding to the title field. Setting titleModel.model.value automatically sends a debounced PATCH to the API. The <string> type parameter is optional but helps with TypeScript.

v-model="titleModel.model.value" — binds the input directly. As the admin types, the API is updated automatically.

defineExpose(exposeMeta) — also required in admin tabs.


Step 4: Register in nuxt.config

// nuxt.config.ts
export default defineNuxtConfig({
  cwa: {
    resources: {
      Title: {
        name: 'Title',
        description: 'A heading or section title',
        instantAdd: false,
        defaultData: {
          title: 'New Title'
        }
      }
    }
  }
})

Without this the component still works, but it won't appear with a name and description in the "Add Component" dialog.


Step 5: Try It

  1. Open your local site in edit mode (log in as admin, click Edit)
  2. Navigate to a page with an editable component group
  3. Click the + hotspot at the top or bottom of the group
  4. Find Title in the dialog and add it
  5. Click the component — the manager panel appears at the bottom
  6. Click the Title tab and type something
  7. The heading updates live as you type
  8. Click Publish to make it live for all visitors

File Structure Summary


Multiple Admin Tabs

A component can have multiple admin tabs. Just create multiple files in the admin/ directory:

Each file is its own tab. Use the order option to control which appears first.


Next Steps