Blocks rich text editor

I see that 4.14 introduces a new Blocks rich text editor in alpha

Are there any more details about the functionality and roadmap for this ?

2 Likes

A schema for the JSON model would be super helpful.

3 Likes

Is there an option to extend this editor with custom components ?

Indeed! Anybody?

Just went down a bit of a rabbit hole on this myself after not understanding how to save content into a Rich text ā€œBlocksā€ field from the API :thinking:

Populated an entry with all the existing blocks and converted resulting output into a typescript type as well as a JSON Schema.

Typescript Type

interface TextNode {
    text: string;
    type: 'text';
    bold?: boolean;
    underline?: boolean;
    italic?: boolean;
    strikethrough?: boolean;
    code?: boolean;
}

interface LinkNode {
    url: string;
    type: 'link';
    children: TextNode[];
}

interface ListItemNode {
    type: 'list-item';
    children: (TextNode | LinkNode)[];
}

interface ImageFormat {
    ext: string;
    url: string;
    hash: string;
    mime: string;
    name: string;
    path: null | string;
    size: number;
    width: number;
    height: number;
}

interface ImageNode {
    type: 'image';
    image: {
        ext: string;
        url: string;
        hash: string;
        mime: string;
        name: string;
        size: number;
        width: number;
        height: number;
        caption: string;
        formats: {
            large?: ImageFormat;
            small?: ImageFormat;
            medium?: ImageFormat;
            thumbnail?: ImageFormat;
        };
        provider: string;
        createdAt: string;
        updatedAt: string;
        previewUrl: null | string;
        alternativeText: string;
        provider_metadata: null | any;
    };
    children: TextNode[];
}

interface BlockNode {
    type: 'heading' | 'paragraph' | 'list' | 'quote';
    level?: number;
    format?: 'unordered' | 'ordered';
    children: (TextNode | LinkNode | ListItemNode | ImageNode)[];
}

type RichTextInput = BlockNode[];

JSON Schema

{
    "$schema": "http://json-schema.org/draft-07/schema#",
    "type": "array",
    "items": {
        "type": "object",
        "properties": {
            "type": {
                "type": "string"
            },
            "level": {
                "type": "integer"
            },
            "format": {
                "type": "string"
            },
            "children": {
                "type": "array",
                "items": {
                    "oneOf": [
                        {
                            "$ref": "#/definitions/textNode"
                        },
                        {
                            "$ref": "#/definitions/linkNode"
                        },
                        {
                            "$ref": "#/definitions/listItemNode"
                        },
                        {
                            "$ref": "#/definitions/imageNode"
                        }
                    ]
                }
            }
        },
        "required": ["type", "children"]
    },
    "definitions": {
        "textNode": {
            "type": "object",
            "properties": {
                "text": {
                    "type": "string"
                },
                "type": {
                    "type": "string"
                },
                "bold": {
                    "type": "boolean"
                },
                "underline": {
                    "type": "boolean"
                },
                "italic": {
                    "type": "boolean"
                },
                "strikethrough": {
                    "type": "boolean"
                },
                "code": {
                    "type": "boolean"
                }
            },
            "required": ["text", "type"]
        },
        "linkNode": {
            "type": "object",
            "properties": {
                "url": {
                    "type": "string"
                },
                "type": {
                    "type": "string"
                },
                "children": {
                    "type": "array",
                    "items": {
                        "$ref": "#/definitions/textNode"
                    }
                }
            },
            "required": ["url", "type", "children"]
        },
        "listItemNode": {
            "type": "object",
            "properties": {
                "type": {
                    "type": "string"
                },
                "children": {
                    "type": "array",
                    "items": {
                        "oneOf": [
                            {
                                "$ref": "#/definitions/textNode"
                            },
                            {
                                "$ref": "#/definitions/linkNode"
                            }
                        ]
                    }
                }
            },
            "required": ["type", "children"]
        },
        "imageNode": {
            "type": "object",
            "properties": {
                "type": {
                    "type": "string"
                },
                "image": {
                    "type": "object",
                    "properties": {
                        // Define image properties here
                    },
                    "required": [
                        // List of required image properties
                    ]
                },
                "children": {
                    "type": "array",
                    "items": {
                        "$ref": "#/definitions/textNode"
                    }
                }
            },
            "required": ["type", "image", "children"]
        }
        // TODO: Define remaining image block params...
    }
}

