How It All Works
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
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:
- Nuxt middleware calls the API:
GET /_/routes//about-us, and in parallelGET /_/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 - The Route points to a Page (static) or PageData record (dynamic)
- The Page names its Layout (whose
uiComponentisCwaLayoutPrimary) and its page template (CwaPagePrimaryPageTemplate) - Your layout Vue file renders — it shows the header, footer, and a
<slot />for the page - Your page template renders inside the slot — it places
<CwaComponentGroup>component groups - Each group fetches its ComponentPositions → Components from the API
- 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.
| Framework | What CWA uses it for | Its docs |
|---|---|---|
| Symfony | The API application the bundle installs into: config, services, console commands, security | symfony.com/doc |
| API Platform | The REST endpoints for every resource, with JSON-LD/Hydra, filters, pagination and validation. Your component entities are API Platform resources | api-platform.com/docs |
| Doctrine ORM | Entities and the database. Components use JOINED inheritance, and schema changes are Doctrine migrations | doctrine-project.org |
| Mercure | Real-time updates: the API publishes a change and open browsers receive it | mercure.rocks/docs |
| LexikJWTAuthenticationBundle | Sign-in, with a JWT held in a cookie | symfony.com/bundles |
| Nuxt | The front-end application the module installs into: server-side rendering, the layer, nuxt.config | nuxt.com/docs |
| Vue | Every layout, page template and component you write is a Vue single-file component | vuejs.org |
| Pinia | The module's store, which holds every resource it has fetched | pinia.vuejs.org |
| Tailwind CSS | The 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:
| Tool | What the template uses it for | Its docs |
|---|---|---|
| FrankenPHP | The PHP application server, built on the Caddy web server | frankenphp.dev/docs |
| Souin | The HTTP cache in front of the API, as a Caddy module | docs.souin.io |
| PostgreSQL | The database | postgresql.org/docs |
| Docker Compose | The local stack: php, app and database | docs.docker.com/compose |
| Helm and Kubernetes | Production deploys | helm.sh/docs |
| Nuxt UI | The template's own Vue components and forms | ui.nuxt.com |
| TipTap | The rich-text editor that edits HtmlContent in place on the page | tiptap.dev/docs |
Naming Conventions
A few names have to line up across the API and the front end. Everything else is up to you.
| What | Convention | Example |
|---|---|---|
| A component type | The PHP class name, the Vue folder and the display file are the same PascalCase name | Title → app/app/cwa/components/Title/Title.vue |
| A component's endpoint | The bundle prefixes it with /component/ and API Platform makes the plural, snake_case path | HtmlContent → /component/html_contents |
| A component's admin tabs and variants | One file per tab in admin/, and alternative templates in ui/ | Title/admin/Title.vue, HtmlContent/ui/AltHtmlContent.vue |
| A layout's Vue file | Any name in app/app/cwa/layouts/, registered with the CwaLayout prefix | primary.vue → CwaLayoutPrimary |
| A page template's Vue file | Any name in app/app/cwa/pages/, registered with the CwaPage prefix | BlogDetail.vue → CwaPageBlogDetail |
A record's uiComponent | The full registered name, prefix included | CwaLayoutPrimary |
nuxt.config keys under cwa.layouts and cwa.pages | The name without the prefix | Primary, BlogDetail |
| The bundle's own tables | Prefixed with _acb_ by default (table_prefix) | _acb_component_group |
| A resource's identity | Its 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.