Layouts
A CWA layout is the outer shell of your site — the header, footer, navigation, and any component groups that persist across pages. The PHP API has a Layout entity whose uiComponent value maps to your Vue file.
File Convention
app/cwa/layouts/Primary.vue → registered as CwaLayoutPrimary
The file name (without .vue) must match the uiComponent value stored in the Layout API entity. Capitalisation matters.
Minimal Layout
<!-- app/cwa/layouts/Primary.vue -->
<template>
<div :class="uiClassNames">
<header>
<NuxtLink to="/">Home</NuxtLink>
</header>
<main>
<slot />
</main>
<footer>
<p>© 2024 My Site</p>
</footer>
</div>
</template>
<script setup lang="ts">
const cwa = useCwa()
const layout = computed(() => cwa.resources.layout.value)
const uiClassNames = computed(() => layout.value?.data?.uiClassNames ?? '')
</script>
No special props needed — the page content renders into the default <slot />.
Accessing Layout Data
The layout resource is available through cwa.resources.layout:
const cwa = useCwa()
const layout = computed(() => cwa.resources.layout.value)
const uiClassNames = computed(() => layout.value?.data?.uiClassNames ?? '')
const isLoading = computed(() => cwa.resources.isLoading.value)
uiClassNames contains the CSS class name(s) the admin selected from the options you register in nuxt.config. Apply them to the layout's root element:
<div :class="uiClassNames">...</div>
Component Groups
Add named content regions that admins can populate with components. The reference is a stable name unique within this layout's IRI:
<template>
<div>
<header>
<CwaComponentGroup
reference="navigation"
:location="cwa.resources.layoutIri.value"
/>
</header>
<slot />
<footer>
<CwaComponentGroup
reference="footer"
:location="cwa.resources.layoutIri.value"
/>
</footer>
</div>
</template>
location must be the IRI of the owning entity — for layout groups, that's cwa.resources.layoutIri.value.
Auth-Conditional UI
Auth state is only available after client hydration. Always wrap auth-dependent UI in <ClientOnly> to avoid SSR hydration mismatches:
<ClientOnly>
<template #default>
<UserMenu v-if="$cwa.auth.signedIn.value" />
<NuxtLink v-else to="/login">Sign in</NuxtLink>
</template>
<template #fallback>
<!-- server-side: render nothing or a skeleton -->
<div class="w-20 h-8 bg-gray-200 rounded animate-pulse" />
</template>
</ClientOnly>
Registering Layout Options
Register your layout in nuxt.config to expose it in the admin panel and define which CSS classes an admin can apply:
// nuxt.config.ts
cwa: {
layouts: {
Primary: {
name: 'Primary Layout',
classes: [
{ value: '', label: 'Default' },
{ value: 'theme-light', label: 'Light' },
{ value: 'theme-dark', label: 'Dark' }
]
}
}
}
A layout that isn't registered in nuxt.config still renders correctly — it just has no name in the admin and no class options.
Multiple Layouts
Create one file per layout variant. A common setup:
app/cwa/layouts/
Primary.vue # full layout with header + footer
Minimal.vue # just a centered container (login pages, errors)
Landing.vue # marketing layout with large hero
Each is independently configurable in nuxt.config.layouts.
Loading State
Show a skeleton while the layout resource is loading. The layout loads first in the CWA fetch sequence, so this is typically only visible on the initial server-side render:
<div v-if="cwa.resources.isLoading.value" class="animate-pulse ...">
<!-- skeleton -->
</div>
<div v-else :class="uiClassNames">
<!-- real content -->
</div>