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.
Cwa Components

<CwaComponentGroup />

Render an ordered list of CMS-managed components within a named region of your layout, page, or component.

<CwaComponentGroup> is the primary building block of every CWA page. Place it inside any layout, page template, or component to create a named content region that admins can populate with components in the CMS.

<CwaComponentGroup reference="hero" :location="iri" />

Props

PropTypeRequiredDefaultDescription
referencestringYesName of the component group within this resource. Determines which ComponentGroup entity this region maps to.
locationstringYesIRI of the parent resource that owns this group (your layout, page, or component IRI).
allowed-componentsstring[] | nullNonullCollection endpoints of the component types admins may add to this group (e.g. ['/component/hero_banners', '/component/text_blocks']). All CWA component endpoints are prefixed with /component/. When set, the value is synced to the ComponentGroup entity in the API whenever an admin is signed in. null allows all types.

Basic Usage

Every layout, page template, and component that has editable content regions uses <CwaComponentGroup>. Pass the component's own iri prop as location:

<!-- app/cwa/pages/HomePage.vue -->
<template>
  <div>
    <CwaComponentGroup reference="hero" :location="iri" />
    <CwaComponentGroup reference="features" :location="iri" />
    <CwaComponentGroup reference="cta" :location="iri" />
  </div>
</template>

<script setup lang="ts">
import type { IriProp } from '@cwa/nuxt/runtime/composables'
defineProps<IriProp>()
</script>

The reference name is how the admin CMS identifies the region. It can be any string — keep it short and descriptive. Multiple groups on the same resource each need a unique reference.

In a Layout

<!-- app/cwa/layouts/PrimaryLayout.vue -->
<template>
  <div class="min-h-screen flex flex-col">
    <header>
      <CwaComponentGroup reference="navigation" :location="iri" />
    </header>

    <main class="flex-1">
      <!-- CWA renders the current page template here -->
      <slot />
    </main>

    <footer>
      <CwaComponentGroup reference="footer" :location="iri" />
    </footer>
  </div>
</template>

<script setup lang="ts">
import type { IriProp } from '@cwa/nuxt/runtime/composables'
defineProps<IriProp>()
</script>

In a Component

Components can themselves contain component groups, enabling nested composition:

<!-- app/cwa/components/TwoColumn/TwoColumn.vue -->
<template>
  <div class="grid grid-cols-2 gap-8">
    <div>
      <CwaComponentGroup reference="left" :location="iri" />
    </div>
    <div>
      <CwaComponentGroup reference="right" :location="iri" />
    </div>
  </div>
</template>

<script setup lang="ts">
import type { IriProp } from '@cwa/nuxt/runtime/composables'
defineProps<IriProp>()
</script>

How It Works

When <CwaComponentGroup> mounts it:

  1. Looks up the ComponentGroup entity for {reference}_{location IRI} in the resource store
  2. Reads the ordered list of ComponentPosition entities from that group
  3. Renders each ComponentPosition's component using the uiComponent field as the Vue component name
  4. In admin edit mode, wraps each component with selection handles and the add-component button

The group reference is stable — it identifies the same region across environments as long as the location IRI is the same.

Events

<CwaComponentGroup> emits two events so you can react when its content is ready or changes — useful for fade-ins, analytics, or gating a "content ready" state.

EventFiresPayload
@components-loadedOnce, when every one of the group's own positions has resolved to a persisted component in a terminal API state{ component, position }[]
@components-updatedOn later persisted changes to the loaded set — an add, publish, or remove (debounced){ component, position }[]

Each payload entry pairs a component IRI with the ComponentPosition IRI that placed it:

<template>
  <CwaComponentGroup
    reference="gallery"
    :location="iri"
    @components-loaded="onLoaded"
    @components-updated="onLoaded"
  />
</template>

<script setup lang="ts">
import type { IriProp } from '@cwa/nuxt/runtime/composables'
defineProps<IriProp>()

function onLoaded(pairs: { component: string, position: string }[]) {
  // e.g. reveal the section, fire analytics, mark "content ready"
  console.log(`${pairs.length} components ready`, pairs)
}
</script>
What counts toward "loaded":
  • Temporary/unpersisted components (a draft being added in the admin) are excluded — they never fire either event or appear in a payload.
  • A component that errored or is absent still counts as terminal (so components-loaded never hangs waiting on it), but it is omitted from the payload.
  • An empty but loaded group fires components-loaded once with an empty array.
  • Scope is this group's own positions only — a nested <CwaComponentGroup> emits its own events independently.

Allowed Components

The API can restrict which component types are allowed in a specific group. The restriction is enforced in two places:

  • Write-side (admin UI) — components not in the allowed list are hidden from the "Add Component" dialog for that group.
  • Read-side (rendering)ComponentPosition entries whose resolved component type is not in allowedComponents are omitted from the API response entirely. This means a component that was positioned before allowedComponents was set (or whose type was later removed from the list) will silently not render.

Via the Vue prop — pass collection endpoint paths directly in the template. The module syncs this to the API entity whenever an admin is signed in, creating or updating the group as needed:

<CwaComponentGroup
  reference="hero"
  :location="iri"
  :allowed-components="['/component/hero_banners', '/component/video_blocks']"
/>

Via fixtures — pass an array of PHP FQCNs as the second argument to ->group():

$cwa->layout('primary', 'PrimaryLayout')
    ->group('hero', [App\Entity\HeroBanner::class, App\Entity\VideoBlock::class]);

Via REST API — send PHP FQCNs in allowedComponents when creating or updating a ComponentGroup. The API normalizes them to collection IRIs automatically:

{
  "allowedComponents": [
    "App\\Entity\\HeroBanner",
    "App\\Entity\\VideoBlock"
  ]
}

The normalizer converts these FQCNs to their /component/ collection endpoints (e.g. /component/hero_banners) before persisting.

The allowedComponents field is also returned when reading a Layout or Page with embedded component groups.

Opt-in component types (explicitAllowOnly)

allowedComponents is a per-group allow list — leaving it null allows every component type. Sometimes you want the inverse for a specific type: a component that should never appear in a group unless that group explicitly opts in. Mark the component entity with #[Silverback\ExplicitAllowOnly]:

use Silverback\ApiComponentsBundle\Annotation as Silverback;
use Silverback\ApiComponentsBundle\Entity\Core\AbstractComponent;

#[Silverback\ExplicitAllowOnly] #[ORM\Entity]
#[ApiResource]
class SectionDivider extends AbstractComponent
{
    // ...
}

Once flagged, the type is opt-in everywhere:

  • It is hidden from the "Add Component" dialog for any group that does not list it in allowedComponents.
  • It is rejected on save by the API — a ComponentPosition pointing at it in a group that doesn't allow it fails validation. This is enforced for both directly placed components and dynamic page-data positions, so the dynamic path can't bypass the rule.

To use the component, add its collection IRI to the group's allowedComponents (via any of the three methods above). A group whose allowedComponents is null still allows all non-flagged types, but flagged types must always be listed by name.

The flag is surfaced to the front-end as a bare explicitAllowOnly: true boolean on the component's Hydra supportedClass entry in the API docs, which is how the admin UI knows to filter it out of the add dialog.

For the full attribute reference and server-side enforcement details, see #[Silverback\ExplicitAllowOnly].