Registre média
Façade vers @lrvo/module-medias, attacher des fichiers à un contenu sans dépendre du package.
Le gestionnaire de médias est distribué comme un module externe, @lrvo/module-medias. Le core ne dépend pas de ce package : il fournit un registre que le module remplit au runtime.
Un module tiers peut donc téléverser un fichier et attacher des médias à son contenu sans déclarer @lrvo/module-medias en dépendance, et sans conditionner ses appels. Le mode d'emploi côté consommateur est dans Intégrer les médias.
Quand le module est absent ou désactivé (LRVO_MODULE_MEDIAS différent de true), les façades restent appelables et se comportent de façon neutre : les lectures retournent vide, les écritures retournent un échec explicite, getMediaPicker() retourne null. Le code consommateur reste naïf.
Points d'entrée
| Sous-chemin | Fichier | Runtime |
|---|---|---|
@lrvo/core/media-registry | index.server.js | serveur |
@lrvo/core/media-registry/client | index.client.js | client |
@lrvo/core/media-registry/file-route | file-route.server.js | serveur |
@lrvo/core/media-registry/block-editor | block-editor.server.js | serveur |
Registre serveur
setMediaProvider(provider) et getMediaProvider()
Appelées par @lrvo/module-medias au boot. Un module consommateur n'y touche pas.
Forme de l'objet provider : toutes les méthodes sont optionnelles, mais le module les publie en bloc :
{
enabled: true,
fileRouteGET,
uploadMedia, replaceMedia, updateMedia, deleteMedia, listMedia,
attachMedia, detachMedia, detachAllForSource,
countReferences, listReferences,
getMediaById, getMediaByIds, getMediaByFilename, buildMediaUrl,
normalizeImageBlocks, enrichBlocksWithMedia, syncBlockImageReferences,
}isMediaEnabled()
Retourne true si un provider actif est enregistré. À utiliser pour masquer une UI, pas pour conditionner un appel : les façades gèrent déjà l'absence.
Façades
Les écritures rendent un échec explicite sans provider, les lectures une valeur vide. Aucun appel ne lève.
| Fonction | Comportement sans provider |
|---|---|
uploadMedia({ file, filename, size, uploadedBy, altText, caption }) | { success: false, error: 'media module not enabled' }. |
replaceMedia(id, { file, filename, size, uploadedBy }) | Idem. |
updateMedia(id, { filename, altText, caption }) | Idem. |
deleteMedia(id) | Idem. |
attachMedia({ mediaId, sourceType, sourceId, field }) | Idem. |
detachMedia(args) | Idem. |
detachAllForSource({ sourceType, sourceId }) | Idem. |
listMedia({ page, limit, search, kind, uploadedBy }) | Liste vide et pagination à zéro. |
countReferences(mediaId) | 0. |
listReferences(mediaId) | Liste vide. |
getMediaById(id) | null. |
getMediaByIds(ids) | Liste vide. |
getMediaByFilename(filename) | null. |
buildMediaUrl(media) | null. |
Le détail du comportement de chaque écriture, avec le module actif, est dans Téléversement et API.
import { attachMedia, detachAllForSource } from '@lrvo/core/media-registry';
// à l'enregistrement d'un billet
await attachMedia({
mediaId: payload.featuredImageId,
sourceType: '@lrvo/module-blog:post',
sourceId: post.id,
field: 'featured_image',
});
// à la suppression du billet : libérer toutes les références
await detachAllForSource({ sourceType: '@lrvo/module-blog:post', sourceId: post.id });Le sourceType porte le nom du package en préfixe. C'est ce qui garantit qu'aucun module ne libère par erreur les références d'un autre.
Déposer un fichier depuis du code
uploadMedia accepte un File de formulaire ou un tampon binaire, auquel cas le nom d'origine doit être fourni. C'est la voie à emprunter depuis une server action, une route ou une tâche planifiée : les routes d'admin du module exigent une session et ne doivent jamais être appelées en fetch depuis le serveur.
import { uploadMedia, attachMedia, buildMediaUrl } from '@lrvo/core/media-registry';
const response = await fetch(sourceUrl);
const buffer = Buffer.from(await response.arrayBuffer());
// Pas de session : aucun auteur n'est passé, la fiche restera anonyme.
const result = await uploadMedia({ file: buffer, filename: 'rapport-2026.pdf' });
if (!result.success) return { error: result.error };
await attachMedia({
mediaId: result.media.id,
sourceType: '@lrvo/module-blog:post',
sourceId: post.id,
field: 'attachment',
});
const url = buildMediaUrl(result.media);uploadedBy est optionnel. Omis, la colonne uploaded_by reste nulle et la médiathèque n'affiche aucun auteur, ce qui est exact pour un dépôt machine. La colonne porte une clé étrangère vers la table des utilisateurs : seul un identifiant réel, ou rien, est accepté. Un identifiant inventé fait échouer l'enregistrement.
Les formats, les tailles et les validations sont les mêmes que pour un téléversement depuis l'interface : la règle vit dans la fonction, pas dans la route. Voir Téléversement.
deleteMedia refuse un média encore référencé. Interroger countReferences avant, ou lire le champ referenceCount de l'échec.
Ces façades n'ont un provider que dans un processus où le module a booté, c'est-à-dire le serveur Next, après initializeLrvo(). Un script lancé hors du serveur, comme le CLI de base de données, ne déclenche pas l'enregistrement des modules : le registre y est vide et les façades rendent leurs valeurs neutres. Une tâche planifiée doit donc passer par une route ou une server action.
Registre client
@lrvo/module-medias/client enregistre ici les composants MediaPicker et MediaDetailsModal.
| Fonction | Rôle |
|---|---|
setMediaClientProvider({ MediaPicker, MediaDetailsModal }) | Appelée par le module. |
getMediaPicker() | Composant, ou null. |
getMediaDetailsModal() | Composant, ou null. |
isMediaPickerAvailable() | true si un sélecteur est disponible. |
'use client';
import { getMediaPicker, isMediaPickerAvailable } from '@lrvo/core/media-registry/client';
function FeaturedImageField({ onSelect }) {
const [open, setOpen] = useState(false);
const MediaPicker = getMediaPicker();
if (!isMediaPickerAvailable()) return null;
return (
<MediaPicker
isOpen={open}
onClose={() => setOpen(false)}
accept="image/*"
onSelect={onSelect}
/>
);
}Vérifier isMediaPickerAvailable() avant de rendre : getMediaPicker() retourne null sans le module, et rendre null comme composant lève.
| Propriété | Rôle |
|---|---|
isOpen, onClose | Contrôle de l'ouverture. |
onSelect | Reçoit le média choisi, ou un tableau de médias en mode multiple. |
accept | Filtre initial, dérivé en catégorie de contenu. |
multiple | Sélection multiple. Faux par défaut. |
initialSelected | Tableau de médias déjà retenus, affichés cochés à l'ouverture. Mode multiple. |
title | Titre de la fenêtre. |
En mode multiple, la sélection retient les objets média et survit donc à la pagination et aux filtres, onSelect rend le tableau dans l'ordre où la sélection a été constituée, et le téléversement accepte plusieurs fichiers. Voir Intégrer les médias.
Route fichier
app/lrvo/media/[filename]/route.js est câblé par @lrvo/start :
export { GET } from '@lrvo/core/media-registry/file-route';Le shim forwarde au handler enregistré par le module. Sans module, il retourne 404 : la route reste câblée statiquement mais inerte, ce qui évite d'avoir à générer conditionnellement des fichiers de routes.
Helpers BlockEditor
Pour les modules qui hébergent du contenu BlockEditor avec des images.
| Fonction | Sans provider |
|---|---|
normalizeImageBlocks(blocks) | Passthrough : les blocs à mediaId restent non résolus, les snapshots _media* posés par le client suffisent à un rendu approximatif. |
enrichBlocksWithMedia(blocks) | Passthrough. |
syncBlockImageReferences(args) | { success: true, count: 0 }. |
Ces fonctions doivent voir les blocs imbriqués dans les colonnes : elles s'appuient sur flattenBlocks / mapBlocksDeep.
Notes
Le module n'exporte aucun composant d'affichage. Une image se rend avec next/image sur une URL construite par buildMediaUrl, ou reconstruite depuis le _resolvedFilename posé par enrichBlocksWithMedia. Tous les médias étant publics, l'optimisation par /_next/image ne présente pas de risque de fuite. Voir Gérer les médias.
Le provider vit sur globalThis via Symbol.for, pour que le client.js du module et le code core consommateur partagent le même slot malgré le double bundle.