# Blocks

> Your Vue components, written in Markdown.

## Writing one

Attributes become props, `#name` sections become slots:

```md
::callout{type="warning"}
#title
Heads up
#default
Body with **Markdown**.
::
```

```vue
<script setup lang="ts">
// `::callout` renders components/blocks/Callout.vue, in the editor and on the page.
// Type / on an empty line for the block menu.
const body = ref(`::callout{type="warning"}
Click me to edit my source.
::

`);
</script>

<template>
  <div class="writer-demo">
    <WriterEditor v-model="body" label="Body" />
  </div>
</template>
```

## Registering

In Nuxt, every component in `components/blocks/` is a block: `Callout.vue` is `::callout`, `media/Video.vue` is `::media-video`. They load on demand.

In Vue, pass them to `createWriter({ blocks: { callout: Callout } })`.

The slash menu's labels and snippets go in `app.config.ts` (or `{ component, label, snippet }` in Vue):

```ts
export default defineAppConfig({
  writer: {
    blocks: {
      callout: { label: "Callout", snippet: '::callout{type="${note}"}\n${}\n::' },
    },
  },
});
```

Snippets use CodeMirror's syntax: `${}` is where the cursor lands, `${name}` a field Tab moves to.

## Unknown blocks

Only registered blocks render. Anything else renders its content alone, so a removed block never breaks a page.

GitHub alerts (`> [!NOTE]`) render as a blockquote, or as your `note` block if you register one.

## In the editor

Blocks render inside the editor through the app, not the page around it: they get app-level plugins and `provide`s, but nothing provided by a parent of `<WriterEditor>`. Declare a `__node` prop to receive the block's raw node.
