@qkix/strapi-plugin-better-blocks 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +795 -0
  3. package/dist/_chunks/en-BcPniuRQ.js +1 -0
  4. package/dist/_chunks/en-DYxpcVy9.mjs +4 -0
  5. package/dist/admin/index.js +1933 -0
  6. package/dist/admin/index.mjs +17091 -0
  7. package/dist/admin/src/components/BlocksInput/Blocks/Audio.d.ts +6 -0
  8. package/dist/admin/src/components/BlocksInput/Blocks/AudioEditorModal.d.ts +21 -0
  9. package/dist/admin/src/components/BlocksInput/Blocks/Button.d.ts +11 -0
  10. package/dist/admin/src/components/BlocksInput/Blocks/ButtonEditorModal.d.ts +17 -0
  11. package/dist/admin/src/components/BlocksInput/Blocks/Callout.d.ts +21 -0
  12. package/dist/admin/src/components/BlocksInput/Blocks/Code.d.ts +18 -0
  13. package/dist/admin/src/components/BlocksInput/Blocks/Details.d.ts +12 -0
  14. package/dist/admin/src/components/BlocksInput/Blocks/Diagram.d.ts +15 -0
  15. package/dist/admin/src/components/BlocksInput/Blocks/Embed.d.ts +22 -0
  16. package/dist/admin/src/components/BlocksInput/Blocks/EmbedEditorModal.d.ts +27 -0
  17. package/dist/admin/src/components/BlocksInput/Blocks/Heading.d.ts +14 -0
  18. package/dist/admin/src/components/BlocksInput/Blocks/HorizontalLine.d.ts +10 -0
  19. package/dist/admin/src/components/BlocksInput/Blocks/Image.d.ts +5 -0
  20. package/dist/admin/src/components/BlocksInput/Blocks/Link.d.ts +3 -0
  21. package/dist/admin/src/components/BlocksInput/Blocks/List.d.ts +3 -0
  22. package/dist/admin/src/components/BlocksInput/Blocks/Math.d.ts +17 -0
  23. package/dist/admin/src/components/BlocksInput/Blocks/MediaEmbed.d.ts +7 -0
  24. package/dist/admin/src/components/BlocksInput/Blocks/Paragraph.d.ts +3 -0
  25. package/dist/admin/src/components/BlocksInput/Blocks/Quote.d.ts +3 -0
  26. package/dist/admin/src/components/BlocksInput/Blocks/SocialEmbed.d.ts +8 -0
  27. package/dist/admin/src/components/BlocksInput/Blocks/SocialEmbedEditorModal.d.ts +41 -0
  28. package/dist/admin/src/components/BlocksInput/Blocks/SourceEditorModal.d.ts +19 -0
  29. package/dist/admin/src/components/BlocksInput/Blocks/Table.d.ts +14 -0
  30. package/dist/admin/src/components/BlocksInput/Blocks/TableGridPicker.d.ts +20 -0
  31. package/dist/admin/src/components/BlocksInput/Blocks/TableIcons.d.ts +31 -0
  32. package/dist/admin/src/components/BlocksInput/Blocks/TableToolbar.d.ts +14 -0
  33. package/dist/admin/src/components/BlocksInput/Blocks/Video.d.ts +6 -0
  34. package/dist/admin/src/components/BlocksInput/Blocks/VideoEditorModal.d.ts +29 -0
  35. package/dist/admin/src/components/BlocksInput/Blocks/embedProviders.d.ts +43 -0
  36. package/dist/admin/src/components/BlocksInput/Blocks/muxClient.d.ts +40 -0
  37. package/dist/admin/src/components/BlocksInput/Blocks/socialFrame.d.ts +13 -0
  38. package/dist/admin/src/components/BlocksInput/Blocks/tableGrid.d.ts +90 -0
  39. package/dist/admin/src/components/BlocksInput/Blocks/tableOperations.d.ts +124 -0
  40. package/dist/admin/src/components/BlocksInput/Blocks/videoProviders.d.ts +37 -0
  41. package/dist/admin/src/components/BlocksInput/BlocksContent.d.ts +7 -0
  42. package/dist/admin/src/components/BlocksInput/BlocksEditor.d.ts +71 -0
  43. package/dist/admin/src/components/BlocksInput/BlocksToolbar.d.ts +27 -0
  44. package/dist/admin/src/components/BlocksInput/EditorLayout.d.ts +10 -0
  45. package/dist/admin/src/components/BlocksInput/EmojiPicker.d.ts +5 -0
  46. package/dist/admin/src/components/BlocksInput/FindReplace.d.ts +11 -0
  47. package/dist/admin/src/components/BlocksInput/FloatingSelectionToolbar.d.ts +11 -0
  48. package/dist/admin/src/components/BlocksInput/FontModifiersIcons.d.ts +32 -0
  49. package/dist/admin/src/components/BlocksInput/InlineColorPicker.d.ts +3 -0
  50. package/dist/admin/src/components/BlocksInput/MenuScrollbar.d.ts +30 -0
  51. package/dist/admin/src/components/BlocksInput/Modifiers.d.ts +16 -0
  52. package/dist/admin/src/components/BlocksInput/SlashCommands.d.ts +3 -0
  53. package/dist/admin/src/components/BlocksInput/SpecialCharPicker.d.ts +5 -0
  54. package/dist/admin/src/components/BlocksInput/WordCount.d.ts +3 -0
  55. package/dist/admin/src/components/BlocksInput/plugins/withAutoTransform.d.ts +3 -0
  56. package/dist/admin/src/components/BlocksInput/plugins/withLinks.d.ts +8 -0
  57. package/dist/admin/src/components/BlocksInput/plugins/withMarkdownPaste.d.ts +3 -0
  58. package/dist/admin/src/components/BlocksInput/plugins/withStrapiSchema.d.ts +6 -0
  59. package/dist/admin/src/components/BlocksInput/utils/constants.d.ts +13 -0
  60. package/dist/admin/src/components/BlocksInput/utils/conversions.d.ts +14 -0
  61. package/dist/admin/src/components/BlocksInput/utils/enterKey.d.ts +6 -0
  62. package/dist/admin/src/components/BlocksInput/utils/links.d.ts +12 -0
  63. package/dist/admin/src/components/BlocksInput/utils/optionsDefaults.d.ts +28 -0
  64. package/dist/admin/src/components/BlocksInput/utils/optionsParser.d.ts +34 -0
  65. package/dist/admin/src/components/BlocksInput/utils/parseMarkdownToSlate.d.ts +2 -0
  66. package/dist/admin/src/components/BlocksInput/utils/prefixFileUrlWithBackendUrl.d.ts +8 -0
  67. package/dist/admin/src/components/BlocksInput/utils/shiki.d.ts +29 -0
  68. package/dist/admin/src/components/BlocksInput/utils/shortcuts.d.ts +25 -0
  69. package/dist/admin/src/components/BlocksInput/utils/types.d.ts +404 -0
  70. package/dist/admin/src/components/Initializer.d.ts +5 -0
  71. package/dist/admin/src/components/Input.d.ts +45 -0
  72. package/dist/admin/src/components/PluginIcon.d.ts +3 -0
  73. package/dist/admin/src/hooks/useDragAndDrop.d.ts +51 -0
  74. package/dist/admin/src/hooks/useKeyboardDragAndDrop.d.ts +14 -0
  75. package/dist/admin/src/index.d.ts +10 -0
  76. package/dist/admin/src/pages/App.d.ts +3 -0
  77. package/dist/admin/src/pages/HomePage.d.ts +3 -0
  78. package/dist/admin/src/pluginId.d.ts +1 -0
  79. package/dist/admin/src/utils/getTranslation.d.ts +2 -0
  80. package/dist/server/index.js +1 -0
  81. package/dist/server/index.mjs +393 -0
  82. package/dist/server/src/bootstrap.d.ts +5 -0
  83. package/dist/server/src/config/index.d.ts +94 -0
  84. package/dist/server/src/content-types/index.d.ts +2 -0
  85. package/dist/server/src/controllers/controller.d.ts +12 -0
  86. package/dist/server/src/controllers/index.d.ts +14 -0
  87. package/dist/server/src/controllers/oembed.d.ts +12 -0
  88. package/dist/server/src/destroy.d.ts +5 -0
  89. package/dist/server/src/index.d.ts +125 -0
  90. package/dist/server/src/middlewares/index.d.ts +2 -0
  91. package/dist/server/src/policies/index.d.ts +2 -0
  92. package/dist/server/src/register.d.ts +5 -0
  93. package/dist/server/src/routes/admin.d.ts +9 -0
  94. package/dist/server/src/routes/content-api.d.ts +9 -0
  95. package/dist/server/src/routes/index.d.ts +25 -0
  96. package/dist/server/src/services/index.d.ts +14 -0
  97. package/dist/server/src/services/oembed.d.ts +27 -0
  98. package/dist/server/src/services/service.d.ts +7 -0
  99. package/package.json +131 -0
