Skip to content

[Nom de la Fonctionnalité Visual Editing]

Type de ressource : Mécanisme de prévisualisation / Visual Editing
Fichier source : sanity-studio/[fichier-concerne].ts ou configuration associée
Statut : [Actif / Déprécié / En développement]
Slice : [Numéro de la tranche verticale]
Version : 1.0


[Description concise de la fonctionnalité de prévisualisation ou d’édition visuelle]

  • [Objectif principal pour les contributeurs]
  • [Bénéfice UX ou workflow]
Type de contenu Statut
post [✅/❌]
project [✅/❌]
page [✅/❌]
author [✅/❌]
category [✅/❌]
Environnement Statut
Development [✅/❌]
Staging [✅/❌]
Production [✅/❌]

[Sanity Studio]
│
▼
[Action utilisateur : ex: "Open Preview"]
│
▼
[Cloudflare Middleware Worker (L3)]
│ • Validation du secret
│ • Pose du cookie de preview
│ • Redirection
▼
[Astro (L2)]
│ • Détection du cookie/header
│ • Configuration du client Sanity
│ • Rendu SSR
▼
[Affichage du contenu draft]
Composant Rôle Couche
Sanity Studio Déclenchement de la preview L1 (CMS)
Middleware Cloudflare Validation et pose du cookie L3 (Backend)
Astro Détection et rendu du draft L2 (Frontend)
getSanityClient() Configuration conditionnelle L2 (Frontend)

Variable Description Obligatoire Valeur par défaut
SANITY_PREVIEW_SECRET Secret de validation de la preview ✅ -
SANITY_API_READ_TOKEN Token API avec droits de lecture des drafts ✅ (en preview) -
// sanity-studio/sanity.config.ts ou fichier dédié
import { defineConfig } from 'sanity';
export default defineConfig({
// ... configuration de base
document: {
productionUrl: async (prev, { document, getClient }) => {
const secret = process.env.SANITY_PREVIEW_SECRET;
const slug = document.slug?.current || '/';
return `${process.env.SITE_URL}/api/preview?secret=${secret}&slug=${slug}`;
},
},
});

3.3. Configuration Middleware Cloudflare (L3)

Section titled “3.3. Configuration Middleware Cloudflare (L3)”
// services/middleware/src/index.ts (extrait pertinent)
if (url.pathname === '/api/preview') {
const secret = url.searchParams.get('secret');
const rawSlug = url.searchParams.get('slug') || '/';
// Validation à temps constant
if (!secret || secret !== env.SANITY_PREVIEW_SECRET) {
return new Response('Jeton invalide', { status: 401 });
}
// Protection anti-open-redirect
const sanitizedSlug = (rawSlug.startsWith('/') && !rawSlug.startsWith('//'))
? rawSlug
: '/';
const response = Response.redirect(new URL(sanitizedSlug, url.origin).toString(), 307);
// Cookie de preview sécurisé
response.headers.append(
'Set-Cookie',
`sanity-preview=true; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=3600`
);
return response;
}
src/lib/sanity.ts
import { createClient } from '@sanity/client';
import type { AstroGlobal } from 'astro';
export function getSanityClient(astro: AstroGlobal) {
const isPreview =
astro.cookies.get('sanity-preview')?.value === 'true' ||
astro.request.headers.get('x-sanity-preview') === 'true';
return createClient({
projectId: import.meta.env.PUBLIC_SANITY_PROJECT_ID,
dataset: import.meta.env.PUBLIC_SANITY_DATASET,
apiVersion: '2025-01-01',
useCdn: !isPreview, // Désactive le CDN en preview
perspective: isPreview ? 'previewDrafts' : 'published',
token: isPreview
? import.meta.env.SANITY_API_READ_TOKEN
: undefined,
});
}
src/pages/api/exit-preview.astro
export const prerender = false;
Astro.cookies.delete('sanity-preview', { path: '/' });
return Astro.redirect('/');

Paramètre Type Source Validation
secret string Query parameter Comparaison à temps constant
slug string Query parameter Validation anti-open-redirect
Élément Type Description
Cookie sanity-preview string Valeur true, durée 1 heure
Redirection 307 Redirection vers le slug demandé
État Condition Comportement
Activé Cookie présent Client Sanity en mode previewDrafts
Désactivé Cookie absent Client Sanity en mode published
Erreur Secret invalide Retour 401 Unauthorized

