From 8e37eb53fff203c0372f08498a9f750e99e61c77 Mon Sep 17 00:00:00 2001 From: Hyko Date: Sun, 26 Apr 2026 19:18:35 -0400 Subject: [PATCH] feat(media): add MediaImage wrapper to handle public/private image rendering - add `MediaImage.client.js` component that routes to `next/image` for public media and native `` for private media to prevent CDN cache leaking private content - replace direct `` usage in `MediaGrid` and `MediaPage` with `MediaImage` - document `MediaImage` usage and rationale in `src/features/media/README.md` - update `docs/dev/ARCHITECTURE.md` to reference the `MediaImage` wrapper convention --- docs/dev/ARCHITECTURE.md | 4 +- src/features/media/README.md | 20 ++++++ .../media/components/MediaGrid.client.js | 15 ++--- .../media/components/MediaImage.client.js | 64 +++++++++++++++++++ src/features/media/pages/MediaPage.client.js | 10 ++- 5 files changed, 102 insertions(+), 11 deletions(-) create mode 100644 src/features/media/components/MediaImage.client.js diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 9c4f818..0062b57 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -33,4 +33,6 @@ Ces modules existent pour éviter la duplication. Avant d'écrire du code utilit **API** — Utiliser `src/core/api` pour l'API admin et publique. Définir les routes avec `defineApiRoutes()` (valide la config au démarrage). L'authentification est déclarée dans la définition de route (`auth: 'public' | 'user' | 'admin'`) — ne jamais la vérifier manuellement dans un handler. Retourner `apiSuccess()` / `apiError()` dans tous les handlers. Voir `src/core/api/README.md` pour le détail. -**Médias** — Pour gérer les fichiers attachés au contenu publié (images, PDFs, vidéos), utiliser `src/features/media`. Activable via `ZEN_MEDIA=true`. Expose `uploadMedia`/`deleteMedia`/`attachMedia`/`detachMedia` côté serveur et un composant `MediaPicker` réutilisable côté client. Voir `src/features/media/README.md`. Distinct d'un futur module `files` (style Drive/Dropbox) — ne pas confondre les deux usages. \ No newline at end of file +**Médias** — Pour gérer les fichiers attachés au contenu publié (images, PDFs, vidéos), utiliser `src/features/media`. Activable via `ZEN_MEDIA=true`. Expose `uploadMedia`/`deleteMedia`/`attachMedia`/`detachMedia` côté serveur et un composant `MediaPicker` réutilisable côté client. Voir `src/features/media/README.md`. Distinct d'un futur module `files` (style Drive/Dropbox) — ne pas confondre les deux usages. + +Pour **afficher** une image média, utiliser le wrapper `MediaImage` du module (pas `` direct, pas `next/image` direct). Il route les médias publics via `next/image` (srcset AVIF/WebP, lazy) et garde un `` natif pour les médias privés — `/_next/image` cache la réponse optimisée par URL et fuiterait l'image privée à tous les visiteurs après le premier hit. \ No newline at end of file diff --git a/src/features/media/README.md b/src/features/media/README.md index dfe36ac..4428aef 100644 --- a/src/features/media/README.md +++ b/src/features/media/README.md @@ -32,6 +32,7 @@ L'item « Médias » apparaît comme entrée top-level de la sidebar admin (sans | [`permissions.js`](./permissions.js) | Catalogue de permissions (`media.view`, `media.upload`, `media.delete`). | | [`pages/MediaPage.client.js`](./pages/MediaPage.client.js) | Page admin `/admin/media`. | | [`components/MediaPicker.client.js`](./components/MediaPicker.client.js) | Composant réutilisable par les modules tiers. | +| [`components/MediaImage.client.js`](./components/MediaImage.client.js) | Wrapper d'affichage qui dispatche `next/image` ↔ `` selon la visibilité. | ### Modèle de données @@ -127,6 +128,25 @@ function FeaturedImageField() { | `uploadVisibility` | `'public' \| 'private'` | Visibilité appliquée aux nouveaux uploads. | | `multiple` | `boolean` | Sélection multiple avec validation explicite. | +## Affichage des images : `` + +Pour afficher un média image dans n'importe quel composant client, utiliser le wrapper `MediaImage` plutôt qu'un `` direct : + +```jsx +import MediaImage from '@zen/core/features/media/components/MediaImage.client'; + + +// ou en mode "remplir le conteneur parent" (parent doit être `position: relative`) : + +``` + +Le wrapper choisit automatiquement entre `next/image` et `` natif : + +- `media.visibility === 'public'` → `next/image` (srcset AVIF/WebP, lazy, cache CDN). +- `media.visibility === 'private'` → `` natif. **C'est volontaire** : `/_next/image` met en cache la réponse optimisée par URL au niveau Next/CDN. Si on optimisait un média privé, l'image serait servie en clair à tout visiteur après le premier hit, contournant la vérification de session faite par `/zen/api/media/file/:slug`. + +Côté projet hôte, **aucune configuration `next.config.js` n'est requise** : `/zen/api/media/file/...` est same-origin et ne nécessite pas de `remotePatterns`. + ## Permissions | Clé | Description | diff --git a/src/features/media/components/MediaGrid.client.js b/src/features/media/components/MediaGrid.client.js index dd75684..fef4fad 100644 --- a/src/features/media/components/MediaGrid.client.js +++ b/src/features/media/components/MediaGrid.client.js @@ -6,6 +6,7 @@ */ import { Image01Icon, Pdf02Icon, Mp402Icon, Mp302Icon, File02Icon, EyeIcon, BlockedIcon, Tick02Icon } from '@zen/core/shared/icons'; +import MediaImage from './MediaImage.client.js'; const KIND_ICON = { 'image/jpeg': Image01Icon, @@ -62,7 +63,6 @@ export default function MediaGrid({ {items.map(media => { const Icon = iconForMime(media.mime_type); const isSelected = selectedIds.has(media.id); - const url = `/zen/api/media/file/${media.slug}`; return (