Mode Aperçu

Sur cette page

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

Fonctionnement des requêtes d’aperçu

  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é, assorti d’une date d’expiration
  3. Le middleware d’EmDash vérifie automatiquement le jeton et met en place le contexte de la requête
  4. Le code de votre modèle appelle getEmDashEntry() comme d’habitude — le contenu en brouillon est servi automatiquement

Le middleware vérifie le jeton avant le rendu de la page et enregistre l’entrée de contenu qu’il autorise. getEmDashEntry() lit automatiquement ce contexte de requête. Le même modèle sert donc le brouillon pour un lien d’aperçu valide et correspondant, et l’entrée publiée pour une requête ordinaire. Un jeton destiné à une autre collection ou à une autre entrée n’accorde jamais l’accès au brouillon de la page demandée.

Configurer l’aperçu

L’aperçu fonctionne dès qu’EmDash est installé. Lors de la première utilisation, EmDash génère un secret d’aperçu propre au site et le stocke dans la base de données ; le cas courant ne nécessite donc aucune configuration.

Ne définissez EMDASH_PREVIEW_SECRET dans votre environnement que si vous devez :

  • Partager le secret entre plusieurs processus (par exemple, un Worker d’aperçu distinct qui signe les URL et les envoie à votre site principal pour vérification)
  • Fixer le secret à une valeur que vous maîtrisez, pour des raisons de conformité ou d’audit
  • Migrer vers une valeur connue lors d’une restauration depuis une sauvegarde
# Optional: override the auto-generated secret
EMDASH_PREVIEW_SECRET="your-random-secret-key-here"

S’il est défini, la valeur de l’environnement l’emporte sur la valeur stockée dans la base de données.

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

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

const { slug } = Astro.params;

const { entry, isPreview, error } = await getEmDashEntry("posts", slug);

if (error) {
  return new Response("Server error", { status: 500 });
}

if (!entry) {
  return Astro.rewrite("/404");
}
---

{isPreview && (
  <div class="preview-banner">
    You are viewing the preview version of this page.
  </div>
)}

<article>
  <h1>{entry.data.title}</h1>
</article>

L’indicateur isPreview vaut true lorsqu’un jeton d’aperçu valide correspond à l’entrée renvoyée par la requête. Les jetons non valides, expirés ou non correspondants n’exposent pas le contenu en brouillon.

Générer des URL d’aperçu

La plupart des éditeurs utilisent View on site dans l’administration. Cette action appelle le point de terminaison d’URL d’aperçu, qui résout le secret du site sans l’exposer au navigateur.

Utilisez getPreviewUrl() lorsque du code applicatif côté serveur doit créer un lien. L’assistant exige explicitement le secret de signature. Lisez les secrets depuis process.env et échouez avant de générer un lien si la variable est absente :

import { getPreviewUrl } from "emdash";

const secret = process.env.EMDASH_PREVIEW_SECRET;
if (!secret) throw new Error("EMDASH_PREVIEW_SECRET is required");

const previewUrl = await getPreviewUrl({
	collection: "posts",
	id: "my-draft-post",
	secret,
	expiresIn: "1h",
});
// Returns: /posts/my-draft-post?_preview=eyJjaWQ...

Lorsque EMDASH_PREVIEW_SECRET n’est pas défini, le point de terminaison d’EmDash génère un secret propre au site et le stocke dans la base de données. L’assistant autonome ne peut pas lire cette valeur stockée ; configurez donc la variable d’environnement si le code de l’application appelle l’assistant.

La valeur id identifie le contenu autorisé et remplace également {id} dans le chemin de l’URL. Il peut s’agir d’un ID de base de données ou d’un slug. Utilisez le même identifiant que la page de destination transmet à getEmDashEntry().

Transmettez baseUrl pour générer une URL absolue :

const fullUrl = await getPreviewUrl({
	collection: "posts",
	id: "my-draft-post",
	secret,
	baseUrl: "https://example.com",
});
// Returns: https://example.com/posts/my-draft-post?_preview=eyJjaWQ...

Transmettez pathPattern pour générer une URL avec un chemin personnalisé :

const blogUrl = await getPreviewUrl({
	collection: "posts",
	id: "my-draft-post",
	secret,
	pathPattern: "/blog/{id}",
});
// Returns: /blog/my-draft-post?_preview=eyJjaWQ...

Chemins tenant compte des paramètres régionaux

pathPattern prend également en charge un espace réservé {locale}. Transmettez un locale vide lorsque l’entrée est dans les paramètres régionaux par défaut et que prefixDefaultLocale vaut false ; les barres obliques adjacentes laissées par la valeur vide sont réduites automatiquement.

L’exemple suivant construit une URL d’aperçu préfixée par les paramètres régionaux :

await getPreviewUrl({
	collection: "posts",
	id: "hello",
	secret,
	pathPattern: "/{locale}/{id}",
	locale: "pt-br",
});
// Returns: /pt-br/hello?_preview=...

await getPreviewUrl({
	collection: "posts",
	id: "hello",
	secret,
	pathPattern: "/{locale}/{id}",
	locale: "", // default locale, no prefix
});
// Returns: /hello?_preview=...

Le lien View on site de l’administration passe par POST /_emdash/api/content/{collection}/{id}/preview-url. Le point de terminaison utilise l’ID de base de données de l’entrée et fournit automatiquement ses paramètres régionaux. Définissez EMDASH_PREVIEW_PATH_PATTERN lorsque votre route publique diffère du modèle par défaut /{collection}/{id}. Par exemple, /{locale}/posts/{id} produit /pt-br/posts/01ABC... pour une entrée en portugais et /posts/01ABC... pour une entrée sans préfixe dans les paramètres régionaux par défaut. La route de destination peut transmettre cet ID de base de données à getEmDashEntry(). Un pathPattern dans le corps de la requête remplace la valeur de l’environnement.

