Guides

Create a layer

Build your own Pluto layer and plug it into the admin.

A Pluto layer is a Nuxt layer. It ships source files, and it plugs into the admin with definePlutoExtension.

Layout

A layer has these folders. Use only the ones that you need.

Step 1: the Nuxt config

Name the layer with $meta.name. This name is also the extension id and the i18n namespace.

nuxt.config.ts
export default defineNuxtConfig({
  extends: ['@plutocms/core'],

  $meta: {
    name: 'my-layer'
  }
})

Extend only the layers that your layer builds on. Each of them already extends its own upstream layers, so you do not list those again.

Your layer builds onPut in extendsYou get too
The admin only@plutocms/coreNothing
Supabase@plutocms/supabase@plutocms/core

For example, @plutocms/supabase-blog extends @plutocms/supabase. It does not extend @plutocms/core, because @plutocms/supabase already does. Add the layers that you extend to the dependencies of the package.json of your layer, with a version range.

An app that uses your layer extends only your layer. See Layers.

Do not register a css entry that imports tailwindcss or @nuxt/ui. See step 5.

Mark a backend layer

A layer that brings its own backend, such as a database and sign-in, must switch the standalone Git CMS off. Set plutoBackend in $meta:

nuxt.config.ts
export default defineNuxtConfig({
  $meta: {
    name: 'my-backend',
    plutoBackend: true
  }
})

With plutoGithub.enabled: 'auto' (the default), the CMS stays off and logs nothing. With plutoGithub.enabled: true, the module logs an error and stays off. The supabase layer, and the @nuxtjs/supabase module, count as a backend without this marker.

Use the local checkouts of other layers

Your layer reads process.env.PLUTO_<NAME>_PATH || '@plutocms/<name>' for each layer that it extends. The name is in upper case, with - changed to _. For @plutocms/core, the variable is PLUTO_CORE_PATH. For @plutocms/supabase-shop, it is PLUTO_SUPABASE_SHOP_PATH.

nuxt.config.ts
const coreLayer = process.env.PLUTO_CORE_PATH || '@plutocms/core'

export default defineNuxtConfig({
  extends: [coreLayer]
})

An app sets these variables with PLUTO_LOCAL_ROOT and the plutoLayer helper. See Local layers.

Do not register the @nuxt/eslint module or other development modules in the layer. A module of a layer runs in every app that extends it. Lint the layer with @nuxt/eslint-config in eslint.config.mjs. See Lint.

Step 2: register with definePlutoExtension

Call definePlutoExtension once, from a plugin, with everything that the layer contributes.

app/plugins/pluto-extension.ts
export default defineNuxtPlugin(() => {
  definePlutoExtension({
    id: 'my-layer',
    nav: [
      { id: 'reports', label: 'my-layer.nav.reports', icon: 'lucide:chart-bar', to: '/admin/reports' }
    ],
    pages: [
      { id: 'reports', path: '/admin/reports', title: 'my-layer.nav.reports', icon: 'lucide:chart-bar' }
    ],
    capabilities: [
      { id: 'reports-read', key: 'reports:read', label: 'my-layer.capabilities.reportsRead' }
    ]
  })
})

Use a plain id for an entry. Use a plain string in key for a capability. See Extension registry and Permissions.

The page file app/pages/admin/reports.vue is a normal Nuxt page. Core's admin layout wraps every /admin/** page.

Register components of widgets, panels, and banners with defineAsyncComponent. See Lazy loading.

Step 3: add a content type

Declare a content type and register it on the client and the server. See Add a content type.

Step 4: add text

Put the text of the layer in i18n/locales/<code>.json, under one top-level namespace: the $meta.name of the layer. Use pluto.common.* for shared words.

i18n/locales/en.json
{
  "my-layer": {
    "nav": { "reports": "Reports" },
    "capabilities": { "reportsRead": "Read reports", "group": "Reports" }
  }
}

Registry entries hold keys. Core translates them. Add the same keys to every locale file. See Internationalization.

Step 5: add styles

A layer can ship app/assets/css/pluto-layer.css. Core imports it through @plutocms/css.

  • Do not import tailwindcss or @nuxt/ui in it.
  • Use only @theme, @custom-variant, @utility, unlayered rules, and imports of plain CSS files next to it.
  • Add no @source for the app/ directory. Nuxt UI already scans every layer.
  • Use Nuxt UI semantic classes in admin code, such as bg-default and text-muted. See Styles.

Step 6: add a database migration

A layer on the supabase backend keeps its migrations in db/migrations/. Name the files NNN_slug.sql. Grant every new table in the same file. See Migrations and Data API grants.

Step 7: publish

A Nuxt layer ships its source. There is no build step for a layer package. Pluto layers use this pipeline:

  • Conventional Commits.
  • release-please.
  • A CI gate.
  • npm trusted publishing.

See Releasing. Set name, version, license, description, publishConfig: { "access": "public" }, and a repository field in package.json. npm provenance refuses to publish without the repository field.