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 — one array entry per selected style
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]
    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;
}

The Title:published validation group is added by the bundle only when a resource is #[Silverback\Publishable] — use it for constraints that should hold on the published version but not block saving a draft. See Publishable.

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,
            "publishedAt": "2024-01-15T10:30:00.000+00:00",
            "locationCount": 1
        }
    }
}

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

Restricting Where a Component Can Be Placed

By default, a component can be placed in any component group. To keep one out of the general add-component dialog, add the #[Silverback\ExplicitAllowOnly] attribute to the entity class — it can then only be added to groups that name it explicitly in their allowedComponents:

#[Silverback\ExplicitAllowOnly]
#[ORM\Entity]
#[ApiResource(mercure: true)]
class SectionDivider extends AbstractComponent
{
}

See Explicit Allow Only for the full reference.