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.
DraftComponents

Creating Components

How to define a custom content resource by extending AbstractComponent, adding API Platform metadata, and running migrations.

A CWA component is a PHP entity that extends AbstractComponent. It defines the data model for one type of content block — a title, an image, an article body. The front-end Nuxt module resolves these by class name and renders the matching Vue component.

What AbstractComponent Gives You

You get these for free. Don't redefine them:

PropertyTypePurpose
idUuidInterfaceAuto-generated UUID identifier
uiComponentstringThe Vue component name to render (Title, Image, etc.)
uiClassNamesstring|nullCSS class name(s) chosen by the admin in the CMS
componentGroupsCollectionWhich component groups contain this instance
componentPositionsCollectionOrdered positions within those groups

The _metadata envelope in every API response carries rendering hints, media objects (for uploadable components), and publish state.

Step 1: Create the Entity

// src/Entity/Component/Title.php
namespace App\Entity\Component;

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

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

mercure: true on #[ApiResource] means every change is broadcast over the Mercure hub in real time — open browser sessions update without a page refresh.

Step 2: Run Migrations

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

The diff produces a migration that creates a joined table for your component (CWA uses JOINED inheritance on AbstractComponent). Always review before running.

URL Prefix Convention

The bundle automatically prefixes all component endpoints with /component/:

GET    /component/titles          # collection
POST   /component/titles          # create
GET    /component/titles/{id}     # single item
PATCH  /component/titles/{id}     # update
DELETE /component/titles/{id}     # delete

This prefix is applied by the bundle's metadata factory — no manual routePrefix needed unless you want to change it.

The Class Name → Vue Component Convention

The PHP class name determines which Vue component renders it:

PHP classAPI @typeVue component file
TitleTitleapp/cwa/components/Title/Title.vue
HeroImageHeroImageapp/cwa/components/HeroImage/HeroImage.vue
BlogCardBlogCardapp/cwa/components/BlogCard/BlogCard.vue

The Nuxt module resolves CwaComponentTitle from the @type in the API response. If a matching Vue component doesn't exist, the CMS shows a placeholder in edit mode.

A Complete Example

Here is a component with publish workflow, timestamps, and validation — equivalent to Title.php in the playground:

namespace App\Entity\Component;

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 Silverback\ApiComponentsBundle\Entity\Utility\TimestampedTrait;
use Symfony\Component\Validator\Constraints as Assert;

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

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

What the API Returns

A GET to /component/titles/{id} returns something like:

{
    "@context": "/contexts/Title",
    "@id": "/component/titles/018e-...",
    "@type": "Title",
    "title": "Welcome to CWA",
    "uiComponent": "Title",
    "uiClassNames": "centered",
    "publishedAt": "2024-01-15T10:30:00+00:00",
    "_metadata": {
        "publishable": {
            "published": true,
            "draftResource": null
        }
    }
}

The _metadata key is your component's runtime state envelope. It is never stored directly — the bundle computes it during serialization.

Restricting Positions

By default, a component can be placed anywhere. Override isPositionRestricted() to lock it to specific groups:

public function isPositionRestricted(): bool
{
    return true;
}

When true, the admin UI prevents dragging this component out of its designated group.