@bladeberg/editor 0.2.0 → 0.2.1

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.
package/README.md CHANGED
@@ -1,732 +1,208 @@
1
- # BladeBerg
1
+ # @bladeberg/editor
2
2
 
3
- ![BladeBerg Logo](BladeBerg.png)
3
+ **Gutenberg, standalone.** No WordPress. No Laravel. Just the block editor in your React app.
4
4
 
5
- > **Gutenberg inside Laravel.** BladeBerg brings the WordPress block editor straight into your Laravel app — a rich, familiar authoring experience for your editors, while your back-end stays pure PHP/Blade. No WordPress install, no PHP-in-templates, no headless WP API.
5
+ BladeBerg wraps [`@automattic/isolated-block-editor`](https://github.com/Automattic/isolated-block-editor) the same pre-built browser bundle Automattic uses to run Gutenberg outside of wp-admin and ships it as a lazy-loaded npm package. You get paragraphs, headings, images, columns, embeds, the whole core block library, without installing a single `@wordpress/*` package yourself.
6
6
 
7
- <p>
8
- <a href="https://github.com/BladeBerg/bladeberg/blob/main/LICENSE"><img alt="License: GPL v2" src="https://img.shields.io/badge/License-GPLv2-blue.svg"></a>
9
- <img alt="PHP 8.2+" src="https://img.shields.io/badge/PHP-8.2%2B-777bb4.svg">
10
- <img alt="Laravel 10–13" src="https://img.shields.io/badge/Laravel-10%20%7C%2011%20%7C%2012%20%7C%2013-ff2d20.svg">
11
- </p>
7
+ > Using Laravel? See the full [BladeBerg docs](https://github.com/BladeBerg/bladeberg#readme) for the Composer package, Blade components, PHP rendering, and media API.
12
8
 
13
9
  ---
14
10
 
15
- ## Table of contents
16
-
17
- - [Why BladeBerg?](#why-bladeberg)
18
- - [How it works](#how-it-works)
19
- - [Features](#features)
20
- - [Requirements](#requirements)
21
- - [Installation](#installation)
22
- - [Quick start](#quick-start)
23
- - [Configuration](#configuration)
24
- - [The `bb:` block format & branding](#the-bb-block-format--branding)
25
- - [Right-click block menu](#right-click-block-menu)
26
- - [Storing & rendering content](#storing--rendering-content)
27
- - [Headless / API usage (SPA, mobile, decoupled)](#headless--api-usage-spa-mobile-decoupled)
28
- - [Building custom blocks](#building-custom-blocks)
29
- - [Media manager](#media-manager)
30
- - [PHP / Facade API](#php--facade-api)
31
- - [JavaScript API](#javascript-api)
32
- - [Updating](#updating)
33
- - [Testing](#testing)
34
- - [Roadmap](#roadmap)
35
- - [Contributing](#contributing)
36
- - [Reporting bugs & requesting features](#reporting-bugs--requesting-features)
37
- - [Security](#security)
38
- - [License](#license)
39
- - [Credits](#credits)
40
-
41
- ---
42
-
43
- ## Why BladeBerg?
44
-
45
- Laravel is a fantastic application framework, but it has no first-class **content editor**. Teams that need WordPress-style block authoring usually end up with one of three bad options:
46
-
47
- 1. **Run WordPress alongside Laravel** — two apps, two databases, two deploy pipelines, and a headless API glued in between.
48
- 2. **Ship a plain `<textarea>` / markdown field** — fast to build, miserable for non-technical editors.
49
- 3. **Adopt a heavyweight commercial page-builder** — proprietary, hard to theme, and locked to its own data format.
50
-
51
- BladeBerg was built to remove that compromise. The Gutenberg block editor is the most widely-used block editor on the web, it is open-source (GPL), and — crucially — its editing UI can run completely standalone thanks to [`@automattic/isolated-block-editor`](https://github.com/Automattic/isolated-block-editor). BladeBerg wraps that standalone editor in a Laravel package so you get:
52
-
53
- - the **exact Gutenberg editing experience** your authors already know,
54
- - content stored as **portable block HTML** in your own database, on your own terms,
55
- - a **pure PHP rendering pipeline** (a Blade component + a block parser) with no Node runtime in production,
56
- - and a **Laravel-native** integration: config, service provider, facade, Artisan installer, Blade components, and your existing filesystem/auth.
57
-
58
- ### How the package came to be
59
-
60
- BladeBerg started as a spike to answer one question: *“Can the Gutenberg editor be embedded in a Laravel Blade view without dragging in all of WordPress?”* The journey shaped most of the architecture decisions documented below:
61
-
62
- - **Don't re-bundle `@wordpress/*` yourself.** Early attempts to bundle the individual `@wordpress/block-editor`, `@wordpress/data`, etc. packages hit unsolvable private-API / store-locking errors (`Cannot unlock an object that was not locked before`). The fix was to stop fighting it and ship the **pre-built `isolated-block-editor` browser bundle** as-is, with a thin React-globals + mount layer on top.
63
- - **Store content under your own brand.** Gutenberg serializes blocks as `<!-- wp:paragraph -->`. BladeBerg rewrites that to a configurable prefix (default `<!-- bb:paragraph -->`) on the way out, and normalizes it back on the way in — so your database never leaks WordPress branding and the format is unmistakably yours.
64
- - **Re-use Laravel's own storage.** The media manager scans the disk configured by your app's `FILESYSTEM_DISK` instead of inventing a parallel storage system or requiring extra tables.
65
-
66
- It's still young software (pre-1.0) but it's already usable for real content. See the [roadmap](#roadmap) for what's next.
67
-
68
- ---
69
-
70
- ## How it works
71
-
72
- ```
73
- ┌─────────────────────────────────────────────────────────────────┐
74
- │ Browser │
75
- │ │
76
- │ <x-bladeberg-editor name="content" /> │
77
- │ │ │
78
- │ ▼ │
79
- │ <textarea data-bladeberg-editor> ← the real form field │
80
- │ │ │
81
- │ editor.jsx mounts the isolated-block-editor browser bundle │
82
- │ on the textarea, hides it, and keeps textarea.value in sync │
83
- │ │ │
84
- │ on submit: rewrite <!-- wp:… --> → <!-- bb:… --> │
85
- └────────┼──────────────────────────────────────────────────────────┘
86
- │ POST content="<!-- bb:paragraph -->…"
87
-
88
- ┌─────────────────────────────────────────────────────────────────┐
89
- │ Laravel (PHP only) │
90
- │ │
91
- │ store $request->input('content') (already bb: prefixed) │
92
- │ │ │
93
- │ ▼ │
94
- │ <x-bladeberg-render :content="$post->content" /> │
95
- │ │ │
96
- │ BlockParser normalizes bb: → wp:, parses blocks, │
97
- │ renders core block HTML + your Blade views for dynamic blocks │
98
- └─────────────────────────────────────────────────────────────────┘
99
- ```
100
-
101
- The key insight: **the editor is JavaScript, but rendering is pure PHP.** Your production servers never need Node — the compiled editor bundle is published once into `public/vendor/bladeberg/`.
102
-
103
- ---
104
-
105
- ## Features
106
-
107
- - 🧱 **Full Gutenberg editing** — paragraphs, headings, lists, images, columns, quotes, embeds, and the rest of the core block library.
108
- - 🖊️ **Drop-in Blade components** — `<x-bladeberg-editor>` to edit, `<x-bladeberg-render>` to display.
109
- - 🏷️ **Configurable block prefix** — your content is saved as `<!-- bb:… -->` (or any prefix you choose), never `wp:`.
110
- - 🎨 **Re-brandable & themable** — accent color and UI labels are CSS-variable driven; “WordPress/wp:” strings are rebranded in the UI.
111
- - 🖱️ **Right-click block menu** — restores the context-menu → block Options behaviour the standalone bundle drops.
112
- - 🗂️ **Optional media manager** — `disabled` / `link` / `select` / `upload` modes, backed by your existing Laravel filesystem disk (no extra tables) or Spatie Media Library.
113
- - 🧩 **Headless-ready** — a separate `@bladeberg/editor` npm package mounts the editor in any SPA/mobile frontend; an optional render API turns stored content into HTML.
114
- - ⚙️ **Laravel-native** — service-provider auto-discovery, publishable config, facade, and an Artisan installer.
115
- - 🚀 **No Node in production** — the editor is pre-built and committed; servers only run PHP.
116
- - ✅ **Tested** — PHPUnit suite covering the block parser and registry.
117
-
118
- ---
119
-
120
- ## Requirements
121
-
122
- - PHP **8.2** or higher
123
- - Laravel **10 / 11 / 12 / 13**
124
-
125
- ---
126
-
127
- ## Installation
11
+ ## Install
128
12
 
129
13
  ```bash
130
- composer require bladeberg/bladeberg
131
- php artisan bladeberg:install
14
+ npm install @bladeberg/editor react react-dom
132
15
  ```
133
16
 
134
- `bladeberg:install` will:
17
+ That's it. The Gutenberg runtime is **bundled inside the package** — you won't hit WordPress dependency hell.
135
18
 
136
- 1. Publish the compiled editor assets to `public/vendor/bladeberg/`.
137
- 2. Publish the config file to `config/bladeberg.php`.
138
- 3. Print next-step guidance.
139
-
140
- Prefer plain `vendor:publish`? The unified tag publishes the same set (assets + config):
141
-
142
- ```bash
143
- php artisan vendor:publish --tag=bladeberg
144
- ```
145
-
146
- Granular tags are also available: `bladeberg-assets`, `bladeberg-config`, `bladeberg-views`, `bladeberg-migrations`.
147
-
148
- > **Heads up:** re-run `php artisan bladeberg:install` (or `php artisan vendor:publish --tag=bladeberg-assets --force`) after every package update so the published assets stay in sync with the installed version.
19
+ **Requirements:** React 18, a modern browser, and a bundler that supports ESM (`import`).
149
20
 
150
21
  ---
151
22
 
152
23
  ## Quick start
153
24
 
154
- ### 1. Add the editor to a create/edit form
155
-
156
- ```blade
157
- <form action="{{ route('posts.store') }}" method="POST">
158
- @csrf
159
-
160
- <input type="text" name="title" placeholder="Title">
161
-
162
- <x-bladeberg-editor name="content" />
163
-
164
- <button type="submit">Publish</button>
165
- </form>
166
- ```
167
-
168
- ### 2. Render the stored content on a show page
169
-
170
- ```blade
171
- <x-bladeberg-render :content="$post->content" />
172
- ```
173
-
174
- ### 3. Re-open existing content for editing
175
-
176
- ```blade
177
- <x-bladeberg-editor name="content" :value="$post->content" />
178
- ```
179
-
180
- The editor re-hydrates the saved block HTML back into Gutenberg blocks automatically.
181
-
182
- #### `<x-bladeberg-editor>` props
183
-
184
- | Prop | Type | Default | Description |
185
- |---------|----------|----------------|--------------------------------------------------------|
186
- | `name` | `string` | *(required)* | The form field name posted to your controller. |
187
- | `value` | `string` | `''` | Existing block HTML to load into the editor. |
188
- | `id` | `string` | auto-generated | Custom DOM id for the underlying textarea. |
189
-
190
- A controller is just ordinary Laravel — the content arrives as a normal request field:
191
-
192
- ```php
193
- public function store(Request $request)
194
- {
195
- $request->validate(['content' => ['required', 'string']]);
196
-
197
- Post::create([
198
- 'title' => $request->string('title'),
199
- 'content' => $request->input('content'), // already <!-- bb:… --> prefixed
200
- ]);
201
-
202
- return redirect()->route('posts.index');
203
- }
204
- ```
205
-
206
- ---
207
-
208
- ## Configuration
209
-
210
- Everything lives in `config/bladeberg.php` (published by the installer).
211
-
212
- ### Editor
213
-
214
- | Key | Type | Default | Description |
215
- |---------------------|-----------------|----------|------------------------------------------------------------------------|
216
- | `block_prefix` | `string` | `'bb'` | Prefix written into block comments on save. `env: BLADEBERG_BLOCK_PREFIX`. |
217
- | `allowed_blocks` | `array\|null` | `null` | Restrict the available blocks. `null` = allow all registered blocks. |
218
- | `has_fixed_toolbar` | `bool` | `false` | Pin the block toolbar to the top of the editor. |
219
- | `align_wide` | `bool` | `true` | Enable wide / full-width alignment for blocks that support it. |
220
- | `content_storage` | `string` | `'html'` | Storage format. Only `'html'` is supported today. |
221
-
222
- **Example — restrict the block palette:**
223
-
224
- ```php
225
- 'allowed_blocks' => [
226
- 'core/paragraph',
227
- 'core/heading',
228
- 'core/image',
229
- 'core/list',
230
- 'core/quote',
231
- ],
232
- ```
233
-
234
- ### Stylesheets
235
-
236
- The `styles` array controls which pre-built CSS bundles the editor loads. The defaults are tuned to avoid version conflicts — only change them if you know you need to:
237
-
238
- | Key | Default | Loads |
239
- |-----------------|---------|--------------------------------------------------------------------|
240
- | `core` | `true` | Gutenberg editor chrome (toolbar, canvas, popovers). |
241
- | `iso` | `true` | `isolated-block-editor` layout shell. |
242
- | `components` | `false` | `@wordpress/components` (already inside `core.css`). |
243
- | `blocks_style` | `false` | Per-block **frontend** styles (loaded independently by the renderer). |
244
- | `blocks_editor` | `false` | Per-block **editor** styles (already inside `core.css`). |
245
-
246
- ### Media
247
-
248
- See the [Media manager](#media-manager) section for the full breakdown of the `media` block.
249
-
250
- ### Content normalization
251
-
252
- A server-side safety net that mirrors the client-side `wp:` → `bb:` rewrite for requests that bypass the browser (AJAX, imports). See [Storing & rendering content](#storing--rendering-content).
253
-
254
- ---
255
-
256
- ## The `bb:` block format & branding
257
-
258
- Gutenberg serializes blocks with a `wp:` prefix inside HTML comments:
259
-
260
- ```html
261
- <!-- wp:paragraph --><p>Hello</p><!-- /wp:paragraph -->
262
- ```
263
-
264
- BladeBerg rewrites this to **your** prefix before the content ever leaves the browser, and normalizes it back internally for parsing — so the round-trip is lossless:
265
-
266
- ```html
267
- <!-- bb:paragraph --><p>Hello</p><!-- /bb:paragraph -->
268
- ```
269
-
270
- This happens in two coordinated places:
271
-
272
- 1. **Client (on save)** — a form-submit interceptor in `editor.jsx` (and `window.Bladeberg.getContent()` for AJAX) rewrites `wp:` → your prefix in the textarea value just before the request is sent. This is the only place stored content is branded; nothing on the server rewrites it.
273
- 2. **Server (parsing only)** — `BlockParser` normalizes your prefix back to `wp:` at its entry point purely so it can render the blocks; it never changes what's stored.
274
-
275
- Change the prefix per project:
276
-
277
- ```php
278
- // config/bladeberg.php
279
- 'block_prefix' => 'acme', // → <!-- acme:paragraph -->
280
- ```
281
-
282
- ```env
283
- BLADEBERG_BLOCK_PREFIX=acme
284
- ```
285
-
286
- The editor UI also rebrands visible “WordPress” / “WP” / `wp:` / `core/` strings (block validation notices, the Code Editor view, etc.) to your branding, and the accent color is driven by CSS variables in `resources/css/editor.scss` (shipped default: BladeBerg red `#e11d1f`).
287
-
288
- ---
289
-
290
- ## Right-click block menu
291
-
292
- The standalone `isolated-block-editor` browser bundle (its final 2.30 release) ships **no** right-click context menu and exposes no data store to open one. BladeBerg restores the expected behaviour with a DOM-only handler:
293
-
294
- - Right-clicking inside a block selects it and opens that block's existing **Options (⋮)** dropdown — Copy, Duplicate, Move to, Edit as HTML, Group, Lock, Remove, and so on.
295
- - Right-clicking **outside** any editor block leaves the browser's native menu (spellcheck, etc.) untouched.
296
-
297
- No configuration required — it's wired automatically when the editor mounts.
298
-
299
- ---
300
-
301
- ## Storing & rendering content
302
-
303
- Store the posted `content` field as-is — it already carries your `bb:` prefix. The rewrite from Gutenberg's `wp:` to your configured prefix happens **entirely on the client**, at save time: the editor's form-submit interceptor (and `window.Bladeberg.getContent()` for AJAX) rewrites the markup before it leaves the browser. There is no server-side middleware to register and nothing rewrites your stored content after the fact.
304
-
305
- ```php
306
- // Your controller is plain Laravel — content arrives already bb:-prefixed.
307
- $post->content = $request->input('content');
308
- ```
309
-
310
- To render it, use the component:
311
-
312
- ```blade
313
- <x-bladeberg-render :content="$post->content" />
314
- ```
315
-
316
- > **Importing or pasting existing content?** Handle any prefix conversion on the client. When you load saved content back into the editor via `:value`, BladeBerg converts your prefix to `wp:` on mount so Gutenberg can parse it, then converts it back to your prefix on save. Pasted block markup is serialized by the editor and rewritten on submit the same way.
317
-
318
- ---
319
-
320
- ## Headless / API usage (SPA, mobile, decoupled)
321
-
322
- The Blade quick-start above is the simplest path, but BladeBerg also works when your frontend is a separate SPA (React, Vue, Next), a mobile webview, or anything that talks to Laravel over JSON. **You do not need a second repository** — the same project ships two artifacts:
323
-
324
- | Artifact | Registry | Install | Use |
325
- |----------|----------|---------|-----|
326
- | `bladeberg/bladeberg` | Packagist | `composer require bladeberg/bladeberg` | PHP: parse, render, dynamic blocks, media + render APIs |
327
- | `@bladeberg/editor` | npm | `npm i @bladeberg/editor react react-dom` | JS: mount the editor anywhere, headless |
328
-
329
- The only contract between them is **a string of block HTML with your configured prefix**. The frontend produces it; the backend parses/renders it.
330
-
331
- ```mermaid
332
- flowchart LR
333
- spa["SPA / mobile (npm @bladeberg/editor)"] -->|"createEditor() -> getContent()"| str["block-HTML string (bb: prefix)"]
334
- str -->|"POST JSON"| api["Your Laravel API"]
335
- api -->|"store as-is"| db[("Database")]
336
- db -->|"on read"| api
337
- api -->|"option A: return raw string"| spa
338
- api -->|"option B: POST /bladeberg/render"| html["rendered HTML"]
339
- ```
340
-
341
- ### Mount the editor (no Blade, no form)
342
-
343
- ```js
25
+ ```jsx
344
26
  import { createEditor } from '@bladeberg/editor';
345
27
  import '@bladeberg/editor/style.css';
346
28
 
347
29
  const editor = await createEditor({
348
- target: '#editor', // selector or element (textarea or any container)
349
- value: post.content, // stored content (your configured prefix); optional
350
- blockPrefix: 'bb', // must match the backend config
351
- onChange: (html) => { draft = html; }, // optional live updates
352
- media: { // optional; omit to disable media
353
- mode: 'upload', // 'disabled' | 'select' | 'upload'
354
- apiUrl: '/bladeberg/media',
355
- csrfToken: window.csrfToken,
30
+ target: '#editor', // selector or DOM element
31
+ value: savedContent, // optional existing block HTML
32
+ blockPrefix: 'bb', // prefix in stored content (default: bb)
33
+ onChange: (html) => {
34
+ draft = html; // live updates (optional)
356
35
  },
357
36
  });
358
37
 
359
38
  // When the user saves:
360
- await fetch('/api/posts', {
361
- method: 'POST',
39
+ await fetch('/api/posts/1', {
40
+ method: 'PUT',
362
41
  headers: { 'Content-Type': 'application/json' },
363
42
  body: JSON.stringify({ content: editor.getContent() }),
364
43
  });
365
44
 
366
- // On teardown (SPA route change, unmount):
45
+ // On route change / unmount:
367
46
  editor.destroy();
368
47
  ```
369
48
 
370
- `createEditor()` is async because it lazy-loads the Gutenberg runtime on first use. `react` and `react-dom` are peer dependencies — your app's copies are reused.
371
-
372
- ### Render stored content for a headless backend
373
-
374
- Server-rendered Blade apps just use `<x-bladeberg-render>`. Headless backends have two choices:
375
-
376
- - **Return the raw string** and render it in the SPA yourself, or
377
- - **Enable the render API** and let Laravel return finished HTML:
378
-
379
- ```php
380
- // config/bladeberg.php
381
- 'render_api' => ['enabled' => true, 'middleware' => ['api']],
382
- ```
383
-
384
- ```
385
- POST /bladeberg/render { "content": "<!-- bb:paragraph -->..." }
386
- -> { "html": "<div class=\"bb-content\">...</div>" }
387
- ```
388
-
389
- Or render in-process from any controller/job via the facade:
390
-
391
- ```php
392
- $html = Bladeberg::render($post->content);
393
- ```
394
-
395
- > **SSR note:** the package loads a browser-only Gutenberg bundle, so in Next.js/Nuxt import it from a client component (`'use client'` / dynamic `import()` with `ssr: false`).
49
+ `createEditor()` is async it lazy-loads the Gutenberg runtime on first call (~2 MB, cached after that).
396
50
 
397
51
  ---
398
52
 
399
- ## Building custom blocks
400
-
401
- There are two conceptual ways to extend a Gutenberg-style editor with your own blocks. BladeBerg fully supports one of them today; the other is constrained by the standalone editor bundle. Here's the honest picture:
402
-
403
- | Approach | "bb-style" — server-rendered | "wp-style" — editor (React) block |
404
- |----------|------------------------------|-----------------------------------|
405
- | Renders via | **PHP / Blade** on the server | **React** inside the editor canvas |
406
- | Editor UI | Re-uses an existing block's editing UI (often `core/html`, `core/group`, or a core block you map) | A bespoke `edit()` / `save()` React component |
407
- | Registered with | `Bladeberg::registerDynamicBlock()` (PHP) | `registerBlockType()` (JS) |
408
- | Status | ✅ **Supported now** | 🧪 **Not yet** — see the limitation below |
409
- | Best for | data-aware output, server logic, Blade reuse | rich custom in-canvas editing |
410
-
411
- > **Why no custom React blocks yet?** BladeBerg embeds the pre-built `@automattic/isolated-block-editor` browser bundle (v2.30, its final release). That bundle only exposes `window.wp.attachEditor` / `window.wp.detachEditor` — it does **not** expose the `wp.blocks` / `wp.element` / `wp.blockEditor` registration APIs. Without them, custom `edit`/`save` components can't be registered from your app's JS. `window.Bladeberg.registerBlock()` exists and queues calls for the day that becomes possible, but the queue currently does not flush. This is tracked on the [roadmap](#roadmap). **For now, server-rendered blocks below are the supported path.**
412
-
413
- ### bb-style: server-rendered dynamic blocks (recommended)
414
-
415
- You map a **block name** to a **Blade view**. When `<x-bladeberg-render>` encounters that block in the stored content, it renders your view instead of the raw HTML — ideal for data-aware components (a related-posts list, a pricing table, a CTA pulled from config).
416
-
417
- The block name can be one you author (e.g. `bladeberg/callout`) or even an existing core block whose output you want to take over.
418
-
419
- #### Step 1 — map the block name to a view
420
-
421
- ```php
422
- // app/Providers/AppServiceProvider.php
423
- use Bladeberg\Facades\Bladeberg;
424
-
425
- public function boot(): void
426
- {
427
- Bladeberg::registerDynamicBlock('bladeberg/callout', 'blocks.callout');
428
- }
429
- ```
430
-
431
- #### Step 2 — create the Blade view
432
-
433
- ```blade
434
- {{-- resources/views/blocks/callout.blade.php --}}
435
- @php($type = $attributes['type'] ?? 'info')
436
-
437
- <div class="callout callout--{{ $type }}">
438
- {!! $innerContent !!}
439
- </div>
440
- ```
441
-
442
- Variables available in the view:
443
-
444
- | Variable | Type | Description |
445
- |-----------------|-----------|--------------------------------------|
446
- | `$attributes` | `array` | Block attributes from the editor (the JSON in the block comment). |
447
- | `$innerContent` | `string` | Rendered inner HTML. |
448
- | `$innerBlocks` | `Block[]` | Parsed child blocks. |
449
- | `$block` | `Block` | The full `Block` value object. |
450
-
451
- The `Block` value object exposes `isNamed()`, `hasInnerBlocks()`, `getAttribute($key, $default)`, and the `$isSelfClosing` flag — handy for rendering nested content:
452
-
453
- ```blade
454
- {{-- a block that renders its children explicitly --}}
455
- @foreach ($innerBlocks as $child)
456
- @if ($child->getAttribute('featured'))
457
- <strong>{!! $child->innerHTML !!}</strong>
458
- @else
459
- {!! $child->innerHTML !!}
460
- @endif
461
- @endforeach
462
- ```
463
-
464
- #### How the block gets into content
465
-
466
- Until custom React blocks are available, there's no inserter button for a server-only block. You get the block comment into the stored content one of two ways:
467
-
468
- 1. **Code Editor** — switch the editor to Code Editor mode and paste the block comment directly. Gutenberg preserves unrecognized block markup on save, so it round-trips intact:
469
-
470
- ```html
471
- <!-- bb:bladeberg/callout {"type":"warning"} -->
472
- <p>Heads up — this is a server-rendered callout.</p>
473
- <!-- /bb:bladeberg/callout -->
474
- ```
475
-
476
- 2. **Pre-seed it server-side** — set the field's initial value (e.g. from a template) when rendering the editor: `<x-bladeberg-editor name="content" :value="$template" />`.
477
-
478
- Either way, on display `<x-bladeberg-render>` routes the `bladeberg/callout` block to `resources/views/blocks/callout.blade.php`.
479
-
480
- ### wp-style: custom editor (React) blocks
53
+ ## What you get
481
54
 
482
- Not available with the current editor bundle (see the limitation note above). When the bundle gains a registration API, `window.Bladeberg.registerBlock(name, settings)` will register a standard Gutenberg block type — see the [JavaScript API](#javascript-api). Until then, prefer server-rendered blocks, and use [`allowed_blocks`](#editor) to curate which core blocks authors can use.
55
+ | Feature | Details |
56
+ |---------|---------|
57
+ | **Full core blocks** | Paragraph, heading, list, image, quote, columns, embeds, etc. |
58
+ | **Portable HTML** | Content serializes to block comments: `<!-- bb:paragraph -->…` |
59
+ | **Branded prefix** | Default `bb:` instead of WordPress's `wp:` — configurable |
60
+ | **Right-click menu** | Block Options (Copy, Duplicate, Remove, …) restored |
61
+ | **Media upload** | Optional — wire to your own API (see below) |
62
+ | **Zero WordPress deps** | Runtime is pre-built and shipped in the tarball |
483
63
 
484
64
  ---
485
65
 
486
- ## Media manager
487
-
488
- BladeBerg ships an optional media manager that wires into Gutenberg's image / file / gallery blocks. It is **disabled by default** and, by design, re-uses the storage your Laravel app already configures.
489
-
490
- ### Modes
491
-
492
- Set `media.mode` (env: `BLADEBERG_MEDIA_MODE`):
493
-
494
- | Mode | Behaviour | API routes |
495
- |------------|--------------------------------------------------------------------------------------------|-------------------------|
496
- | `disabled` | No media integration. Blocks fall back to a plain URL input. *(default)* | none |
497
- | `link` | Same as `disabled` server-side (reserved for a future URL-picker). | none |
498
- | `select` | Browse and insert files already on the disk. **Upload disabled.** | `GET` only |
499
- | `upload` | Full library: browse existing files **and** upload new ones. | `GET` + `POST` + `DELETE` |
500
-
501
- ### Drivers
502
-
503
- | Driver | Requires | Notes |
504
- |--------------|---------------------------------------|------------------------------------------------------------------------------------|
505
- | `filesystem` | nothing *(default)* | Scans the configured disk directory directly. **No database table or migration.** |
506
- | `spatie` | `composer require spatie/laravel-medialibrary` | Automatic thumbnails/conversions and multi-disk management. |
507
-
508
- ### Configuration
509
-
510
- ```php
511
- // config/bladeberg.php
512
- 'media' => [
513
- 'mode' => env('BLADEBERG_MEDIA_MODE', 'disabled'), // disabled|link|select|upload
514
- 'driver' => env('BLADEBERG_MEDIA_DRIVER', 'filesystem'), // filesystem|spatie
515
- 'disk' => env('BLADEBERG_MEDIA_DISK', env('FILESYSTEM_DISK', 'public')),
516
- 'directory' => env('BLADEBERG_MEDIA_DIRECTORY', 'bladeberg'),
517
- 'route_prefix' => 'bladeberg', // → /bladeberg/media
518
- 'middleware' => ['web', 'auth'],
519
- 'max_file_size_kb' => 10240, // 10 MB
520
- 'allowed_mime_types' => [/* images, video, audio, pdf … */],
521
- 'conversions' => [ // spatie driver only
522
- 'thumbnail' => ['width' => 300, 'height' => 300],
523
- 'medium' => ['width' => 768, 'height' => null],
524
- 'large' => ['width' => 1200, 'height' => null],
525
- ],
526
- ],
527
- ```
66
+ ## API
528
67
 
529
- > Because `disk` defaults to your app's `FILESYSTEM_DISK`, BladeBerg stores media exactly where the rest of your application does — no separate storage setup. Run `php artisan storage:link` if you're using the `public` disk.
68
+ ### `createEditor(options)`
530
69
 
531
- ### Filesystem driver minimal setup
70
+ | Option | Type | Default | Description |
71
+ |--------|------|---------|-------------|
72
+ | `target` | `string \| Element` | *(required)* | CSS selector or element. A `<textarea>` is mounted directly; any other element gets a hidden textarea appended. |
73
+ | `value` | `string` | `''` | Initial block HTML (your configured prefix). |
74
+ | `blockPrefix` | `string` | `'bb'` | Prefix written into block comments on save. |
75
+ | `settings` | `object` | `{}` | Forwarded to Gutenberg's `attachEditor()`. |
76
+ | `media` | `object` | — | `{ mode, apiUrl, csrfToken }` — see [Media](#media). |
77
+ | `branding` | `boolean` | `true` | Rebrand "WordPress" strings in the UI. |
78
+ | `contextMenu` | `boolean` | `true` | Restore right-click block menu. |
79
+ | `onChange` | `(html) => void` | — | Called when content changes (polled every 300 ms). |
532
80
 
533
- ```bash
534
- php artisan storage:link
535
- ```
81
+ **Returns** `Promise<EditorHandle>`:
536
82
 
537
- ```php
538
- 'media' => ['mode' => 'upload', 'driver' => 'filesystem'],
83
+ ```js
84
+ editor.getContent() // current block HTML (prefixed)
85
+ editor.onChange(fn) // subscribe; returns unsubscribe fn
86
+ editor.destroy() // detach editor + stop listeners
87
+ editor.textarea // underlying textarea element
539
88
  ```
540
89
 
541
- ### Spatie driver — setup
90
+ ### `registerBlock(name, settings)`
542
91
 
543
- ```bash
544
- composer require spatie/laravel-medialibrary
545
- php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="medialibrary-migrations"
546
- php artisan migrate
547
- php artisan storage:link
548
- ```
92
+ ```js
93
+ import { registerBlock } from '@bladeberg/editor';
549
94
 
550
- ```php
551
- 'media' => ['mode' => 'upload', 'driver' => 'spatie'],
95
+ registerBlock('my-plugin/callout', { /* block settings */ });
552
96
  ```
553
97
 
554
- ### API endpoints
555
-
556
- When `mode` is `select` or `upload`, the following JSON routes are registered under `media.route_prefix`:
557
-
558
- | Method | URL | Modes | Description |
559
- |----------|---------------------------|------------------|--------------------------------------|
560
- | `GET` | `/bladeberg/media` | select, upload | List media (paginated, searchable). |
561
- | `GET` | `/bladeberg/media/{id}` | select, upload | Single attachment. |
562
- | `POST` | `/bladeberg/media` | upload only | Upload a file (`multipart/form-data`). |
563
- | `DELETE` | `/bladeberg/media/{id}` | upload only | Delete attachment + file. |
564
-
565
- Query parameters for the list endpoint: `page`, `per_page`, `search`, `media_type` (`image` / `video` / `audio` / `application`).
98
+ > **Note:** The current isolated-block-editor bundle (v2.30) does not expose `window.wp.blocks`, so custom React blocks are queued but not registered yet. Use server-rendered blocks with [BladeBerg's PHP package](https://github.com/BladeBerg/bladeberg) instead.
566
99
 
567
100
  ---
568
101
 
569
- ## PHP / Facade API
102
+ ## Content format
570
103
 
571
- ```php
572
- use Bladeberg\Facades\Bladeberg;
104
+ Gutenberg saves blocks as HTML comments:
573
105
 
574
- // Dynamic block registration (server-rendered)
575
- Bladeberg::registerDynamicBlock('bladeberg/hero', 'blocks.hero');
576
- Bladeberg::isDynamicBlock('bladeberg/hero'); // true
577
- Bladeberg::hasBlock('bladeberg/hero'); // alias of isDynamicBlock()
578
- Bladeberg::getDynamicBlockView('bladeberg/hero'); // 'blocks.hero'
579
- Bladeberg::getRegisteredBlocks(); // ['bladeberg/hero' => 'blocks.hero', …]
580
-
581
- // Render stored block content to HTML (same output as <x-bladeberg-render>).
582
- // Useful for JSON render endpoints, queued jobs, feeds, sitemaps, etc.
583
- Bladeberg::render($post->content); // '<div class="bb-content">…</div>'
106
+ ```html
107
+ <!-- bb:paragraph --><p>Hello world</p><!-- /bb:paragraph -->
584
108
  ```
585
109
 
586
- > Block-comment prefix rewriting (`wp:` your prefix) is handled on the client at save time there is no server-side conversion helper or middleware. Stored content always uses your configured prefix.
110
+ The `bb:` prefix (configurable via `blockPrefix`) keeps your database free of WordPress branding. On load, BladeBerg converts it back to `wp:` internally so Gutenberg can parse it, then converts it back on save.
587
111
 
588
112
  ---
589
113
 
590
- ## JavaScript API
591
-
592
- ### Blade integration (global `window.Bladeberg`)
114
+ ## Media (optional)
593
115
 
594
- When loaded via the Blade component, BladeBerg exposes a small global surface:
116
+ Wire the editor to your own upload API:
595
117
 
596
118
  ```js
597
- // Get the current block content for a field, already bb:-prefixed
598
- // (handy for AJAX submits instead of a native <form>).
599
- const content = window.Bladeberg.getContent('content');
600
- fetch('/posts', { method: 'POST', body: JSON.stringify({ content }) });
601
-
602
- // Programmatic mount (same as the npm createEditor — see "Headless / API usage").
603
- const editor = await window.Bladeberg.createEditor({ target: '#editor' });
119
+ const editor = await createEditor({
120
+ target: '#editor',
121
+ media: {
122
+ mode: 'upload', // 'disabled' | 'select' | 'upload'
123
+ apiUrl: '/api/media', // your JSON media endpoints
124
+ csrfToken: getCsrfToken(), // optional
125
+ },
126
+ });
604
127
  ```
605
128
 
606
- ### Headless / npm (`@bladeberg/editor`)
129
+ If you're using [BladeBerg for Laravel](https://github.com/BladeBerg/bladeberg), the backend ships ready-made routes at `/bladeberg/media`.
607
130
 
608
- ```js
609
- import { createEditor, registerBlock } from '@bladeberg/editor';
610
- import '@bladeberg/editor/style.css';
131
+ ---
611
132
 
612
- const editor = await createEditor({ target: '#editor', value, onChange });
613
- editor.getContent(); // branded block-HTML string
614
- editor.destroy(); // detach + stop listeners
615
- ```
133
+ ## SSR / Next.js / Nuxt
616
134
 
617
- See [Headless / API usage](#headless--api-usage-spa-mobile-decoupled) for the full round-trip.
135
+ The editor is **browser-only**. Import it from a client component:
618
136
 
619
- ```js
620
- // RESERVED — not functional with the current editor bundle.
621
- // The shipped isolated-block-editor build does not expose window.wp.blocks,
622
- // so this call is queued but never flushes. It is kept as forward-compatible
623
- // API surface; use server-rendered blocks instead (see "Building custom blocks").
624
- registerBlock('bladeberg/callout', { /* block settings */ });
625
- ```
137
+ ```jsx
138
+ 'use client';
626
139
 
627
- In the Blade build, `window.React` and `window.ReactDOM` are also published (the editor bundle declares them as externals).
140
+ import { useEffect, useRef } from 'react';
628
141
 
629
- ---
142
+ export default function PostEditor({ content }) {
143
+ const ref = useRef(null);
630
144
 
631
- ## Updating
145
+ useEffect(() => {
146
+ let editor;
147
+ import('@bladeberg/editor').then(({ createEditor }) => {
148
+ createEditor({ target: ref.current, value: content }).then((e) => { editor = e; });
149
+ });
150
+ return () => editor?.destroy();
151
+ }, [content]);
632
152
 
633
- ```bash
634
- composer update bladeberg/bladeberg
635
- php artisan bladeberg:install # or: vendor:publish --tag=bladeberg-assets --force
636
- php artisan view:clear
637
- php artisan config:clear
153
+ return <div ref={ref} />;
154
+ }
638
155
  ```
639
156
 
640
- Then hard-refresh the browser (Ctrl/Cmd + Shift + R) so the cached editor bundle is replaced.
157
+ Don't forget `import '@bladeberg/editor/style.css'` somewhere in your client bundle.
641
158
 
642
159
  ---
643
160
 
644
- ## Testing
161
+ ## Rendering stored content
645
162
 
646
- ```bash
647
- # Package unit tests (run from packages/bladeberg/)
648
- composer test
163
+ This npm package is **editor-only**. To turn block HTML into visitor-facing HTML you need a renderer:
649
164
 
650
- # Your application's feature tests
651
- php artisan test
652
- ```
165
+ - **[BladeBerg Laravel package](https://github.com/BladeBerg/bladeberg)** — `<x-bladeberg-render>`, `Bladeberg::render()`, or `POST /bladeberg/render`
166
+ - **Your own backend** — parse `<!-- bb:… -->` comments and render block HTML yourself
167
+ - **Return raw block HTML** to the frontend and render client-side
653
168
 
654
169
  ---
655
170
 
656
- ## Roadmap
171
+ ## How it works
657
172
 
658
- - [ ] First-class custom editor blocks (stable `wp.blocks` exposure).
659
- - [ ] `link` media mode URL-picker UI.
660
- - [ ] JSON content storage format.
661
- - [ ] More built-in dynamic block examples.
662
- - [ ] Publishing to Packagist.
173
+ ```
174
+ Your React app
175
+
176
+ ├─ import { createEditor } from '@bladeberg/editor'
177
+ ├─ import '@bladeberg/editor/style.css'
178
+
179
+
180
+ createEditor() lazy-loads isolated-block-editor.js (bundled in the package)
181
+
182
+
183
+ window.wp.attachEditor(textarea) ← full Gutenberg UI
184
+
185
+
186
+ editor.getContent() → "<!-- bb:paragraph -->…" → POST to your API
187
+ ```
663
188
 
664
- Have an idea? [Open a feature request](https://github.com/BladeBerg/bladeberg/issues/new).
189
+ No `@wordpress/block-editor`, no `@wordpress/data`, no dependency resolver nightmares. The hard part is already done.
665
190
 
666
191
  ---
667
192
 
668
- ## Contributing
669
-
670
- Contributions are very welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for the full guide. In short:
193
+ ## Development (maintainers)
671
194
 
672
195
  ```bash
673
- git clone https://github.com/BladeBerg/bladeberg.git
674
- cd bladeberg
675
- composer install # PHP deps
676
- npm install # editor build deps
677
- npm run build # build the editor bundle into dist/
678
- composer test # run the PHP test suite
196
+ cd packages/bladeberg
197
+ npm install
198
+ npm run build:npm # dist-npm/bladeberg.js + style.css + isolated-block-editor.js
199
+ npm pack # smoke-test the tarball locally
679
200
  ```
680
201
 
681
- 1. Fork the repo and branch from `main`.
682
- 2. Add or update tests for your change.
683
- 3. Run `composer test` (and `npm run build` if you touched any JS).
684
- 4. Note your change under `[Unreleased]` in [CHANGELOG.md](CHANGELOG.md).
685
- 5. Open a pull request with a clear before/after description.
686
-
687
- ---
688
-
689
- ## Reporting bugs & requesting features
690
-
691
- Please use the **GitHub issue tracker**: <https://github.com/BladeBerg/bladeberg/issues>
692
-
693
- When reporting a bug, include:
694
-
695
- - PHP and Laravel versions,
696
- - the installed BladeBerg version,
697
- - clear steps to reproduce,
698
- - expected vs. actual behaviour,
699
- - any console errors (browser dev-tools) and relevant `storage/logs/laravel.log` output.
700
-
701
- For feature requests, describe the problem you're trying to solve — not just the solution you have in mind.
702
-
703
- ---
704
-
705
- ## Security
706
-
707
- If you discover a security vulnerability, please **do not** open a public issue. Email `security@bladeberg.dev` (or open a private GitHub security advisory) so it can be addressed before disclosure.
202
+ Publish happens via GitHub Actions on `v*` tags. See [RELEASE.md](RELEASE.md).
708
203
 
709
204
  ---
710
205
 
711
206
  ## License
712
207
 
713
- BladeBerg is open-source software licensed under the **GNU General Public License v2.0 (or later)**see [LICENSE](LICENSE).
714
-
715
- This license was chosen deliberately: BladeBerg embeds the WordPress/Gutenberg block editor, which is itself GPL-2.0-or-later. Distributing under the same license keeps BladeBerg fully compatible with the code it builds on.
716
-
717
- ```
718
- Copyright (C) 2026 BladeBerg
719
-
720
- This program is free software; you can redistribute it and/or modify it under
721
- the terms of the GNU General Public License as published by the Free Software
722
- Foundation; either version 2 of the License, or (at your option) any later
723
- version.
724
- ```
725
-
726
- ---
727
-
728
- ## Credits
729
-
730
- - [Gutenberg](https://github.com/WordPress/gutenberg) and [`@automattic/isolated-block-editor`](https://github.com/Automattic/isolated-block-editor) — the editor BladeBerg stands on.
731
- - [Laravel](https://laravel.com) — the framework that makes it feel at home.
732
- - Everyone who [contributes](CONTRIBUTING.md) to BladeBerg.
208
+ GPL-2.0-or-later — same as Gutenberg. See [LICENSE](LICENSE).