Skip to main content
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:

Installation and Setup

1

Install the Drupal module

Install paragraphs_blokkli via Composer, then enable it with Drush:
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.
2

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.
3

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
4

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.
// 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 },
      )
    },
  }
})
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 for the full schema reference.

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:
<!-- 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>

paragraphs_blokkli on Drupal.org

The official Drupal module that bridges Paragraphs content with blökkli’s editing interface.

GraphQL Compose Module

Automatically generates a GraphQL schema from your Drupal content types and fields.