Guides

Author components for the editor

Make a Vue component of your Nuxt Content site show in the block palette, with typed fields and help text.

The visual editor lets editors add blocks to a page. A block is a component. This guide shows how to make your own component a block. It works for docs pages and for other pages of a Nuxt Content site.

This needs @plutocms/docs 0.16 or newer.

Where the file goes

Put the component in components/content/. This is the folder where Nuxt Content looks for the components that Markdown uses.

The folder can be in your app or in a layer. The editor reads the folder of the app and the folder of every layer. If two files have the same name, the app wins.

The editor skips two kinds of file. It skips Prose* files, because they replace the Markdown elements. It also skips .client and .server variants.

Add the macro

A component shows in the palette only when it calls definePlutoBlock. A component without the call still works on a page. It is just not in the palette.

The macro is an auto-import. Call it in <script setup>. You do not need an import line.

This is PlutoChip, from this site:

app/components/content/PlutoChip.vue
<script setup lang="ts">
definePlutoBlock<{ icon?: string, center?: boolean }>({
  label: 'Chip',
  icon: 'i-lucide-tag',
  group: 'landing',
  description: 'A short label above a section title.',
  props: {
    center: { label: 'Center the label' }
  },
  slots: { default: { label: 'Text', content: 'inline' } },
  insert: '::pluto-chip{icon="i-lucide-heart"}\nA short label\n::'
})

withDefaults(defineProps<{
  /** The icon before the text, such as `i-lucide-heart`. */
  icon?: string
  /** Put the label in the middle of the line. */
  center?: boolean
}>(), {
  icon: undefined,
  center: false
})
</script>

All keys are optional. definePlutoBlock({}) is valid. The props and the slots still come from the component.

KeyWhat it does
labelThe name in the palette. Default: the component name in words.
descriptionThe short text under the name in the palette.
iconAn Iconify name, such as i-lucide-tag.
groupThe palette group: text, callouts, layout, code, landing, or site.
propsChanges to single props. See below.
slotsThe slots, with a label, a content kind, and accepts.
insertThe Markdown that the palette adds.
previewfalse shows a card in the preview, not the component.
hiddentrue keeps the block out of the palette.

The generic <{ icon?: string, center?: boolean }> is optional. With it, TypeScript checks that each key of props is a prop of the component.

Props become fields

The editor reads the types of defineProps. Each prop with a known type gets a field in the Settings section.

TypeField
stringOne line of text.
string named icon, or ending in IconAn icon field with a preview.
string named to or hrefA link field.
numberA number field.
booleanA switch.
A union of string literals, such as 'slow' | 'fast'A list of choices, in the order of the type.
A union of theme color namesA list of the theme colors.
An object, an array, or a functionNo field.

A prop with no field is not in the panel. If a page sets it, the prop shows in the Advanced section.

To choose the field yourself, set type in the entry of the prop in props. The types are string, text (a box with more lines), boolean, number, select (add options), icon, color, and link.

To move a prop to Advanced, set advanced: true in its entry in props.

Help text from JSDoc

Write a JSDoc comment above a prop. The editor shows the text as help under the field.

defineProps<{
  /** How many layers to show. */
  limit?: number
}>()

Keep the text short. Use plain words.

Defaults

A simple default shows in the field. A number shows as the placeholder. A list of choices shows the value in the Default item. A switch shows its default state.

withDefaults(defineProps<{
  /** How many layers to show. */
  limit?: number
}>(), { limit: 6 })

The default never goes into the Markdown. A page keeps its text until an editor changes the field.

A default must be a plain value, such as 6, 'slow', or false.

Slots

The editor reads the slots of the template. Each slot gets a field in the Content section.

A slot that has the mdc-unwrap attribute gets a one-line field. The attribute removes the paragraph around the text. Use it for a title or a label.

<slot mdc-unwrap="p" />

A slot without mdc-unwrap gets a text box, or a list of blocks, based on the content of the page.

To set the field yourself, use slots in the macro. Use content: 'blocks' for a list of blocks. Add accepts to limit the components that can go in the list.

slots: {
  links: { label: 'Buttons', content: 'blocks', accepts: ['u-button'] }
}

PlutoHero on this site uses this for its buttons.

Change a block in app.config.ts

The macro is the main place for the settings of a block. The site can still override it. Put an entry in plutoBlocks in app.config.ts. It wins over the macro. Set a prop to false to remove its field.

app/app.config.ts
export default defineAppConfig({
  plutoBlocks: {
    'pluto-chip': {
      label: 'Tag',
      props: { center: false }
    }
  }
})

The entry changes only the keys that it names. The other keys keep the value from the component. An entry can also add a component that does not call the macro.

The order, from lowest to highest, is:

  1. The preset of the layer.
  2. The component: its types and its macro.
  3. plutoBlocks in app.config.ts.

Labels in more than one language

A label or a description can be plain text or an i18n key. If the key exists in your locale files, the editor shows the translation. If not, it shows the text.

The cache

The build reads the macro in each file. It keeps the result in .nuxt/cache/pluto-blocks.

In dev, a change to a component file updates the palette. A change to a file that the component imports does not. This is true for a shared type file. After such a change, delete the .nuxt/cache/pluto-blocks folder and restart the dev server.

Limits

  • The argument of definePlutoBlock must be an object literal. It can use constants that you import from a .ts file. It cannot use a variable from <script setup>.
  • If the build cannot read the call, it logs one warning with the file name. The component is not in the palette. The rest of the build goes on.
  • To turn off the discovery, set plutoDocs.blocks.discover to false.