docs(BlockEditor): document mediaSlug media library link and add server helpers

- update image block schema in README table to include `mediaSlug?` and clarify fields
- add "Liaison avec la médiathèque" section documenting mediaSlug behavior, read-only alt/caption, and internal `_` fields
- document new server helpers (`normalizeImageBlocks`, `enrichBlocksWithMedia`, `syncBlockImageReferences`) with usage examples
- add `block_image` field convention to media feature README with cross-references
- implement `mediaLink.server.js` with the three server-side helpers
- store `mediaSlug` on image block at insertion time in `Image.client.js`
- persist `mediaSlug` through clipboard paste/duplicate in `clipboard.js`
- export `mediaLink` entry point in `package.json` exports map
This commit is contained in:
2026-04-26 20:22:22 -04:00
parent 8c5c3baec4
commit 31d0359163
6 changed files with 501 additions and 43 deletions
+79 -10
View File
@@ -30,7 +30,7 @@ Chaque bloc a un `id` (UUID) et un `type`. Selon le type :
| `quote` | `content` | citation |
| `code` | `content` | bloc de code (monospace) |
| `divider` | — | séparateur horizontal |
| `image` | `src`, `alt`, `caption`, `align`, `href`, `newTab` | image (URL uniquement) `align``left\|center\|right\|full`, `href` optionnel |
| `image` | `src`, `mediaSlug?`, `alt?`, `caption?`, `align`, `href`, `newTab` | image — voir [Bloc image](#bloc-image) ; `align``left\|center\|right\|full` |
`content` est un **tableau `InlineNode[]`** depuis Phase 2 — voir ci-dessous.
@@ -291,10 +291,75 @@ Le formulaire d'insertion accepte deux sources :
- **URL externe** : champ libre + bouton « Insérer ».
- **Bibliothèque media** : bouton « Choisir un média » qui ouvre le `MediaPicker`
de `@zen/core/features/media`. À la sélection, le bloc est inséré avec
`block.src = "/zen/api/media/file/<slug>"`. Les nouveaux uploads passent par
ce picker en visibilité `public` (les blocs image vivent dans du contenu
publié — un média privé serait illisible côté frontal).
de `@zen/core/features/media`. À la sélection, le bloc reçoit
`block.src = "/zen/api/media/file/<slug>"` **et** `block.mediaSlug = "<slug>"`.
Les nouveaux uploads passent par ce picker en visibilité `public` (les blocs
image vivent dans du contenu publié — un média privé serait illisible côté
frontal).
### Liaison avec la médiathèque (`mediaSlug`)
Quand un bloc image est lié à un média (`mediaSlug` présent), le **média est
la source unique de vérité** pour `alt` et `caption`. Le bloc ne stocke ni l'un
ni l'autre — l'éditeur affiche les valeurs du média en lecture seule avec un
lien « Modifier dans la médiathèque ». Toute mise à jour de l'alt ou de la
légende côté médiathèque est immédiatement répercutée sur tous les blocs qui
référencent ce média, sans toucher aux contenus.
Pour les **URL externes** (pas de `mediaSlug`), les inputs alt/caption locaux
restent disponibles sur le bloc — il n'y a pas d'autre source possible.
Champs internes au composant (préfixe `_`, jamais persistés) :
- `_mediaAlt` / `_mediaCaption` : snapshot capté à l'insertion via le
`MediaPicker`, pour affichage immédiat dans le bandeau lecture-seule sans
appel réseau.
- `_resolvedAlt` / `_resolvedCaption` : injectés au rendu serveur par
`enrichBlocksWithMedia` (cf. ci-dessous).
### Helpers serveur — `mediaLink.server.js`
Trois helpers à appeler depuis les actions/routes serveur du module qui
héberge le contenu BlockEditor :
```js
import {
normalizeImageBlocks,
enrichBlocksWithMedia,
syncBlockImageReferences,
} from '@zen/core/shared/components/BlockEditor/mediaLink';
// Au save :
const normalized = await normalizeImageBlocks(blocks);
await persistDocument(documentId, normalized);
await syncBlockImageReferences({
sourceType: 'post', // ou 'page', '@zen/module-shop:product', …
sourceId: documentId,
blocks: normalized,
});
// Au render (page SSR) :
const blocks = await loadDocument(slug);
const enriched = await enrichBlocksWithMedia(blocks);
return <BlockEditor value={enriched} disabled />;
```
`normalizeImageBlocks` :
- Lazy upgrade : extrait `mediaSlug` depuis `src` quand l'URL matche
`/zen/api/media/file/<slug>`.
- Back-fill : si le média n'a pas encore d'`alt_text` / `caption` mais que le
bloc en porte (legacy), remonte la valeur côté `zen_media`.
- Strict : nettoie `alt` et `caption` du bloc dès qu'un `mediaSlug` est résolu.
`enrichBlocksWithMedia` : une seule requête batch pour tous les `mediaSlug`
du document, attache `_resolvedAlt` / `_resolvedCaption`. Le composant de
rendu lit ces champs en priorité.
`syncBlockImageReferences` : synchronise `zen_media_references` avec
`field = 'block_image'`. La contrainte FK `ON DELETE RESTRICT` empêche
ensuite la suppression d'un média référencé par un bloc image tant que le
document hôte existe. Pensez à appeler `detachAllForSource` (de
`@zen/core/features/media`) à la suppression du document hôte.
Une fois l'URL insérée, l'image affiche au survol une **toolbar flottante**
(coin haut-droit) reprenant le style `BOX_CLASS` partagé :
@@ -313,14 +378,18 @@ Une fois l'URL insérée, l'image affiche au survol une **toolbar flottante**
- **Supprimer** : vide tous les champs ; le formulaire URL réapparaît.
Les inputs *Légende* et *Texte alternatif* sous l'image sont des `<input>`
HTML standards. Le clic dans ces inputs ne déclenche pas la sélection
multi-blocs (cf. garde dans `handleContainerMouseDown` : tout `input`,
`textarea`, `select` ou `[contenteditable="true"]` reçoit son focus normal
et n'entre pas en mode sélection).
HTML standards (uniquement pour les images sans `mediaSlug` — cf.
[Liaison avec la médiathèque](#liaison-avec-la-médiathèque-mediaslug)).
Le clic dans ces inputs ne déclenche pas la sélection multi-blocs (cf. garde
dans `handleContainerMouseDown` : tout `input`, `textarea`, `select` ou
`[contenteditable="true"]` reçoit son focus normal et n'entre pas en mode
sélection).
Sérialisation HTML : `<figure data-align="…"><a href><img></a><figcaption></figcaption></figure>`.
Le parser inverse (`htmlToBlocks`) lit `data-align`, et descend dans `<a>`
quand l'`<img>` y est imbriqué pour récupérer `href` / `target`.
quand l'`<img>` y est imbriqué pour récupérer `href` / `target`. Si le `src`
de l'image matche `/zen/api/media/file/<slug>`, il reconstitue `mediaSlug`
pour préserver la liaison média lors d'un copier-coller interne.
## Limitations connues