Features Apograph CMS on GitHub

Admin plugins

Routes, layout and slot contributions, plus the data-layer conventions the admin expects.

Documents 0.5.2 Updated Edit this page Report a problem

On this page

An admin plugin is a plain object:

type AdminPlugin = {
    name: string;
    routes?: RouteItem[];
    layout?: ReactNode;
    slots?: SlotContribution[];
};
export const reviewsPlugin: AdminPlugin = {
    name: 'reviews',
    routes: [
        {
            path: '/workspaces/:id/reviews',
            element: lazy(() => import('./presentation/pages/ReviewsPage'))
        }
    ],
    slots: [
        {
            slot: WORKSPACE_NAV_SLOT,
            items: [{ id: 'reviews', label: 'Reviews', to: 'reviews', permission: 'reviews:read' }]
        }
    ]
};

Routes

Routes are flattened from every plugin and split by a public flag. Public routes mount as top-level siblings; everything else mounts under one pathless parent whose element is the contributed layout.

Code-split them. A route element should be lazily imported, so a page nobody opens is not in the initial bundle.

Layout

The shell plugin contributes the layout, and it is the one that composes identity’s auth provider and gate.

A second layout takes the auth gate with it

Two plugins contributing a layout is “first one wins”, decided by registration order. The host warns and names both, but it cannot choose for you.

This is the collision that bites: the shell’s layout is what composes RequireAuth, the sidebar, the skip link and the <main> landmark. A layout registered ahead of it takes all of that, and every private route renders ungated.

Do not contribute a layout from a feature plugin.

Two plugins claiming the same route path are the same “first one wins” with the same warning.

Slots

A slot is a named extension point another plugin defined. Contributions are data, wired once at boot and read sorted.

See slots for the catalogue and the semantics.

The data layer

The convention across the admin plugins:

  • One gateway per plugin — the single place apiClient is used, holding every request function. Nothing else in the plugin imports the HTTP client.
  • TanStack Query hooks call the gateway, never apiClient directly.
  • A mapper converts wire shapes to view shapes — the anti-corruption layer.
  • Query keys live in one file so invalidation is not guesswork.

The larger plugins are laid out in four layers — domain, application, infrastructure, presentation — with domain holding pure TypeScript, no React and no transport. content/admin and workspaces/admin are the reference implementations.

Permission gating

const canPublish = useHasPermission('content:publish');

Gate the navigation item and the component. A nav section has to know whether it has any visible children before it renders its own heading, or a user without the permission gets a heading sitting over nothing.

Gating the UI is a courtesy. The server enforces it regardless.

Copy is localized

Each component co-locates its own messages:

const messages = defineMessages({
    title: { id: 'reviews.title', defaultMessage: 'Reviews' }
});

There is no shared messages.ts. The host provides the single IntlProvider.

Pitfalls the review checklist calls out

These are real bugs that have shipped in this codebase, and they are worth knowing before writing your first page.

PitfallWhat happens
Not clamping the page after a mutationDeleting the last row of page 4 leaves you on an empty page 4
Conflating error and emptyA failed request renders as “nothing here yet”, and the user creates a duplicate
A mapper with a silent fallbackMissing data becomes plausible-looking wrong data
Over-invalidatingOne save refetches half the app
Under-invalidatingOne save leaves a sibling record showing pre-save values

That last pair is a genuine tension. A write returns the canonical record, so seed the cache with it rather than invalidating the read — but invalidate the other cached records of the type, because one save can rewrite rows it did not name: the localization sync writes a shared field to every locale sibling.

  • Slots — the extension points available.
  • Testing — the admin end-to-end harness.

The admin-plugin and accessibility skills in the repository are the full checklists.