Core

Extension registry

Plug a layer into the admin with one function.

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

app/plugins/pluto-registrations.ts
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.

FieldPurpose
navbarActionsSmall buttons in the navbar. A component action or a link action.
navSidebar entries, with optional children and match.
pagesMetadata (title, icon, parent) for a route that Nuxt already made from a file.
dashboardWidgetsCards on /admin/home.
settingsPanelsForms on /admin/settings.
settingsPagesTabs of the settings section.
mailPanelsSections on the Email settings page.
settingsDriverLoad and save functions for settings storage.
localeDriverLoad and save functions for the admin language of the user.
authDriverThe sign-in backend of the admin. The last one wins.
permissionsDriverLoads the capability list of the user.
capabilitiesThe capabilities that the layer declares.
accountMenuExtra items in the user menu.
adminBannersBanner components for the admin.
navGroupsGroups of content types in the sidebar and the admin bar. See Nav groups.
sitename() and url() of the public site.
contentTypesContent types.
contentFieldWidgetsWidgets for content fields.
contentActionsButtons for the generic content form.
mediaKindsKinds of files for the media library. See Media.
contentToolbarItemsComponents 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.

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 id home with no clash.
  • order. A lower number comes first. The default is 100. Core reserves 0 for Home and 1000 for Settings. A normal layer item sorts between them.
  • enabled. An optional function. Return false to hide the entry. It runs inside a computed, 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 definePlutoExtension again with the same id replaces 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.