Le point de terminaison ne peut pas substituer le slug de l’entrée séparément de son ID de base de données. Si une URL d’aperçu doit utiliser le slug, générez-la dans le code applicatif côté serveur avec getPreviewUrl() et transmettez le slug comme id.

L’espace réservé {locale} reçoit le code de paramètres régionaux configuré. Il n’applique pas les correspondances path personnalisées des paramètres régionaux d’Astro. Si la route publique utilise un autre segment, appelez l’assistant avec ce segment ou fournissez une route qui accepte le code de paramètres régionaux utilisé par le point de terminaison de l’administration.

Définir l’expiration du jeton

Contrôlez la durée de validité des liens d’aperçu :

const preview = {
	collection: "posts",
	id: "my-draft-post",
	secret,
};

// Valid for 1 hour (default)
await getPreviewUrl(preview);

// Valid for 30 minutes
await getPreviewUrl({ ...preview, expiresIn: "30m" });

// Valid for 1 day
await getPreviewUrl({ ...preview, expiresIn: "1d" });

// Valid for 2 weeks
await getPreviewUrl({ ...preview, expiresIn: "2w" });

// Valid for 3600 seconds
await getPreviewUrl({ ...preview, expiresIn: 3600 });

Unités prises en charge : s (secondes), m (minutes), h (heures), d (jours), w (semaines).

Vérifier les jetons dans un flux personnalisé

Les pages Astro ordinaires n’appellent pas verifyPreviewToken() ; le middleware d’EmDash vérifie déjà _preview. N’utilisez cet assistant que lorsque du code extérieur à ce middleware doit valider un jeton :

import { verifyPreviewToken } from "emdash";

// From a URL (extracts _preview query parameter)
const fromUrl = await verifyPreviewToken({
	url: Astro.url,
	secret,
});

// Or with a token directly
const fromToken = await verifyPreviewToken({
	token: Astro.url.searchParams.get("_preview"),
	secret,
});

Le résultat indique si le jeton est valide :

if (fromUrl.valid) {
	// Token is valid
	console.log(fromUrl.payload.cid); // "posts:my-draft-post"
	console.log(fromUrl.payload.exp); // Expiry timestamp
	console.log(fromUrl.payload.iat); // Issued-at timestamp
} else {
	// Token is invalid
	console.log(fromUrl.error);
	// "none" - no token present
	// "malformed" - token structure is invalid
	// "invalid" - signature verification failed
	// "expired" - token has expired
}

Afficher un indicateur d’aperçu

Utilisez l’indicateur isPreview renvoyé par getEmDashEntry pour signaler que la page provient d’une requête d’aperçu :

{isPreview && (
  <div class="preview-banner" role="alert">
    <strong>Preview</strong> — You are viewing the preview version of this page.
    <a href={Astro.url.pathname}>Exit preview</a>
  </div>
)}

Inspecter les URL d’aperçu

isPreviewRequest(url)

Vérifie si une URL contient un paramètre _preview. Cela ne vérifie pas le jeton :

import { isPreviewRequest } from "emdash";

if (isPreviewRequest(Astro.url)) {
	// Handle preview request
}

getPreviewToken(url)

Extrait la chaîne du jeton d’une URL :

import { getPreviewToken } from "emdash";

const token = getPreviewToken(Astro.url);
// Returns the token string or 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" }

Sécurité des jetons

Les jetons d’aperçu sont signés et limités dans le temps. Le point de terminaison de l’administration et les fonctions d’assistance les génèrent et les vérifient pour vous ; vous ne les construisez ni ne les analysez à la main. Un jeton identifie une seule entrée et cesse de fonctionner après son expiration.

Exemple complet

La page suivante combine l’aperçu et la prise en charge de l’édition visuelle dans un modèle complet d’article de blog :

---
import { getEmDashEntry } from "emdash";
import BaseLayout from "../../layouts/Base.astro";
import { PortableText } from "emdash/ui";

const { slug } = Astro.params;

const { entry, isPreview, error } = await getEmDashEntry("posts", slug);

if (error) {
  return new Response("Server error", { status: 500 });
}

if (!entry) {
  return Astro.rewrite("/404");
}
---

<BaseLayout title={entry.data.title}>
  {isPreview && (
    <div class="preview-banner" role="alert">
      <strong>Preview</strong> — You are viewing the preview version of this page.
    </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">Draft</span>
      )}
    </header>

    <div class="content" {...entry.edit.content}>
      <PortableText value={entry.data.content} />
    </div>
  </article>
</BaseLayout>

Remarquez 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 de l’API

getPreviewUrl(options)

Génère une URL d’aperçu avec un jeton signé.

Options :

  • collection — Slug de la collection (string)
  • id — ID du contenu ou slug (string)
  • secret — Secret de signature (string)
  • expiresIn — Durée de validité du jeton (par défaut : "1h")
  • baseUrl — URL de base facultative pour les liens absolus
  • pathPattern — Modèle d’URL avec les espaces réservés {collection}, {id} et {locale} (par défaut : "/{collection}/{id}")
  • locale — Valeur substituée à {locale}. Une chaîne vide omet le segment de paramètres régionaux (les barres obliques sont réduites).

Renvoie : Promise<string>

verifyPreviewToken(options)

Vérifie un jeton d’aperçu.

Options :

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

Renvoie : Promise<VerifyPreviewTokenResult>

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

generatePreviewToken(options)

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

Options :

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

Renvoie : Promise<string>