@aduptive/strapi-blockscene 2.0.0-alpha.1 → 2.0.0-alpha.10

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 (5) hide show
  1. package/README.md +917 -48
  2. package/dist/admin.cjs +8769 -1067
  3. package/dist/admin.mjs +8835 -1073
  4. package/dist/server.cjs +1678 -39
  5. package/package.json +3 -5
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Blockscene for Strapi
2
2
 
3
3
  A visual gallery for the components already allowed in a native Dynamic Zone,
4
- with configurable thumbnails, SVG wireframes, editor preferences and, on
5
- Strapi 5, a whole-page preview that edits the form through your own frontend.
4
+ with configurable thumbnails, SVG wireframes, editor preferences and a
5
+ whole-page preview that edits the form through your own frontend.
6
6
  No renderer, framework or content migration is required.
7
7
 
8
8
  **Alpha.** Published as pre-releases; APIs, settings and the preview bridge may
@@ -33,7 +33,8 @@ module.exports = {
33
33
  'blocks.hero': {
34
34
  label: 'Hero banner',
35
35
  description: 'Introduce a page with a prominent title and image.',
36
- category: 'Editorial',
36
+ typology: 'hero', // optional; guessed from the name otherwise
37
+ tags: ['Editorial'],
37
38
  keywords: 'heading introduction',
38
39
  image: '/block-previews/hero.png',
39
40
  },
@@ -44,19 +45,113 @@ module.exports = {
44
45
  ```
45
46
 
46
47
  Rebuild/restart the admin. Open a content type with a Dynamic Zone. The
47
- editor panel provides one gallery button per editable, non-full zone plus
48
- "Open all blocks" / "Close all blocks" for zones that already have blocks.
48
+ native "Add a component to <zone>" button of each editable, non-full zone opens
49
+ the gallery instead of Strapi's category picker; a full zone keeps its native
50
+ button.
51
+
52
+ ### Zone bar
53
+
54
+ Right beside the native zone label ("blocks (3)") each zone gets a short bar:
55
+
56
+ - **Expand all / Collapse all**: one button with text. It reads the rows' own
57
+ state: "Expand all" while any block of the zone is collapsed, "Collapse all"
58
+ when every block is open (a header opened or closed by hand counts too), and
59
+ exposes it as `aria-expanded`. Blocks open or close a few at a time around the
60
+ viewport first (long pages stay responsive). Rows of a folded layout group
61
+ are left alone (see [Groups in the form](#groups-in-the-form)). Options `showOpenAll` /
62
+ `showCloseAll`: with both on (the default) the button toggles; with only one
63
+ on it offers that action alone, disabled while there is nothing to do; with
64
+ both off there is no button.
65
+ - **Undo and Redo** (Strapi 5, first zone only): icon and text, the shortcut in
66
+ the tooltip. See [Undo and redo](#undo-and-redo).
67
+ - **Editing mode** (first zone only, when a preview route exists): a
68
+ menu with Fields, Fields + page and Visual editor. Without a preview route the
69
+ menu is not shown; Settings, Blockscene says why.
70
+ - **"…" (More actions)**: Select blocks, and "Paste N blocks" while the
71
+ clipboard holds blocks this zone allows (see [Row actions](#row-actions)).
72
+ No entries, no menu.
73
+
74
+ "Select blocks" swaps the bar for a selection bar: a select-all box with the
75
+ count ("2 selected"; indeterminate while only some are selected, a click then
76
+ selects none), Copy (disabled until a block is selected) and Done; Esc leaves
77
+ too. Each row gets a checkbox at the start of its header.
78
+
79
+ An empty zone has no label row: its bar (Paste, and on the first zone Undo,
80
+ Redo and the mode) sits right after its native add button.
81
+
82
+ The plugin adds no side panel of its own for any of this. The Blockscene side
83
+ panel appears only for what has no place in the form: version history (when
84
+ enabled) and layout group problems. With neither, there is no panel.
49
85
 
50
86
  ## Gallery
51
87
 
52
- The modal has one header row: block count (`34 blocks`, or `3 of 34` while
53
- filtering), search, a category filter (only when the zone spans more than one
54
- category, with counts), a "Fields" switch that shows each block's attribute
55
- list on its card, and a columns slider (1 to 5, remembered per browser). Blocks
56
- are grouped by category, system-like categories (`admin`) last; search and the
57
- category filter combine, and the empty state offers a reset. Categories come
58
- from `config.components[uid].category`, else the component's `category` in its
59
- schema. The modal is 80vw wide.
88
+ The modal is a block browser (80vw wide):
89
+
90
+ - **Sidebar** (collapsible to icons; remembered per browser): All, Recently
91
+ used and Starred with counts, then the typologies present in the zone.
92
+ - **Top bar**: count (`34 blocks`, or `3 of 34` while filtering), search,
93
+ filter menus Tags, Media and Content (checkboxes; several values in one menu
94
+ match any of them, different menus combine), a "Fields" switch that shows
95
+ each block's attribute list on its card, and a columns slider (1 to 5,
96
+ remembered per browser). Active values show as removable chips with "Clear
97
+ all"; the empty state offers a reset.
98
+ - **Cards**, grouped by typology: facet badges (IMAGE, VIDEO, GALLERY, RICH
99
+ TEXT, LIST, DYNAMIC, FORM), a star and a hover "+" that inserts at once.
100
+ - A click **magnifies** the block: the card grows into a large panel over the
101
+ grid (sidebar and top bar stay usable, the grid dims underneath) and shrinks
102
+ back into its card on close (close button, Esc or a click on the dimmed
103
+ grid). The animation is transform/opacity only, about 250 ms, and is skipped
104
+ under `prefers-reduced-motion: reduce`. The panel shows a large live preview
105
+ with Fit / Mobile / Tablet / Desktop widths (scaled to fit, as in the page
106
+ preview) and a compact strip: label, uid, description, badges, fields (on
107
+ demand), the variant choice (see [Insert variants](#insert-variants)), star
108
+ and Insert. Double click, "+" or Enter on a focused card inserts without it
109
+ (the first variant, when the block has variants).
110
+
111
+ The magnified preview shows the card's thumbnail at once and fades the live
112
+ page in over it once it has loaded. Live source, first match wins:
113
+
114
+ 1. **Block preview URL** (`editor.blockPreviewUrl`, Settings, both versions):
115
+ a plain page per block in a plain iframe, no bridge. Placeholders, URL
116
+ encoded: `{uid}`, `{name}` (after the dot), `{category}` (before the dot),
117
+ `{variant}` (the chosen [insert variant](#insert-variants)'s id, else
118
+ `default`) and `{locale}` (the entry's locale, empty when the type is not
119
+ localized). Example:
120
+ `http://localhost:3026/{locale}/block-preview/{name}/{variant}`. Such a
121
+ fixtures route is usually dev-only: leave the option empty on production.
122
+ 2. **Page preview route** (the preview route base URL, else on Strapi 5 the
123
+ native Preview origin + `/block-preview/page`): the same bridge as the
124
+ whole-page preview, sent one block made from the chosen variant over the
125
+ schema defaults, mode `preview`. Read-only: every message except `ready`
126
+ is ignored.
127
+ 3. Otherwise the image only.
128
+
129
+ If the live page neither loads nor says `ready` within 8 s, the image stays
130
+ with a short note. The iframe uses the page preview's sandbox
131
+ (`allow-scripts allow-same-origin`) and `referrerpolicy="no-referrer"`.
132
+
133
+ Stars and recently used blocks are stored per admin user on the server
134
+ (`GET`/`PUT /blockscene/me/prefs`, `{ starred, recent }`, at most 200 and 20
135
+ existing component uids).
136
+
137
+ ### Taxonomy
138
+
139
+ Facets are read from each component schema and the components it nests (one
140
+ level): media fields (`image`, `video` by allowed types, `gallery` when
141
+ multiple), `richtext` (rich text, blocks or a CKEditor custom field), `list`
142
+ (repeatable component), `dynamic` (a relation, or a uid with query, archive or
143
+ related) and `form` (uid or display name with form or contact).
144
+
145
+ The typology is one of hero, text, media, listing, cards, cta, form and
146
+ layout, guessed from the component name and display name (hero, banner,
147
+ cover; text, rich, quote, title; image, media, video, gallery, carousel; list,
148
+ query, archive, related, posts, projects; card; cta, button, link; form,
149
+ contact; wrapper, column, grid, divider, spacer, section; else text). Override
150
+ it, and add up to 10 tags (1 to 24 characters), in Settings, Blockscene, in
151
+ the plugin config (`components[uid].typology` and `components[uid].tags`) or in
152
+ the component schema ([`pluginOptions.blockscene`](#describing-blocks-and-fields-in-the-schema)).
153
+ A Settings value wins over the config, the config over the schema. The former `category` config key still
154
+ works: it becomes the block's tag when no tags are set.
60
155
 
61
156
  The native "Add a component to <zone>" button of an editable zone opens this
62
157
  gallery as well (the click is intercepted in the capture phase; Strapi's
@@ -66,6 +161,130 @@ label ending with the raw zone name, which Strapi's message carries in every
66
161
  locale; a zone whose name ends another zone's name (`blocks` and `sub blocks`)
67
162
  is the documented limit.
68
163
 
164
+ ### Insert variants
165
+
166
+ A block can offer named presets of its field values, so the editor inserts
167
+ "Hero, dark" instead of the bare schema defaults. Variants come from the
168
+ project's code, never from the Settings page: the plugin config
169
+ `components[uid].variants`, a list of `{ id, label, values }`.
170
+
171
+ ```js
172
+ // config/plugins.js
173
+ components: {
174
+ 'blocks.hero': {
175
+ variants: [
176
+ { id: 'default', label: 'Default', values: {} },
177
+ { id: 'dark', label: { en: 'Dark', 'pt-BR': 'Escuro' }, values: {
178
+ title: 'Big launch', theme: 'dark',
179
+ link: { text: 'Read more', url: '/news' },
180
+ items: [{ label: 'One' }, { label: 'Two' }],
181
+ } },
182
+ // A frontend fixture works as is: ids, __component, media and relations are dropped.
183
+ { id: 'cases', label: 'Cases', values: require('../fixtures/hero/cases.json').data },
184
+ ],
185
+ },
186
+ },
187
+ ```
188
+
189
+ - **Choosing**: with two or more variants the magnified block shows them as a
190
+ row of buttons; the live preview follows the choice (`{variant}` in the
191
+ block preview URL, or the variant's block sent through the bridge) and
192
+ Insert inserts it. Double click, "+" and Enter insert the first variant.
193
+ The picker opened from a page preview insertion seam offers the same
194
+ choice. A block without variants behaves as before; Settings, Blockscene
195
+ lists each block's variants read-only.
196
+ - **Merge**: `values` is a partial field map over the schema defaults. A
197
+ nested component merges over its own defaults; a repeatable list replaces
198
+ the default list, each item over the item's defaults. The row goes through
199
+ the same form path as any gallery insert (Strapi 5 `addFieldRow`, Strapi 4
200
+ `addComponentToDynamicZone` then the fields' `onChange`), so nothing is
201
+ saved until Save and undo/redo covers it.
202
+ - **Supported values**, checked against the schema once at boot: text-like
203
+ fields (strings, rich text, email, uid, dates as strings), numbers
204
+ (integers, big integers, floats, decimals), booleans, enumerations (one of
205
+ their values), JSON, blocks (an array) and `null`; nested components,
206
+ single or repeatable (up to the attribute's `max`, and 100), each validated
207
+ against its own schema. Media and relation values are dropped with a boot
208
+ warning: their ids belong to one install and are not checked here. `id` and
209
+ `__component` keys are ignored, so a content-API shaped block works.
210
+ Unknown attributes, passwords and Dynamic Zones reject the variant.
211
+ - **Bounds**: `id` is 1 to 40 of `a-z 0-9 - _`, unique per block (it is the
212
+ `{variant}` URL segment); `label` a string of 1 to 60 characters or
213
+ `{ "<locale>": string }`; at most 12 variants per block, 16 KB of values
214
+ per variant and 256 KB for all of them. An invalid variant is left out with
215
+ a warning naming the problem; the block keeps its valid ones.
216
+
217
+ ## Row thumbnails
218
+
219
+ Each added block shows its thumbnail at the left of the native accordion
220
+ header, and clicking it opens the image full size in a modal instead of
221
+ opening or closing the row. The sources are the same as the gallery cards,
222
+ minus the wireframe: a block with no image shows nothing, so nothing new is
223
+ invented for blocks that were never captured. The thumbnail is inserted into
224
+ Strapi's own header through the DOM (no Content Manager patch) and is
225
+ restored when Strapi re-renders the list. Turn it off with the editor option
226
+ `showRowThumbnails: false`.
227
+
228
+ ## Row actions
229
+
230
+ Each block header gets a few icons right before Strapi's own delete button
231
+ (inserted through the DOM, like the thumbnails; read-only rows get none):
232
+
233
+ - **Hide on the site** (eye). The block stays in the document and in the
234
+ admin, dimmed with a "Hidden" badge (also in the page preview, which receives
235
+ `hidden: true` for it). What the content API does is the editor option
236
+ `hiddenBlocks`: `strip` (default) removes hidden rows from Dynamic Zones in
237
+ content-API reads (REST and GraphQL, including zones of populated relations
238
+ and components; admin and Content Manager reads always return every row),
239
+ `flag` sends them with the attribute so the frontend decides, `off` removes
240
+ the eye and strips nothing. On a configured group OPEN the eye hides the
241
+ whole group.
242
+ - **Duplicate**. A deep copy right below: ids removed from the row and its
243
+ nested components, new row key, media kept, relations kept (Strapi 5 reads
244
+ the source row's relations from the server and adds its unsaved changes).
245
+ A group OPEN duplicates with its whole range. Option `duplicate`.
246
+ - **Copy / Paste** across pages. The copy icon copies one block; "Select
247
+ blocks" in the zone bar's "…" menu shows a checkbox per row, then Copy. The
248
+ blocks are stored in this browser's localStorage (`blockscene:clipboard:v1`,
249
+ with the source content type and locale). "Paste N blocks" appears in the
250
+ zone bar's "…" menu of every zone that allows all the copied components, and
251
+ in the page preview seams. Paste is all or nothing: every block must be
252
+ allowed in the target zone, fit its maximum, and groups must be whole;
253
+ otherwise a notice explains why and the form is not changed. Media and relations are kept by id/documentId, so paste
254
+ within the same install. Option `clipboard`.
255
+ - **Confirm delete**. The native delete asks "Delete block …?" first;
256
+ confirming runs Strapi's own removal. On a group marker the dialog says only
257
+ the marker is removed. Option `confirmDelete`.
258
+
259
+ Everything goes through the form: nothing is saved until Save, and undo/redo
260
+ (Strapi 5) covers every action. Strapi 4 supports the same actions; relations
261
+ are copied as the form holds them there (the loaded relations of an opened
262
+ block).
263
+
264
+ **The hidden attribute is a database column.** At register time the plugin adds
265
+ a boolean attribute (like i18n adds `locale`) to every component used in a
266
+ Dynamic Zone: `{ type: 'boolean', default: false, configurable: false }`, so
267
+ Strapi's schema sync creates a `bs_hidden` column in each of those component
268
+ tables on the next start (existing rows read as not hidden). It is never shown
269
+ as an input: the plugin removes it from the components' edit layouts through
270
+ the Content Manager's layout hook (it may be listed in "Configure the view" of
271
+ a component; leave it out of the layout). It is not marked `visible: false`,
272
+ because before Strapi 5.45 the edit view crashes on a value whose attribute the
273
+ admin does not know. It is kept out of the Content-Type Builder instead (the
274
+ plugin wraps the builder's component reads), so saving a component there in
275
+ `strapi develop` never writes it to the component's JSON file. With earlier
276
+ versions (up to 2.0.0-alpha.8, and 1.0.0-alpha.8 on Strapi 4) the builder did
277
+ write it (`"bsHidden": { "type": "boolean", "configurable": false, "default": false }`);
278
+ a file that has it keeps working, and the builder keeps it there on later saves.
279
+ To clean it up, delete that entry from the component JSON by hand (the plugin
280
+ still adds the attribute at startup, so the column and its data stay). To check,
281
+ edit a Dynamic Zone component in the builder (for example its display name),
282
+ save, and diff its JSON file: only your change is there. Rename it with the plugin config
283
+ `hiddenAttribute: 'myName'`, or set `hiddenAttribute: false` to add nothing (no
284
+ eye, nothing stripped). A component that already has an attribute of that name
285
+ with another type is skipped with a warning. Removing the plugin (or setting
286
+ `false`) leaves the column in place with its data; drop it yourself if you want.
287
+
69
288
  ## Thumbnail priority
70
289
 
71
290
  Each card tries these sources in order and moves on when one fails to load.
@@ -99,26 +318,108 @@ needed for the gallery to pick them up. Reading requires the
99
318
  - Wireframe palette (background, surfaces, text, accent) with a live preview
100
319
  and "Restore default colors". Colors must be `#RRGGBB`.
101
320
  - Component list with the effective source, choose/replace image from the
102
- Media Library, "Use automatic image" and the wireframe template.
321
+ Media Library, "Use automatic image", the wireframe template, the gallery
322
+ typology ("Automatic" shows the guess) and tags (comma separated); a group
323
+ OPEN also has its layout grid fields (see [Layout grid](#layout-grid)).
103
324
  - Editor preferences: enhancements on/off, visibility of each collective
104
325
  button, initial accordion state and the initial block mode.
326
+ - Version history (Strapi 5): module switch, covered content types, retention
327
+ (see "Version history").
328
+ - Content types: every project type with a Dynamic Zone, each with on/off
329
+ (off leaves its edit view fully native), the mode its edit
330
+ views open in ("Default" follows the global preview mode), its visual
331
+ editor sidebar and optionally its own page preview toolbar (see below).
332
+ - "Restore project defaults" (or "Reset to defaults" when the project has no
333
+ code defaults), after a confirmation, deletes what was saved here
334
+ (`DELETE /blockscene/settings`, same permission as saving).
335
+
336
+ ### Project defaults from code
337
+
338
+ A project can ship its baseline in the plugin config, with the same shape as
339
+ the stored settings (every key optional):
340
+
341
+ ```js
342
+ // config/plugins.js
343
+ module.exports = {
344
+ blockscene: { config: { settings: require('./blockscene.json') } },
345
+ }
346
+ // config/blockscene.json
347
+ {
348
+ "palette": { "accent": "#AA3300" },
349
+ "editor": { "previewMode": "split", "initialState": "open" },
350
+ "components": { "blocks.faq": { "template": "faq", "typology": "text" } },
351
+ "contentTypes": {
352
+ "api::page.page": {
353
+ "sidebarPosition": "right",
354
+ "sidebar": [{ "label": "SEO", "icon": "seo", "open": "drawer", "fields": ["seo"] }]
355
+ }
356
+ }
357
+ }
358
+ ```
359
+
360
+ The effective settings are the built-in defaults, then the code `settings`,
361
+ then what was saved on the page:
362
+
363
+ - `palette` and `editor`: a saved key wins over the code key by key, except
364
+ an empty `previewUrl` or `blockPreviewUrl`, which means "not set": the code
365
+ value applies. Clearing either field on the page therefore falls back to the
366
+ project default (there is no saved "off" for a URL the code provides; remove
367
+ it from the code settings, or set `previewMode` per content type, to keep a
368
+ type out of the page preview).
369
+ - `components` and `contentTypes`: a saved entry replaces the code entry for
370
+ that uid as a whole (a saved `api::page.page` entry replaces its code
371
+ sidebar too); other uids keep their code values.
372
+
373
+ Saving stores only what differs from the code defaults, so later changes to the
374
+ code reach every key nobody overrode. A document saved before the project had
375
+ code defaults holds every palette and editor key, and those keep winning until
376
+ "Restore project defaults". The code settings are checked at boot with the same
377
+ rules as `PUT`; if they are invalid a warning names the problem and they are
378
+ ignored (boot continues). `GET /blockscene/settings` returns them as
379
+ `projectDefaults` (null when absent or invalid).
105
380
 
106
381
  ### Editor preferences
107
382
 
108
383
  | Option | Values | Default |
109
384
  | --- | --- | --- |
110
385
  | Editor enhancements enabled | on/off | on |
111
- | Show "Open all blocks" | yes/no | yes |
112
- | Show "Close all blocks" | yes/no | yes |
386
+ | Show "Expand all" (`showOpenAll`) | yes/no | yes |
387
+ | Show "Collapse all" (`showCloseAll`) | yes/no | yes |
388
+ | Show block thumbnails in the rows (`showRowThumbnails`) | yes/no | yes |
113
389
  | Initial accordion state | all closed / all open / remember | all closed |
114
- | Preview route base URL (Strapi 5) | full URL or empty | empty: native Preview origin |
115
- | Initial preview mode (Strapi 5) | form / side by side / preview | form |
390
+ | Load rich-text editors on demand (`lazyEditors`, Strapi 5) | yes/no | yes |
391
+ | Custom fields loaded on demand (`lazyFields`, Strapi 5) | up to 20 uids `plugin::x.y` / `global::x` | `["plugin::ckeditor5.CKEditor"]` |
392
+ | Preview route base URL | full URL or empty | empty: native Preview origin (Strapi 5; none on Strapi 4) |
393
+ | Block preview URL | http(s) URL with placeholders, up to 500 characters, or empty | empty: page preview route, else image |
394
+ | Initial preview mode | form / side by side / preview | form |
395
+ | Confirm before deleting a block (`confirmDelete`) | yes/no | yes |
396
+ | Duplicate on each block (`duplicate`) | yes/no | yes |
397
+ | Copy and Paste (`clipboard`) | yes/no | yes |
398
+ | Blocks hidden on the site (`hiddenBlocks`) | `strip` / `flag` / `off` | `strip` |
399
+ | Friendly field labels (`friendlyLabels`, see [Field labels](#field-labels)) | yes/no | yes |
400
+ | Page preview toolbar (`previewToolbar`, see [Pane toolbar](#pane-toolbar)) | ordered list of `modes`, `history`, `devices`, `status`, `actions` | all five |
401
+ | Page widths (`previewDevices`) | 1 to 8 of `fit`, `mobile`, `tablet`, `desktop`, `{ label, width }` | the four built-in |
402
+
403
+ Every edit view opens in its content type's mode, else the initial preview
404
+ mode. Editors can switch while they edit; the switch is not remembered.
116
405
 
117
406
  The initial state is applied once per document/locale after the blocks
118
- render; edits, reorders and newly inserted blocks are not re-applied. Hiding a
119
- button only hides it; native accordion headers keep working.
407
+ render; edits, reorders and newly inserted blocks are not re-applied.
408
+ `showOpenAll` and `showCloseAll` shape the zone bar's single toggle (both: it
409
+ toggles; one: only that action; none: no button). Hiding it only hides it;
410
+ native accordion headers keep working.
411
+
412
+ Lazy editors: a custom field listed in `lazyFields` first shows a read-only
413
+ preview of its HTML (same label, hint and error; scripts, frames, styles, event
414
+ handlers and `javascript:` URLs removed) and mounts the real editor on click,
415
+ Enter/Space or keyboard focus, then puts the caret in it. Once mounted it stays
416
+ for that view; closing and reopening the block shows the preview again.
417
+ Disabled fields only show the preview. The wrapper is installed on every
418
+ registered custom field at boot and passes the others straight through; until
419
+ the catalog loads, and when the plugin or the content type is off, every field
420
+ is native.
120
421
 
121
- "Remember" stores only the last explicit open all / close all click, as
422
+ "Remember" stores only the last explicit Expand all / Collapse all click, as
122
423
  `open`/`closed` in the browser's localStorage, keyed by admin URL, user id,
123
424
  content type and zone. Another user in the same browser does not inherit it.
124
425
  Blocked storage or an invalid value falls back to all closed. No sync between
@@ -128,20 +429,72 @@ The accordion controls drive the native Dynamic Zone accordions through their
128
429
  `aria-expanded` headers, so no Strapi source is patched. They are validated on
129
430
  the two supported versions only.
130
431
 
131
- ## Whole-page preview (Strapi 5)
432
+ ## Whole-page preview
132
433
 
133
- The Strapi 5 distribution adds a "Page preview" panel with three modes: form,
134
- side by side (resizable pane, native actions moved to a bar above the form) and
135
- preview. The panel posts the live values of the first Dynamic Zone to a page
434
+ Both distributions add a page preview with three modes, chosen from the
435
+ first zone's bar and from the pane toolbar: Fields (form), Fields + page (side
436
+ by side: resizable pane, native actions moved to a bar above the form) and
437
+ Visual editor. It posts the live values of the first Dynamic Zone to a page
136
438
  your frontend serves; the page renders them with its own components and styles
137
439
  and can ask the admin to open a block, edit a field or pick media. Nothing is
138
440
  saved or published by the preview.
139
441
 
442
+ Side by side, hovering a zone row in the form (open or closed) outlines its
443
+ block in the page, and hovering a block in the page outlines its form row in the
444
+ theme's primary color; hovering never scrolls. When that row is outside the
445
+ visible form, an arrow at the top or bottom edge of the form scrolls to it. The
446
+ divider between form and page drags to resize, takes the arrow keys, Home and
447
+ End, and a double click resets it to half.
448
+
449
+ ### Visual editor sidebar
450
+
451
+ Per content type, a bar of buttons on the left, right or bottom edge of the
452
+ visual editor. Each button (label, optional icon) opens some of the document's
453
+ own fields in a modal or a drawer: the native form itself, with every other
454
+ field hidden, so components, repeatable components, media, relations and custom
455
+ fields all keep working. Done, Esc or the backdrop put the form back.
456
+
457
+ ```js
458
+ // Settings, Blockscene, Content types (or PUT /blockscene/settings, or the code `settings`):
459
+ contentTypes: {
460
+ 'api::page.page': {
461
+ sidebarPosition: 'left', // left | right | bottom
462
+ sidebar: [
463
+ { label: 'Title', icon: 'text', open: 'modal', fields: ['title'] },
464
+ { label: 'SEO', icon: 'seo', open: 'drawer', fields: ['pageSeo'] },
465
+ ],
466
+ },
467
+ }
468
+ ```
469
+
470
+ Icons: text, tag, seo, settings, image, link, palette, list, globe, info (or
471
+ none). Fields are the type's own top-level attributes; an item naming a field
472
+ removed from the schema is dropped on read. Up to 12 items.
473
+
474
+ The Settings page edits it under each enabled content type: position, then per
475
+ item the label, icon, drawer or modal, the fields (checkboxes), move up/down and
476
+ remove. An item without a label or without fields is flagged and blocks Save.
477
+ Removing every item removes the override.
478
+
479
+ The pane toolbar sets the page width: Fit (the pane), Mobile (390 px), Tablet
480
+ (834 px) or Desktop (1440 px). A device renders at its own width and scales down
481
+ when the pane is narrower, so the desktop layout fits side by side; switching
482
+ never reloads the page. The choice is remembered per browser.
483
+
140
484
  Minimal configuration:
141
485
 
142
486
  1. Serve a page that implements the bridge. `examples/page-preview/index.html`
143
487
  is a framework-free reference: it answers `ready`/`ping`, renders
144
488
  `update-page`, and sends `select`, `focus`, `edit`, `media`, `media-remove`.
489
+ Hover sync is optional: the page sends `{ type: 'hover', key }` (a block
490
+ key, or `null` off blocks) when it changes, and marks the block of the
491
+ admin's `{ type: 'hover', key }` without scrolling. A page that ignores it
492
+ keeps working.
493
+ Row actions: a block hidden on the site arrives with `hidden: true` (the
494
+ example dims it), and `update-page` carries `clipboard` (the number of copied
495
+ blocks, 0 when none or not editable); while it is positive the page may show
496
+ a Paste button in its seams that sends `{ type: 'paste', after }` (a block
497
+ key, or `null` for the start). Both are optional.
145
498
  Keep the origin check (`admin` query parameter or your own constant).
146
499
  2. Allow the admin origin to embed it (`Content-Security-Policy: frame-ancestors`)
147
500
  and allow the page origin in the Strapi admin CSP (`frame-src`).
@@ -151,7 +504,7 @@ Minimal configuration:
151
504
  Use the same host name the frontend dev server was started for
152
505
  (`localhost` vs `127.0.0.1`): Next 16 refuses its dev WebSocket for the
153
506
  other one and the page then never hydrates, so `ready` never arrives and
154
- the panel reports the preview as unavailable with no error in either console.
507
+ the pane reports the preview as unavailable with no error in either console.
155
508
 
156
509
  The bridge only exposes what the schema allows: attributes marked `private`
157
510
  are never projected, focused or edited; media URLs are passed through for any
@@ -161,13 +514,133 @@ update permission on it. Pending dialogs are dropped when the document or
161
514
  locale changes.
162
515
 
163
516
  Editing from the page: plain-text areas the page explicitly maps
164
- (`data-block-field` + `contenteditable="plaintext-only"`) send `edit`; any other
165
- editable field opens an admin modal with the field's own native input (custom
166
- fields such as CKEditor keep their options; string/text use the admin input).
167
- Apply updates the form and the preview; Cancel discards only the modal. Media
517
+ (`data-block-field` + `contenteditable="plaintext-only"`) send `edit`. In the
518
+ visual editor, a click on any other field or on the block opens the whole block
519
+ over the page: its own native form (every field type, CKEditor, media, nested
520
+ and repeatable components), with the clicked field focused and outlined. The
521
+ page updates while you type; Done, Esc or the backdrop put the block back. Side
522
+ by side focuses the field in the form on the left instead. Media
168
523
  fields open the native Media Library. Everything is validated against the schema
169
524
  and the zone's edit permission; a published page never receives unsaved values
170
- by itself. Strapi 4 does not have this panel yet (see `docs/BACKLOG.md`).
525
+ by itself.
526
+
527
+ On Strapi 4 (run on 4.26.1; the edit view APIs it uses are the same in 4.11.0)
528
+ the page preview is the same: the three modes, the same bridge
529
+ (`blockscene:page-preview:v1`, so one frontend route serves both), widths,
530
+ hover sync, the block dialog, the visual editor sidebar, custom panels,
531
+ insertion, groups and paste from the page, and the per content type settings.
532
+ It reads and writes the edit view's own data (`useCMEditViewDataManager`); new
533
+ blocks use the native insertion at a position. Differences:
534
+
535
+ - No undo and redo (no History buttons, no shortcuts). Strapi 4's form has no
536
+ way to set all values at once, and restoring each field would also undo the
537
+ relations it loads lazily, which Save would then disconnect.
538
+ - No native Preview to fall back on: set the preview route base URL.
539
+ - The toolbar's status shows Draft or Published (Strapi 4 has no Modified
540
+ state) and the unsaved hint; its Save and Publish (or Unpublish) click the
541
+ edit view header's own buttons, so their checks and dialogs are Strapi's.
542
+ - The browser smoke covers split mode, select, inline and live edits, widths,
543
+ hover, the block dialog, a sidebar item, media, insertion and toolbar Save
544
+ on 4.26.1; group tools, paste and custom panels from the page share the
545
+ Strapi 5 code and are checked there only.
546
+ - With Strapi 4's two navigation columns the content area is narrower: the
547
+ form keeps at least 520 px, so side by side the page gets less than half on
548
+ a 1440 px screen.
549
+
550
+ ### Pane toolbar
551
+
552
+ Which controls the toolbar shows, and in which order, is `previewToolbar`:
553
+ `modes` (Fields, Fields + page, Visual editor), `history` (Undo, Redo),
554
+ `devices` (the width menu), `status` (Draft/Modified/Published and the unsaved
555
+ hint) and `actions` (Save, Publish). A missing id is hidden; `status` and
556
+ `actions` stay one group on the right, placed where the first of them is
557
+ listed. The preview's loading or error notice always shows. Without `modes` the
558
+ pane stays in the mode the edit view opened in (for example a visual editor
559
+ only view with `previewMode: 'preview'`); the form's zone bar keeps its own
560
+ Undo, Redo and mode menu.
561
+
562
+ `previewDevices` lists the width menu's entries in order; the first is used
563
+ until an editor picks another. Besides the four names, `{ label, width }` adds
564
+ a custom width (label up to 24 characters, width a whole number from 240 to
565
+ 3840 px; no name or width twice; up to 8 entries). With a single entry the menu
566
+ is hidden.
567
+
568
+ ```js
569
+ // Settings, Blockscene (Editor preferences, or per content type), PUT /blockscene/settings, or the code `settings`:
570
+ editor: {
571
+ previewToolbar: ['modes', 'devices', 'actions'],
572
+ previewDevices: ['fit', 'mobile', { label: 'Laptop', width: 1280 }, 'desktop'],
573
+ },
574
+ contentTypes: {
575
+ 'api::landing.landing': { previewToolbar: ['actions'], previewDevices: ['desktop'] },
576
+ }
577
+ ```
578
+
579
+ A content type's value replaces the global one. The Settings page edits both as
580
+ checklists (check to show, arrows to order, "Add a width" for custom widths);
581
+ "Own page preview toolbar" under a content type starts as a copy of the global
582
+ choice, and turning it off goes back to the global one.
583
+
584
+ ### Custom sidebar panels
585
+
586
+ A project (in its `src/admin/app.tsx`) or another plugin can add its own panel
587
+ to the visual editor sidebar. Registered panels come after the configured field
588
+ items, in registration order; the rail shows in the Visual editor mode as soon
589
+ as there is one item.
590
+
591
+ ```tsx
592
+ // src/admin/app.tsx
593
+ const Notes = ({ values, onChange, disabled, close }) => (
594
+ <textarea value={values.notes ?? ''} disabled={disabled}
595
+ onChange={(e) => onChange('notes', e.target.value)} />
596
+ )
597
+ export default {
598
+ bootstrap(app) {
599
+ app.getPlugin('blockscene')?.apis.registerPanel({
600
+ id: 'notes', // 1 to 40 letters, digits, _ or -; the same id again replaces the panel
601
+ label: 'Notes', // 1 to 40 characters
602
+ icon: 'info', // optional: an icon name (the sidebar icons above, for example) or a React element
603
+ open: 'drawer', // 'drawer' (default) or 'modal'
604
+ contentTypes: ['api::page.page'], // optional: only these types (default: every type)
605
+ Component: Notes,
606
+ })
607
+ },
608
+ }
609
+ ```
610
+
611
+ `Component` receives `model` (content type uid), `documentId` (undefined while
612
+ creating), `locale`, `values` (the live form values), `onChange(name, value)`
613
+ (the form's own setter: the change is unsaved, goes through undo and redo, and
614
+ Save or Publish store it with the usual permission checks), `disabled` (the form
615
+ is read-only) and `close()`. It renders inside the Content Manager edit view, so
616
+ it can also use the admin's hooks (`useForm`, `useFetchClient`...). Strapi runs
617
+ every plugin's `register` before any `bootstrap`, then the host's `bootstrap`:
618
+ calling `registerPanel` from either bootstrap works. An invalid panel is logged
619
+ (`[blockscene] registerPanel: ...`) and ignored; `registerPanel` returns `true`
620
+ or `false`. A panel that throws while rendering shows the plugin's error notice
621
+ in its drawer; the editor keeps working. On Strapi 4 the same call works (its
622
+ `values` and `onChange` are the edit view data manager's); the lab smoke covers
623
+ panels on Strapi 5 only.
624
+
625
+ ### Undo and redo
626
+
627
+ The first zone's bar and the pane toolbar have Undo and Redo buttons (Strapi 5),
628
+ on one shared history (the shortcuts use it too).
629
+ They step through the whole edit view: blocks inserted, removed or moved from the
630
+ gallery, the page, groups or the native actions, and any field edit. Changes
631
+ within 400 ms of each other are one step, so a burst of typing undoes at once;
632
+ a block dragged with the mouse is one step however long the drag lasts; up to
633
+ 100 steps. Shortcuts: Cmd/Ctrl+Z, Cmd/Ctrl+Shift+Z and Ctrl+Y, only while
634
+ the focus is outside a text field, select or rich text editor (those keep their
635
+ own undo).
636
+
637
+ Limits: the history lives in the open edit view and starts over when the
638
+ document, the locale or its loaded values change, so a Save clears it. Shortcuts
639
+ pressed inside the preview page are not seen (it is another origin); use the
640
+ buttons there. A custom field that reads its value only once on mount keeps
641
+ showing the old text after an undo until the view reloads; the CKEditor 5
642
+ custom field (@_sh/strapi-plugin-ckeditor) follows the restored value (checked
643
+ on Strapi 5.31).
171
644
 
172
645
  ## Layout groups (optional)
173
646
 
@@ -192,7 +665,8 @@ pairs naming unknown components are dropped. Several OPENs may share one CLOSE.
192
665
  With a valid map:
193
666
 
194
667
  - The gallery (form mode) and the page preview insert OPEN and its CLOSE in the
195
- same unsaved change: an empty group, never a lone marker. When the CLOSE is
668
+ same unsaved change: an empty group, never a lone marker. A CLOSE is not
669
+ offered on its own in the gallery or the preview picker. When the CLOSE is
196
670
  not allowed in that zone, or the zone has no room for two rows, nothing is
197
671
  inserted and a notice explains why (two form dispatches in one React batch;
198
672
  no partial state is rendered or saved). Children are added
@@ -204,7 +678,7 @@ With a valid map:
204
678
  groups may nest; a CLOSE right after its OPEN is a valid empty group. Errors
205
679
  are `closeBeforeOpen` (CLOSE with no open group), `mismatch` (CLOSE is not the
206
680
  one expected by the innermost open group) and `unclosed` (OPEN never closed).
207
- - Nothing is repaired on read: the panel lists the problems (row, marker) and
681
+ - Nothing is repaired on read: the Blockscene side panel lists the problems (row, marker) and
208
682
  the stray rows stay visible so the editor can fix them. Drafts save normally.
209
683
  - Publishing is refused server-side while a configured pair is unbalanced.
210
684
  Strapi 5: a document-service middleware intercepts every facade path that
@@ -228,6 +702,397 @@ With a valid map:
228
702
  Configured groups are stored exactly as before: the flat list of components in
229
703
  the zone. Removing the config only removes the group tools and the guard.
230
704
 
705
+ ### Groups in the form
706
+
707
+ The edit view draws the same structure over Strapi's native rows (Strapi 5 and
708
+ 4; data attributes on the rows, nothing of the Content Manager is patched):
709
+
710
+ - The rows between an OPEN and its CLOSE are indented under it with a guide line
711
+ in the admin's primary colour, one level (and one line) per nested group.
712
+ - The CLOSE row reads as the group's end: compact, dimmed, without a drag handle
713
+ (it cannot be dragged on its own; its delete stays, with the marker notice).
714
+ - Each closed group's OPEN header gets a chevron before its title. It folds the
715
+ group: its rows and CLOSE are hidden and the header shows "N blocks" (every
716
+ row inside, nested ones too, CLOSE markers excluded). Folding is kept in memory
717
+ for the session, per document, zone and group; a reload shows every group open.
718
+ A validation error inside a folded group (on Save or Publish) unfolds it.
719
+ Expand all / Collapse all act on the visible block accordions only: they never
720
+ open the forms of a folded group's rows, and a group keeps its own chevron.
721
+ - Moving an OPEN moves its group. Nothing intercepts Strapi's drag and drop:
722
+ after any change of the zone, if the only thing that changed is one OPEN
723
+ moving, its members as they were (children and CLOSE) are put back right after
724
+ it, in the same unsaved form change. This covers the mouse (settled on drop:
725
+ Strapi moves the row on every hover), the keyboard (Space, arrows, Space on the
726
+ drag handle) and the move arrows of small screens. An OPEN dropped among its
727
+ own rows (a single keyboard or arrow step down) moves the group one row down
728
+ instead. The whole drag and the correction are one undo step (Strapi 5).
729
+ - Everything else keeps the flat-list meaning: a child dragged out of the range
730
+ is an ordinary block from then on, a block dropped inside becomes a child, and
731
+ an OPEN dropped inside another group is nested there with its rows. A CLOSE
732
+ moved by other means (the small-screen arrows) ends the group where it lands.
733
+ Unclosed groups and stray markers are indented as the page preview shows them
734
+ but have no chevron and are never moved (the diagnostics list them).
735
+
736
+ ### Layout grid
737
+
738
+ An OPEN whose component lays its children out in columns (a "grid columns"
739
+ wrapper) can show them that way in the form. Name the OPEN attribute that holds
740
+ its desktop column count, per component, in Settings (the OPEN's card, "Layout
741
+ grid") or in the code `settings`:
742
+
743
+ ```json
744
+ { "components": { "wrappers.grid-columns": { "layout": { "columnsField": "columnsMd", "mobileColumnsField": "columns", "maxColumns": 3 } } } }
745
+ ```
746
+
747
+ - `columnsField` (required) and `mobileColumnsField` (optional) must be
748
+ attributes of that component that can hold a count: `integer`, `biginteger`,
749
+ `float`, `decimal`, `string`, or an `enumeration` whose every value is a whole
750
+ number (`["1", "2", "3"]`). `maxColumns` is 1 to 12 (default 12). Checked on
751
+ `PUT` and on the code settings at boot; a saved layout whose attribute was
752
+ removed is dropped on read. A layout on a component that is not a configured
753
+ OPEN does nothing.
754
+ - The count is read live from the OPEN's form: its value, else the attribute's
755
+ default, else 1, capped at `maxColumns` (a value that is not a whole number
756
+ from 1 counts as missing). The grid shows the desktop count; the mobile one is
757
+ named in its caption.
758
+ - Each child is a cell under the OPEN's header (its name and the first text of
759
+ its fields); a nested group is one cell ("N blocks"); the CLOSE stays a row
760
+ below. The children's own rows are hidden but stay mounted, so the form,
761
+ validation, Save and the row tools keep working on them; Expand all skips
762
+ them as it skips a folded group.
763
+ - A cell opens its block's native form in the block dialog (the one of the
764
+ visual editor), in any mode, with every field type. A validation error inside
765
+ a child gives its cell a red border; the dialog shows the field.
766
+ - Reorder by dragging a cell onto another, or with Alt+Arrow keys on a focused
767
+ cell (left/right one cell, up/down one row of the grid); nested groups move
768
+ whole. "Remove from group" in a cell's "…" menu moves the block right after
769
+ the CLOSE. "Add a block" opens the gallery (the zone's allowed components) and
770
+ inserts at the end of the group; an OPEN brings its CLOSE. Each of these is
771
+ one form change: one undo step on Strapi 5, nothing saved before Save.
772
+ - The icon on the OPEN's header switches that group between grid and list (the
773
+ native rows) for the session, per document and group. A folded group shows no
774
+ grid.
775
+ - Dragging a cell out of the grid onto the list is not supported (it would need
776
+ Strapi's own drag and drop): use "Remove from group", or the list view.
777
+
778
+ Strapi 5 and 4 (the block dialog is shared by both since the page preview core).
779
+
780
+ ## Field labels
781
+
782
+ The Content Manager shows each field under its "Configure the view" label, which
783
+ defaults to the raw attribute name (`mobileColumnsCount`). Blockscene rewrites
784
+ the edit view labels, hints and placeholders through the Content Manager's own
785
+ `Admin/CM/pages/EditView/mutate-edit-view-layout` hook (Strapi 5 and 4, the
786
+ same hook the i18n plugin uses), so nothing in the DOM is patched and the
787
+ saved view configuration is never changed. Per field, the first that applies:
788
+
789
+ 1. **Project texts** from the plugin config `fields` (below);
790
+ 2. **schema metadata**: `pluginOptions.blockscene` on the attribute, with its
791
+ translations (see [Describing blocks and fields in the schema](#describing-blocks-and-fields-in-the-schema));
792
+ 3. **a label set in "Configure the view"** (anything other than the attribute name);
793
+ 4. **the humanized name** when "Friendly field labels" is on: `mobileColumnsCount`
794
+ reads "Mobile columns count", `page_seo` "Page seo", acronym runs stay
795
+ (`pageSEO` "Page SEO", `ctaURL` "Cta URL").
796
+
797
+ Description (the hint under the field) and placeholder change only when the
798
+ project texts have them. Texts follow the admin user's interface language
799
+ (`strapi-admin-language`, else English), resolved as: exact locale, then its
800
+ language (`pt-BR` → `pt`), then any variant of that language, then `en`, then
801
+ the first value.
802
+
803
+ ```js
804
+ // config/plugins.js
805
+ blockscene: { config: { fields: {
806
+ 'api::page.page': {
807
+ pageSeo: { label: { en: 'Search engine settings', 'pt-BR': 'Configurações de SEO' },
808
+ description: { en: 'Title and description shown on Google', 'pt-BR': 'Título e descrição no Google' } },
809
+ },
810
+ 'shared.seo': { metaTitle: { placeholder: 'Up to 60 characters' },
811
+ metaDescription: { help: { en: 'Shown under the title in search results. Google cuts it around 155 characters; write for people, not keywords.' } } },
812
+ } } },
813
+ ```
814
+
815
+ Keys are content type or component uids, then attribute names; each of
816
+ `label` (up to 80 characters), `description` (300), `placeholder` (120) and
817
+ `help` (500) is a string or `{ "<locale>": string }`.
818
+
819
+ `description` stays the line under the input. `help` is for the longer
820
+ explanation that would stretch the form: on Strapi 5 it becomes an "i" icon next
821
+ to the label, in the Content Manager's own label action slot (where the i18n
822
+ plugin puts its globe; both icons show on localized fields), with the text in a
823
+ tooltip on hover and on keyboard focus (the button is reachable with Tab and
824
+ named "More information"). A field can have both. On Strapi 4 the Content
825
+ Manager gives component fields, where blocks live, no label action, so `help`
826
+ is appended to the description under the input instead. The map is validated at boot: an unknown
827
+ key, an empty or too long text, or a malformed locale code logs a warning
828
+ naming it and the whole `fields` config is ignored (labels fall back to the two
829
+ other steps). Entries for a uid or attribute the schema no longer has (a
830
+ renamed or removed field) are skipped with one warning listing them; the rest
831
+ keep working. It is code only, versioned with the
832
+ project; the Settings page has the on/off switch but does not edit texts.
833
+
834
+ To start the file, generate every field of a project with its humanized English
835
+ label from a clone of this repository, then keep what you want to change:
836
+
837
+ ```bash
838
+ node scripts/fields-skeleton.mjs ../my-project/backend > ../my-project/backend/config/blockscene-fields.json
839
+ # config/plugins.js: fields: require('./blockscene-fields.json')
840
+ ```
841
+
842
+ Limits: the hook runs synchronously and reads the last catalog the admin
843
+ loaded. The first edit or list view of a session starts that request and shows
844
+ native labels; the next layout computed (another document, list → edit, a
845
+ locale switch) has them. Strapi 5 gives the hook no content type uid, so the
846
+ layout is matched by display name and field names; two content types that
847
+ share both are left untouched (component fields always work: they are keyed by
848
+ uid). The switch "Editor enhancements enabled", `BLOCKSCENE_DISABLED` and a
849
+ content type turned off all leave the native labels; any error leaves the
850
+ layout untouched. Changing the interface language applies from the next edit
851
+ view.
852
+
853
+ ## Describing blocks and fields in the schema
854
+
855
+ The same texts can live in the component and content type JSON files, next to
856
+ what they describe, instead of the long `components` and `fields` config maps:
857
+
858
+ ```jsonc
859
+ // src/components/sections/rich-text.json
860
+ {
861
+ "collectionName": "components_sections_rich_texts",
862
+ "info": { "displayName": "Rich text", "description": "Formatted text with an optional title and anchor." },
863
+ "pluginOptions": { "blockscene": { "tags": ["Editorial"] } },
864
+ "attributes": {
865
+ "anchor": { "type": "string", "pluginOptions": { "blockscene": { "help": "Id for links to this block (#id), without the \"#\"." } } },
866
+ "text": { "type": "richtext" }
867
+ }
868
+ }
869
+ // src/components/shared/tag.json, attribute "label"
870
+ "label": { "type": "string", "pluginOptions": { "blockscene": { "label": "Tag text", "help": "One or two words." } } }
871
+ ```
872
+
873
+ - **Block**: the native `info.description` is the gallery description;
874
+ `pluginOptions.blockscene` takes `label`, `typology`, `tags`, `keywords` and
875
+ `image`, with the same rules as the `components` config entries.
876
+ - **Field** (any content type or component attribute): `pluginOptions.blockscene`
877
+ takes `label` (up to 80 characters), `description` (300), `placeholder` (120)
878
+ and `help` (500), each a plain string in the source language, English.
879
+
880
+ Other languages go in one flat file per locale, given to the plugin config:
881
+
882
+ ```js
883
+ // config/plugins.js
884
+ blockscene: { config: { translations: { 'pt-BR': require('./blockscene/pt-BR.json') } } },
885
+ ```
886
+
887
+ ```json
888
+ {
889
+ "sections.rich-text": "Texto formatado",
890
+ "sections.rich-text.description": "Texto formatado com título e âncora opcionais.",
891
+ "sections.rich-text.anchor": "Âncora",
892
+ "sections.rich-text.anchor.help": "Id para links a este bloco (#id), sem o \"#\".",
893
+ "shared.tag.label": "Texto da tag"
894
+ }
895
+ ```
896
+
897
+ Keys are `<uid>` (block label, else the `displayName`), `<uid>.description`
898
+ (block description), `<uid>.<attribute>` (field label) and
899
+ `<uid>.<attribute>.<description|placeholder|help>`. A component attribute named
900
+ `description` takes its label as `<uid>.description.label`. The admin picks the
901
+ language as for the field labels above; a missing key shows the schema's
902
+ English text, and a translated field label with no English one in the schema
903
+ shows Strapi's own label in English.
904
+
905
+ Precedence, per text: the plugin config (`components`, `fields`, and Settings
906
+ for typology and tags) > schema metadata > native Strapi ("Configure the view"
907
+ labels, `displayName`) > automatic (humanized names, guessed typology). The
908
+ existing config keeps working unchanged. Everything is read once at boot:
909
+ invalid values (a typology outside the list, a label over 80 characters, an
910
+ unknown key) and translation keys naming no known block or field are left out,
911
+ with one warning listing them by uid.
912
+
913
+ **Content-Type Builder.** Tested on Strapi 5.52.1 and 4.26.1 in development
914
+ mode: editing and saving a component in the Content-Type Builder keeps
915
+ `pluginOptions.blockscene` (block and attributes) and `info.description`. It
916
+ has no field for them, so add and change them in the JSON files.
917
+
918
+ To move an existing config, from a clone of this repository (idempotent; each
919
+ schema file keeps its indentation and key order; labels equal to what the
920
+ plugin would show anyway, the humanized name or the `displayName`, are dropped):
921
+
922
+ ```bash
923
+ node scripts/schema-metadata.mjs ../my-project/backend --fields fields.json --components components.json [--out dir] [--source en] [--dry-run]
924
+ ```
925
+
926
+ `fields.json` and `components.json` hold the two config maps as JSON. The
927
+ translation files go to `config/blockscene/<locale>.json`; the script lists
928
+ what it could not place and the `components` keys that stay in the config
929
+ (`variants`, `template`). Remove the migrated entries from the config
930
+ afterwards: the config wins.
931
+
932
+ ## Version history (Strapi 5)
933
+
934
+ Off by default. Turn it on in Settings, Blockscene, "Version history" (or from
935
+ code, `settings.history`, see below). Strapi Community has no content history
936
+ (it is a Growth feature); this module records one event per content change and
937
+ keeps a snapshot of the content, so an editor can see what changed and load an
938
+ earlier version back into the form.
939
+
940
+ **What it captures.** Every create, save, publish, unpublish, discard draft and
941
+ delete of the covered content types (all `api::` types by default, or a list)
942
+ made through Strapi's document service: the admin (single and bulk actions),
943
+ the REST and GraphQL APIs, and any plugin or custom code that uses
944
+ `strapi.documents`. One event per affected locale, with who (admin user, API
945
+ token, Users & Permissions user, or `system`), when, the action and a summary
946
+ (blocks added, removed and changed per Dynamic Zone, top-level fields changed)
947
+ against the previous snapshot of the same locale. The snapshot is the stored
948
+ entry with components and Dynamic Zones in full, relations as documentIds and
949
+ media as file ids (a 34-block page is a few KB; the capture adds a few
950
+ milliseconds to a save). A snapshot identical to the previous one is not stored
951
+ again (the event is, with the same hash).
952
+
953
+ **What it does not see.** Writes that bypass the document service: raw
954
+ `strapi.db.query`, knex or SQL, imports that write the database directly. This
955
+ is content activity for editors, not a security audit log.
956
+
957
+ **Failure policy.** A failed capture never blocks a save or a publish: the
958
+ change goes through, the error is logged and the event is marked "Not
959
+ recorded". A failed capture fails a delete: the delete runs in one transaction
960
+ with its events, so nothing is deleted without its last version (a bulk delete
961
+ is rolled back whole). `BLOCKSCENE_DISABLED=true` or the module switch off stop
962
+ all capture. The plugin never records its own content types.
963
+
964
+ **In the edit view.** The Blockscene side panel gets a History section (saved
965
+ documents of covered types, also on types without a Dynamic Zone): the
966
+ versions of the document in the current locale, newest first. Selecting one
967
+ shows a block-level diff against the form (relations are not compared) and
968
+ "Load this version", which puts the version into the form the way undo does:
969
+ nothing is written until the editor saves, so validation, permissions and
970
+ lifecycles apply as for any edit, and undo reverts the load. Components and
971
+ blocks come back as new rows; relations become the connect/disconnect change
972
+ from what the document has now; fields the editor may not update, fields the
973
+ schema no longer has, blocks whose component is no longer allowed, and media or
974
+ related entries that no longer exist are left out and listed.
975
+
976
+ **Permission.** `Blockscene: Read version history` (`history.read`), plus read
977
+ access to the content type in the Content Manager. Snapshots hold removed and
978
+ unpublished text, so the section is hidden without it. Admin API:
979
+ `GET /blockscene/history/:uid/:documentId?locale=` (the latest 100 events, no
980
+ snapshots) and `GET /blockscene/history-events/:id` (one event, its snapshot and
981
+ the media and relation targets that still exist).
982
+
983
+ **Retention.** Snapshots are kept `retentionDays` (90) and at most
984
+ `maxSnapshots` (100) per document, newest first; events without a snapshot are
985
+ kept `eventDays` (365, at least `retentionDays`). A cron job added by the plugin
986
+ runs nightly at 03:00 server time, in batches of 500; it only empties snapshots
987
+ and deletes events, never media files, and running it on several instances is
988
+ harmless.
989
+
990
+ ```js
991
+ // config/plugins.js
992
+ blockscene: { config: { settings: { history: { enabled: true, contentTypes: ['api::page.page', 'api::post.post'],
993
+ retentionDays: 90, maxSnapshots: 100, eventDays: 365, trashDays: 90 } } } }
994
+ ```
995
+
996
+ Events live in the `blockscene_events` table (content type
997
+ `plugin::blockscene.event`, hidden from the Content Manager and the Content-Type
998
+ Builder). Removing the plugin, or installing a Blockscene version older than
999
+ the one that added history, lets Strapi drop that table and every recorded
1000
+ version on the next start: back up `blockscene_events` first if you need it.
1001
+
1002
+ ### Trash (Strapi 5)
1003
+
1004
+ Part of the version history module: on with it, for the same content types.
1005
+ Strapi has no trash in any plan; here a delete made through the document service
1006
+ (the admin's Delete, bulk delete and "Delete locale", the APIs, plugins) also
1007
+ writes one trash entry per document, in the same transaction as the delete.
1008
+
1009
+ **What an entry keeps.** Every deleted locale, draft and published rows (they
1010
+ can differ), as snapshots like the history's; the document's title (its Content
1011
+ Manager main field); who deleted it and when; and the links other documents and
1012
+ blocks had to it. Strapi removes those links with the document, and the
1013
+ document's own snapshot does not hold them when the relation is unidirectional
1014
+ (declared only on the other side, like a menu listing pages or a block linking
1015
+ to a page), so they are captured before the delete: owner, field, locale and the
1016
+ position in the owner's list. A delete without a locale removes the default
1017
+ locale only (Strapi's rule), `'*'` every locale; the entry has exactly what was
1018
+ deleted. If the capture fails, nothing is deleted.
1019
+
1020
+ **Restore.** From the Trash page, which first shows what the restore would do
1021
+ and writes nothing if something blocks it:
1022
+
1023
+ - Blocking: the content type is gone; a unique field (`unique: true` or a uid
1024
+ field, such as a slug) is now used by another document in that locale; the
1025
+ document has that locale again; a single type has a document again; none of
1026
+ its locales is configured any more.
1027
+ - Reported, then restored anyway: a published version existed (see below);
1028
+ required fields are empty under the current schema (drafts may be, Strapi
1029
+ checks them on publish); fields the schema no longer has, blocks whose
1030
+ component the zone no longer allows, media files and related documents that no
1031
+ longer exist (left out); locales no longer configured (skipped); links whose
1032
+ owner is gone or now points elsewhere.
1033
+
1034
+ The document comes back as a **draft with its draft content**: the draft is the
1035
+ latest work, and publishing is the editor's decision (the site may have moved
1036
+ on). The published snapshot stays in the entry and in the preview. Every locale
1037
+ is restored under one documentId, the default locale first, through the
1038
+ document service (validation and the project's lifecycles run). A document that
1039
+ is gone gets a new documentId, since Strapi's create does not take one; a locale
1040
+ deleted from a document that still exists goes back into that document. The
1041
+ captured links are put back where they were (same position in the owner's
1042
+ list) when the owner still exists; links from published versions come back
1043
+ when those owners are published again after this document is. A restore is one
1044
+ transaction and is idempotent: the entry is claimed first, so a second restore
1045
+ (a double click, another editor, another instance) returns the first one's
1046
+ document. The history gets a `restore` event per locale.
1047
+
1048
+ **Delete forever** removes one entry (recorded as a `purge` event in the
1049
+ activity). The nightly job deletes entries past their expiry, restored ones too,
1050
+ in batches; neither ever deletes media files.
1051
+
1052
+ **Retention.** `trashDays` (90, 1 to 3650) in the same settings; each entry's
1053
+ expiry is fixed when the document is deleted, so a later change applies to later
1054
+ deletes.
1055
+
1056
+ ```js
1057
+ blockscene: { config: { settings: { history: { enabled: true, trashDays: 30 } } } }
1058
+ ```
1059
+
1060
+ Entries live in `blockscene_trash` (`plugin::blockscene.trash`, hidden from the
1061
+ Content Manager and the Content-Type Builder); the same backup note as for
1062
+ `blockscene_events` applies.
1063
+
1064
+ ### Activity and Trash pages (Strapi 5)
1065
+
1066
+ A main menu link, "Activity and trash", opens one page with a tab each (the
1067
+ pages are tools for editors, not configuration, so they are not under
1068
+ Settings). **Activity** lists every recorded event, newest first, 25 per page,
1069
+ filtered by person, content type, action, date range and title (documents whose
1070
+ main field contains the text now, or whose trash entry's title does); each row
1071
+ links to the document's edit view while that locale exists and shows the
1072
+ summary. **Trash** lists the entries (title, type, locales, who, when, when it
1073
+ is deleted for good), filtered by content type, with restored entries on a
1074
+ switch; Preview shows the fields and blocks of each deleted locale, read-only;
1075
+ Restore shows the pre-check report and asks to confirm; Delete forever asks
1076
+ first.
1077
+
1078
+ **Permissions.** `Read content activity` (`activity.read`), `Read the trash`
1079
+ (`trash.read`), `Restore from the trash` (`trash.restore`), `Delete from the
1080
+ trash forever` (`trash.purge`), none granted to non-super-admin roles by
1081
+ default. On top of them, every list shows only content types the user may read
1082
+ in the Content Manager (an entry of another type answers 404), restoring needs
1083
+ create there and deleting forever needs delete. Admin API: `GET /blockscene/activity`
1084
+ (`actor`, `contentType`, `action`, `from`, `to` as ISO dates, `q`, `page`,
1085
+ `pageSize` up to 100), `GET /blockscene/trash` (`contentType`, `status`
1086
+ trashed or restored, `page`, `pageSize`), `GET /blockscene/trash/:id` (the
1087
+ preview), `GET /blockscene/trash/:id/check` (the pre-check),
1088
+ `POST /blockscene/trash/:id/restore` (409 with the report when blocked) and
1089
+ `DELETE /blockscene/trash/:id`. Lists never load snapshots.
1090
+
1091
+ **Coverage boundary**, the same as the history's: the document service only.
1092
+ A raw `strapi.db.query` delete, SQL or a database restore leaves no trash
1093
+ entry; deleting a content type (Content-Type Builder) drops its data without
1094
+ one. The trash is a safety net for editors, not a backup: keep database backups.
1095
+
231
1096
  ## Languages
232
1097
 
233
1098
  All plugin chrome (gallery, settings, page preview, editor dialogs, diagnostics)
@@ -241,15 +1106,15 @@ component display names are never translated by the plugin.
241
1106
 
242
1107
  ## Turning the editor enhancements off
243
1108
 
244
- - **From the panel**: switch "Editor enhancements enabled" off. The gallery
245
- button, controls and initial-state logic disappear and the native picker and
1109
+ - **From the settings page**: switch "Editor enhancements enabled" off. The
1110
+ gallery, the zone bars and the initial-state logic disappear and the native picker and
246
1111
  accordions remain. Content, config, media and unsaved edits are preserved; the
247
1112
  form is not reloaded. The settings page stays available to turn it back on.
248
- - **Emergency, outside the panel**: start the server with
1113
+ - **Emergency, outside the admin**: start the server with
249
1114
  `BLOCKSCENE_DISABLED=true` (or set `config.disabled: true`). It is read at
250
1115
  boot and reported by the catalog, wins over the saved settings and cannot be
251
1116
  undone from the client. Changing it requires a restart.
252
- - **If the catalog request fails**, the panel renders nothing and the native
1117
+ - **If the catalog request fails**, the plugin renders nothing and the native
253
1118
  editor is used, so a plugin outage does not block editing.
254
1119
  - **Completely**: set `'blockscene': { enabled: false }` in `config/plugins.js`
255
1120
  and rebuild/restart. This is the only way out of an import/build failure; a
@@ -292,10 +1157,11 @@ the configured static base.
292
1157
 
293
1158
  ## Guarantees and limits
294
1159
 
295
- Tested versions: Strapi 4.26.1 (Node 20) and 5.52.1 (Node 22) in the local
296
- labs of this repository, and 5.23.5 and 5.31.0 in two real site projects. The
297
- peer ranges (`>=5.23.0 <6`, `>=4.20.0 <5`) express what installs without
298
- overrides, not a promise for every minor in between.
1160
+ Tested versions: Strapi 4.11.0 and 4.26.1 (Node 20), 5.0.0 and 5.52.1
1161
+ (Node 22) in the local labs of this repository, and 5.23.5 and 5.31.0 in two
1162
+ real site projects. The peer ranges (`>=5.0.0 <6`, `>=4.11.0 <5`) express what
1163
+ installs without overrides, not a promise for every minor in between. Strapi
1164
+ 4.0–4.10 is out of reach: those admins run React 17 and lack `useFetchClient`.
299
1165
 
300
1166
 
301
1167
  - Native component payloads and ordinary Strapi save/publish flow.
@@ -306,14 +1172,17 @@ overrides, not a promise for every minor in between.
306
1172
  - Authenticated admin catalog endpoint exposes only supported metadata, resolved
307
1173
  override URLs, palette and editor preferences; never the whole config.
308
1174
  - Top-level Dynamic Zones only. Conditional fields are omitted conservatively.
309
- - "Open all" / "Close all" drive the native accordions: opening is batched into
310
- one render; closing yields a frame per block so the admin stays responsive.
311
- On a page with about 34 blocks whose fields include CKEditor, closing still
312
- takes a few seconds with pauses up to about 1.7 s: that is the editors being
313
- torn down by the host, not the plugin's loop.
1175
+ - "Expand all" / "Collapse all" drive the native accordions without touching the
1176
+ whole list at once: expanding opens the rows in and near the visible area, a
1177
+ few per frame, and the others as they approach while scrolling (until Collapse
1178
+ all or leaving the document); collapsing closes the visible rows at once and
1179
+ the rest in idle time. With lazy editors, on a 34-block page with CKEditor
1180
+ fields (Strapi 5.31) the longest pause went from about 2.9 s to 0.14 s on
1181
+ open all and from 1.6 s to 0.14 s on close all.
314
1182
  - No screenshot server, no hourly job, no in-editor rendered preview.
315
1183
  - Removing the plugin leaves native content intact; remove its config entry and
316
- rebuild. No plugin data migration is needed.
1184
+ rebuild. No plugin data migration is needed. The "hide on the site" column
1185
+ (see Row actions) stays in the component tables until you drop it.
317
1186
 
318
1187
  Configuration edits require a server restart. Backend URL/prefix deployments
319
1188
  should set `previewBaseUrl` explicitly. HTTP(S) image hosts must also be allowed
@@ -324,7 +1193,7 @@ uploads share an origin.
324
1193
  ## Development
325
1194
 
326
1195
  - `admin/`: shared gallery, settings page, wireframes and version-specific adapters.
327
- - `server/`: settings store, validation, permissions, the catalog endpoint and the layout-group publish guard (`groups.js` is shared with the admin).
1196
+ - `server/`: settings store, validation, permissions, the catalog endpoint and the layout-group publish guard (`groups.js` is shared with the admin); `history.js` and `diff.js` for version history (`diff.js` is shared with the admin).
328
1197
  - `admin/translations/`: one flat catalogue per admin locale (English is the source of truth).
329
1198
  - `scripts/capture-previews.mjs`: local thumbnail capture.
330
1199
  - `packages/strapi4`, `packages/strapi5`: independently installable distributions.