Your First Component
A complete component has three parts:
- The PHP entity — defines the data stored in the API
- The Vue display component — how it looks on the front end
- 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,uiComponentfield, 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
- Open your local site in edit mode (log in as admin, click Edit)
- Navigate to a page with an editable component group
- Click the + hotspot at the top or bottom of the group
- Find Title in the dialog and add it
- Click the component — the manager panel appears at the bottom
- Click the Title tab and type something
- The heading updates live as you type
- 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
- Add an image to your component → Images & Media Guide
- Add style variants → Alternative UI Variants Guide
- Add rich text editing → HTML Content Guide (coming soon)