Extension registry
A layer plugs its admin pieces into core with one function: definePlutoExtension. Core collects every layer contribution into one reactive registry. The admin renders from that registry.
Call it from a plugin
export default defineNuxtPlugin(() => {
definePlutoExtension({
id: 'supabase-blog',
nav: [
{ id: 'posts', label: 'Posts', icon: 'lucide:file-text', to: '/admin/posts' }
],
pages: [
{ id: 'posts', path: '/admin/posts', title: 'Posts', icon: 'lucide:file-text' }
],
dashboardWidgets: [
{ id: 'recent-posts', component: RecentPostsWidget, colSpan: 2 }
]
})
})
Use the $meta.name of the package as the extension id, for example supabase-blog.
The registrable things
Every field is optional. A layer sets only what it has.
| Field | Purpose |
|---|---|
navbarActions | Small buttons in the navbar. A component action or a link action. |
nav | Sidebar entries, with optional children and match. |
pages | Metadata (title, icon, parent) for a route that Nuxt already made from a file. |
dashboardWidgets | Cards on /admin/home. |
settingsPanels | Forms on /admin/settings. |
settingsPages | Tabs of the settings section. |
mailPanels | Sections on the Email settings page. |
settingsDriver | Load and save functions for settings storage. |
localeDriver | Load and save functions for the admin language of the user. |
authDriver | The sign-in backend of the admin. The last one wins. |
permissionsDriver | Loads the capability list of the user. |
capabilities | The capabilities that the layer declares. |
accountMenu | Extra items in the user menu. |
adminBanners | Banner components for the admin. |
navGroups | Groups of content types in the sidebar and the admin bar. See Nav groups. |
site | name() and url() of the public site. |
contentTypes | Content types. |
contentFieldWidgets | Widgets for content fields. |
contentActions | Buttons for the generic content form. |
mediaKinds | Kinds of files for the media library. See Media. |
contentToolbarItems | Components in the toolbar of the generic content form, such as a status. See Lists and forms. |
The sidebar groups
An entry with children is a group. The sidebar opens the group of the current page after each navigation. The user can close a group. A group with defaultOpen opens when it first shows.
A page belongs to a group when its path is the to of the group or of a child, or a page below one of them. Use match for more paths. A group at the basePath of a content type also gets the new page and the edit pages of that type. So a group at /admin/products opens on /admin/product/edit/7 when the editPath of the product type gives /admin/product/edit/<id>.
nav: [{
id: 'orders',
label: 'Orders',
to: '/admin/orders',
match: ['/admin/order'],
children: [{ label: 'All orders', to: '/admin/orders' }],
}]
A child can have enabled, a function like the enabled of an entry (see Entries). An item and a child can have newTo and newLabel: the new page of a content type. The sidebar then shows a + on hover.
An item with its own to and only one visible child is a plain link.
Section tabs
An item with two or more visible child pages gets section tabs. Show them at the top of a page of the section:
<template>
<PlutoAdminNavTabs />
</template>
The tabs are the children of the item of the current page. A child that is a new page (its to is the newTo of the item, or ends in /new) is not a tab. It is a button at the end of the tabs. variant="toolbar" gives the smaller tabs of a list toolbar. usePlutoAdminNavTabs() returns the same data (tabs, action, label) for a custom layout.
Nav groups
A navGroups entry is { id, label, icon, order }. The label is a translation key or text. Core declares one group, pluto:pages (Pages, order 100).
A content type joins a group with navGroup. Use the full id ('pluto:pages') or the bare id ('pages') when only one group has it.
- The sidebar shows the types of a group as one item. Its children are one link per type. A group with one visible type shows that type as a normal item. A type with an unknown group is a normal item too.
- The admin bar lists the types in one New menu. See New and Edit buttons.
definePlutoExtension({
id: 'shop',
navGroups: [{ id: 'catalog', label: 'Catalog', icon: 'i-lucide-store', order: 20 }],
})
Entries
Every entry has these fields:
id. It must be unique inside the extension. Core stores it as<extensionId>:<id>. Two layers can each use the idhomewith no clash.order. A lower number comes first. The default is 100. Core reserves0for Home and1000for Settings. A normal layer item sorts between them.enabled. An optional function. Returnfalseto hide the entry. It runs inside acomputed, so a value that it reads can show or hide the entry later.
Text: a key or a literal
Every visible string in an entry is a plain string. It can be an English literal or a translation key such as supabase-blog.nav.posts. The component that renders the entry translates the value only when it looks like a key and the key exists. Any other value stays as is.
A layer can move to keys one entry at a time. See Internationalization.
Ordering and replacement
Each registry bucket keeps the entries by extension id.
- Replace, not append. Calling
definePlutoExtensionagain with the sameidreplaces the whole contribution of that extension. A plugin that re-runs on HMR does not duplicate entries. - Stable order. The merged list sorts by
order, then by registration order for ties.
Drivers
A driver is a registered backend function set. The last registered driver wins. This holds for the settings, locale, auth, and permissions drivers, and for the media adapter.
A mail driver is different. It lives on the server. A Nitro plugin registers it with registerMailDriver. See Mail.
The admin menu on small screens
usePlutoAdminMenu() turns the nav entries into menu items. usePlutoAdminMenuOpen() holds the open state. On /admin/**, the admin sidebar opens. On a public page, PlutoRoot shows the same menu in a left slideover, with the admin theme.
Load components on demand
Register the components of contentFieldWidgets, dashboardWidgets, settingsPanels, mailPanels, and adminBanners with defineAsyncComponent. See Lazy loading.