> ## Documentation Index
> Fetch the complete documentation index at: https://docs.blockli.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Integrate blökkli with Drupal via Paragraphs

> Connect blökkli to a Drupal backend using the paragraphs_blokkli module. Covers Composer install, paragraph type mapping, and GraphQL adapter setup.

blökkli has first-class support for Drupal through the official **paragraphs\_blokkli** Drupal module. It exposes your Drupal paragraphs as blocks via GraphQL, and the adapter communicates with Drupal to read and write content in real time — so you can build a fully functional WYSIWYG editing experience on top of your existing Drupal content model.

## Prerequisites

Before you begin, make sure your environment meets the following requirements:

* **Drupal 9 or 10** with the Composer-based project layout
* **[Paragraphs module](https://www.drupal.org/project/paragraphs)** installed and enabled
* **[GraphQL Compose](https://www.drupal.org/project/graphql_compose)** or the **[Drupal GraphQL](https://www.drupal.org/project/graphql)** module configured and exposing a schema
* The **[paragraphs\_blokkli](https://www.drupal.org/project/paragraphs_blokkli)** Drupal module

## Installation and Setup

<Steps>
  <Step title="Install the Drupal module">
    Install `paragraphs_blokkli` via Composer, then enable it with Drush:

    ```bash theme={null}
    composer require drupal/paragraphs_blokkli
    drush en paragraphs_blokkli
    drush cr
    ```

    Once enabled, the module registers a GraphQL schema extension that exposes your paragraph content as blökkli-compatible block state, and adds the editing mutation endpoints blökkli needs.
  </Step>

  <Step title="Configure paragraph types">
    For each paragraph type you want to expose as a blökkli block, navigate to its type configuration page in the Drupal admin UI:

    1. Go to **Structure → Paragraph types** and click **Edit** next to the paragraph type.
    2. Open the **blökkli** settings tab and enable the integration for that type.
    3. Repeat for every paragraph type you want editors to add and move in the blökkli editor.
  </Step>

  <Step title="Set permissions">
    Under `/admin/people/permissions`, grant the appropriate roles access to create, edit, and delete blökkli-managed paragraphs. At minimum the editing role needs:

    * `Use the blökkli editor`
    * `Create [type] paragraph` for each exposed paragraph type
    * `Edit [type] paragraph`
    * `Delete [type] paragraph`
  </Step>

  <Step title="Create the Nuxt adapter">
    Create the adapter file at `app/blokkli.editAdapter.ts`. The adapter communicates with Drupal over GraphQL — it loads the current block state and sends mutations when editors make changes.

    ```typescript theme={null}
    // app/blokkli.editAdapter.ts
    import { defineBlokkliEditAdapter } from '#blokkli/adapter'

    export default defineBlokkliEditAdapter((ctx) => {
      const graphqlEndpoint = '/graphql'
      const entityUuid = computed(() => ctx.state.value.currentEntityUuid)

      async function gql(query: string, variables: Record<string, unknown> = {}) {
        const response = await $fetch<{ data: unknown }>(graphqlEndpoint, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ query, variables }),
        })
        return (response as any).data
      }

      return {
        async loadState() {
          return gql(
            `query LoadBlokkliState($uuid: String!) {
               blokkliState(uuid: $uuid) {
                 currentIndex
                 mutations { timestamp }
                 blocks {
                   uuid
                   bundle
                   hostField
                   options
                 }
               }
             }`,
            { uuid: entityUuid.value },
          ).then((data: any) => data.blokkliState)
        },

        async mapState(state) {
          // blokkliState from the GraphQL schema already matches blökkli's
          // internal format, so no field-name transformation is needed.
          return {
            blocks: (state as any).blocks.map((b: any) => ({
              uuid: b.uuid,
              bundle: b.bundle,
              hostField: b.hostField,
              options: b.options ?? {},
            })),
          }
        },

        async getAllBundles() {
          return gql(
            `query GetBlokkliBundles {
               blokkliBundles {
                 id
                 label
                 allowedFieldTypes
               }
             }`,
          ).then((data: any) => data.blokkliBundles)
        },

        async getFieldConfig() {
          return gql(
            `query GetBlokkliFieldConfig($uuid: String!) {
               blokkliFieldConfig(uuid: $uuid) {
                 name
                 label
                 allowedBundles
                 entityType
                 entityBundle
               }
             }`,
            { uuid: entityUuid.value },
          ).then((data: any) => data.blokkliFieldConfig)
        },

        async addNewBlock({ bundle, hostField, preceedingUuid, options }) {
          await gql(
            `mutation AddBlokkliBlock($input: BlokkliAddBlockInput!) {
               blokkliAddBlock(input: $input) { uuid }
             }`,
            { input: { bundle, hostField, preceedingUuid, options } },
          )
        },

        async moveBlock({ uuid, hostField, preceedingUuid }) {
          await gql(
            `mutation MoveBlokkliBlock($input: BlokkliMoveBlockInput!) {
               blokkliMoveBlock(input: $input) { uuid }
             }`,
            { input: { uuid, hostField, preceedingUuid } },
          )
        },

        async moveMultipleBlocks({ uuids, hostField, preceedingUuid }) {
          await gql(
            `mutation MoveBlokkliBlocks($input: BlokkliMoveMultipleBlocksInput!) {
               blokkliMoveMultipleBlocks(input: $input) { uuids }
             }`,
            { input: { uuids, hostField, preceedingUuid } },
          )
        },

        async deleteBlocks({ uuids }) {
          await gql(
            `mutation DeleteBlokkliBlocks($uuids: [String!]!) {
               blokkliDeleteBlocks(uuids: $uuids)
             }`,
            { uuids },
          )
        },

        async updateOptions({ uuid, options }) {
          await gql(
            `mutation UpdateBlokkliOptions($uuid: String!, $options: JSON!) {
               blokkliUpdateOptions(uuid: $uuid, options: $options) { uuid }
             }`,
            { uuid, options },
          )
        },

        async publish() {
          await gql(
            `mutation PublishBlokkliEntity($uuid: String!) {
               blokkliPublish(uuid: $uuid)
             }`,
            { uuid: entityUuid.value },
          )
        },
      }
    })
    ```

    <Note>
      The exact GraphQL schema depends on your Drupal configuration — specifically which GraphQL module you are using and how your paragraph types are set up. The query and mutation names above match the schema provided by `paragraphs_blokkli`. Refer to the [paragraphs\_blokkli module documentation](https://www.drupal.org/project/paragraphs_blokkli) for the full schema reference.
    </Note>
  </Step>
</Steps>

## How Paragraph Types Map to Block Bundles

blökkli maps Drupal paragraph types to Vue block components using the bundle identifier. The mapping is straightforward and relies entirely on machine names:

* **Paragraph type machine name → bundle** — each Drupal paragraph type becomes a bundle in blökkli. The bundle identifier is exactly the paragraph type's machine name (e.g., `text`, `image`, `call_to_action`).
* **Vue component file** — create a corresponding Vue SFC in `components/Blokkli/` for each paragraph type you want to render. Name the file using PascalCase derived from the machine name.
* **`defineBlokkli()` bundle** — the `bundle` value passed to `defineBlokkli()` inside each component must exactly match the Drupal paragraph machine name.

```
Drupal paragraph type machine name:  "call_to_action"
                 ↓
   blökkli bundle identifier:        "call_to_action"
                 ↓
   Vue component file:                components/Blokkli/CallToAction.vue
                 ↓
   Component registration:            defineBlokkli({ bundle: 'call_to_action' })
```

For example, a `call_to_action` paragraph type maps to:

```vue theme={null}
<!-- components/Blokkli/CallToAction.vue -->
<template>
  <div class="call-to-action">
    <h2>{{ options.heading }}</h2>
    <a :href="options.url">{{ options.label }}</a>
  </div>
</template>

<script lang="ts" setup>
const { options } = defineBlokkli({
  bundle: 'call_to_action',
  options: {
    heading: { type: 'text', label: 'Heading', default: '' },
    url: { type: 'text', label: 'URL', default: '' },
    label: { type: 'text', label: 'Button label', default: 'Learn more' },
  },
})
</script>
```

## Useful Links

<CardGroup cols={2}>
  <Card title="paragraphs_blokkli on Drupal.org" icon="drupal" href="https://www.drupal.org/project/paragraphs_blokkli">
    The official Drupal module that bridges Paragraphs content with blökkli's editing interface.
  </Card>

  <Card title="GraphQL Compose Module" icon="plug" href="https://www.drupal.org/project/graphql_compose">
    Automatically generates a GraphQL schema from your Drupal content types and fields.
  </Card>
</CardGroup>
