[Nom de la Ressource de Stockage Cloudflare]
Type de ressource : Bucket Cloudflare R2 / Bibliothèque Cloudflare Stream
Fichier source :services/[worker-name]/wrangler.tomlou configuration associée
Statut : [Actif / Déprécié / En développement]
1. Vue d’ensemble
Section titled “1. Vue d’ensemble”1.1. Description
Section titled “1.1. Description”[Description concise du rôle de cette ressource de stockage dans l’architecture du site]
1.2. Objectif
Section titled “1.2. Objectif”- [Objectif principal : ex. stocker les images du blog et du portfolio]
- [Bénéfice pour l’architecture : ex. découplage des médias du CMS Sanity]
1.3. Périmètre concerné
Section titled “1.3. Périmètre concerné”- Type de médias : [Images / Vidéos / Documents / Backups / Les deux]
- Accès : [Public / Privé / URLs signées / Les deux]
- Consommateurs : [Sanity / Astro / Workers L3 / Les deux]
- Optimisation : [Cloudflare Polish / Cloudflare Images / Cloudflare Stream / Aucune]
2. Architecture et flux
Section titled “2. Architecture et flux”2.1. Diagramme de flux
Section titled “2.1. Diagramme de flux”[Sanity Studio] │ (upload via Worker L3) ▼[Worker media-upload (L3)] │ • Génération URL présignée R2 │ • Ou Direct Upload Token Stream ▼[Cloudflare R2 / Stream] │ • Stockage structuré │ • Optimisation Edge ▼[Sanity] │ • Stocke uniquement URL + métadonnées ▼[Astro / Site] │ • Consomme les URLs R2/Stream ▼[Utilisateur final]2.2. Composants impliqués
Section titled “2.2. Composants impliqués”| Composant | Rôle | Couche |
|---|---|---|
| Bucket R2 / Bibliothèque Stream | Stockage des médias | Infrastructure |
Worker media-upload |
Génération d’URLs présignées | L3 |
Worker media-cleanup |
Nettoyage des orphelins | L3 |
Worker sanity-backup |
Backups vers R2 | L3 |
| Sanity | Stockage des métadonnées + URLs | L1 |
| Astro | Consommation des URLs | L2 |
| Cloudflare CDN | Distribution Edge | Infrastructure |
3. Configuration du bucket
Section titled “3. Configuration du bucket”3.1. Variables d’environnement requises
Section titled “3.1. Variables d’environnement requises”| Variable | Description | Obligatoire | Valeur par défaut |
|---|---|---|---|
R2_ACCOUNT_ID |
ID du compte Cloudflare | ✅ | - |
R2_BUCKET_NAME |
Nom du bucket (ex: pub-medias) |
✅ | - |
R2_ACCESS_KEY_ID |
Clé d’accès API R2 | ✅ | - |
R2_SECRET_ACCESS_KEY |
Clé secrète API R2 | ✅ | - |
R2_PUBLIC_DOMAIN |
Domaine public (ex: pub-medias.votredomaine.com) |
✅ | - |
3.2. Configuration Wrangler (wrangler.toml)
Section titled “3.2. Configuration Wrangler (wrangler.toml)”name = "media-upload"main = "src/index.ts"compatibility_date = "2026-09-01"
[[r2_buckets]]binding = "R2_BUCKET"bucket_name = "pub-medias"preview_bucket_name = "pub-medias-staging"
[vars]R2_ACCOUNT_ID = "your-account-id"R2_BUCKET_NAME = "pub-medias"R2_PUBLIC_DOMAIN = "pub-medias.votredomaine.com"
[env.production]vars = { R2_BUCKET_NAME = "pub-medias" }
[env.staging]vars = { R2_BUCKET_NAME = "pub-medias-staging" }3.3. Injection des secrets
Section titled “3.3. Injection des secrets”# Injection des credentials R2 via Wrangler CLIwrangler secret put R2_ACCESS_KEY_ID --env productionwrangler secret put R2_SECRET_ACCESS_KEY --env production
wrangler secret put R2_ACCESS_KEY_ID --env stagingwrangler secret put R2_SECRET_ACCESS_KEY --env staging3.4. Configuration du domaine public
Section titled “3.4. Configuration du domaine public”Dans le Dashboard Cloudflare :
- R2 → Bucket
pub-medias→ Settings - Custom domains → Connect Domain
- Domaine :
pub-medias.votredomaine.com - Cloudflare proxy activé (orange cloud)
4. Organisation des chemins
Section titled “4. Organisation des chemins”4.1. Structure des dossiers
Section titled “4.1. Structure des dossiers”pub-medias/├── medias/│ ├── images/│ │ ├── blog/ # Images des articles de blog│ │ │ └── 2026-09-03-article-title-image.webp│ │ ├── portfolio/ # Images des projets portfolio│ │ │ └── project-name-gallery-01.webp│ │ └── pages/ # Images des pages vitrine│ │ └── hero-background.webp│ ├── videos/ # Fichiers sources (si besoin)│ │ └── raw-video.mp4│ ├── documents/│ │ ├── pdf/ # Documents PDF│ │ │ └── whitepaper-2026.pdf│ │ └── autres/ # Autres documents│ │ └── presentation.pptx│ └── temp/ # Uploads temporaires (nettoyage auto)│ └── upload-123456.jpg└── backups/ └── sanity/ # Backups Sanity └── backup-2026-09-03.tar.gz4.2. Convention de nommage
Section titled “4.2. Convention de nommage”| Type | Format | Exemple |
|---|---|---|
| Images blog | YYYY-MM-DD-slug-image.webp |
2026-09-03-mon-article-hero.webp |
| Images portfolio | project-name-gallery-XX.webp |
projet-alpha-gallery-01.webp |
| Images pages | page-slug-block-image.webp |
accueil-hero-background.webp |
| Documents | document-name-YYYY.pdf |
whitepaper-2026.pdf |
| Temp | upload-{timestamp}-{filename} |
upload-1693735200-image.jpg |
| Backups | backup-YYYY-MM-DD.tar.gz |
backup-2026-09-03.tar.gz |
4.3. Règles d’organisation
Section titled “4.3. Règles d’organisation”- ✅ Toujours préfixer par
medias/pour les médias du site - ✅ Séparer par type (images, videos, documents)
- ✅ Séparer par contexte (blog, portfolio, pages)
- ✅ Utiliser des noms de fichiers descriptifs et en kebab-case
- ✅ Inclure la date dans le nom pour les articles de blog
- ✅ Le dossier
temp/est réservé aux uploads temporaires
5. Types de médias et traitement
Section titled “5. Types de médias et traitement”5.1. Images
Section titled “5.1. Images”| Aspect | Configuration | Rationale |
|---|---|---|
| Formats | WebP, AVIF (auto via Cloudflare) | Performance optimale |
| Optimisation | Cloudflare Polish (lossy/lossless) | Réduction de taille automatique |
| Redimensionnement | Cloudflare Images (si besoin) | Transformation à la volée |
| Dimensions max | 1920x1080 (contenu), 3840x2160 (hero) | Équilibre qualité/performance |
| Poids max | 500 KB (contenu), 2 MB (hero) | Performance de chargement |
| Métadonnées | Alt, hotspot, dimensions dans Sanity | Accessibilité + cropping intelligent |
URL publique :
https://pub-medias.votredomaine.com/medias/images/blog/2026-09-03-article-image.webpURL avec transformation Cloudflare Images :
https://pub-medias.votredomaine.com/cdn-cgi/image/width=800,height=600,fit=cover/medias/images/blog/2026-09-03-article-image.webp5.2. Vidéos
Section titled “5.2. Vidéos”| Aspect | Configuration | Rationale |
|---|---|---|
| Stockage | Cloudflare Stream (pas R2) | Transcoding + adaptive bitrate |
| Upload | Direct Upload Token via Worker L3 | Évite de transiter par le Worker |
| Stockage métadonnées | UID Stream dans Sanity | Référence vers la vidéo |
| Lecture | Iframe Stream ou SDK officiel | Player intégré |
| Durée max | 3600 secondes (1 heure) | Limite Stream |
UID Stream stocké dans Sanity :
{ "_type": "video", "streamUid": "abc123def456", "title": "Présentation du projet"}Embed dans Astro :
<iframe src="https://iframe.videodelivery.net/abc123def456" allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture" allowfullscreen></iframe>5.3. Documents (PDF, etc.)
Section titled “5.3. Documents (PDF, etc.)”| Aspect | Configuration | Rationale |
|---|---|---|
| Stockage | Cloudflare R2 | Simplicité + performance |
| Accès | URL publique ou signée | Selon sensibilité |
| Prévisualisation | Miniature générée par Worker (optionnel) | UX améliorée |
| Poids max | 10 MB | Limite raisonnable |
URL publique :
https://pub-medias.votredomaine.com/medias/documents/pdf/whitepaper-2026.pdfURL signée (si sensible) :
https://pub-medias.votredomaine.com/medias/documents/pdf/confidentiel.pdf?X-Amz-Algorithm=...5.4. Backups Sanity
Section titled “5.4. Backups Sanity”| Aspect | Configuration | Rationale |
|---|---|---|
| Stockage | Bucket R2 dédié (backups-sanity) |
Isolation des backups |
| Fréquence | Quotidienne (Cron: 0 2 * * *) |
RPO de 24h |
| Format | .tar.gz |
Compression efficace |
| Rétention | 30 jours (Lifecycle Rules R2) | Équilibre coût/sécurité |
| Streaming | Pipe direct Sanity → R2 (pas de RAM) | Évite OOM |
Chemin de backup :
backups-sanity/backups/sanity/backup-2026-09-03.tar.gz6. Upload et génération d’URLs présignées
Section titled “6. Upload et génération d’URLs présignées”6.1. Flux d’upload (Worker media-upload)
Section titled “6.1. Flux d’upload (Worker media-upload)”// services/media-upload/src/index.ts (extrait)import { AwsClient } from 'aws4fetch';
export interface Env { R2_BUCKET: R2Bucket; R2_ACCOUNT_ID: string; R2_ACCESS_KEY_ID: string; R2_SECRET_ACCESS_KEY: string; R2_BUCKET_NAME: string; CLOUDFLARE_STREAM_API_TOKEN: string; CLOUDFLARE_ACCOUNT_ID: string;}
export default { async fetch(request: Request, env: Env): Promise<Response> { const { type, filename, contentType } = await request.json();
// Case 1: Upload vidéo via Cloudflare Stream Direct Upload if (type === 'video') { const streamRes = await fetch( `https://api.cloudflare.com/client/v4/accounts/${env.CLOUDFLARE_ACCOUNT_ID}/stream/direct_upload`, { method: 'POST', headers: { 'Authorization': `Bearer ${env.CLOUDFLARE_STREAM_API_TOKEN}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ maxDurationSeconds: 3600 }), } ); const streamData = await streamRes.json(); return new Response(JSON.stringify({ uploadUrl: streamData.result.uploadURL, uid: streamData.result.uid }), { status: 200 }); }
// Case 2: Upload images/documents via URL présignée R2 const aws = new AwsClient({ accessKeyId: env.R2_ACCESS_KEY_ID, secretAccessKey: env.R2_SECRET_ACCESS_KEY, service: 's3', region: 'auto', });
const key = `medias/${type}s/${Date.now()}-${filename}`; const url = new URL(`https://${env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com/${env.R2_BUCKET_NAME}/${key}`); url.searchParams.set('X-Amz-Expires', '900'); // Valide 15 minutes
const signedRequest = await aws.sign(new Request(url.toString(), { method: 'PUT', headers: { 'Content-Type': contentType }, }), { aws: { signQuery: true } });
return new Response(JSON.stringify({ uploadUrl: signedRequest.url, fileUrl: `https://${env.R2_PUBLIC_DOMAIN}/${key}`, key, }), { status: 200 }); },};6.2. Utilisation depuis Sanity Studio
Section titled “6.2. Utilisation depuis Sanity Studio”const handleUpload = async (file: File) => { const res = await fetch('https://api.votredomaine.com/media-upload', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: file.type.startsWith('image/') ? 'image' : 'document', filename: file.name, contentType: file.type, }), });
const { uploadUrl, fileUrl, key } = await res.json();
// Upload direct vers R2 (sans transiter par le Worker) await fetch(uploadUrl, { method: 'PUT', headers: { 'Content-Type': file.type }, body: file, });
// Retourne l'URL publique à Sanity return { asset: { _type: 'file', url: fileUrl, metadata: { key, originalFilename: file.name, }, }, };};7. Nettoyage et garbage collection
Section titled “7. Nettoyage et garbage collection”7.1. Worker media-cleanup (Cron hebdomadaire)
Section titled “7.1. Worker media-cleanup (Cron hebdomadaire)”Spécifications :
- Fréquence :
0 3 * * 0(tous les dimanches à 3h00) - Objectif : Supprimer les fichiers orphelins (non référencés dans Sanity depuis 30 jours)
- Safety Locks :
- Annulation si Sanity renvoie moins de 5 documents (protection panne API)
- Annulation si suppression > 10% du bucket (protection contre erreur massive)
Code de production :
// services/media-cleanup/src/index.ts (extrait)export default { async scheduled(event: ScheduledEvent, env: Env): Promise<void> { // 1. Requête GROQ pour récupérer toutes les URLs référencées const query = encodeURIComponent(`*[_type in ["post", "project", "page"]]{ "images": [mainImage.asset->url, gallery[].asset->url, pageBuilder[].images[].asset->url] }`);
const sanityRes = await fetch( `https://${env.SANITY_PROJECT_ID}.api.sanity.io/v2025-01-01/data/query/${env.SANITY_DATASET}?query=${query}`, { headers: { Authorization: `Bearer ${env.SANITY_READ_TOKEN}` } } );
const sanityData = await sanityRes.json(); const referencedUrls = new Set<string>(); sanityData.result.forEach(doc => { doc.images?.flat().filter(Boolean).forEach(url => referencedUrls.add(url)); });
// Safety Lock #1 if (referencedUrls.size < 5) { console.error('SAFETY LOCK : Volume Sanity trop faible. Annulation.'); return; }
// 2. Listing des objets R2 const objects = await env.R2_BUCKET.list({ prefix: 'medias/' }); const toDelete: string[] = []; const now = Date.now(); const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
for (const obj of objects.objects) { const publicUrl = `https://${env.R2_PUBLIC_DOMAIN}/${obj.key}`; const isOrphan = !referencedUrls.has(publicUrl); const isOlderThan30Days = (now - obj.uploaded.getTime()) > THIRTY_DAYS_MS;
if (isOrphan && isOlderThan30Days) { toDelete.push(obj.key); } }
// Safety Lock #2 if (toDelete.length > (objects.objects.length * 0.10)) { console.error(`SAFETY LOCK : Suppression > 10%. Annulation.`); return; }
// 3. Suppression for (const key of toDelete) { await env.R2_BUCKET.delete(key); } },};7.2. Nettoyage du dossier temp/
Section titled “7.2. Nettoyage du dossier temp/”- Fréquence : Quotidienne (Cron:
0 4 * * *) - Rétention : 7 jours
- Méthode : Lifecycle Rules natives de R2 ou Worker dédié
7.3. Lifecycle Rules R2
Section titled “7.3. Lifecycle Rules R2”| Règle | Condition | Action |
|---|---|---|
Suppression temp/ |
Fichiers > 7 jours | Delete |
Archivage backups/ |
Fichiers > 30 jours | Delete |
Transition videos/raw/ |
Fichiers > 90 jours | Delete (après transcoding Stream) |
8. Sécurité et accès
Section titled “8. Sécurité et accès”8.1. Types d’accès
Section titled “8.1. Types d’accès”| Type | Usage | Configuration |
|---|---|---|
| Public | Images du site, documents non sensibles | Domaine public R2 + CDN Cloudflare |
| Signé | Documents sensibles, previews | URLs présignées avec expiration |
| Privé | Backups, fichiers internes | Accès via Worker uniquement |
8.2. Génération d’URLs signées
Section titled “8.2. Génération d’URLs signées”// Exemple de génération d'URL signée (valide 1 heure)const aws = new AwsClient({ accessKeyId: env.R2_ACCESS_KEY_ID, secretAccessKey: env.R2_SECRET_ACCESS_KEY, service: 's3', region: 'auto',});
const url = new URL(`https://${env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com/${env.R2_BUCKET_NAME}/${key}`);url.searchParams.set('X-Amz-Expires', '3600'); // 1 heure
const signedRequest = await aws.sign(new Request(url.toString(), { method: 'GET',}), { aws: { signQuery: true } });
return signedRequest.url;8.3. Règles de sécurité
Section titled “8.3. Règles de sécurité”- ✅ Les credentials R2 ne sont jamais exposés côté client
- ✅ Les credentials sont injectés via
wrangler secret put - ✅ Les URLs signées ont une durée de vie limitée (1h à 24h max)
- ✅ Le domaine public R2 est protégé par le proxy Cloudflare (WAF, DDoS)
- ✅ Les buckets de backups sont isolés des buckets publics
- ✅ Les uploads temporaires sont automatiquement nettoyés
8.4. CORS (si nécessaire)
Section titled “8.4. CORS (si nécessaire)”# Configuration CORS dans le Dashboard R2[cors]allowed_origins = ["https://votresite.com", "https://studio.votresite.com"]allowed_methods = ["GET", "PUT", "POST"]allowed_headers = ["*"]max_age = 36009. Intégration avec Sanity
Section titled “9. Intégration avec Sanity”9.1. Stockage des métadonnées
Section titled “9.1. Stockage des métadonnées”Sanity ne stocke que les métadonnées et les URLs R2/Stream :
{ "_type": "image", "asset": { "_type": "reference", "_ref": "image-abc123" }, "url": "https://pub-medias.votredomaine.com/medias/images/blog/2026-09-03-article.webp", "metadata": { "key": "medias/images/blog/2026-09-03-article.webp", "dimensions": { "width": 1920, "height": 1080 }, "alt": "Description de l'image pour l'accessibilité", "hotspot": { "x": 0.5, "y": 0.5, "width": 0.5, "height": 0.5 } }}9.2. Champs personnalisés dans Sanity
Section titled “9.2. Champs personnalisés dans Sanity”export const customImageField = { name: 'mainImage', title: 'Image principale', type: 'image', options: { hotspot: true }, fields: [ { name: 'alt', title: 'Texte alternatif', type: 'string', validation: Rule => Rule.required(), }, { name: 'r2Key', title: 'Clé R2', type: 'string', readOnly: true, description: 'Chemin du fichier dans R2 (généré automatiquement)', }, ],};9.3. Requêtes GROQ pour les médias
Section titled “9.3. Requêtes GROQ pour les médias”*[_type == "post"]{ title, mainImage { url, metadata { dimensions, alt, hotspot } }, gallery[] { url, metadata { dimensions, alt } }}10. Intégration avec Astro
Section titled “10. Intégration avec Astro”10.1. Consommation des URLs R2
Section titled “10.1. Consommation des URLs R2”---import { getSanityClient } from '../../lib/sanity';
const post = await getSanityClient(Astro).fetch(`*[_type == "post" && slug.current == $slug][0]{ title, mainImage { url, metadata { dimensions, alt } }}`, { slug: Astro.params.slug });---
<img src={post.mainImage.url} alt={post.mainImage.metadata.alt} width={post.mainImage.metadata.dimensions.width} height={post.mainImage.metadata.dimensions.height} loading="lazy" decoding="async"/>10.2. Utilisation de Cloudflare Images (transformation à la volée)
Section titled “10.2. Utilisation de Cloudflare Images (transformation à la volée)”---const imageUrl = post.mainImage.url;const transformedUrl = `https://pub-medias.votredomaine.com/cdn-cgi/image/width=800,height=600,fit=cover,format=webp/${imageUrl.replace('https://pub-medias.votredomaine.com/', '')}`;---
<img src={transformedUrl} alt={post.mainImage.metadata.alt} width="800" height="600" loading="lazy"/>10.3. Composant Image réutilisable
Section titled “10.3. Composant Image réutilisable”---interface Props { src: string; alt: string; width?: number; height?: number; loading?: 'lazy' | 'eager'; sizes?: string;}
const { src, alt, width, height, loading = 'lazy', sizes } = Astro.props;
// Génération automatique du srcset pour le responsiveconst srcset = width ? [ `${transformUrl(src, width * 2)} 2x`, `${transformUrl(src, width)} 1x`, `${transformUrl(src, width / 2)} 0.5x`, ].join(', ') : undefined;---
<img src={src} srcset={srcset} sizes={sizes || '(max-width: 768px) 100vw, 50vw'} alt={alt} width={width} height={height} loading={loading} decoding="async"/>11. Performance et optimisation
Section titled “11. Performance et optimisation”11.1. Optimisation des images
Section titled “11.1. Optimisation des images”| Technique | Implémentation | Gain |
|---|---|---|
| Cloudflare Polish | Activé sur la zone | -30% à -50% de taille |
| Cloudflare Images | Transformation à la volée | Adaptation automatique |
| Formats modernes | WebP/AVIF auto | -25% à -40% vs JPEG |
| Lazy loading | loading="lazy" |
Chargement différé |
| Srcset responsive | srcset + sizes |
Adaptation à l’écran |
| Dimensions explicites | width + height |
Évite le CLS |
11.2. Cache Edge Cloudflare
Section titled “11.2. Cache Edge Cloudflare”| Type de fichier | Cache-Control | TTL |
|---|---|---|
| Images | public, max-age=31536000, immutable |
1 an |
| Vidéos Stream | Géré par Stream | N/A |
| Documents PDF | public, max-age=86400 |
1 jour |
| Backups | private, no-cache |
0 (accès Worker uniquement) |
11.3. Bonnes pratiques
Section titled “11.3. Bonnes pratiques”- ✅ Toujours fournir
width,heightetalt - ✅ Utiliser
loading="lazy"sauf pour les images LCP (Hero) - ✅ Générer des
srcset/sizespour le responsive - ✅ Optimiser les images avant upload (max 500 KB)
- ✅ Utiliser WebP/AVIF (conversion automatique via Cloudflare)
- ✅ Ne jamais servir de vidéos depuis R2 (utiliser Stream)
12. Débogage et troubleshooting
Section titled “12. Débogage et troubleshooting”12.1. Problèmes courants
Section titled “12.1. Problèmes courants”| Symptôme | Cause probable | Solution |
|---|---|---|
| Image 404 | Clé R2 incorrecte | Vérifier le chemin dans Sanity et R2 |
| Upload échoue | URL présignée expirée | Régénérer l’URL (valide 15 min) |
| CORS error | Domaine non autorisé | Ajouter le domaine dans la config CORS R2 |
| Image non optimisée | Polish non activé | Activer Polish dans le Dashboard Cloudflare |
| CLS (Cumulative Layout Shift) | Dimensions manquantes | Toujours fournir width + height |
| Vidéo ne lit pas | UID Stream incorrect | Vérifier l’UID stocké dans Sanity |
12.2. Logs et monitoring
Section titled “12.2. Logs et monitoring”# Vérifier les objets dans R2 via Wrangler CLIwrangler r2 object list pub-medias --prefix medias/images/blog/
# Vérifier les logs du Worker media-cleanupwrangler tail media-cleanup
# Vérifier les logs du Worker media-uploadwrangler tail media-upload12.3. Tests manuels
Section titled “12.3. Tests manuels”# Test d'upload via URL présignéecurl -X PUT "https://[signed-url]" \ -H "Content-Type: image/webp" \ --data-binary @test.webp
# Test d'accès publiccurl -I "https://pub-medias.votredomaine.com/medias/images/blog/test.webp"# Attendu : 200 OK + headers de cache
# Test d'accès signécurl -I "https://[signed-url]"# Attendu : 200 OK (si URL valide)13. Bonnes pratiques
Section titled “13. Bonnes pratiques”13.1. Pour les développeurs
Section titled “13.1. Pour les développeurs”- ✅ Toujours utiliser des URLs présignées pour les uploads (pas de transit par Worker)
- ✅ Valider les types de fichiers avant upload (images, vidéos, documents uniquement)
- ✅ Logger tous les uploads et suppressions pour l’audit
- ✅ Tester les safety locks du Worker
media-cleanupen staging - ✅ Documenter les changements de configuration dans ce fichier
13.2. Pour les contributeurs
Section titled “13.2. Pour les contributeurs”- ✅ Optimiser les images avant upload (max 500 KB, dimensions adaptées)
- ✅ Toujours fournir un texte alternatif descriptif
- ✅ Utiliser des noms de fichiers descriptifs (pas
IMG_1234.jpg) - ✅ Ne pas uploader de vidéos directement (passer par Cloudflare Stream)
- ✅ Vérifier le rendu responsive avant publication
13.3. Considérations de coût
Section titled “13.3. Considérations de coût”- ⚠️ R2 facture le stockage + les opérations (lectures/écritures)
- ⚠️ Stream facture le stockage + le transcoding + la lecture
- ✅ Nettoyer régulièrement les orphelins (Worker
media-cleanup) - ✅ Utiliser les Lifecycle Rules pour supprimer automatiquement les vieux fichiers
- ✅ Optimiser les images pour réduire le stockage et la bande passante
14. Évolutions et roadmap
Section titled “14. Évolutions et roadmap”14.1. Améliorations planifiées
Section titled “14.1. Améliorations planifiées”- [Évolution 1 : ex. Génération automatique de miniatures pour les PDF]
- [Évolution 2 : ex. Support du format AVIF natif]
- [Évolution 3 : ex. Dashboard d’audit des médias orphelins]
14.2. Limitations connues
Section titled “14.2. Limitations connues”- ❌ R2 ne supporte pas nativement la transformation d’images (nécessite Cloudflare Images)
- ❌ Les URLs signées ont une durée de vie limitée (pas de partage long terme)
- ❌ Le nettoyage des orphelins peut être lent avec beaucoup de fichiers (> 10 000)
15. Ressources liées
Section titled “15. Ressources liées”- Documentation Cloudflare R2 : Getting Started
- Documentation Cloudflare Stream : Getting Started
- Documentation Cloudflare Images : Getting Started
- DAT L1 : §4.8 - Gestion des Médias (Cloudflare R2 + Stream)
- DAT L2 : §5.4 - Consommation et optimisation des médias
- DAT L3 : §5.3 - Worker
media-upload - DAT L3 : §5.4 - Worker
media-cleanup - DAT L3 : §5.5 - Worker
sanity-backup - Fichiers concernés :
services/media-upload/wrangler.tomlservices/media-cleanup/wrangler.tomlservices/sanity-backup/wrangler.toml- Configuration Dashboard Cloudflare → R2 → Buckets
16. Historique des modifications
Section titled “16. Historique des modifications”| Date | Version | Description | Auteur |
|---|---|---|---|
| YYYY-MM-DD | 1.0 | Création initiale | [Nom] |
| YYYY-MM-DD | 1.1 | [Modification] | [Nom] |
---
**Points clés de ce template :**- Documente l'architecture complète de stockage (R2 + Stream)- Détaille l'organisation des chemins et conventions de nommage- Inclut les mécanismes d'upload (URLs présignées, Direct Upload Stream)- Explique le nettoyage automatique (orphelins, temp, backups)- Fournit l'intégration avec Sanity (métadonnées) et Astro (consommation)- Couvre la sécurité (accès public/signé/privé, CORS)- Inclut les bonnes pratiques de performance et d'optimisation