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

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