Scénario Description Résultat attendu
Validation du secret Secret correct vs incorrect 307 Redirect vs 401 Unauthorized
Protection anti-open-redirect Slug malformé Redirection vers /
Pose du cookie Activation preview Cookie sanity-preview=true posé
Scénario Dépendances Description Résultat attendu
Flux complet Studio + Middleware + Astro Activation → Cookie → Draft affiché Contenu draft visible
Sortie du mode Route exit-preview Suppression du cookie Retour au mode published
Scénario Parcours utilisateur Description Résultat attendu
Preview depuis Studio Sanity Studio → Preview → Site Cliquer sur “Open Preview” Contenu draft affiché dans le vrai design

Composant Type Rôle Méthode d’intégration
Middleware Cloudflare L3 Validation et pose du cookie Route /api/preview
Astro L2 Détection et rendu getSanityClient() conditionnel
Sanity Studio L1 Déclenchement Bouton “Open Preview”
  • Le secret de preview ne doit jamais être exposé côté client
  • Le token de lecture des drafts est uniquement disponible en runtime
  • La documentation Starlight n’est pas concernée par ce mécanisme
  • Le secret est identique dans Cloudflare et Sanity
  • Le cookie est posé correctement
  • Le client Sanity bascule en mode previewDrafts
  • Les tests E2E passent

Mécanisme Implémentation Rationale
Validation du secret Comparaison à temps constant Évite les attaques temporelles
Cookie sécurisé HttpOnly, Secure, SameSite=Lax Protection XSS et CSRF
Durée de vie limitée Max-Age=3600 (1 heure) Limite la fenêtre d’exposition
Anti-open-redirect Validation stricte du slug Évite les redirections malveillantes
Bypass cache Edge cf.cacheEverything = false Garantit le contenu draft frais
Token Permissions Stockage
SANITY_PREVIEW_SECRET Validation de la requête de preview Cloudflare Secrets + Sanity env
SANITY_API_READ_TOKEN Lecture des drafts uniquement Cloudflare Secrets (runtime uniquement)
  • ✅ Le secret de preview n’est jamais exposé côté client
  • ✅ Le token de lecture des drafts est uniquement disponible en runtime
  • ✅ Aucune page en mode preview n’est générée en SSG
  • ✅ Les webhooks de rebuild ne sont déclenchés que sur publish

Mode useCdn Cache Edge Rendu
Production true Activé SSG
Preview false Désactivé SSR
  • ❌ Le mode preview désactive le CDN Sanity (impact sur les performances)
  • ❌ Le mode preview force le bypass du cache Edge Cloudflare
  • ❌ Les pages en preview sont rendues en SSR (plus lent que SSG)

Symptôme Cause probable Solution
“Jeton de prévisualisation invalide” Secret incorrect ou manquant Vérifier SANITY_PREVIEW_SECRET
Page blanche en preview Token de lecture des drafts manquant Vérifier SANITY_API_READ_TOKEN
Contenu publié affiché au lieu du draft Cookie non détecté Vérifier que le navigateur accepte les cookies
Redirection infinie Slug malformé ou open-redirect bloqué Vérifier le format du slug
Terminal window
# Test de validation du secret
curl -v "https://votresite.com/api/preview?secret=INVALID&slug=/"
# Attendu : 401 Unauthorized
# Test avec secret valide
curl -v "https://votresite.com/api/preview?secret=VALID_SECRET&slug=/blog/test"
# Attendu : 307 Redirect + Set-Cookie

  • ✅ Toujours utiliser la comparaison à temps constant pour valider les secrets
  • ✅ Ne jamais logger les secrets ou tokens dans les logs
  • ✅ Tester le mécanisme de preview en staging avant production
  • ✅ Utiliser la preview systématiquement avant publication
  • ✅ Vérifier le rendu sur différents appareils (responsive)
  • ✅ Ne pas partager l’URL de preview (elle contient le secret)
  • ✅ Se déconnecter de la preview après utilisation (/api/exit-preview)

  • [Évolution 1 : ex. Intégration de Sanity Visual Editing (overlays)]
  • [Évolution 2 : ex. Support du mode preview pour la documentation Starlight]
  • ❌ La documentation Starlight n’est pas compatible avec ce mécanisme (Git-based)
  • ❌ Le mode preview ne fonctionne pas avec le SSG (SSR uniquement)
  • ❌ Les médias uploadés en draft ne sont pas accessibles publiquement avant publication

  • Documentation Sanity : Preview Content
  • Documentation Astro : Draft Mode
  • DAT L1 : §4.7 - Stratégie de Preview (Draft Mode)
  • DAT L2 : §5.3 - Gestion du mode Preview
  • DAT L3 : §3.1 - Middleware Global

Date Version Description Auteur
YYYY-MM-DD 1.0 Création initiale [Nom]
Edit page

Last updated: