Le système d’aperçu d’EmDash permet aux éditeurs de visualiser le contenu non publié via des URLs sécurisées à durée limitée. Les liens d’aperçu utilisent des tokens signés HMAC-SHA256 que vous pouvez partager avec des réviseurs sans exposer l’intégralité de votre contenu brouillon.
Comment ça Marche
- L’administrateur génère une URL d’aperçu pour un article brouillon
- L’URL contient un paramètre de requête
_previewsigné avec un temps d’expiration - Le middleware d’EmDash vérifie automatiquement le token et configure le contexte de requête
- Votre code de template appelle
getEmDashEntry()normalement — le contenu brouillon est servi automatiquement
L’aperçu est implicite. Le middleware vérifie le token et les fonctions de requête le lisent via AsyncLocalStorage, de sorte que le même code de template sert le contenu brouillon pendant un aperçu et le contenu publié autrement.
Configuration de l’Aperçu
L’aperçu fonctionne dès qu’EmDash est installé. À la première utilisation, EmDash génère un secret d’aperçu par site et le stocke dans la base de données, donc le cas commun ne nécessite aucune configuration.
Définissez EMDASH_PREVIEW_SECRET dans votre environnement uniquement si vous devez :
- Partager le secret entre plusieurs processus (par ex. un Worker d’aperçu séparé qui signe les URLs et les envoie à votre site principal pour vérification)
- Fixer le secret à une valeur que vous contrôlez pour des raisons de conformité/audit
- Migrer vers une valeur connue lors de la restauration à partir d’une sauvegarde
# Optionnel : remplace le secret généré automatiquement
EMDASH_PREVIEW_SECRET="your-random-secret-key-here"
Si défini, la valeur d’environnement prime sur la valeur stockée en BD.
Les templates existants fonctionnent automatiquement avec l’aperçu, comme dans la page suivante :
---
import { getEmDashEntry } from "emdash";
const { slug } = Astro.params;
// Aucune gestion spéciale d'aperçu nécessaire — le middleware
// détecte les tokens _preview et sert le contenu 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 visualisez un aperçu. Ce contenu n'est pas publié.
</div>
)}
<article>
<h1>{entry.data.title}</h1>
</article>
Le drapeau isPreview est true lorsque le contenu brouillon est servi via un token d’aperçu valide.
Génération d’URLs d’Aperçu
Utilisez getPreviewUrl() pour créer des liens d’aperçu. La fonction prend le secret comme argument explicite :
import { getPreviewUrl } from "emdash";
const previewUrl = await getPreviewUrl({
collection: "posts",
id: "my-draft-post",
secret: import.meta.env.EMDASH_PREVIEW_SECRET,
expiresIn: "1h",
});
// Retourne : /posts/my-draft-post?_preview=eyJjaWQ...
Lorsque EMDASH_PREVIEW_SECRET n’est pas défini, EmDash génère et stocke automatiquement un secret par site dans la base de données pour la vérification des tokens. L’helper de template getPreviewUrl() nécessite toujours que vous passiez le secret explicitement — fixez votre variable d’environnement si vous l’appelez depuis des templates de page. La plupart des sites utilisent plutôt le bouton “Générer un lien d’aperçu” de l’interface d’administration, qui passe par l’API et utilise automatiquement le secret résolu.
Passez baseUrl pour générer une URL absolue :
const fullUrl = await getPreviewUrl({
collection: "posts",
id: "my-draft-post",
secret: import.meta.env.EMDASH_PREVIEW_SECRET,
baseUrl: "https://example.com",
});
// Retourne : https://example.com/posts/my-draft-post?_preview=eyJjaWQ...
Passez pathPattern pour générer une URL avec un chemin personnalisé :
const blogUrl = await getPreviewUrl({
collection: "posts",
id: "my-draft-post",
secret: import.meta.env.EMDASH_PREVIEW_SECRET,
pathPattern: "/blog/{id}",
});
// Retourne : /blog/my-draft-post?_preview=eyJjaWQ...
Chemins conscients de la locale
pathPattern prend également en charge un espace réservé {locale}. Passez une locale vide lorsque l’entrée est dans la locale par défaut et que prefixDefaultLocale est false ; les barres obliques adjacentes laissées par la valeur vide sont automatiquement regroupées.
L’exemple suivant construit une URL d’aperçu préfixée par locale :
await getPreviewUrl({
collection: "posts",
id: "hello",
secret,
pathPattern: "/{locale}/{id}",
locale: "pt-br",
});
// Retourne : /pt-br/hello?_preview=...
await getPreviewUrl({
collection: "posts",
id: "hello",
secret,
pathPattern: "/{locale}/{id}",
locale: "", // locale par défaut, pas de préfixe
});
// Retourne : /hello?_preview=...
Le lien “Voir sur le site” de l’administrateur passe par POST /_emdash/api/content/{collection}/{id}/preview-url, qui lit la locale de l’entrée, consulte la configuration i18n du site et fournit la locale automatiquement. Pour changer le modèle par défaut utilisé par ce point de terminaison, définissez EMDASH_PREVIEW_PATH_PATTERN (par ex. /{locale}/{id}) — les corps de requête l’emportent toujours lorsqu’ils incluent leur propre pathPattern.
Expiration des Tokens
Contrôlez la durée de validité des liens d’aperçu :
// Valide pendant 1 heure (par défaut)
await getPreviewUrl({ ..., expiresIn: "1h" });
// Valide pendant 30 minutes
await getPreviewUrl({ ..., expiresIn: "30m" });
// Valide pendant 1 jour
await getPreviewUrl({ ..., expiresIn: "1d" });
// Valide pendant 2 semaines
await getPreviewUrl({ ..., expiresIn: "2w" });
// Valide pendant 3600 secondes
await getPreviewUrl({ ..., expiresIn: 3600 });
Unités prises en charge : s (secondes), m (minutes), h (heures), d (jours), w (semaines).
Vérification des Tokens
Utilisez verifyPreviewToken() pour valider les requêtes d’aperçu entrantes :
import { verifyPreviewToken } from "emdash";
// Depuis une URL (extrait le paramètre de requête _preview)
const result = await verifyPreviewToken({
url: Astro.url,
secret: import.meta.env.EMDASH_PREVIEW_SECRET,
});
// Ou avec un token directement
const result = await verifyPreviewToken({
token: someTokenString,
secret: import.meta.env.EMDASH_PREVIEW_SECRET,
});
Le résultat indique si le token est valide :
if (result.valid) {
// Le token est 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 {
// Le token est invalide
console.log(result.error);
// "none" - aucun token présent
// "malformed" - structure du token invalide
// "invalid" - échec de la vérification de signature
// "expired" - le token a expiré
}
Indicateur d’Aperçu
Vous pouvez afficher un indicateur visuel lorsque le contenu est en cours d’aperçu. Le drapeau isPreview retourné par getEmDashEntry vous indique quand le contenu brouillon est servi :
{isPreview && (
<div class="preview-banner" role="alert">
<strong>Aperçu</strong> — Vous visualisez du contenu non publié.
<a href={Astro.url.pathname}>Quitter l'aperçu</a>
</div>
)}
Fonctions d’Aide
isPreviewRequest(url)
Vérifiez si une URL contient un token d’aperçu :
import { isPreviewRequest } from "emdash";
if (isPreviewRequest(Astro.url)) {
// Gérer la requête d'aperçu
}
getPreviewToken(url)
Extrayez la chaîne de token d’une URL :
import { getPreviewToken } from "emdash";
const token = getPreviewToken(Astro.url);
// Retourne la chaîne de token ou null
parseContentId(contentId)
Analysez 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" }
Sécurité des Tokens
Les tokens d’aperçu sont signés et limités dans le temps. La CLI et les fonctions d’aide les génèrent et les vérifient pour vous ; vous ne les construisez ni ne les analysez à la main. Un token identifie une entrée et cesse de fonctionner après son expiration.
Exemple Complet
La page suivante combine le support de l’aperçu et de l’édition visuelle dans un template complet d’article de blog :
---
import { getEmDashEntry } from "emdash";
import BaseLayout from "../../layouts/Base.astro";
import { PortableText } from "emdash/ui";
const { slug } = Astro.params;
// L'aperçu est automatique — le middleware gère la vérification des tokens
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>
Notez les spreads {...entry.edit} et {...entry.edit.title} — ceux-ci ajoutent des attributs data-emdash-ref qui activent l’édition visuelle pour les éditeurs authentifiés. En production, ils ne produisent aucune sortie.
Référence API
getPreviewUrl(options)
Générez une URL d’aperçu avec un token signé.
Options :
collection— Slug de collection (string)id— ID de contenu ou slug (string)secret— Secret de signature (string)expiresIn— Durée de validité du token (par défaut :"1h")baseUrl— URL de base optionnelle pour les liens absoluspathPattern— Modèle d’URL avec espaces réservés{collection},{id}et{locale}(par défaut :"/{collection}/{id}")locale— Valeur substituée pour{locale}. Chaîne vide omet le segment de locale (les barres obliques sont regroupées).
Retourne : Promise<string>
verifyPreviewToken(options)
Vérifiez un token d’aperçu.
Options :
secret— Secret de vérification (string)url— URL pour extraire le token, OUtoken— Chaîne de token directement
Retourne : Promise<VerifyPreviewTokenResult>
type VerifyPreviewTokenResult =
| { valid: true; payload: PreviewTokenPayload }
| { valid: false; error: "invalid" | "expired" | "malformed" | "none" };
generatePreviewToken(options)
Générez un token sans construire d’URL.
Options :
contentId— ID de contenu au formatcollection:idexpiresIn— Durée de validité du token (par défaut :"1h")secret— Secret de signature
Retourne : Promise<string>