Pro Content Editor Plugin
Viewing latest docs.
Switch version: v3

Content Editor — adding your own plug-in

Adding an Editor.js plug-in to Petal Pro and wiring up the renderer.

Introduction

The Content Editor component wraps Editor.js, which is plug-in based. Petal Pro ships with a set of plug-ins for the common stuff — text, lists, tables, images, etc.

When you want to add your own plug-in, there are two pieces:

  1. The plug-in itself (a JavaScript class)
  2. An update to the Petal Pro renderer so it knows how to display the data your plug-in produces

Both are easier than they look.

What we’ll build

A Markdown plug-in. Requirements:

  • User pastes markdown into a textarea inside the editor
  • The markdown is stored as-is in the Editor.js JSON
  • When the post is rendered, the markdown is converted to HTML using Earmark

If you’d rather skip the manual steps, drop this into Claude Code:

Add a custom Editor.js plug-in for Markdown. The plug-in should let users paste
markdown into a textarea, save it as-is in the editor JSON, and render it as
HTML using Earmark. Wire it into the Content Editor renderer.

The rest of this guide is the same flow, manually.

Folder layout

The relevant files for Editor.js in Petal Pro:

├── assets
│   ├── css
│   │   └── editorjs.css
│   ├── js
│   │   ├── editorjs
│   │   │   └── petal-image.js          # Built-in plug-in for File Browser
│   │   └── hooks
│   │       └── editorjs-hook.js        # The Phoenix LiveView hook
│   └── package.json                    # Where you install Editor.js plug-ins
└── lib
    └── petal_pro_web
        └── components
            └── pro_components
                └── content_editor.ex   # Component + renderer

Your new plug-in will sit alongside petal-image.js.

Build the plug-in

Create assets/js/editorjs/petal-markdown.js:

petal-markdown.js
export default class PetalMarkdown {
  static get toolbox() {
    return {
      title: "Markdown",
      icon: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"><path stroke-linecap="round" stroke-linejoin="round" d="M5.25 8.25h15m-16.5 7.5h15m-1.8-13.5-3.9 19.5m-2.1-19.5-3.9 19.5" /></svg>',
    };
  }

  constructor({ data, block }) {
    this.data = data;
    this.block = block;
    this.wrapper = undefined;
  }

  render() {
    this.wrapper = document.createElement("div");

    const markdown_input = document.createElement("textarea");
    markdown_input.placeholder = "Paste your markdown here...";
    markdown_input.value = this.data.markdown || "";
    markdown_input.classList.add("petal-markdown");

    this.wrapper.append(markdown_input);

    return this.wrapper;
  }

  save(blockContent) {
    const markdown_input = blockContent.querySelector("textarea");

    return {
      markdown: markdown_input ? markdown_input.value : "",
    };
  }
}

Three methods worth understanding:

  • toolbox() — Defines what shows up in the “+” block menu inside the editor. Title and icon.
  • render() — Returns the DOM the user interacts with. Here, just a textarea.
  • save() — Returns the data structure to persist into the editor’s JSON output. We save the raw markdown.

Style the textarea

Add to assets/css/editorjs.css:

editorjs.css
.codex-editor textarea.petal-markdown {
  @apply w-full rounded-md min-h-32 bg-gray-100 dark:bg-gray-950 dark:text-gray-400 border-gray-200 dark:border-gray-600 ring-0;
}

Register it in the hook

In assets/js/hooks/editorjs-hook.js, import the plug-in and add it to the tools config:

editorjs-hook.js
import EditorJS from "@editorjs/editorjs";
import PetalMarkdown from "../editorjs/petal-markdown";  // add this

const EditorJsHook = {
  mounted() {
    this.editor = new EditorJS({
      tools: {
        petalMarkdown: PetalMarkdown,  // add this
        // ... other tools
      },
    });
  },
};

export default EditorJsHook;

Reload your editor, click the “+” button, pick Markdown, and a textarea appears. Type or paste markdown into it. The first two requirements are done — the plug-in works inside the editor and saves data correctly.

Update the renderer

Now we need Petal Pro to actually render the markdown as HTML when displaying a post. Open lib/petal_pro_web/components/pro_components/content_editor.ex. You’ll find a content/1 function that decodes the JSON and dispatches each block to a block/1 function:

elixir
attr :json, :string, required: true

def content(assigns) do
  json_object = decode_json(assigns.json)

  blocks =
    case json_object do
      %{"blocks" => blocks} -> MapExt.atomize_keys(blocks)
      _ -> []
    end

  assigns = assign(assigns, :blocks, blocks)

  ~H"""
  <.block :for={block <- @blocks} block={block} />
  """
end

block/1 uses pattern matching to render different block types. Add a clause for our plug-in above the catch-all:

content_editor.ex
defp block(%{block: %{type: "petalMarkdown", data: %{markdown: markdown}}} = assigns) do
  assigns = assign(assigns, :markdown, markdown)

  ~H"""
  <PetalProWeb.Markdown.pretty_markdown content={@markdown} />
  """
end

# Catch-all (must come after the specific clauses)
defp block(assigns) do
  ~H""
end

PetalProWeb.Markdown.pretty_markdown/1 uses Earmark under the hood. That’s the third requirement done — markdown saved by your plug-in now renders as styled HTML on the public side.

Installing third-party plug-ins

To use one of the community plug-ins, install it via npm:

shell
cd assets
npm add @editorjs/checklist

Then register it in editorjs-hook.js the same way as our custom plug-in:

javascript
import Checklist from "@editorjs/checklist";

// In tools:
checklist: {
  class: Checklist,
  inlineToolbar: true,
}

And add a block/1 clause in content_editor.ex to render whatever data structure the plug-in saves. Each plug-in’s docs tell you the shape of its output JSON.