Client & deploy
Admin UI
The admin components you import, how to configure the sidebar, and what is still app-local.
@forge-cms/admin is not a mounted black box — it is a set of Angular components and route helpers you
compose into your own router. The URLs, the guards and the API locations stay yours.
Peer dependencies: @angular/cdk, @voltui/components, lumen-icons, rxjs (plus Angular itself).
Mounting the admin
The whole reusable admin — sign-in, shell, collections, the document editor and the users workspace — is route composition, with no host-written CRUD or auth code:
// admin.routes.ts — lazy-loaded under whatever path you choose, e.g. `{ path: 'admin', loadChildren }`
import type { Routes } from '@angular/router';
import {
ForgeAdminLayoutComponent,
ForgeUsersWorkspaceComponent,
forgeAdminAuthRoutes,
forgeAdminContentRoutes
} from '@forge-cms/admin';
import { forgeAuthGuard } from '@forge-cms/angular';
export const ADMIN_ROUTES: Routes = [
...forgeAdminAuthRoutes({ signup: false }), // login (+ signup when true)
{
path: '',
component: ForgeAdminLayoutComponent,
canActivate: [forgeAuthGuard()],
children: [
{ path: '', pathMatch: 'full', redirectTo: 'collections' },
...forgeAdminContentRoutes(), // collections, collections/:collection(/new|/:id)
{
path: 'users',
component: ForgeUsersWorkspaceComponent,
canActivate: [forgeAuthGuard({ roles: ['admin'] })]
}
]
}
];
forgeAdminContentRoutes() and forgeAdminAuthRoutes() return relative subtrees, so they work
wherever you nest them. /admin is only the default assumption the pieces make about where you did.
Mounting somewhere other than /admin
Say the admin lives at /studio (or a nested /ops/cms). Tell the pieces that name a URL where that is,
using the same path everywhere:
const MOUNT = '/studio';
export const ADMIN_ROUTES: Routes = [
...forgeAdminAuthRoutes({ signup: true, basePath: MOUNT }),
{
path: '',
component: ForgeAdminLayoutComponent,
data: { config: { title: 'Studio', basePath: MOUNT } satisfies ForgeAdminConfig },
canActivate: [forgeAuthGuard({ signInPath: `${MOUNT}/login`, forbiddenPath: MOUNT })],
children: [
{ path: '', pathMatch: 'full', redirectTo: 'collections' },
...forgeAdminContentRoutes(),
{
path: 'users',
component: ForgeUsersWorkspaceComponent,
canActivate: [
forgeAuthGuard({ roles: ['admin'], signInPath: `${MOUNT}/login`, forbiddenPath: MOUNT })
]
}
]
}
];
basePath(ForgeAdminConfig.basePathandforgeAdminAuthRoutes({ basePath })) is a same-app absolute path; a trailing slash is ignored. It roots the breadcrumbs, the built-in nav, the default sign-in path ({basePath}/login), and where sign-in/sign-up land. A return URL is honoured only if it isbasePathitself or a real child (/studio/...) — never/admin/...,/studio-evil,//hostorhttps://….forgeAdminAuthRoutesthrows on an invalid value.forgeAuthGuard()takes the existingsignInPath/forbiddenPathoptions (defaults/admin/loginand/admin).- The layout's config is also found by the collections index when you set it as route
data.configon the layout route, as above. - Where the APIs live is a separate matter and only
provideForgeCms()knows it:provideForgeCms({ baseUrl: '/content-api', authBaseUrl: '/account-api', credentials: 'include' }). The admin package contains no API URL of its own. - Use
withComponentInputBinding()inprovideRouter(...): routedatareaches the sign-in page and the layout as component inputs.
Configuring the shell
import { type ForgeAdminConfig } from '@forge-cms/admin';
const adminConfig: ForgeAdminConfig = {
title: 'Lumea Admin',
nav: [
{
label: 'Content',
items: [
{ label: 'Dashboard', routerLink: '/admin', icon: 'dashboard', exact: true },
{ label: 'Bookings', routerLink: '/admin/collections/bookings', icon: 'collections' },
{ label: 'Media', routerLink: '/admin/media', icon: 'media' }
]
},
{
label: 'Access',
items: [{ label: 'Users', routerLink: '/admin/users', icon: 'users', adminOnly: true }]
}
]
};
| Key | Default | Effect |
|---|---|---|
title |
ForgeCMS |
Sidebar heading. |
basePath |
/admin |
Mount root (above). |
nav |
DEFAULT_ADMIN_NAV |
Sidebar groups. Your links are used as written — spell the mount into them. |
collections |
all | Slugs ({ slug }) shown on the collections index, in that order. |
signInPath |
{basePath}/login |
"Log in" link and post-logout destination (e.g. a branded top-level /login). |
logo, features |
— | Deprecated, no effect. Kept for compatibility; removed after 1.0. |
The default nav lists only what the package itself mounts: Collections and an admin-only Users.
A dashboard, media library, API or settings page is yours — add it to nav (the dashboard, media,
api, settings and analytics icons are drawn by the package). adminOnly: true hides an item from
non-admins.
Document list
<forge-collection-list
[collection]="collection()"
[documents]="documents()"
[meta]="meta()"
[sort]="sort()"
[readOnly]="!canWrite()"
(create)="openCreate()"
(edit)="openEdit($event)"
(delete)="remove($event)"
(sortChange)="sort.set($event)"
(pageChange)="page.set($event)"
(statusChange)="publish($event)"
/>
Schema-driven: it renders columns from the collection metadata, with per-kind cells (dates, booleans,
relations, uploads as thumbnails, richtext as plain text), sorting, pagination, and — on a
drafts: true collection — a status column with publish-in-place.
Schema-driven form
<forge-collection-form
[fields]="collection().fieldDefinitions"
[initialValue]="editing() ?? {}"
[fieldErrors]="fieldErrors()"
submitLabel="Save"
(save)="submit($event)"
(cancel)="close()"
/>
The form is a loop over ForgeFieldControlComponent, which recurses into itself for group,
array and blocks — so arbitrary nesting renders, with add/remove row and a block-type picker.
Values flow strictly upward: a nested control emits, the owning branch merges into a fresh object and
re-emits, so the form value stays immutable.
Widgets: ForgeRelationPickerComponent (server-side search), ForgeUploadPickerComponent (preview,
upload, library), ForgeRichTextEditorComponent (block editor with a JSON fallback).
fieldErrors takes { fieldName: message } — map it from an ApiValidationError's details to
show server-side validation inline.
State components
PageHeaderComponent, LoadingStateComponent, ErrorStateComponent, EmptyStateComponent — so a
page that lists documents does not have to reinvent four states.
Helpers
import {
documentLabel,
documentImageUrl,
richTextToPlainText,
shortId,
truncate
} from '@forge-cms/admin';
documentLabel(doc) picks a human label from a document (title, name, filename, …) — useful in
relation pickers and breadcrumbs.
What is still app-local
Dashboards and the media, API and settings pages are not in the package — apps/www and
apps/demo-aesthetics have their own to copy as a starting point. The reusable users workspace
(ForgeUsersWorkspaceComponent) is in the package and needs only the auth-API routes. Forge Analytics
(forgeAdminAnalyticsRoutes()) is public but experimental and outside the 1.0 stability guarantee.
Still missing in the package: a WYSIWYG richtext editor, saved filters, bulk actions, configurable columns, conditional fields and live preview.
Building against it
Both admin packages compile in Angular's partial mode, so the consuming app's build must run the Angular linker. Use the one ForgeCMS ships — do not copy a private plugin:
// vite.config.ts
import { angularLinker } from '@forge-cms/admin/vite'; // or '@forge-cms/angular/vite' — the same plugin
export default defineConfig({ plugins: [angularLinker() /* , analog() … */] });
It needs @angular/compiler-cli, @babel/core and vite (all optional peers). Without the linker,
production AOT builds crash at runtime with Error: JIT compiler unavailable.