CWA 2 is a prerelease (2.0.0-alpha), heading for a beta. It runs production sites; configuration can still change between releases, always with upgrade notes. View versions and support
Building Your Ui

Page Templates

How to create CWA page template components that place a page's component groups using CwaComponentGroup and CwaPage.
New to page templates? Start with the Your First Page Template guide. This page is the reference.

A page template defines the structure of a page — the component groups where admins add and manage content. The PHP Page entity's uiComponent field maps to your Vue file. For dynamic pages (isTemplate: true), many routes share one template but each route points to its own PageData record.

File Convention

app/app/cwa/pages/Primary.vue    →  CwaPagePrimary
app/app/cwa/pages/BlogDetail.vue →  CwaPageBlogDetail

The Page's uiComponent holds the full registered name, CwaPageBlogDetail. The API adds the CwaPage prefix if a page is saved with the short name, so the admin and the fixture builder store the same value. See How uiComponent Names Resolve.

Every Page Component Receives :iri

The module passes the page IRI as a prop. Declare it:

import type { IriProp } from '#cwa/composables/cwa-resource'
defineProps<IriProp>()

The iri prop is the IRI of the current Page entity. Pass it as the location to any CwaComponentGroup.

Minimal Page Template

<!-- app/app/cwa/pages/Primary.vue -->
<template>
    <main>
        <CwaComponentGroup reference="content" :location="iri" />
    </main>
</template>

<script setup lang="ts">
import type { IriProp } from '#cwa/composables/cwa-resource'
defineProps<IriProp>()
</script>

Multiple Component Groups

A real page usually has several groups, such as hero, main body and sidebar:

<template>
    <div>
        <section class="hero">
            <CwaComponentGroup reference="hero" :location="iri" />
        </section>

        <div class="grid lg:grid-cols-3 gap-8">
            <div class="lg:col-span-2">
                <CwaComponentGroup reference="main" :location="iri" />
            </div>
            <aside>
                <CwaComponentGroup reference="sidebar" :location="iri" />
            </aside>
        </div>
    </div>
</template>

<script setup lang="ts">
import type { IriProp } from '#cwa/composables/cwa-resource'

defineProps<IriProp>()
</script>

The style classes an admin selects for the page are applied to your page component's root element automatically — you don't bind them yourself.

Dynamic Pages: Accessing Page Data

When the page is a template (isTemplate: true), cwa.resources.pageData contains the current record's data:

const cwa = useCwa()

const pageData = computed(() => cwa.resources.pageData.value)
const headline = computed(() => pageData.value?.data?.headline)
const createdAt = computed(() => pageData.value?.data?.createdAt)

The module sets the <title> and <meta name="description"> from the page data and page for you, so you don't need useHead for the basics. See Per-Page SEO for how they are chosen and how to override them.

Registering Pages

// nuxt.config.ts
cwa: {
    pages: {
        Primary: {
            name: 'Standard Page',
            classes: {
                'Full Width': 'full-width'
            }
        },
        BlogDetail: {
            name: 'Blog Article'
        }
    }
}

classes is a map of label → class string. A "Default" option is added automatically, and you omit classes entirely when a template has no style variants.

Loading Bar

Put <CwaLoadingIndicator /> in app/app/app.vue, above <NuxtLayout>:

<!-- app/app/app.vue -->
<template>
    <CwaLoadingIndicator />    <NuxtLayout>
        <NuxtPage />
    </NuxtLayout>
</template>

It wraps Nuxt's <NuxtLoadingIndicator> and passes its props through, such as color, height, throttle and duration. On a CWA page the bar shows how much of the page's content has loaded, and it never goes back when the load finds more resources, such as a route's page data. On your own Nuxt pages it shows Nuxt's usual timed estimate. One bar covers both.

The module holds Nuxt's page:loading:end hook until the new CWA page shows. So the bar finishes, and the page scrolls to the top, when the new page is on screen, not while the old one is still showing. Anything your app hooks on page:loading:end runs then too. Pages with definePageMeta({ cwa: { disabled: true } }) are never held.

Don't render <CwaUiProgressBar> as well. It's driven by pageLoadProgress and still works, but <CwaLoadingIndicator> replaces it. With both, two bars show.
VIDEO: a client-side navigation between two CWA pages, with the loading bar filling as the page's components load and finishing as the new page appears

Page Load State

const isPageLoading = computed(() => cwa.resources.isLoading.value)
const progress = computed(() => cwa.resources.pageLoadProgress.value)
// { total: 8, complete: 5, percent: 63, resources: [...IRIs] }
// percent is rounded to a whole number

<CwaLoadingIndicator> already shows percent. Read it yourself only for your own loading UI.

Nested Pages

If your pages have a parent–child URL structure — events listing → event detail, blog → post, docs section → page — add <CwaPage /> wherever the child should render inside the parent template. It works like <NuxtPage /> but for CWA's depth-aware hierarchy.

See <CwaPage /> for complete working examples, the inject key reference, and configuration.