<CwaPage />
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:
| Parent | Child | URL shape |
|---|---|---|
| Events listing | Individual event | /events → /events/2024-conference |
| Blog | Article | /blog → /blog/my-first-post |
| Docs section | Page | /docs/api → /docs/api/authentication |
| Product category | Product detail | /shop/shoes → /shop/shoes/white-runners |
Without <CwaPage /> in the parent template, the child page has no position of its own: CWA appends it after the parent template's output (see Auto-Fallback).
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/composables/cwa-resource'
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/composables/cwa-resource'
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/composables/cwa-resource'
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/composables/cwa-resource'
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/composables/cwa-resource'
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:
| Key | Type | Value |
|---|---|---|
cwa-page-data-iri | ComputedRef<string | undefined> | IRI of the PageData record at this depth, if any |
cwa-page-own-depth | number | The depth level this template is rendering at |
cwa-page-depth | number | The 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, // registers a route for each depth 0–3, i.e. up to /a/b/c
pages: {
BlogListing: { name: 'Blog Listing' },
BlogCategory: { name: 'Blog Category' },
BlogPost: { name: 'Blog Post' }
}
}
pagesDepth controls how many route levels the module registers with Nuxt's router (default 4). It registers one route for each depth from 0 to pagesDepth, so pagesDepth: 3 covers URLs up to three segments deep (/blog/tech/my-post).
It is a build-time setting only — it doesn't affect what CWA fetches. Rendering depth itself comes from the resource manifest.
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.
Scroll Position
A link to a new page opens it at the top. Going Back returns to where you were, and a link to another page's #section scrolls to that section. CWA fetches a page's content after the page mounts, so for these two cases the module waits until the page is tall enough, or the target element exists. It waits for at most one second.
To keep the scroll position on one of your own Nuxt pages, set scrollToTop: false in its definePageMeta. An app that has its own app/router.options.ts with a scrollBehavior replaces the module's, and loses the wait.
@cwa/nuxt-edge 0.0.0-29836672.c74ef41 or later. Before it, going Back could land near the top, and a new page could open at the last page's scroll position.