Frontend layer
Most admin screens need no frontend code — the backend describes them and cms-nuxt renders them (Architecture). A frontend layer is optional: you add one only when a screen needs a custom component the standard builders don't cover, or to override a component the DMS already ships. That code lives in a Nuxt layer, registered with AddNuxtLayer (Project setup) and served by the machine the Frontend CLI runs. Both sides live here: authoring the layer, and how the loader turns it into rendered pages.
Layout
A Nuxt layer uses this directory structure:
nuxt-layer/
nuxt.config.ts # component registration, auto-imports, i18n
app/
components/ # Vue SFCs (auto-imported with a prefix — see below)
composables/ # useXxx composables (auto-imported)
types/ # TS types (auto-imported)
i18n/
locales/*.json # one file per locale — see Localization
nuxt.config.ts registers the layer's components with a prefix:
components: {
dirs: [{
path: join(__dirname, "app/components"),
prefix: "YourModule", // your layer's component prefix
pathPrefix: false, // subdirectories do NOT contribute to the name
global: true,
}],
},
imports: { dirs: [join(__dirname, "app/composables/**"), join(__dirname, "app/types/**")] },
nuxt.config.ts that just extends sub-layers, each registering its own components and prefix. Structure yours however works best for you.Component naming
With pathPrefix: false, a component's auto-import name is <prefix> + filename only — Nuxt drops the subdirectory. A file at app/components/Editor/GraphCanvas.vue is <YourModuleGraphCanvas>, not <YourModuleEditorGraphCanvas>. Referencing the wrong name renders nothing — Vue does not warn on a missing component in a template, so it looks like an empty pane, not an error.
- New file
app/components/Foo/Bar.vue→ use<YourModuleBar>. - Each layer sets its own component prefix(es) in its
nuxt.config.ts; when copying a pattern from another layer, copy its prefix scheme verbatim. - A component that renders blank with no console error → suspect a name mismatch first.
Overriding an existing component
Layers compose — every loaded layer's components coexist, because each registers under its own prefix, so names from different layers never collide. Adding a layer adds components; by default it replaces nothing.
Replacing a component the DMS ships is therefore an explicit act — and the heavy one: for logos, colors, and design tokens, prefer Theming & branding. When you do need a real override, it takes two conditions — the same mechanic as overriding a file in any Nuxt layer:
- Register the exact same name. The auto-import name is
prefix+ filename (Component naming). To override the base<CmsFoo>— a base-layerFoo.vueregistered with theCmsprefix — your file must also be namedFoo.vueand registered withprefix: "Cms". A different prefix produces a different component, not an override. - Give your layer a negative
priority. When two layers register the same name, the last-loaded layer wins, and the DMS loads layers from the highestprioritynumber down to the lowest — so the smallest number has the last word. The base layer sits atpriority: 0; beat it by passing a negativepriorityto yourAddNuxtLayercall.
How layer config reaches Vue
AddNuxtLayer (Project setup) carries two config objects, and they land in different places:
await AddNuxtLayer({
name: "@antelopejs/my-module-nuxt-layer",
path: resolve(__dirname, "../nuxt-layer"),
configKey: "myModule",
options: { readOnly: true }, // PUBLIC → runtimeConfig.public.myModule
privateOptions: { apiKey: secret }, // SERVER → runtimeConfig.myModule (server-only)
});
optionsbecome publicruntimeConfig.public[configKey]— readable in the browser.privateOptionsbecome server-onlyruntimeConfig[configKey]— never sent to the client.
privateOptions reach the frontend only when the build presents the instance's nuxt.bootstrapSecret. Build without it and the key is simply absent from runtimeConfig[configKey], so read it defensively, or state the requirement in your module's setup docs. The layer's source directory is served through the same gate — it ships whole, server/ routes included, to whoever can fetch it.Read them in the layer with useRuntimeConfig():
const { readOnly } = useRuntimeConfig().public.myModule; // anywhere
const { apiKey } = useRuntimeConfig().myModule; // server-only (Nitro/server routes)
Frontend conventions
State is composable-based — there is no global store (no Vuex or Pinia). Follow the existing layer's composables (useXxx) rather than introducing one.
How the catch-all renders a page
A single route, [...slug].vue, renders every backend-defined page:
useSiteLayout()loads the route tree and matches the current path to a page.- It checks access and validation (404 if no page, redirect/403 on access denied).
- It fetches the serialized component tree for the page from the backend.
- It maps each node's
componentNameto a real component withresolveCmsComponent(name) || "div"— which looks the name up in the global Vue component registry and falls back to a"div"when nothing matches (in dev mode the resolver logs a[resolveCmsComponent] No globally registered component matches …warning to the browser console) — and recurses through the children. - It renders each top-level node with the recursive component, and wires page realtime, permission refresh, and page-setup functions.
That is why a backend edit changes the UI: the page is data the frontend draws, not a compiled Vue route.
The .acms layer cache is a physical copy
The acms CLI, not the backend, materializes every resolved layer as physical files under ~/.acms/<sha256(workspaceKey)>/layers/<safeName>/.... The workspace directory is the SHA-256 hash of the workspace key (the canonical backend URL, or project:<absolute project path> when dev auto-discovers the backend). The safeName is the layer's package name with a leading @ stripped and every / replaced by __ (e.g. @antelopejs/my-module → antelopejs__my-module). The frontend dev server serves from those copies, not from your module's nuxt-layer/ source.
In dev, acms dev runs file watchers that continuously resync source edits into the materialized copy — adds, changes, and deletes mirror in real time, not just on restart. So a .vue or composable edit normally propagates on its own. If a reload still doesn't show the change, suspect a stale copy.
This live-resync applies to every resolved layer, not just the ones you develop locally — layer inclusion itself is the AddNuxtLayer call.