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 uiComponent value stored on the Layout API entity is the full registered component name — CwaLayoutPrimary for app/cwa/layouts/Primary.vue. Nuxt PascalCases the file name, so primary.vue also registers as CwaLayoutPrimary.
uiComponent with no prefix handling. A Layout entity storing Primary instead of CwaLayoutPrimary will not resolve. The admin panel always stores the full name for you.Minimal Layout
<!-- app/cwa/layouts/Primary.vue -->
<template>
<div>
<header>
<NuxtLink to="/">Home</NuxtLink>
</header>
<main>
<slot />
</main>
<footer>
<p>© 2024 My Site</p>
</footer>
</div>
</template>
<script setup lang="ts">
useCwaLayout()
</script>
No special props needed — the page content renders into the default <slot />.
Accessing Layout Data
useCwaLayout() gives you the layout resource and the style classes the admin selected:
const cwa = useCwa()
const { layout, uiClassNames } = useCwaLayout()
const isLoading = computed(() => cwa.resources.isLoading.value)
uiClassNames is a string[] of the CSS classes the admin selected from the options you register in nuxt.config. You don't need to bind it — the composable applies those classes to the layout's root element for you. Pass useCwaLayout({ autoClass: false }) if you'd rather bind uiClassNames yourself.
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: {
Light: 'theme-light',
Dark: 'theme-dark'
}
}
}
}
classes is a map of label → class string, keyed by the label the admin sees. A "Default" option (no classes) is added automatically — don't declare an empty entry for it.
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>
<!-- real content -->
</div>