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-cheminFichierRuntime
@lrvo/core/media-registryindex.server.jsserveur
@lrvo/core/media-registry/clientindex.client.jsclient
@lrvo/core/media-registry/file-routefile-route.server.jsserveur
@lrvo/core/media-registry/block-editorblock-editor.server.jsserveur

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.

FonctionComportement 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.

FonctionRô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, onCloseContrôle de l'ouverture.
onSelectReçoit le média choisi, ou un tableau de médias en mode multiple.
acceptFiltre initial, dérivé en catégorie de contenu.
multipleSélection multiple. Faux par défaut.
initialSelectedTableau de médias déjà retenus, affichés cochés à l'ouverture. Mode multiple.
titleTitre 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.

FonctionSans 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.

Sur cette page