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 ?
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 ?
A schema for the JSON model would be super helpful.
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
Populated an entry with all the existing blocks and converted resulting output into a typescript type as well as a JSON Schema.
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[];
{
"$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...
}
}
content
field in Admin[
{
"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!
With this in mindā¦ how exactly are we supposed to render data of this format in the markup?
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;
dangerouslySetInnerHTML
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.
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!
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 ?
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.
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
I have just started experiencing this myself in a next app. How are you dealing with it?