ā€œkitchen sinkā€ content field in Admin

Resulting API Response

[
  {
    "type": "heading",
    "level": 1,
    "children": [
      {
        "text": "Heading 1",
        "type": "text"
      }
    ]
  },
  {
    "type": "heading",
    "level": 2,
    "children": [
      {
        "text": "Heading 2",
        "type": "text"
      }
    ]
  },
  {
    "type": "heading",
    "level": 3,
    "children": [
      {
        "text": "Heading 3",
        "type": "text"
      }
    ]
  },
  {
    "type": "paragraph",
    "children": [
      {
        "text": "",
        "type": "text"
      }
    ]
  },
  {
    "type": "paragraph",
    "children": [
      {
        "text": "basic paragraph",
        "type": "text"
      }
    ]
  },
  {
    "type": "paragraph",
    "children": [
      {
        "bold": true,
        "text": "this is the bold",
        "type": "text"
      }
    ]
  },
  {
    "type": "paragraph",
    "children": [
      {
        "text": "this is underlined",
        "type": "text",
        "underline": true
      }
    ]
  },
  {
    "type": "paragraph",
    "children": [
      {
        "text": "this is an italic paragraph",
        "type": "text",
        "italic": true
      }
    ]
  },
  {
    "type": "paragraph",
    "children": [
      {
        "text": "this has strikethrough",
        "type": "text",
        "strikethrough": true
      }
    ]
  },
  {
    "type": "paragraph",
    "children": [
      {
        "code": true,
        "text": "some code",
        "type": "text"
      }
    ]
  },
  {
    "type": "paragraph",
    "children": [
      {
        "text": "",
        "type": "text"
      },
      {
        "url": "https://google.com",
        "type": "link",
        "children": [
          {
            "text": "a link",
            "type": "text"
          }
        ]
      },
      {
        "text": "",
        "type": "text"
      }
    ]
  },
  {
    "type": "list",
    "format": "unordered",
    "children": [
      {
        "type": "list-item",
        "children": [
          {
            "text": "bulleted",
            "type": "text"
          }
        ]
      },
      {
        "type": "list-item",
        "children": [
          {
            "text": "list",
            "type": "text"
          }
        ]
      }
    ]
  },
  {
    "type": "list",
    "format": "ordered",
    "children": [
      {
        "type": "list-item",
        "children": [
          {
            "text": "numbered",
            "type": "text"
          }
        ]
      },
      {
        "type": "list-item",
        "children": [
          {
            "text": "list",
            "type": "text"
          }
        ]
      }
    ]
  },
  {
    "type": "quote",
    "children": [
      {
        "text": "Quote content here",
        "type": "text"
      }
    ]
  },
  {
    "type": "image",
    "image": {
      "ext": ".png",
      "url": "http://localhost:1337/uploads/mascot_1_b49785a30c.png",
      "hash": "mascot_1_b49785a30c",
      "mime": "image/png",
      "name": "mascot-1.png",
      "size": 284.58,
      "width": 1024,
      "height": 1024,
      "caption": "imagine caption",
      "formats": {
        "large": {
          "ext": ".png",
          "url": "/uploads/large_mascot_1_b49785a30c.png",
          "hash": "large_mascot_1_b49785a30c",
          "mime": "image/png",
          "name": "large_mascot-1.png",
          "path": null,
          "size": 948.28,
          "width": 1000,
          "height": 1000
        },
        "small": {
          "ext": ".png",
          "url": "/uploads/small_mascot_1_b49785a30c.png",
          "hash": "small_mascot_1_b49785a30c",
          "mime": "image/png",
          "name": "small_mascot-1.png",
          "path": null,
          "size": 267.64,
          "width": 500,
          "height": 500
        },
        "medium": {
          "ext": ".png",
          "url": "/uploads/medium_mascot_1_b49785a30c.png",
          "hash": "medium_mascot_1_b49785a30c",
          "mime": "image/png",
          "name": "medium_mascot-1.png",
          "path": null,
          "size": 570.48,
          "width": 750,
          "height": 750
        },
        "thumbnail": {
          "ext": ".png",
          "url": "/uploads/thumbnail_mascot_1_b49785a30c.png",
          "hash": "thumbnail_mascot_1_b49785a30c",
          "mime": "image/png",
          "name": "thumbnail_mascot-1.png",
          "path": null,
          "size": 31.83,
          "width": 156,
          "height": 156
        }
      },
      "provider": "local",
      "createdAt": "2024-01-18T03:02:24.519Z",
      "updatedAt": "2024-01-18T03:03:02.023Z",
      "previewUrl": null,
      "alternativeText": "alt text",
      "provider_metadata": null
    },
    "children": [
      {
        "text": "",
        "type": "text"
      }
    ]
  }
]