package/README.md ADDED
@@ -0,0 +1,795 @@
1
+ <h1 align="center">Strapi - Better Blocks Plugin</h1>
2
+
3
+ <p align="center">An enhanced Rich Text (Blocks) editor for Strapi v5 with inline text color, background highlight, and more.</p>
4
+
5
+ <p align="center">
6
+ <a href="https://www.npmjs.com/package/@qkix/strapi-plugin-better-blocks">
7
+ <img alt="npm version" src="https://img.shields.io/npm/v/@qkix/strapi-plugin-better-blocks.svg" />
8
+ </a>
9
+ <a href="https://www.npmjs.com/package/@qkix/strapi-plugin-better-blocks">
10
+ <img alt="npm downloads" src="https://img.shields.io/npm/dm/@qkix/strapi-plugin-better-blocks.svg" />
11
+ </a>
12
+ <a href="https://github.com/qkix/strapi-plugin-better-blocks/blob/main/LICENSE">
13
+ <img alt="license" src="https://img.shields.io/npm/l/@qkix/strapi-plugin-better-blocks.svg" />
14
+ </a>
15
+ <a href="https://buymeacoffee.com/k11k">
16
+ <img alt="Buy Me a Coffee" src="https://img.shields.io/badge/Buy%20Me%20a%20Coffee-support-FFDD00?logo=buymeacoffee&logoColor=black" />
17
+ </a>
18
+ </p>
19
+
20
+ <p align="center">
21
+ <img src="./docs/better-blocks-demo.gif" alt="Better Blocks Demo" width="800" />
22
+ </p>
23
+
24
+ ---
25
+
26
+ ## Table of Contents
27
+
28
+ 1. [Features](#features)
29
+ 2. [Compatibility](#compatibility)
30
+ 3. [Installation](#installation)
31
+ 4. [Configuration](#configuration)
32
+ 5. [Usage](#usage)
33
+ 6. [Custom Color Presets](#custom-color-presets)
34
+ 7. [Media Embeds (CSP Configuration)](#media-embeds-csp-configuration)
35
+ 8. [Frontend Rendering](#frontend-rendering)
36
+ 9. [Contributing](#contributing)
37
+ 10. [License](#license)
38
+
39
+ ---
40
+
41
+ ## Feature showcase
42
+
43
+ ### Text color & background highlight
44
+
45
+ ![Text color & highlight](./docs/features/feature-text-color-highlight.gif)
46
+
47
+ ### Tables
48
+
49
+ ![Tables](./docs/features/feature-tables.gif)
50
+
51
+ ### Nested lists
52
+
53
+ ![Nested lists](./docs/features/feature-nested-lists.gif)
54
+
55
+ ### Media embeds (YouTube / Vimeo)
56
+
57
+ ![Media embeds](./docs/features/feature-media-embeds.gif)
58
+
59
+ ### Text alignment
60
+
61
+ ![Text alignment](./docs/features/feature-text-alignment.gif)
62
+
63
+ ### Line height & indentation
64
+
65
+ ![Line height & indentation](./docs/features/feature-line-height-indent.gif)
66
+
67
+ ### Image captions & alignment
68
+
69
+ ![Image captions](./docs/features/feature-image-captions.gif)
70
+
71
+ ### Emoji & special character pickers
72
+
73
+ ![Emoji & special characters](./docs/features/feature-emoji-special-chars.gif)
74
+
75
+ ### Find & replace
76
+
77
+ ![Find and replace](./docs/features/feature-find-replace.gif)
78
+
79
+ ### Undo / redo, remove formatting & word count
80
+
81
+ ![Undo/redo, word count](./docs/features/feature-undo-redo-wordcount.gif)
82
+
83
+ ## Features
84
+
85
+ - **Inline Text Color** &mdash; Apply foreground color to selected text from a configurable palette
86
+ - **Background Highlight** &mdash; Apply background color to selected text for highlighting
87
+ - **Live Preview Button** &mdash; The toolbar button reflects the currently active text and highlight colors
88
+ - **Customizable Palettes** &mdash; Define custom color presets per field via Content-Type Builder
89
+ - **Dark & Light Mode** &mdash; Fully compatible with both Strapi themes
90
+ - **Drop-in Replacement** &mdash; Works as a custom field alongside the native Rich Text (Blocks) field
91
+ - **Markdown Paste Support** &mdash; Paste CommonMark or GitHub Flavored Markdown (GFM) from tools like ChatGPT, Claude, DeepSeek, Kimi, docs, and issue trackers, and convert headings, lists, task lists, tables, links, code, quotes, horizontal rules, LaTeX math, and inline marks into rich Better Blocks content automatically
92
+ - **Nested Lists** &mdash; Infinitely nestable ordered and unordered lists with per-level format switching (Tab to indent, Shift+Tab to outdent)
93
+ - **To-do Lists** &mdash; Checkbox list items with click-to-toggle and strikethrough on checked items
94
+ - **Tables** &mdash; Insert tables with header row, add/remove rows and columns via hover toolbar
95
+ - **Embeds (generic iframe / URL)** &mdash; Insert any third-party embed from the `+` Insert menu, the `/embed` slash command, or the toolbar's media button. Two modes: **URL** (YouTube, Vimeo, Loom, Wistia, Dailymotion and api.video are converted to an iframe automatically) and **Embed code** (paste any platform's `<iframe>` snippet). The pasted markup is never stored verbatim &mdash; the iframe is _rebuilt_ from an attribute allowlist over an https-only `src`, so scripts, event handlers and unknown attributes cannot survive into your frontend. Live preview in the editor, plus aspect ratio (16:9 / 21:9 / 4:3 / 1:1 / custom), alignment, caption and an accessible title. Supersedes the older YouTube/Vimeo-only **media embed** block, which still renders existing content but is no longer inserted
96
+ - **Video (provider-aware)** &mdash; Insert a video from the `+` Insert menu or the `/video` slash command. Source it from the **Media Library**, a **direct URL**, or a hosting provider &mdash; Mux, api.video and Cloudinary URLs are detected automatically, and a bare **Mux playback ID** fills in the stream URL and poster frame for you. When [`strapi-plugin-mux-video-uploader`](https://github.com/muxinc/strapi-plugin-mux-video-uploader) is installed and configured, a **Mux** button lists and searches your Mux assets inline with thumbnails. Set a poster image, title, caption, WebVTT captions/transcript, alignment, aspect ratio, and player behaviour (controls, autoplay, muted, loop)
97
+ - **Math (LaTeX / KaTeX)** &mdash; Inline and block math rendered with KaTeX; insert from the toolbar, the `/math` slash command, the blocks selector, or by typing `$$ ` (block) / `$…$ ` (inline), then edit in a full-screen modal with a side-by-side source editor and live preview. Block math supports multi-line equations via `\\` and LaTeX environments such as `aligned` and `cases`
98
+ - **Diagrams (Mermaid)** &mdash; Block-level [Mermaid](https://mermaid.js.org/) diagrams (flowcharts, sequence, class, state, ER, pie, and more) rendered to SVG; insert from the blocks selector, the `/mermaid` slash command, or by typing ` ```mermaid ` then a space, then edit the definition in a full-screen modal with live preview and zoom controls. Theme follows Strapi's light/dark mode
99
+ - **Callouts / Admonitions** &mdash; GitHub-style callouts in five variants (`Note`, `Tip`, `Important`, `Warning`, `Caution`) with an optional custom title and nested rich-text content (paragraphs, lists, links). Insert from the blocks selector or the `/note`, `/tip`, `/important`, `/warning`, `/caution` slash commands; switch variant, edit the title, or remove from the header popover. Colors follow Strapi's design tokens and adapt to light/dark mode
100
+ - **Details / Collapsible** &mdash; GitHub-style collapsible `<details>` / `<summary>` sections for managing content density. Insert from the blocks selector or the `/details` slash command; edit the summary label and toggle open/closed-by-default (`defaultOpen`) from the header. Holds full rich-text block content (paragraphs, lists, tables, images) and supports nesting. Admins can set the default summary text and choose a GitHub-minimal (default) or Custom (bordered + background) style. Stored as `{ "type": "details", "summary": "…", "defaultOpen": false, "children": [...] }`
101
+ - **Button (WordPress-style CTA)** &mdash; Insert a styled call-to-action button from the blocks selector or by typing `[button]` then a space. Two modes: **Link** (URL + open-in-new-tab + ARIA label) and **File** (pick any asset from the Media Library to render a download button with optional file size and type icon, and choose direct download or preview-in-new-tab). A full-screen editor with live preview controls alignment, background/text colors (including hover colors), border radius, font size/weight, padding presets, border, and a custom CSS class. One-click **style presets** (Primary / Secondary / Outline / Filled) keep CTAs on-brand. Admins can set default button colors and customize the presets. Stored as `{ "type": "button", "buttonType": "link" | "file", "label": "…", "alignment": "center", "link": {…} | "file": {…}, "style": {…} }`
102
+ - **Audio (HTML5 player)** &mdash; Insert an audio block from the `+` Insert menu, the `/audio` slash command, or by typing `[audio]` then a space. Pick a file from the Media Library (upload included) or paste a direct URL, with a live native `<audio>` preview in the editor. Set an optional title and caption, block alignment (left / center / right / none), and player behaviour: controls, autoplay, loop, and preload (none / metadata / auto). Stored as `{ "type": "audio", "file": {…}, "title": "…", "caption": "…", "player": {…}, "alignment": "center" }`
103
+ - **Horizontal Line** &mdash; Insert `<hr>` dividers between content blocks
104
+ - **Text Alignment** &mdash; Per-block left, center, right, and justify alignment
105
+ - **Undo / Redo** &mdash; Toolbar buttons wired to Slate's built-in history
106
+ - **Remove Formatting** &mdash; One-click button to strip all marks from selected text
107
+ - **Link Decorators** &mdash; "Open in new tab" option with `target="_blank"` and `rel="noopener noreferrer"`
108
+ - **Word & Character Count** &mdash; Live counter displayed at the bottom of the editor
109
+ - **Line Height** &mdash; Per-block line spacing control (1, 1.15, 1.5, 2, 2.5, 3)
110
+ - **Indent / Outdent** &mdash; Block-level indentation buttons (up to 6 levels)
111
+ - **Image Captions** &mdash; Editable figcaption below images
112
+ - **Image Alignment** &mdash; Left, center, and right alignment for images via hover buttons
113
+ - **Emoji Picker** &mdash; Searchable popup grid with 130+ common emojis
114
+ - **Special Characters** &mdash; Categorized picker for currency, math, arrows, Greek, legal symbols and more
115
+ - **Find and Replace** &mdash; Search with real-time highlighting (yellow for all matches, orange for active), prev/next navigation, replace and replace all
116
+ - **Font Family** &mdash; Inline font family selector (Arial, Georgia, Times New Roman, and more)
117
+ - **Font Size** &mdash; Inline font size selector (10px to 48px)
118
+ - **Slash Commands** &mdash; Type `/` to open a block insertion menu with search, arrow key navigation, and Enter to select
119
+ - **Auto Text Transformations** &mdash; Automatic symbol replacement on space: `(c)` &rarr; &copy;, `1/2` &rarr; &frac12;, `--` &rarr; &mdash;, `->` &rarr; &rarr;, and more
120
+ - **Editor Placeholder** &mdash; "Start writing..." placeholder shown when the editor is empty
121
+ - **Responsive Toolbar** &mdash; Wraps to multiple rows on smaller screens so all buttons remain accessible
122
+ - **Full Blocks Editor** &mdash; Paragraphs, headings, lists, links, quotes, code blocks, and all standard text modifiers (bold, italic, underline, strikethrough, code, uppercase, superscript, subscript)
123
+
124
+ ## Compatibility
125
+
126
+ | Strapi Version | Plugin Version |
127
+ | -------------- | -------------- |
128
+ | v5.x | v0.1.x |
129
+
130
+ ## Installation
131
+
132
+ ```bash
133
+ # Using yarn
134
+ yarn add @qkix/strapi-plugin-better-blocks
135
+
136
+ # Using npm
137
+ npm install @qkix/strapi-plugin-better-blocks
138
+ ```
139
+
140
+ After installation, rebuild your Strapi admin panel:
141
+
142
+ ```bash
143
+ yarn build
144
+ # or
145
+ npm run build
146
+ ```
147
+
148
+ ## Configuration
149
+
150
+ ### 1. Enable the plugin
151
+
152
+ Add the plugin to your Strapi configuration in `config/plugins.ts` (or `config/plugins.js`):
153
+
154
+ ```ts
155
+ // config/plugins.ts
156
+ export default () => ({
157
+ 'better-blocks': {
158
+ enabled: true,
159
+ },
160
+ });
161
+ ```
162
+
163
+ #### Details / Collapsible defaults (optional)
164
+
165
+ Set plugin-wide defaults for the collapsible **Details** block. These apply to every
166
+ Better Blocks field, and can still be overridden per field in the Content-Type Builder.
167
+
168
+ ```ts
169
+ // config/plugins.ts
170
+ export default () => ({
171
+ 'better-blocks': {
172
+ enabled: true,
173
+ config: {
174
+ details: {
175
+ defaultSummary: 'Show more', // summary label for newly inserted blocks
176
+ style: 'custom', // 'github' (minimal) | 'custom' (bordered + tinted background)
177
+ },
178
+ },
179
+ },
180
+ });
181
+ ```
182
+
183
+ #### Mux video hosting (optional)
184
+
185
+ The **Video** block works out of the box with the Media Library and direct URLs.
186
+ To also browse your Mux library from inside the editor, install the community
187
+ Mux plugin alongside Better Blocks:
188
+
189
+ ```bash
190
+ yarn add strapi-plugin-mux-video-uploader
191
+ ```
192
+
193
+ ```ts
194
+ // config/plugins.ts
195
+ export default ({ env }) => ({
196
+ 'better-blocks': { enabled: true },
197
+ 'mux-video-uploader': {
198
+ enabled: true,
199
+ config: {
200
+ accessTokenId: env('MUX_ACCESS_TOKEN_ID'),
201
+ secretKey: env('MUX_SECRET_KEY'),
202
+ },
203
+ },
204
+ });
205
+ ```
206
+
207
+ Create the credentials in the Mux dashboard under **Settings → Access Tokens**
208
+ with **Full Access**. Once they are set, the video block's source picker grows a
209
+ **Mux** button that lists and searches your Mux assets with thumbnails.
210
+
211
+ Notes:
212
+
213
+ - Only assets **uploaded through the Mux plugin** appear — the plugin lists its
214
+ own `mux-asset` records, not everything in your Mux account.
215
+ - Assets with a **signed** playback policy are shown but not selectable: signed
216
+ playback needs a short-lived JWT minted per request, and a token stored in the
217
+ document body would expire.
218
+ - Better Blocks detects the plugin by calling its asset-list route, not its
219
+ `mux-settings` route — the latter reports "not configured" unless a webhook
220
+ signing secret is also set, which listing assets does not require.
221
+
222
+ #### Button defaults (optional)
223
+
224
+ Set plugin-wide default colors for newly inserted **Button** blocks, and customize the
225
+ **Style presets** (Primary / Secondary / Outline / Filled) offered in the editor's
226
+ "Style preset" picker so authors deploy on-brand CTAs in one click. These apply to every
227
+ Better Blocks field, and can still be overridden per field in the Content-Type Builder
228
+ (and per button in the editor).
229
+
230
+ ```ts
231
+ // config/plugins.ts
232
+ export default () => ({
233
+ 'better-blocks': {
234
+ enabled: true,
235
+ config: {
236
+ button: {
237
+ defaultStyle: {
238
+ backgroundColor: '#4945ff',
239
+ textColor: '#ffffff',
240
+ },
241
+ // Brand variants for the "Style preset" picker (each applies
242
+ // background, text and border; the rest of the styling is untouched).
243
+ presets: {
244
+ primary: {
245
+ backgroundColor: '#4945ff',
246
+ textColor: '#ffffff',
247
+ border: 'none',
248
+ },
249
+ secondary: {
250
+ backgroundColor: '#dcdce4',
251
+ textColor: '#32324d',
252
+ border: 'none',
253
+ },
254
+ outline: {
255
+ backgroundColor: 'transparent',
256
+ textColor: '#4945ff',
257
+ border: '2px solid #4945ff',
258
+ },
259
+ filled: {
260
+ backgroundColor: '#32324d',
261
+ textColor: '#ffffff',
262
+ border: 'none',
263
+ },
264
+ },
265
+ },
266
+ },
267
+ },
268
+ });
269
+ ```
270
+
271
+ #### Button JSON shape (for frontend renderers)
272
+
273
+ A button is stored as a single block. `buttonType` selects the rendering mode:
274
+
275
+ ```jsonc
276
+ {
277
+ "type": "button",
278
+ "buttonType": "link", // "link" | "file"
279
+ "label": "Get started",
280
+ "alignment": "center", // "left" | "center" | "right"
281
+
282
+ // Link mode
283
+ "link": {
284
+ "url": "https://example.com",
285
+ "target": "_blank", // "_self" | "_blank" | "_parent" | "_top"
286
+ "rel": "noopener noreferrer", // auto-added when target is "_blank"
287
+ "ariaLabel": "Get started",
288
+ },
289
+
290
+ // File mode (instead of `link`)
291
+ "file": {
292
+ "id": 123,
293
+ "url": "/uploads/whitepaper.pdf",
294
+ "name": "Product Whitepaper.pdf",
295
+ "size": 5242880, // bytes
296
+ "ext": ".pdf",
297
+ "mime": "application/pdf",
298
+ },
299
+ "showFileSize": true,
300
+ "showFileIcon": true,
301
+ "filePreview": false, // false → download the file, true → open/preview in a new tab
302
+
303
+ "style": {
304
+ "variant": "custom", // "primary" | "secondary" | "outline" | "filled" | "custom"
305
+ "backgroundColor": "#4945ff",
306
+ "textColor": "#ffffff",
307
+ "borderRadius": "4px",
308
+ "fontSize": "16px",
309
+ "fontWeight": "600",
310
+ "padding": "12px 24px",
311
+ "border": "none",
312
+ "hoverBackgroundColor": "#3732c9",
313
+ "hoverTextColor": "#ffffff",
314
+ },
315
+ "cssClass": "my-cta",
316
+ }
317
+ ```
318
+
319
+ Render link mode as `<a href={link.url} target={link.target} rel={link.rel}>`. For file
320
+ mode, honour `filePreview`: when `true` open the file in a new tab
321
+ (`<a href={file.url} target="_blank" rel="noopener noreferrer">`), otherwise force a
322
+ download (`<a href={file.url} download={file.name}>`) — optionally prefixing
323
+ `file.name`/size with `showFileIcon`/`showFileSize`. Only the keys for the active mode are
324
+ present. `style.variant` records the selected preset for the editor UI; renderers can
325
+ ignore it.
326
+
327
+ #### Audio JSON shape (for frontend renderers)
328
+
329
+ An audio block is a void block referencing a Media Library asset (or an external URL) plus
330
+ display and player settings:
331
+
332
+ ```jsonc
333
+ {
334
+ "type": "audio",
335
+ "file": {
336
+ "id": 123, // Media Library file id (absent when inserted from a raw URL)
337
+ "url": "/uploads/episode.mp3", // direct URL (backend-prefixed for Media Library assets)
338
+ "name": "episode.mp3",
339
+ "ext": ".mp3",
340
+ "mime": "audio/mpeg",
341
+ "size": 5242880, // bytes
342
+ "provider": "local", // local | cloudinary | …
343
+ },
344
+ "title": "Episode 1: Introduction", // optional — key omitted when empty
345
+ "caption": "Our first podcast episode", // optional — key omitted when empty
346
+ "player": {
347
+ "autoplay": false,
348
+ "loop": false,
349
+ "controls": true,
350
+ "preload": "metadata", // "none" | "metadata" | "auto"
351
+ },
352
+ "alignment": "center", // "left" | "center" | "right" | "none"
353
+ }
354
+ ```
355
+
356
+ Render as a native HTML5 `<audio src={file.url} controls={player.controls} autoPlay={player.autoplay}
357
+ loop={player.loop} preload={player.preload}>` wrapped in an alignment container (`left`/`center`/`right`
358
+ map to `flex-start`/`center`/`flex-end`; `none` flows full-width). Include fallback text and a download
359
+ link inside `<audio>` for unsupported browsers, and associate the `caption` via `aria-describedby`.
360
+ `file.url` is already backend-prefixed for Media Library assets, and the `title`/`caption` keys are
361
+ omitted when empty. See the full React/Astro renderer contract on [issue #43](https://github.com/qkix/strapi-plugin-better-blocks/issues/43).
362
+
363
+ #### Embed JSON shape (for frontend renderers)
364
+
365
+ A generic embed is a void block. `embedHtml` is the **only** field a renderer
366
+ needs — it is already sanitized (rebuilt from an attribute allowlist, https-only
367
+ `src`), so render it as-is and ignore `url` / `iframe`, which exist to
368
+ round-trip the editor UI:
369
+
370
+ ```jsonc
371
+ {
372
+ "type": "embed",
373
+ "source": "url", // "url" | "iframe" — which input the author used
374
+ "url": "https://www.youtube.com/watch?v=abc12345678", // source: "url" only
375
+ "iframe": "<iframe src=…>", // source: "iframe" only — the raw paste
376
+ "embedHtml": "<iframe src=\"https://www.youtube.com/embed/abc12345678\" …></iframe>",
377
+ "embedSrc": "https://www.youtube.com/embed/abc12345678", // hoisted for host checks
378
+ "provider": "youtube", // youtube | vimeo | loom | wistia | dailymotion | api-video | generic
379
+ "thumbnail": "https://img.youtube.com/vi/abc12345678/hqdefault.jpg", // when derivable
380
+ "aspectRatio": "16:9", // "16:9" | "21:9" | "4:3" | "1:1" | "custom"
381
+ "customAspectRatio": "3 / 2", // only when aspectRatio is "custom"
382
+ "alignment": "center", // "left" | "center" | "right" | "none"
383
+ "caption": "A video explaining the feature", // optional
384
+ "title": "Product Demo", // optional — becomes the iframe's accessible name
385
+ }
386
+ ```
387
+
388
+ Wrap `embedHtml` in an alignment container (`left`/`center`/`right` map to
389
+ `flex-start`/`center`/`flex-end`; `none` flows full-width) and a box with CSS
390
+ `aspect-ratio`. Convert `aspectRatio` for CSS by replacing `:` with `/`
391
+ (`"16:9"` → `16 / 9`); when it is `"custom"`, use `customAspectRatio` verbatim.
392
+ Render `caption` as a `<figcaption>`. Optional keys are omitted when empty.
393
+
394
+ > **Deprecated:** the older `media-embed` block (`{ type: "media-embed", url,
395
+ originalUrl }`) is no longer inserted by the editor, but renderers should keep
396
+ > handling it so content authored before the `embed` block still displays.
397
+
398
+ #### Video JSON shape (for frontend renderers)
399
+
400
+ ```jsonc
401
+ {
402
+ "type": "video",
403
+ "provider": "mux", // "local" | "mux" | "api-video" | "cloudinary" | "custom"
404
+ "url": "https://stream.mux.com/def456.m3u8", // playback URL or direct file URL
405
+ "assetId": "abc123", // provider asset id, when known
406
+ "playbackId": "def456", // provider playback id, when known
407
+ "file": {
408
+ // present when sourced from the Media Library (provider: "local")
409
+ "id": 123,
410
+ "name": "demo.mp4",
411
+ "ext": ".mp4",
412
+ "mime": "video/mp4",
413
+ "size": 5242880, // bytes
414
+ "duration": 132.4, // seconds, when reported
415
+ "provider": "local",
416
+ },
417
+ "poster": "https://image.mux.com/def456/thumbnail.jpg", // optional
418
+ "title": "Introduction Video", // optional
419
+ "caption": "Watch this to get started", // optional
420
+ "transcript": "https://example.com/captions.vtt", // optional WebVTT
421
+ "player": {
422
+ "autoplay": false,
423
+ "loop": false,
424
+ "muted": false,
425
+ "controls": true,
426
+ },
427
+ "alignment": "center", // "left" | "center" | "right" | "none"
428
+ "aspectRatio": "16:9",
429
+ "customAspectRatio": "3 / 2", // only when aspectRatio is "custom"
430
+ }
431
+ ```
432
+
433
+ Note `player` is nested rather than flattened onto the node, matching the
434
+ sibling `audio` block so renderers only learn one shape.
435
+
436
+ Rendering rules:
437
+
438
+ - **`provider: "local"` / `"custom"` with a direct file URL** — a native
439
+ `<video src={url} poster={poster} controls={player.controls}
440
+ autoPlay={player.autoplay} loop={player.loop} muted={player.muted}>`. Add
441
+ `<track kind="captions" src={transcript}>` when `transcript` is set, and
442
+ associate `caption` via `aria-describedby`.
443
+ - **HLS/DASH sources** (`url` ending in `.m3u8` / `.mpd`, i.e. most Mux assets)
444
+ — a bare `<video>` only plays these in Safari. Use the provider's player
445
+ (`<mux-player playback-id={playbackId}>` for Mux) or attach `hls.js`.
446
+ - **`provider: "mux"`** — `playbackId` alone is enough for a **public** playback
447
+ policy; no credentials are needed on the frontend. Assets with a **signed**
448
+ policy are intentionally not selectable in the editor, because they need a
449
+ short-lived JWT minted per request and a token baked into the document body
450
+ would expire.
451
+
452
+ Apply `alignment` and `aspectRatio` exactly as for the embed block. Optional
453
+ keys are omitted when empty.
454
+
455
+ #### Table JSON shape (for frontend renderers)
456
+
457
+ A table is a `table` block holding `table-row` children, each holding
458
+ `table-cell` or `table-header-cell` children. Cells contain **inline** content —
459
+ text leaves with marks, links, inline math — so render their `children` through
460
+ the same inline renderer used for paragraphs, never as plain text.
461
+
462
+ ```jsonc
463
+ {
464
+ "type": "table",
465
+ "children": [
466
+ {
467
+ "type": "table-row",
468
+ "children": [
469
+ {
470
+ "type": "table-header-cell",
471
+ "colSpan": 2, // absent means 1
472
+ "children": [{ "type": "text", "text": "Spans two columns" }],
473
+ },
474
+ {
475
+ "type": "table-header-cell",
476
+ "children": [{ "type": "text", "text": "Plain header" }],
477
+ },
478
+ ],
479
+ },
480
+ {
481
+ "type": "table-row",
482
+ "children": [
483
+ {
484
+ "type": "table-cell",
485
+ "align": "center", // absent means "left"
486
+ "children": [{ "type": "text", "text": "Centered", "bold": true }],
487
+ },
488
+ // ...two more cells; the first row's colSpan:2 cell covers two columns
489
+ ],
490
+ },
491
+ ],
492
+ }
493
+ ```
494
+
495
+ Rendering rules:
496
+
497
+ - **`align`** maps to `text-align`. **Absent means `left`** — left-aligned cells
498
+ store nothing, so documents authored before alignment existed stay valid.
499
+ - **`colSpan` / `rowSpan`** map to the HTML attributes of the same name.
500
+ **Absent means 1**, so unmerged cells — the overwhelming majority — carry
501
+ neither key. A spanned-over slot has no node at all: the cell that covers it
502
+ simply carries the span, exactly like hand-written HTML.
503
+ - **`table-header-cell`** renders as `<th scope="col">`, `table-cell` as `<td>`.
504
+ When the first row consists entirely of header cells, wrap it in `<thead>` and
505
+ the rest in `<tbody>`. Authors toggle this per table, so don't assume a header
506
+ row exists.
507
+ - **A `rowSpan` never crosses the header/body divide.** The editor won't merge
508
+ across it and normalizes away any span that does, because HTML doesn't allow a
509
+ cell to span `<thead>` into `<tbody>` — so `<thead>` is always exactly the
510
+ leading header rows, and no cell straddles the two groups.
511
+
512
+ ### 2. Restart Strapi
513
+
514
+ ```bash
515
+ yarn develop
516
+ ```
517
+
518
+ ### 3. Add a Better Blocks field
519
+
520
+ 1. Go to **Content-Type Builder**
521
+ 2. Select or create a content type
522
+ 3. Click **Add new field**
523
+ 4. Switch to the **CUSTOM** tab
524
+ 5. Select **Better Blocks**
525
+ 6. Configure the field name and color settings
526
+ 7. Save and wait for Strapi to restart
527
+
528
+ ## Usage
529
+
530
+ Once added to a content type, the Better Blocks field provides an enhanced Rich Text editor with:
531
+
532
+ ### Text Color
533
+
534
+ 1. Select text in the editor
535
+ 2. Click the **A** button in the toolbar
536
+ 3. Switch to the **Text** tab
537
+ 4. Choose a color from the palette
538
+ 5. Click **Remove color** to reset
539
+
540
+ ### Background Highlight
541
+
542
+ 1. Select text in the editor
543
+ 2. Click the **A** button in the toolbar
544
+ 3. Switch to the **Highlight** tab
545
+ 4. Choose a background color from the palette
546
+ 5. Click **Remove highlight** to reset
547
+
548
+ The toolbar button shows a live preview of the active colors &mdash; the icon color reflects the text color, and the button background reflects the highlight color.
549
+
550
+ ### Math (LaTeX)
551
+
552
+ Insert a math block from the toolbar, the `/math` slash command, the blocks selector, or by typing `$$ ` (block) / `$…$ ` (inline). The editor opens in a full-screen modal with a source editor and live preview. Press <kbd>Cmd/Ctrl</kbd> + <kbd>Enter</kbd> to save.
553
+
554
+ A single block can render **multi-line equations** &mdash; there is no need to create a separate block per line. Use the LaTeX line break `\\`, and an alignment environment when you want the lines to line up:
555
+
556
+ ```latex
557
+ \begin{aligned}
558
+ 2x + 3y &= 5 \\
559
+ x - y &= 1
560
+ \end{aligned}
561
+ ```
562
+
563
+ Other supported environments include `cases` (piecewise), `gathered` (centered lines), and `matrix` / `pmatrix` / `bmatrix`. A bare `\\` outside an environment also breaks to a new (centered) line:
564
+
565
+ ```latex
566
+ E = mc^2 \\
567
+ a^2 + b^2 = c^2
568
+ ```
569
+
570
+ > Pressing <kbd>Enter</kbd> in the source editor only adds a line break to your LaTeX source for readability &mdash; it does not create a new block. The rendered line break comes from the LaTeX `\\`.
571
+
572
+ ## Custom Color Presets
573
+
574
+ You can customize both text and background color palettes per field in the Content-Type Builder:
575
+
576
+ ### Text Colors
577
+
578
+ In the field's **Base settings**:
579
+
580
+ - **Disable default text colors** &mdash; Check to replace default colors with your own
581
+ - **Custom text color presets** &mdash; One color per line in `Label:#HEX` format
582
+
583
+ Example:
584
+
585
+ ```
586
+ Black:#000000
587
+ White:#FFFFFF
588
+ Brand Red:#E53E3E
589
+ Brand Blue:#3182CE
590
+ ```
591
+
592
+ ### Background Colors
593
+
594
+ - **Disable default background colors** &mdash; Check to replace default highlights with your own
595
+ - **Custom background color presets** &mdash; One color per line in `Label:#HEX` format
596
+
597
+ Example:
598
+
599
+ ```
600
+ Warning:#FED7D7
601
+ Info:#BEE3F8
602
+ Success:#C6F6D5
603
+ Neutral:#EDF2F7
604
+ ```
605
+
606
+ ### Default Palettes
607
+
608
+ **Text colors:** Teal, Dark, Gray, Light Gray, Silver, Medium Gray, White
609
+
610
+ **Background colors:** Yellow, Green, Blue, Pink, Purple, Orange, Gray, Teal, Red, Cyan
611
+
612
+ ## Media Embeds (CSP Configuration)
613
+
614
+ If you use the **embed** or **video** blocks, you need to update your Strapi security middleware so the editor can load thumbnails, posters and embed iframes.
615
+
616
+ In `config/middlewares.ts`:
617
+
618
+ ```ts
619
+ export default [
620
+ 'strapi::logger',
621
+ 'strapi::errors',
622
+ {
623
+ name: 'strapi::security',
624
+ config: {
625
+ contentSecurityPolicy: {
626
+ directives: {
627
+ 'img-src': [
628
+ "'self'",
629
+ 'data:',
630
+ 'blob:',
631
+ 'https://img.youtube.com',
632
+ 'https://image.mux.com',
633
+ 'https://res.cloudinary.com',
634
+ ],
635
+ 'media-src': ["'self'", 'data:', 'blob:', 'https://stream.mux.com'],
636
+ 'frame-src': [
637
+ "'self'",
638
+ 'https://www.youtube.com',
639
+ 'https://player.vimeo.com',
640
+ 'https://www.loom.com',
641
+ 'https://fast.wistia.net',
642
+ 'https://www.dailymotion.com',
643
+ 'https://embed.api.video',
644
+ ],
645
+ },
646
+ },
647
+ },
648
+ },
649
+ 'strapi::cors',
650
+ 'strapi::poweredBy',
651
+ 'strapi::query',
652
+ 'strapi::body',
653
+ 'strapi::session',
654
+ 'strapi::favicon',
655
+ 'strapi::public',
656
+ ];
657
+ ```
658
+
659
+ Without this, thumbnails and in-editor embed previews are blocked by the Content
660
+ Security Policy in the Strapi admin panel. Add only the hosts you actually use.
661
+
662
+ If you paste an embed code from a provider not in that list, add its iframe host
663
+ to `frame-src` too &mdash; the editor tells you which provider it detected, and an
664
+ embed whose host is missing simply renders as an empty frame in the admin. The
665
+ saved content is unaffected and still renders on your frontend.
666
+
667
+ ### Live social-embed previews
668
+
669
+ A **social embed** shows a compact card in the editor, with a **Show live post**
670
+ button that swaps in the real post. The preview uses each platform's script-free
671
+ iframe embed (no widget JavaScript runs in the admin), so it only needs those
672
+ hosts added to `frame-src`:
673
+
674
+ ```ts
675
+ 'frame-src': [
676
+ "'self'",
677
+ 'https://platform.twitter.com', // X / Twitter
678
+ 'https://www.instagram.com',
679
+ 'https://www.facebook.com',
680
+ 'https://www.tiktok.com',
681
+ 'https://www.linkedin.com',
682
+ 'https://assets.pinterest.com',
683
+ ],
684
+ ```
685
+
686
+ Add only the platforms you enable. Without them the card still works — clicking
687
+ **Show live post** just renders an empty frame, since the admin CSP blocks it.
688
+ Third-party frames load only after that click, never on page load.
689
+
690
+ ## Frontend Rendering
691
+
692
+ To render Better Blocks content in your React frontend, use the companion renderer:
693
+
694
+ ```bash
695
+ # Using yarn
696
+ yarn add @qkix/better-blocks-react-renderer
697
+
698
+ # Using npm
699
+ npm install @qkix/better-blocks-react-renderer
700
+ ```
701
+
702
+ ```tsx
703
+ import { BlocksRenderer } from '@qkix/better-blocks-react-renderer';
704
+
705
+ const MyComponent = ({ content }) => {
706
+ return <BlocksRenderer content={content} />;
707
+ };
708
+ ```
709
+
710
+ The renderer supports all Better Blocks features including text colors, background highlights, images, and all standard block types.
711
+
712
+ See the [@qkix/better-blocks-react-renderer](https://github.com/qkix/strapi-plugin-better-blocks/tree/main/packages/better-blocks-react-renderer) documentation for details.
713
+
714
+ ## Requirements
715
+
716
+ - **Node.js** &ge; 20.0.0
717
+ - **Strapi** v5.x
718
+ - **Slate** 0.94.1 (bundled with Strapi)
719
+
720
+ ## Contributing
721
+
722
+ Contributions are welcome! This package lives in a monorepo together with the
723
+ React and Astro renderers. The easiest way to get started is with Docker:
724
+
725
+ ```bash
726
+ # Clone the repository
727
+ git clone https://github.com/qkix/strapi-plugin-better-blocks.git
728
+ cd strapi-plugin-better-blocks
729
+
730
+ docker compose up --build
731
+ ```
732
+
733
+ That builds the plugin and starts a Strapi v5 app (SQLite) at
734
+ `http://localhost:1337/admin`, seeded with showcase articles and an admin
735
+ account (`admin@example.com` / `admin12#`). Both renderers come up alongside it
736
+ on `http://localhost:5173` and `http://localhost:4321`, showing the same
737
+ content.
738
+
739
+ To see the field itself, open any article, or add it to your own content type
740
+ via **Content-Type Builder** &rarr; **Add new field** &rarr; **CUSTOM** tab
741
+ &rarr; **Better Blocks**.
742
+
743
+ ### Development workflow
744
+
745
+ 1. Make changes to the plugin source in `packages/strapi-plugin-better-blocks/admin/src/` or `server/src/`
746
+ 2. Rebuild and restart:
747
+ ```bash
748
+ docker compose up --build
749
+ ```
750
+ The plugin is compiled into the image, so a plain `restart` will not pick up
751
+ source changes.
752
+
753
+ ### Full reset
754
+
755
+ To wipe the seeded database and uploaded media and start fresh:
756
+
757
+ ```bash
758
+ docker compose down -v && docker compose up --build
759
+ ```
760
+
761
+ ### Without Docker
762
+
763
+ ```bash
764
+ pnpm install
765
+ pnpm build # the plugin, the core and both renderers
766
+
767
+ pnpm --filter @qkix/example-strapi-app develop
768
+ ```
769
+
770
+ Run the checks the way CI does:
771
+
772
+ ```bash
773
+ pnpm lint
774
+ pnpm typecheck
775
+ pnpm test
776
+ ```
777
+
778
+ ## Community & Support
779
+
780
+ - [GitHub Issues](https://github.com/qkix/strapi-plugin-better-blocks/issues) &mdash; Bug reports and feature requests
781
+ - [GitHub Discussions](https://github.com/qkix/strapi-plugin-better-blocks/discussions) &mdash; Questions and ideas
782
+
783
+ ## Support this project
784
+
785
+ This plugin is built and maintained in my free time, and it's free for everyone. If it has saved you time on a project, you can help keep it caffeinated and actively developed:
786
+
787
+ <a href="https://buymeacoffee.com/k11k">
788
+ <img alt="Buy Me a Coffee" src="https://img.shields.io/badge/Buy%20Me%20a%20Coffee-support-FFDD00?logo=buymeacoffee&logoColor=black&style=for-the-badge" />
789
+ </a>
790
+
791
+ Every coffee goes toward fixing bugs, reviewing PRs, writing docs, and shipping the features you ask for. Thank you! ☕
792
+
793
+ ## License
794
+
795
+ [MIT License](LICENSE) &copy; [qkix](https://github.com/qkix)