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

<CwaPage />

Render nested page levels inside a page template — CWA's equivalent of <NuxtPage />.

In standard Nuxt, <NuxtPage /> is how you tell the router where to mount the next matched route's page component. <CwaPage /> fills the same role in CWA: place it inside a page template to render whatever page or PageData record the router has resolved at the next depth level.

Unlike <NuxtPage />, you never manage route nesting in definePageMeta — the CWA API owns the hierarchy. <CwaPage /> reads from the manifest and renders the right template automatically.

When You Need It

Any time URLs have a parent–child relationship:

ParentChildURL shape
Events listingIndividual event/events/events/2024-conference
BlogArticle/blog/blog/my-first-post
Docs sectionPage/docs/api/docs/api/authentication
Product categoryProduct detail/shop/shoes/shop/shoes/white-runners

Without <CwaPage /> in the parent template, navigating to a child URL loads the correct data but has nowhere to render it.

A Complete Example

Events listing with a detail page. The listing template has content regions of its own; the detail template reads custom fields from the event's PageData record.

Parent template

<!-- app/cwa/pages/EventsList.vue -->
<template>
  <div>
    <CwaComponentGroup reference="hero" :location="iri" />
    <CwaComponentGroup reference="events-grid" :location="iri" />

    <!--
      Renders the child event page.
      Empty when at /events; renders EventDetail at /events/2024-conference.
    -->
    <CwaPage />
  </div>
</template>

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

Child template

<!-- app/cwa/pages/EventDetail.vue -->
<template>
  <article>
    <header class="mb-8">
      <h1 class="text-4xl font-bold">{{ title }}</h1>
      <time v-if="date" class="text-neutral-500">{{ formattedDate }}</time>
    </header>
    <CwaComponentGroup reference="content" :location="iri" />
  </article>
</template>

<script setup lang="ts">
import { inject, computed } from 'vue'
import type { ComputedRef } from 'vue'
import type { IriProp } from '@cwa/nuxt/runtime/composables'

defineProps<IriProp>()
const cwa = useCwa()

// Injected by <CwaPage /> — the IRI of the EventData record at this depth
const pageDataIri = inject<ComputedRef<string | undefined>>('cwa-page-data-iri')

const eventData = computed(() => {
  if (!pageDataIri?.value) return null
  return cwa.resources.getResource(pageDataIri.value).value?.data
})

const title = computed(() => eventData.value?.title ?? '')
const date = computed(() => eventData.value?.date as string | null ?? null)

const formattedDate = computed(() => {
  if (!date.value) return ''
  return new Intl.DateTimeFormat('en-GB', { dateStyle: 'long' }).format(new Date(date.value))
})
</script>

The pattern is the same at every depth: inject cwa-page-data-iri, read via getResource(), derive computed properties from .data.

Three Levels Deep

<CwaPage /> stacks — a mid-level template can include its own <CwaPage /> to render the next level down. A blog with listing → category → post looks like this:

Listing (depth 0) — shell with no PageData

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

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

Category (depth 1) — reads its own PageData, renders child posts

<!-- app/cwa/pages/BlogCategory.vue -->
<template>
  <section>
    <h2 class="text-2xl font-semibold">{{ name }}</h2>
    <p class="text-neutral-500">{{ description }}</p>
    <CwaPage />
  </section>
</template>

<script setup lang="ts">
import { inject, computed } from 'vue'
import type { ComputedRef } from 'vue'
import type { IriProp } from '@cwa/nuxt/runtime/composables'

defineProps<IriProp>()
const cwa = useCwa()

const pageDataIri = inject<ComputedRef<string | undefined>>('cwa-page-data-iri')

const data = computed(() => {
  if (!pageDataIri?.value) return null
  return cwa.resources.getResource(pageDataIri.value).value?.data
})

const name = computed(() => data.value?.name ?? '')
const description = computed(() => data.value?.description ?? '')
</script>

Post (depth 2) — leaf node, no <CwaPage />

<!-- app/cwa/pages/BlogPost.vue -->
<template>
  <article>
    <h1 class="text-4xl font-bold">{{ title }}</h1>
    <CwaComponentGroup reference="body" :location="iri" />
  </article>
</template>

<script setup lang="ts">
import { inject, computed } from 'vue'
import type { ComputedRef } from 'vue'
import type { IriProp } from '@cwa/nuxt/runtime/composables'

defineProps<IriProp>()
const cwa = useCwa()

const pageDataIri = inject<ComputedRef<string | undefined>>('cwa-page-data-iri')

const title = computed(() => {
  if (!pageDataIri?.value) return ''
  return cwa.resources.getResource(pageDataIri.value).value?.data?.title ?? ''
})
</script>

The inject key pattern is identical at every depth. <CwaPage /> always provides cwa-page-data-iri scoped to its own level, so BlogCategory and BlogPost both use the same inject without any depth arithmetic.

Auto-Fallback

If your page template doesn't include <CwaPage /> but the router has resolved a child page at the next depth level, CWA automatically appends one after your template renders. No child page is silently dropped.

This kicks in when depthCount > currentDepth + 1 and no explicit <CwaPage /> was registered inside the template. The injected instance uses :auto-fallback="true" internally, which prevents it from self-registering as a child (which would cause a loop).

The practical effect: intermediate templates that don't care about positioning the child can omit <CwaPage /> entirely and the child will still render — appended after the template's own output. For control over where the child appears, include <CwaPage /> explicitly.

What <CwaPage /> Provides

When <CwaPage /> renders a child template, it injects three keys available to that template and all descendants:

KeyTypeValue
cwa-page-data-iriComputedRef<string | undefined>IRI of the PageData record at this depth, if any
cwa-page-own-depthnumberThe depth level this template is rendering at
cwa-page-depthnumberThe depth for a nested <CwaPage /> inside this template

cwa-page-data-iri is undefined when the depth has a static sub-page with no PageData (e.g. /about/team as a Page child of /about).

Configuration

// nuxt.config.ts
cwa: {
  pagesDepth: 3,  // supports depth 0, 1, 2 (three URL segments)
  pages: {
    BlogListing: { name: 'Blog Listing' },
    BlogCategory: { name: 'Blog Category' },
    BlogPost: { name: 'Blog Post' }
  }
}

pagesDepth controls how many levels the module pre-fetches from the manifest. Three URL segments (/blog/tech/my-post) means pagesDepth: 3.

How Navigation Works

Navigating between child pages re-renders only the <CwaPage /> subtree. The parent layout, page shell, and component groups outside <CwaPage /> stay mounted, making in-section navigation feel instant even for server-rendered pages.

For the API side of nested pages — setting parentPage / parentPageData in PHP, route auto-prefixing, cascading renames, and the resource manifest format — see Dynamic & Nested Pages.