Mode aperçu

Sur cette page

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

  1. L’administration génère une URL d’aperçu pour un article en brouillon
  2. L’URL contient un paramètre de requête _preview signé avec une date d’expiration
  3. Le middleware EmDash vérifie automatiquement le jeton et configure le contexte de la requête
  4. 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 format collection:id
  • exp — 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 absolus
  • pathPattern — 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, OU
  • token — 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 format collection:id
  • expiresIn — Durée de validité (défaut : "1h")
  • secret — Secret de signature

Retour : Promise<string>