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
Core Concepts

How It All Works

A high-level map of the CWA — what you get out of the box, what you write yourself, and how the three layers fit together.

The CWA is a framework for building content-managed websites where the content structure is managed through an API and the front-end renders whatever structure the API describes.

The key idea: you never write routing logic, CMS code, or admin UI. You only write the UI components — what they look like on the front end, and the form fields an admin uses to edit them. Everything else is handled for you.

The Three Layers

Your ProjectBackendYour entities & PageData+ API Components Bundle+ API Platform / SymfonyREST + MercureFrontendYour Vue UI templates+ @cwa/nuxt module+ Nuxt / Vue / Pinia

The API Bundle

Built on Symfony and API Platform. You get: a pre-built content data model, REST endpoints for all resources, JWT auth, file uploads, Mercure real-time, and a user system.

You write: custom component entities and dynamic page data entities.

The Nuxt Module

Handles routing (URL → API → Page → Layout → Components), server-side rendering, the inline admin CMS, all admin pages, and auth pages.

You write: layout Vue components, page template Vue components, and component Vue components (display + admin tabs).

The Template App

A starter project wiring both together with Docker Compose. components-web-app is a complete working site — Title, HtmlContent, Image, NavigationLink, Collection, an example form, blog articles and nested page data — and it's what pnpm create cwa scaffolds from. (The Nuxt module repo also carries its own development playground; that's for working on the module, not for starting a project.)

The Request Lifecycle

When a user visits /about-us:

Browser
Nuxt module
API
visit /about-us
In parallel
GET /_/routes//about-us
GET /_/resource_manifest//about-us
the Route, pointing to a Page (static) or a PageData record (dynamic)
the manifest: every resource the page renders
Concurrently
GET the layout, page, component groups, positions and components
renders the layout, then the page template in its slot, then each component as its data arrives
the rendered page
  1. Nuxt middleware calls the API: GET /_/routes//about-us, and in parallel GET /_/resource_manifest//about-us, which lists every resource the page renders. The double slash isn't a typo: the module appends the visitor's path, with its leading /, to the /_/routes/ endpoint
  2. The Route points to a Page (static) or PageData record (dynamic)
  3. The Page names its Layout (whose uiComponent is CwaLayoutPrimary) and its page template (CwaPagePrimaryPageTemplate)
  4. Your layout Vue file renders — it shows the header, footer, and a <slot /> for the page
  5. Your page template renders inside the slot — it places <CwaComponentGroup> component groups
  6. Each group fetches its ComponentPositions → Components from the API
  7. Each Component renders via the matching CwaComponent* Vue file

The whole tree fetches concurrently. The Pinia store updates reactively as data arrives.

What You Never Write

  • URL routing — routes are owned by the API, not defined in Vue files
  • CMS admin UI (built into the module)
  • Auth pages (provided automatically, and overridable with your own styles)
  • Sitemap generation
  • Real-time update handling

What CWA Is Built On

CWA adds a content model and an admin to well-known frameworks. You write ordinary code for each one, so their own docs apply to everything CWA doesn't change.

FrameworkWhat CWA uses it forIts docs
SymfonyThe API application the bundle installs into: config, services, console commands, securitysymfony.com/doc
API PlatformThe REST endpoints for every resource, with JSON-LD/Hydra, filters, pagination and validation. Your component entities are API Platform resourcesapi-platform.com/docs
Doctrine ORMEntities and the database. Components use JOINED inheritance, and schema changes are Doctrine migrationsdoctrine-project.org
MercureReal-time updates: the API publishes a change and open browsers receive itmercure.rocks/docs
LexikJWTAuthenticationBundleSign-in, with a JWT held in a cookiesymfony.com/bundles
NuxtThe front-end application the module installs into: server-side rendering, the layer, nuxt.confignuxt.com/docs
VueEvery layout, page template and component you write is a Vue single-file componentvuejs.org
PiniaThe module's store, which holds every resource it has fetchedpinia.vuejs.org
Tailwind CSSThe module's admin styles, kept in their own cascade layer (see Styling)tailwindcss.com/docs

The template app adds the services a site runs on:

ToolWhat the template uses it forIts docs
FrankenPHPThe PHP application server, built on the Caddy web serverfrankenphp.dev/docs
SouinThe HTTP cache in front of the API, as a Caddy moduledocs.souin.io
PostgreSQLThe databasepostgresql.org/docs
Docker ComposeThe local stack: php, app and databasedocs.docker.com/compose
Helm and KubernetesProduction deployshelm.sh/docs
Nuxt UIThe template's own Vue components and formsui.nuxt.com
TipTapThe rich-text editor that edits HtmlContent in place on the pagetiptap.dev/docs

Naming Conventions

A few names have to line up across the API and the front end. Everything else is up to you.

WhatConventionExample
A component typeThe PHP class name, the Vue folder and the display file are the same PascalCase nameTitle → app/app/cwa/components/Title/Title.vue
A component's endpointThe bundle prefixes it with /component/ and API Platform makes the plural, snake_case pathHtmlContent → /component/html_contents
A component's admin tabs and variantsOne file per tab in admin/, and alternative templates in ui/Title/admin/Title.vue, HtmlContent/ui/AltHtmlContent.vue
A layout's Vue fileAny name in app/app/cwa/layouts/, registered with the CwaLayout prefixprimary.vue → CwaLayoutPrimary
A page template's Vue fileAny name in app/app/cwa/pages/, registered with the CwaPage prefixBlogDetail.vue → CwaPageBlogDetail
A record's uiComponentThe full registered name, prefix includedCwaLayoutPrimary
nuxt.config keys under cwa.layouts and cwa.pagesThe name without the prefixPrimary, BlogDetail
The bundle's own tablesPrefixed with _acb_ by default (table_prefix)_acb_component_group
A resource's identityIts IRI, the path it's served at/component/titles/0190…

The details are in The Class Name › Vue Component Convention, How uiComponent Names Resolve and Where things live.

Next: The Data Model

The Data Model describes the records behind every page and how they chain together.