Le système d’aperçu d’EmDash permet aux éditeurs et éditrices de consulter du contenu non publié via des URL sécurisées et à durée limitée. Les liens d’aperçu utilisent des jetons signés HMAC-SHA256 que vous pouvez partager avec les relecteurs sans exposer l’intégralité du brouillon.
Fonctionnement
- L’administration génère une URL d’aperçu pour un article en brouillon
- L’URL contient un paramètre de requête
_previewsigné avec une date d’expiration - Le middleware EmDash vérifie automatiquement le jeton et configure le contexte de la requête
- Votre modèle appelle
getEmDashEntry()comme d’habitude — le brouillon est servi automatiquement
L’aperçu est implicite. Votre modèle n’a pas besoin de gérer les jetons ni d’options d’aperçu — le middleware et les fonctions de requête s’en chargent via AsyncLocalStorage.
Configurer l’aperçu
Ajoutez un secret d’aperçu à votre environnement :
EMDASH_PREVIEW_SECRET="your-random-secret-key-here"
Générez une chaîne aléatoire sécurisée. Ce secret signe et vérifie les jetons d’aperçu.
C’est tout. Vos modèles existants fonctionnent automatiquement avec l’aperçu :
---
import { getEmDashEntry } from "emdash";
const { slug } = Astro.params;
// Aucune logique d’aperçu spéciale — le middleware
// détecte les jetons _preview et sert le brouillon automatiquement
const { entry, isPreview, error } = await getEmDashEntry("posts", slug);
if (error) {
return new Response("Server error", { status: 500 });
}
if (!entry) {
return Astro.redirect("/404");
}
---
{isPreview && (
<div class="preview-banner">
Vous consultez un aperçu. Ce contenu n’est pas publié.
</div>
)}
<article>
<h1>{entry.data.title}</h1>
</article>
Le drapeau isPreview vaut true lorsque le brouillon est servi via un jeton d’aperçu valide.
Générer des URL d’aperçu
Utilisez getPreviewUrl() pour créer des liens d’aperçu :
import { getPreviewUrl } from "emdash";
const previewUrl = await getPreviewUrl({
collection: "posts",
id: "my-draft-post",
secret: import.meta.env.EMDASH_PREVIEW_SECRET,
expiresIn: "1h",
});
// Returns: /posts/my-draft-post?_preview=eyJjaWQ...
Avec une URL de base pour des liens absolus :
const fullUrl = await getPreviewUrl({
collection: "posts",
id: "my-draft-post",
secret: import.meta.env.EMDASH_PREVIEW_SECRET,
baseUrl: "https://example.com",
});
// Returns: https://example.com/posts/my-draft-post?_preview=eyJjaWQ...
Avec un motif de chemin personnalisé :
const blogUrl = await getPreviewUrl({
collection: "posts",
id: "my-draft-post",
secret: import.meta.env.EMDASH_PREVIEW_SECRET,
pathPattern: "/blog/{id}",
});
// Returns: /blog/my-draft-post?_preview=eyJjaWQ...
Expiration du jeton
Contrôlez la durée de validité des liens d’aperçu :
// Valide 1 heure (par défaut)
await getPreviewUrl({ ..., expiresIn: "1h" });
// Valide 30 minutes
await getPreviewUrl({ ..., expiresIn: "30m" });
// Valide 1 jour
await getPreviewUrl({ ..., expiresIn: "1d" });
// Valide 2 semaines
await getPreviewUrl({ ..., expiresIn: "2w" });
// Valide 3600 secondes
await getPreviewUrl({ ..., expiresIn: 3600 });
Unités prises en charge : s (secondes), m (minutes), h (heures), d (jours), w (semaines).
Vérifier les jetons
Utilisez verifyPreviewToken() pour valider les requêtes d’aperçu :
import { verifyPreviewToken } from "emdash";
// À partir d’une URL (extrait le paramètre _preview)
const result = await verifyPreviewToken({
url: Astro.url,
secret: import.meta.env.EMDASH_PREVIEW_SECRET,
});
// Ou avec un jeton directement
const result = await verifyPreviewToken({
token: someTokenString,
secret: import.meta.env.EMDASH_PREVIEW_SECRET,
});
Le résultat indique si le jeton est valide :
if (result.valid) {
// Jeton valide
console.log(result.payload.cid); // "posts:my-draft-post"
console.log(result.payload.exp); // Horodatage d’expiration
console.log(result.payload.iat); // Horodatage d’émission
} else {
// Jeton invalide
console.log(result.error);
// "none" — aucun jeton
// "malformed" — structure invalide
// "invalid" — échec de la signature
// "expired" — jeton expiré
}
Indicateur d’aperçu
Vous pouvez afficher un indicateur visuel lorsque le contenu est en aperçu. Le drapeau isPreview renvoyé par getEmDashEntry indique qu’un brouillon est servi :
{isPreview && (
<div class="preview-banner" role="alert">
<strong>Aperçu</strong> — Vous consultez du contenu non publié.
<a href={Astro.url.pathname}>Quitter l’aperçu</a>
</div>
)}
Fonctions utilitaires
isPreviewRequest(url)
Vérifie si une URL contient un jeton d’aperçu :
import { isPreviewRequest } from "emdash";
if (isPreviewRequest(Astro.url)) {
// Traiter la requête d’aperçu
}
getPreviewToken(url)
Extrait la chaîne du jeton depuis une URL :
import { getPreviewToken } from "emdash";
const token = getPreviewToken(Astro.url);
// Retourne la chaîne du jeton ou null
parseContentId(contentId)
Analyse un ID de contenu en collection et ID :
import { parseContentId } from "emdash";
const { collection, id } = parseContentId("posts:my-draft-post");
// { collection: "posts", id: "my-draft-post" }
Format du jeton
Les jetons d’aperçu utilisent un format compact : base64url(payload).base64url(signature)
La charge utile contient :
cid— ID de contenu au formatcollection:idexp— Horodatage d’expiration (secondes depuis l’epoch)iat— Horodatage d’émission (secondes depuis l’epoch)
Les jetons sont signés avec HMAC-SHA256 à l’aide de votre secret d’aperçu.
Exemple complet
Page d’article de blog complète avec aperçu et édition visuelle :
---
import { getEmDashEntry } from "emdash";
import BaseLayout from "../../layouts/Base.astro";
import { PortableText } from "emdash/ui";
const { slug } = Astro.params;
// Aperçu automatique — le middleware vérifie le jeton
const { entry, isPreview, error } = await getEmDashEntry("posts", slug);
if (error) {
return new Response("Server error", { status: 500 });
}
if (!entry) {
return Astro.redirect("/404");
}
---
<BaseLayout title={entry.data.title}>
{isPreview && (
<div class="preview-banner" role="alert">
<strong>Aperçu</strong> — Ce contenu n’est pas publié.
</div>
)}
<article {...entry.edit}>
<header>
<h1 {...entry.edit.title}>{entry.data.title}</h1>
{entry.data.publishedAt && (
<time datetime={entry.data.publishedAt.toISOString()}>
{entry.data.publishedAt.toLocaleDateString()}
</time>
)}
{isPreview && !entry.data.publishedAt && (
<span class="draft-indicator">Brouillon</span>
)}
</header>
<div class="content" {...entry.edit.content}>
<PortableText value={entry.data.content} />
</div>
</article>
</BaseLayout>
Les spreads {...entry.edit} et {...entry.edit.title} ajoutent les attributs data-emdash-ref pour l’édition visuelle des éditeurs authentifiés. En production, ils ne produisent aucune sortie.
Référence API
getPreviewUrl(options)
Génère une URL d’aperçu avec jeton signé.
Options :
collection— Slug de collection (string)id— ID ou slug du contenu (string)secret— Secret de signature (string)expiresIn— Durée de validité du jeton (défaut :"1h")baseUrl— URL de base optionnelle pour les liens absoluspathPattern— Motif d’URL avec{collection}et{id}(défaut :"/{collection}/{id}")
Retour : Promise<string>
verifyPreviewToken(options)
Vérifie un jeton d’aperçu.
Options :
secret— Secret de vérification (string)url— URL pour extraire le jeton, OUtoken— Chaîne du jeton directement
Retour : Promise<VerifyPreviewTokenResult>
type VerifyPreviewTokenResult =
| { valid: true; payload: PreviewTokenPayload }
| { valid: false; error: "invalid" | "expired" | "malformed" | "none" };
generatePreviewToken(options)
Génère un jeton sans construire l’URL.
Options :
contentId— ID au formatcollection:idexpiresIn— Durée de validité (défaut :"1h")secret— Secret de signature
Retour : Promise<string>