CWA 2 is a prerelease (2.0.0-alpha), heading for a beta. It runs production sites; configuration can still change between releases, always with upgrade notes. View versions and support
Nuxt Module (Front-End)

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.

Paths in this section start at the project root of the components-web-app template. The Nuxt app is app/ and its source directory is also app/, so CWA's Vue files are in app/app/cwa/ and your own pages in app/app/pages/. In a Nuxt project of your own, drop the first app/. See Where things live.

The section overview, Nuxt Module, lists what each part of this section covers.

Install

Tagged releases are published to npm as @cwa/nuxt. Until a stable version exists, each pre-release is published as latest. This is what the template app depends on:

pnpm add @cwa/nuxt

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:

// app/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. A path such as ./node_modules/@cwa/nuxt/dist/layer works too, but the package name is the recommended form.

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. You don't need to list them yourself. If you do, use the module's major (see Nuxt SEO v6).

nuxt-og-image is registered only when the app installs its renderers, satori and @resvg/resvg-js. See Default OG Image.

Requirements

PackageVersionNotes
Nuxt>=4.6.0The module is developed and tested against Nuxt 4.6.2, Vue 3.5.44 and Vite 8, and accepts 4.6.0 or later. Nuxt warns if your app is on an older version
pinia^4.0.3A dependency of the module, so you don't need to install it
@pinia/nuxt1.0.2 exactlyA dependency of the module. If your app installs it too, Nuxt checks your copy against ^1.0.2
Nuxt SEO v6 modules@nuxtjs/sitemap 9, @nuxtjs/robots 7, nuxt-site-config 5, nuxt-seo-utils 9, nuxt-schema-org 7, nuxt-link-checker 6, nuxt-og-image 7Dependencies of the module. If your app lists any of them, or @nuxtjs/seo, it must use the same major. See Nuxt SEO v6
satori, @resvg/resvg-js>=0.47.1, ^2.6.2Optional. Install both for OG images
api-components-bundle2.0.0-alpha.25 or later; 2.0.0-alpha.32 for module 2.0.0-alpha.24; 2.0.0-alpha.35 for module 2.0.0-alpha.26; 2.0.0-alpha.36 for module 2.0.0-alpha.29See Bundle versions below. Update the module and the bundle together

The template app doesn't install Pinia itself, so it gets the right versions from the module. If your app installs @pinia/nuxt, install 1.0.2, the same version as the module's copy, so only one copy is installed.

If your app installs pinia or @pinia/nuxt itself, Nuxt checks that copy against the module's requirement and refuses to boot when it doesn't match:
Module `@pinia/nuxt` version (`0.11.3`) does not satisfy `^1.0.2`
Install the matching versions, pnpm add pinia@^4 @pinia/nuxt@1.0.2, or, if your own code doesn't import Pinia, remove them from your package.json and let the module supply them.

Bundle versions

The bundle release each module release needs, or needs for all of its behaviour. A later module keeps the earlier rows' requirements, so module 2.0.0-alpha.29 needs bundle 2.0.0-alpha.25 at least, and 2.0.0-alpha.36 for everything:

@cwa/nuxtapi-components-bundleWhy
2.0.0-alpha.30No new requirementFixes to co-editing in the rich-text editor
2.0.0-alpha.292.0.0-alpha.36 for checked saves; 2.0.0-alpha.37 for the Yjs editorAdmin saves and deletes send If-Match with _metadata.version, which bundle alpha.36 adds, and a 412 shows Use theirs / Keep mine. With an older bundle, saves aren't checked and crossing saves overwrite each other. Breaking: an API on another origin must allow If-Match in CORS. The Yjs rich-text editor stores its state in /_/collaboration_states, from bundle alpha.37. See Real-Time Updates › When You're Both Changing the Same Field and Rich-Text Editor › Typing at the Same Time
2.0.0-alpha.28No new requirementField presence and useCwaRealtime events use the admin real-time channel of bundle alpha.35
2.0.0-alpha.272.0.0-alpha.35 for who else is hereAdmins announce themselves with POST /_/realtime. With the endpoint missing or turned off, the module logs one warning and shows no one. See Real-Time Updates › Who Else Is Here
2.0.0-alpha.262.0.0-alpha.35 for co-editingThe module tells its own saves from another admin's, and puts updates in order, by the _metadata.lastWrite that bundle alpha.35 adds. With an older bundle, a run of an admin's own quick saves can come back as someone else's change. Breaking: admin writes send X-CWA-Client-Id, so an API on another origin must allow the header in CORS whatever the bundle version. See Real-Time Updates › Co-Editing in the Admin
2.0.0-alpha.25No new requirementThe placement menu for Clone and Move uses the same requests as alpha.24
2.0.0-alpha.242.0.0-alpha.32 for Move and the list of places behind the Shared badgeMove sends one POST {component}/move, which older bundles don't have, so Move doesn't work with an older bundle. The places come from GET {component}/usage, which lists them from bundle alpha.32; with an older bundle the Shared · N places badge is only a count. See The Admin Panel › Moving a Component and The Admin Panel › Shared Components
2.0.0-alpha.23No new requirementMove relocates a position with a PATCH of componentGroup and after/before, which bundle alpha.24 already accepts
2.0.0-alpha.222.0.0-alpha.30 for the full clone behaviour, see The Admin Panel › Cloning a ComponentThe API refuses to place a component inside its own subtree from bundle alpha.30. With an older bundle the module still greys those targets out, but the API accepts them
2.0.0-alpha.212.0.0-alpha.29 recommendedAnonymous Mercure updates and the cookie changes: bundle alpha.27 leaves an unpublished component out of a visitor's component position, and expires the Mercure cookie when a session ends. The module works with older bundles; see Real-Time Updates. Skip bundle alpha.27 and alpha.28: with a debug kernel (dev and test), publishing a component can return a 500 (Event "api_platform.processor.write" is not started), which alpha.29 fixes. Production without debug isn't affected
2.0.0-alpha.202.0.0-alpha.25Required. The page settings' Purge this page's cache sends a body to POST /_/http_cache/purge. An older bundle ignores the body and flushes the whole cache. See Purging One Page
2.0.0-alpha.192.0.0-alpha.24Required, both ways. The API orders component positions: the module sends after/before and reorders through the group's /order endpoint, which older bundles don't have. Bundle alpha.24 in turn needs module alpha.19, because it ignores the sortValue older modules write
2.0.0-alpha.10 to alpha.182.0.0-alpha.13Publishing sends "publishedAt": "now"
2.0.0-alpha.92.0.0-alpha.11For the Invalid files report; everything else works with 2.0.0-alpha.9
2.0.0-alpha.82.0.0-alpha.9For the Public without a route toggle
2.0.0-alpha.4 to alpha.72.0.0-alpha.8Live updates use the Mercure 1.0 protocol, which a 0.x hub rejects

