Module Setup
@cwa/nuxt is the front-end half of a CWA application. It connects your Nuxt application to a Symfony API powered by API Platform, providing automatic route resolution, a reactive resource store, real-time updates via Mercure, authentication pages, a full admin CMS panel, and composables for every content pattern.
You own your Vue templates — CWA drives what data they receive and in what order they render.
Installation
Tagged releases are published to npm as @cwa/nuxt, starting with 2.0.0-alpha.1. Until a stable version exists, each pre-release is published as latest. This is what the template app depends on:
pnpm add @cwa/nuxt
Every commit to the module's dev branch is also published as @cwa/nuxt-edge. To use a change that isn't in a tagged release yet, alias the edge package to @cwa/nuxt, so your imports and config stay the same:
pnpm add @cwa/nuxt@npm:@cwa/nuxt-edge
Tagged releases, from 2.0.0-alpha.1 on, are listed in the module's CHANGELOG.md and on its GitHub releases, which use the same notes. Changes on dev that aren't in a release yet are under Unreleased in the dev branch's copy. For the API bundle's changes, see Bundle Setup.
CWA ships as a Nuxt layer, not just a module. Extend the layer in nuxt.config.ts — it registers the module for you, along with the /login and password-reset pages, the /_cwa/* admin panel, and the cwa-root-layout:
// nuxt.config.ts
export default defineNuxtConfig({
extends: ['@cwa/nuxt/layer'], modules: [
'@nuxt/ui', // optional — for your own components; CWA doesn't require it
'@nuxt/image' // optional — only if you use <NuxtImg>
]
})
Extend it by the package name, @cwa/nuxt/layer, not by a path such as ./node_modules/@cwa/nuxt/dist/layer. With pnpm, that path goes through a symlink, and a Nuxt bug then stops Nuxt leaving the layer's pages out of the prefetch list. Every public page would prefetch the admin panel and auth pages: on the template, 85 prefetch links instead of 24. The package name resolves to the real path, so the bug doesn't arise.
@cwa/nuxt/layer export needs @cwa/nuxt-edge0.0.0-29836407.6c33a6e or later. On an older build, keep the path form. The path form still works on newer builds too: the module resolves the symlink in production builds as a temporary workaround (cwa-nuxt-module#329), but switch to the package name when you can.@cwa/nuxt to modules. extends is what wires it up — registering the module directly skips the layer, and you lose the auth pages, the admin panel and the root layout.The module declares @pinia/nuxt, @nuxtjs/robots, @nuxtjs/sitemap, nuxt-og-image, nuxt-schema-org, nuxt-seo-utils, nuxt-site-config and nuxt-link-checker as module dependencies, so Nuxt resolves and registers them for you. Don't list them yourself.
Requirements
The module is developed and tested against Nuxt 4.5 and Vite 8. It requires Pinia 4 — pinia ^4.0.3 and @pinia/nuxt ^1.0.2. Both are dependencies of the module, so a project that doesn't install Pinia itself (the template app doesn't) gets the right versions automatically.
@pinia/nuxt 0.11? If your app installs pinia or @pinia/nuxt itself, Nuxt checks that copy against the module's requirement and refuses to boot:Module `@pinia/nuxt` version (`0.11.3`) does not satisfy `^1.0.2`
pnpm add pinia@^4 @pinia/nuxt@^1 — or, if your own code doesn't import Pinia, remove them from your package.json and let the module supply them.Minimum Configuration
Set your API URL via runtimeConfig. The browser URL is used client-side; the server URL is used during SSR (can be an internal Docker network address):
export default defineNuxtConfig({
runtimeConfig: {
public: {
cwa: {
apiUrl: 'http://api-internal', // server-side (SSR)
apiUrlBrowser: 'https://api.example.com' // client-side
}
}
}
})
In a Docker Compose setup these are typically different — the internal hostname resolves only within the Docker network.
Full cwa: Config Reference
See Nuxt Config for the complete reference. The key options at a glance:
export default defineNuxtConfig({
cwa: {
resources: { /* your CMS component types */ },
layouts: { /* your layout component types */ },
pages: { /* your page template component types */ },
pageData: { /* your PageData resource classes */ },
pagesDepth: 2, // nested page depth (default: 4)
siteConfig: { siteName: 'My App' }
}
})
What the Module Auto-Provides
You do not create these — the module ships them:
Pages (override by creating the same path in app/pages/):
/login— email + password login form/forgot-password— request a password reset email/reset-password/[username]/[token]— set a new password/verify-email/[username]/[token]— verify email on registration/confirm-new-email/[username]/[newEmail]/[token]— confirm email change/_cwa/*— the full admin panel
Middleware:
- Route resolution on every navigation — fetches the manifest and resolves the layout, page, and page data IRIs from the current URL
- Auth state initialisation on the client — the
cwa_authcookie flags whether a session exists, and the signed-in user is loaded from the API's/meendpoint cwa-authandcwa-admin— named route middleware you opt into per page to require a signed-in user or an admin. See Protecting Pages
Pinia stores (created by the module; most are reached through the useCwa() services rather than directly):
resources— all fetched resource data, keyed by IRIfetcher— in-flight fetch state and the route/resource fetch chainmercure— real-time subscription stateapiDocumentation— the API's Hydra documentationauth— authentication state and methodsadmin— edit-mode and navigation-guard stateerror— API errors surfaced to the admin UIsiteConfig— site-wide settings fetched from the API
cwa.forms is a service, not a store.
The useCwa() Composable
useCwa() (or $cwa in templates) is your entry point to everything the module manages:
const cwa = useCwa()
cwa.resources.layout.value // current layout resource
cwa.resources.page.value // current page resource (undefined until resolved)
cwa.resources.pageData.value // current page data (dynamic pages)
cwa.auth.signedIn.value // boolean
cwa.auth.isAdmin.value // boolean (roles include ROLE_ADMIN)
cwa.auth.user // current user object (reactive — no .value)
cwa.siteConfig.config.siteName // the site name from API settings (no .value)
cwa.admin.isEditing // admin edit mode active (plain boolean — no .value)
Mixing Your Own Pages with CWA
CWA's catch-all route covers / and all paths. But you can still create regular Nuxt pages alongside it — your app/pages/ files take precedence over CWA's dynamic routing.
For pages you write yourself, use definePageMeta to control how much of CWA runs on that page:
Disable CWA route fetching
If your page has nothing to do with CWA content, disable the middleware entirely:
<!-- app/pages/status.vue -->
<script setup lang="ts">
definePageMeta({
cwa: { disabled: true }
})
</script>
CWA will not fetch a route manifest or resolve any resources. Auth state and useCwa() are still available.
Use a static layout component
If you want a fixed layout component instead of the one resolved from the API's Layout resource, name it with staticLayout. The value is a registered component name, such as one of your app/cwa/layouts/ components (Primary.vue registers as CwaLayoutPrimary). It is rendered inside cwa-root-layout, so it is not a Nuxt layout name. To change the Nuxt layout, set layout in definePageMeta as usual.
<script setup lang="ts">
definePageMeta({
cwa: { staticLayout: 'CwaLayoutPrimary' }
})
</script>
Use the CWA root layout on your own page
Your own pages already use cwa-root-layout, because the module applies it (or your layoutName) to every page that doesn't set a layout of its own. Setting it explicitly makes that visible and gives you the admin header and edit-mode overlay on a hand-coded page:
<script setup lang="ts">
definePageMeta({
layout: 'cwa-root-layout',
cwa: { disabled: true } // disable CWA content fetching but keep the layout
})
</script>
This is useful for pages like account settings or checkout flows that are not CMS-managed but should still appear inside the site shell and support admin edit mode for other parts of the page.
TypeScript Support
The module exports types for all component patterns:
import type { IriProp } from '#cwa/composables/cwa-resource'
// Every CWA component receives :iri as a prop
defineProps<IriProp>()
#cwa is an alias the module registers for its runtime directory — that's the import path to use, not a @cwa/nuxt/* subpath (the package only exports its root entry).
API resource data is dynamically shaped (resource.data is typed as Record<string, any> or similar). Under strict mode TypeScript may flag index access on these types as potentially undefined, even in contexts where you've checked for it. If you encounter these conflicts and don't want to add ? or type assertions throughout your component code, set typescript: { strict: false } in nuxt.config.ts — Nuxt generates the real tsconfig.json, so configure it there. This is what the template app does, alongside typeCheck: true. Alternatively, keep strict mode on and use optional chaining (resource.data?.title) consistently.