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.
DraftNuxt Module

Module Setup

Installing the CWA Nuxt module, extending its Nuxt layer, and wiring up the nuxt.config options.

@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.

The @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.
Don't add @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.

Upgrading from a release that used @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`
Bump both packages when you take the new module version — 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_auth cookie flags whether a session exists, and the signed-in user is loaded from the API's /me endpoint
  • cwa-auth and cwa-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 IRI
  • fetcher — in-flight fetch state and the route/resource fetch chain
  • mercure — real-time subscription state
  • apiDocumentation — the API's Hydra documentation
  • auth — authentication state and methods
  • admin — edit-mode and navigation-guard state
  • error — API errors surfaced to the admin UI
  • siteConfig — 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.