Hope this helps!

1 Like

With this in mindā€¦ how exactly are we supposed to render data of this format in the markup?

For react, looks like Strapi releases the following lib; blocks-react-renderer

2 Likes

Have you tried using it? I tried out a guide by one of the Strapi staff members and there is a large amount of boilerplate to implement-- it is extremely impractical.

Iā€™ve since just reverted to using the Ckeditor5 plugin by the Ckeditor team.

Would be nice if Strapi just had a core version of thatā€¦ similar to how Drupal just recently baked Ckeditor into core.

Iā€™m not sure I follow, especially if youā€™re referring to this post. At the end of the day the new BlockRendererClient component they ask you to add is very small, and includes support for Nextā€™s Image component.

e.g.

"use client";
import Image from "next/image";

import {
  BlocksRenderer,
  type BlocksContent,
} from "@strapi/blocks-react-renderer";

export default function BlockRendererClient({
  content,
}: {
  readonly content: BlocksContent;
}) {
  if (!content) return null;
  return (
    <BlocksRenderer
      content={content}
      blocks={{
        image: ({ image }) => {
          console.log(image);
          return (
            <Image
              src={image.url}
              width={image.width}
              height={image.height}
              alt={image.alternativeText || ""}
            />
          );
        },
      }}
    />
  );
}

This strategy, of allowing the React app to manage how the output is handled is seriously preferred in my mind for the following reasons;

  1. The API is delivering structured content with its full context v.s. the baked HTML or markdown.
  2. When we donā€™t send baked HTML we donā€™t have to use dangerouslySetInnerHTML :sparkles:
  3. Customizable boilerplate that lives under your projectā€™s version control, while can take time to configure initially, is typically much more flexible and resilient in the long term.

Why ā€˜Structured Contentā€™ is better than ā€˜Baked Contentā€™

Our frontend application ultimately should have complete control over how to handle the display of the information in the CMS while the WYSIWYG provides a consistent extendable interface for editing the different types of content.

One scenario where this comes into play often is the task of interspersing Ads within content. This task is almost always done programmatically and, for several reasons, is made significantly easier when the front end is sent structured content instead of pre-baked HTML.


Overall, if I understand your frustration, seems like what youā€™re looking for is for Strapi to deliver your front-end HTML(?) Iā€™d imagine this is in the works, it could/will likely be a toggleable setting in the future or easily provided by a plugin.

1 Like

Yes!

There doesnā€™t seem to be much information or a dedicated page in the docs for this yet but Restack.io has a page that includes ā€œExtending the Strapi Block Editor with Custom Pluginsā€.

e.g.

strapi.plugins['editor'].blocks.add({
  type: 'custom-block',
  components: {
    edit: CustomEditComponent,
    view: CustomViewComponent
  }
});

That said, it would be great to see a fully flushed-out docs page or a guide walking us through the process.

Thanks for the reply, Iā€™ll give it another look!

1 Like

Did you try more of this ? Maybe we can write down an blog post to add custom fields into the block editor, I need to do that to try to emulate an MDX, that should be great.

Is there a vue parser for frontend? I personally find this new block editor useless right now.

I am super curious about this component. Is there any react parser or library that our community can prefer?. Really needed at this point

Is the ability to add an image as a child of a link node is something planned in a new version of the plugin ?

1 Like

The react parser released by Strapi Team is mentioned here:

I just recently started using the new WYSIWYG Block Editor in Strapi with images and I have to say the experience did leave a little to desire. Once I had multiple images inside of the content editor it became increasingly difficult to manage various aspects of the editing experience e.g.

  • cursor location
  • selected image
  • image size

In the future Iā€™d like to see a lot more time spent on the UIX of incorperating and managing rich content inside of the WYSIWYG editor :pray: