[data-reveal]{opacity:1!important;transform:none!important}
Building

Frontend layer

Author a custom Nuxt layer and understand the machine that loads it — component naming, overrides, layer config in Vue, the .acms cache, and the catch-all renderer.

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-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:

nuxt-layer/nuxt.config.ts
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/**")] },
This layout is one working example, not a requirement — a layer set can also be split, with a top-level 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:

  1. Register the exact same name. The auto-import name is prefix + filename (Component naming). To override the base <CmsFoo> — a base-layer Foo.vue registered with the Cms prefix — your file must also be named Foo.vue and registered with prefix: "Cms". A different prefix produces a different component, not an override.
  2. 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 highest priority number down to the lowest — so the smallest number has the last word. The base layer sits at priority: 0; beat it by passing a negative priority to your AddNuxtLayer call.

How layer config reaches Vue

AddNuxtLayer (Project setup) carries two config objects, and they land in different places:

src/index.ts
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)
});
  • options become public runtimeConfig.public[configKey] — readable in the browser.
  • privateOptions become server-only runtimeConfig[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():

nuxt-layer
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:

  1. useSiteLayout() loads the route tree and matches the current path to a page.
  2. It checks access and validation (404 if no page, redirect/403 on access denied).
  3. It fetches the serialized component tree for the page from the backend.
  4. It maps each node's componentName to a real component with resolveCmsComponent(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.
  5. 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-moduleantelopejs__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.