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
referencestringYes—Name of the component group within this resource. Determines which ComponentGroup entity this region maps to.
locationstringNo—IRI of the parent resource that owns this group (your layout, page, or component IRI). Optional so it can be bound to a value that resolves later, such as layoutIri.value: the group waits until it is set, so no v-if guard is needed.
location-referencestringNo—Overrides the location half of the group's lookup key, which is resolved as {reference}_{locationReference ?? location}. Use it when the group should be keyed by a stable reference rather than the owning resource's IRI — that's how one group is shared across several resources. Matches the $locationReference argument on the fixture builders' ->group().
allowed-componentsCwaAllowedComponent[] | nullNo—The component types admins may add to this group, by component name ([CwaComponentNames.HtmlContent] or ['HtmlContent']) or by collection IRI ('/component/html_contents'). When passed, it's synced to the ComponentGroup entity in the API whenever a user is signed in. null or [] allows every type. When omitted, the group's stored list is left alone. See Allowed Components.

Basic Usage

Every layout, page template, and component that has editable content regions uses <CwaComponentGroup>. Pass the owning page template's or 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/composables/cwa-resource'
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="$cwa.resources.layoutIri.value" />
    </header>

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

    <footer>
      <CwaComponentGroup reference="footer" :location="$cwa.resources.layoutIri.value" />
    </footer>
  </div>
</template>

<script setup lang="ts">
const $cwa = useCwa()
useCwaLayout()
</script>
What you pass as location depends on where the group is:
  • Page template or component: props.iri.
  • Layout: $cwa.resources.layoutIri.value. Layouts are rendered without an iri prop.
While an admin is editing, a component that has a draft is rendered under its draft IRI. The group resolves location to the component's published IRI itself, so its contents stay in place. On module builds before cwa-nuxt-module#317 it didn't, and a component had to pass publishedIri from useCwaComponent() instead. That still works, but it's no longer needed.

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 { useCwaComponent } from '#imports'
import type { IriProp } from '#cwa/composables/cwa-resource'
const props = defineProps<IriProp>()
const { exposeMeta } = useCwaComponent(props)
defineExpose(exposeMeta)
</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/composables/cwa-resource'
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: list the component names in the template. The module syncs the list to the API entity whenever a user is signed in (the API enforces who may actually write), creating or updating the group as needed:

<CwaComponentGroup
  reference="hero"
  :location="iri"
  :allowed-components="[CwaComponentNames.HeroBanner, CwaComponentNames.VideoBlock]"
/>

CwaComponentNames is auto-imported. It has one entry for each component folder with a matching file, app/cwa/components/<Name>/<Name>.vue, so your editor completes the names and vue-tsc rejects a typo or a component you've removed. A plain string works the same way, ['HeroBanner', 'VideoBlock'], and is type-checked against the CwaComponentName type.

A name is the API resource's short name, the same name as the component's folder. The module looks up each name's collection endpoint in the API docs at runtime, so the Vue file and the API entity must have the same name.

If a name matches no component in the API, the module logs [CWA] allowedComponents was not synced: the API has no component named … and leaves the group's stored list unchanged.

Collection IRIs still work, alone or mixed with names: ['/component/hero_banners', CwaComponentNames.VideoBlock].

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.

Added in @cwa/nuxt 2.0.0-alpha.2: component names, CwaComponentNames, and syncing groups that have no stored list. In 2.0.0-alpha.2 an undefined entry, such as a CwaComponentNames member for a component that doesn't exist yet, throws. From 2.0.0-alpha.3 (cwa-nuxt-module#354) it warns and leave the group unsynced, as they do for an unknown name.

What the prop's value means

ValueResult
A listThe group allows exactly these types. The list must name every type already placed in the group, plus any opt-in type you want to offer. A placed component whose type is missing from the list fails validation the next time its position is saved.
OmittedThe stored list is left as it is, so a list set by fixtures or the REST API is kept.
null or []No restriction: every type can be added, except opt-in types. [] is saved as null.
Upgrading: earlier module builds never synced a list to a group that had none stored, such as a group created by fixtures or the REST API. Now the first signed-in load PATCHes those groups with the template's list. Before you upgrade, check that every list in your templates names all the types already placed in its group.

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 it to the group's allowedComponents by name or collection IRI (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].