Nuxt SEO v6

The module uses the Nuxt SEO v6 modules. If your app lists any of them in its package.json, use the same major, or two majors are installed side by side. The template app lists @nuxtjs/sitemap, on 9:

pnpm add @nuxtjs/sitemap@^9

What an app needs for them:

  • OG images need a secret. nuxt-og-image 7 signs its URLs, so production needs NUXT_APP_SECRET. See The OG image secret.
  • The renderers' minimums are satori 0.47.1 and @resvg/resvg-js 2.6.2.
  • Your own sitemap sources import defineSitemapEventHandler from #sitemap/server, and sitemap hooks get Nuxt's RequestEvent, which has no event.node. See Your own sitemap sources.
Registering @nuxtjs/seo 6 yourself also turns on nuxt-skew-protection and nuxt-ai-ready. Skew protection polls for new builds, and AI Ready serves llms.txt and similar files. Together they add about 5.5 KB gzip to every visitor's entry. The module doesn't register @nuxtjs/seo, so this only applies if your app does. To turn them off:
// app/nuxt.config.ts
export default defineNuxtConfig({
    skewProtection: { enabled: false },
    aiReady: { enabled: false },
})

Each release's bundle requirement is in Releases › The Module's Minimum Bundle.

Set the API URL

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: {
        cwa: {
            apiUrl: 'http://api-internal'                // server-side (SSR), private
        },
        public: {
            cwa: {
                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. Nuxt Config › Runtime Config has the environment variables and URL rules.

The other options live under the cwa: key. The key ones 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' }
    }
})

See Nuxt Config for the complete reference.

What You Get

You do not create these — the module ships them.

Pages (override by creating the same path in app/app/pages/):

  • The auth pages: /login, password reset and email verification. See Auth Pages
  • /_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

useCwa(), or $cwa in templates, is your entry point to everything the module manages:

const cwa = useCwa()

cwa.resources.page.value           // current page resource (undefined until resolved)
cwa.auth.signedIn.value            // boolean
cwa.admin.isEditing                // admin edit mode active (plain boolean — no .value)

The useCwa() API lists every service, and which values are refs. Which of them are available during server rendering is covered in Reading Auth State.

Behind these services the module keeps its state in its own Pinia stores: resources, fetcher, mercure, apiDocumentation, auth, admin, error and siteConfig. Reach them through useCwa() rather than directly. cwa.forms is a service, not a store.

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/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. definePageMeta CWA Options lists every key.

Disable CWA Route Fetching

Every page of your own that isn't CWA content must set cwa: { disabled: true }. The CWA route middleware runs on every navigation, and without the flag it asks the API for a Route at the page's path. There isn't one, so the visitor gets the error page:

<!-- app/app/pages/status.vue -->
<script setup lang="ts">
definePageMeta({
    cwa: { disabled: true }
})
</script>

With the flag, CWA doesn't fetch a route manifest or resolve any resources. Auth state and useCwa() are still available. The one exception is a page that loads a CWA page by IRI, which must leave the flag off.

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/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: {
        disabled: true,
        staticLayout: 'CwaLayoutPrimary',
    }
})
</script>

Load a CWA Page by IRI

A page of your own can render a CWA page instead of the one its path would route to. Use it when Nuxt owns a dynamic path, such as /resources/share/[slug], and a CWA page serves as its layout and template:

<!-- app/app/pages/resources/share/[slug].vue -->
<script setup lang="ts">
definePageMeta({
    layout: 'cwa-root-layout',
    cwa: {
        fetch: {
            iri: '/_/pages/<page id>',
            manifestPath: '/_/resource_manifest/<page id>', // optional
        },
    },
})
</script>

The module fetches that resource as the primary page instead of a Route for the path. manifestPath lets it fetch the page's resources in one batch; without it, they're fetched as they're found.

A page without a route is private when the API sets routable_security, as the template does. Visitors get a 401, and server-side rendering passes that on, while signed-in admins see the page working. Turn on Public without a route in the page's settings (isReachableWithoutRoute).

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.

Pre-Release Builds and Changelogs

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 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. The Releases page shows these notes with the other packages' releases, and which bundle release each module release needs. For the API bundle's changes, see Bundle Setup.