Mode aperçu

Sur cette page

Le système d’aperçu d’EmDash permet aux éditeurs de visualiser le contenu non publié via des URLs sécurisées et limitées dans le temps. Les liens d’aperçu utilisent des tokens signés HMAC-SHA256 que vous pouvez partager avec des relecteurs sans exposer l’ensemble de votre contenu brouillon.

Comment ça fonctionne

  1. L’admin génère une URL d’aperçu pour un brouillon
  2. L’URL contient un paramètre de requête _preview signé avec un temps d’expiration
  3. Le middleware d’EmDash vérifie automatiquement le token et configure le contexte de la requête
  4. 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.

Configurer 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 courant ne nécessite aucune configuration.

Définissez EMDASH_PREVIEW_SECRET dans votre environnement uniquement si vous devez :

  • Partager le secret entre plusieurs processus (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 d’une restauration depuis une sauvegarde
# Optionnel : remplacer le secret généré automatiquement
EMDASH_PREVIEW_SECRET="your-random-secret-key-here"

Si défini, la valeur d’environnement prévaut sur la valeur stockée en BD.

Les templates existants fonctionnent avec l’aperçu automatiquement, comme dans la page suivante :

---
import { getEmDashEntry } from "emdash";

const { slug } = Astro.params;

// Pas de 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("Erreur serveur", { 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 du contenu brouillon est servi via un token d’aperçu valide.

Générer des 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...

Quand EMDASH_PREVIEW_SECRET n’est pas défini, EmDash auto-génère et stocke un secret par site dans la base de données pour la vérification des tokens. Le helper de template getPreviewUrl() requiert 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 le bouton « Générer un lien d’aperçu » de l’UI admin, qui passe par l’API et utilise le secret résolu automatiquement.

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 sensibles au locale

pathPattern supporte aussi un placeholder {locale}. Passez un locale vide quand l’entrée est dans le locale par défaut et prefixDefaultLocale est false ; les barres obliques adjacentes laissées par la valeur vide sont automatiquement fusionnées.

L’exemple suivant construit une URL d’aperçu avec préfixe de 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’admin passe par POST /_emdash/api/content/{collection}/{id}/preview-url, qui lit le locale de l’entrée, cherche la configuration i18n du site et fournit le locale automatiquement. Pour changer le pattern par défaut utilisé par cet endpoint, définissez EMDASH_PREVIEW_PATH_PATTERN (ex : /{locale}/{id}) — les corps de requête prévalent quand ils incluent leur propre pathPattern.

Expiration du token

Contrôlez combien de temps les liens d’aperçu restent valides :

// 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 supportées : s (secondes), m (minutes), h (heures), d (jours), w (semaines).

Vérifier les 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" - pas de token présent
	// "malformed" - la structure du token est invalide
	// "invalid" - la vérification de signature a échoué
	// "expired" - le token a expiré
}

Indicateur d’aperçu

Vous pouvez afficher un indicateur visuel quand le contenu est en cours d’aperçu. Le drapeau isPreview retourné par getEmDashEntry vous indique quand du 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 utilitaires

isPreviewRequest(url)

Vérifier 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)

Extraire la chaîne du token d’une URL :

import { getPreviewToken } from "emdash";

const token = getPreviewToken(Astro.url);
// Retourne la chaîne du token ou null

parseContentId(contentId)

Parser 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 utilitaires les génèrent et les vérifient pour vous ; vous ne les construisez ni ne les parsez manuellement. Un token identifie une entrée et cesse de fonctionner après son expiration.

Exemple complet

La page suivante combine le support d’aperçu et d’é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 du token
const { entry, isPreview, error } = await getEmDashEntry("posts", slug);

if (error) {
  return new Response("Erreur serveur", { 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.status === "draft" && (
        <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} — ils 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ère une URL d’aperçu avec un token signé.

Options :

  • collection — Slug de la 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 absolus
  • pathPattern — Pattern d’URL avec placeholders {collection}, {id} et {locale} (par défaut : "/{collection}/{id}")
  • locale — Valeur substituée pour {locale}. Une chaîne vide omet le segment de locale (les barres obliques sont fusionnées).

Retourne : Promise<string>

verifyPreviewToken(options)

Vérifie un token d’aperçu.

Options :

  • secret — Secret de vérification (string)
  • url — URL dont extraire le token, OU
  • token — Chaîne du token directement

Retourne : Promise<VerifyPreviewTokenResult>

type VerifyPreviewTokenResult =
	| { valid: true; payload: PreviewTokenPayload }
	| { valid: false; error: "invalid" | "expired" | "malformed" | "none" };

generatePreviewToken(options)

Génère un token sans construire d’URL.

Options :

  • contentId — ID de contenu au format collection:id
  • expiresIn — Durée de validité du token (par défaut : "1h")
  • secret — Secret de signature

Retourne : Promise<string>