Vorschaumodus

Auf dieser Seite

Das Vorschausystem von EmDash lässt Redakteure unveröffentlichte Inhalte über sichere, zeitlich begrenzte URLs ansehen. Vorschau-Links verwenden mit HMAC-SHA256 signierte Tokens, die Sie mit Prüfern teilen können, ohne Ihre gesamten Entwurfsinhalte offenzulegen.

So funktionieren Vorschau-Anfragen

  1. Der Admin erzeugt eine Vorschau-URL für einen Beitragsentwurf
  2. Die URL enthält einen signierten Query-Parameter _preview mit einem Ablaufzeitpunkt
  3. Die Middleware von EmDash verifiziert das Token automatisch und richtet den Anfragekontext ein
  4. Ihr Template-Code ruft getEmDashEntry() wie gewohnt auf – Entwurfsinhalte werden automatisch ausgeliefert

Die Middleware verifiziert das Token, bevor die Seite gerendert wird, und merkt sich, welchen Inhaltseintrag es autorisiert. getEmDashEntry() liest diesen Anfragekontext automatisch. Dasselbe Template liefert daher bei einem gültigen, passenden Vorschau-Link den Entwurf und bei einer gewöhnlichen Anfrage den veröffentlichten Eintrag. Ein Token für eine andere Sammlung oder einen anderen Eintrag gewährt nie Entwurfszugriff auf die angeforderte Seite.

Die Vorschau einrichten

Die Vorschau funktioniert, sobald EmDash installiert ist. Bei der ersten Verwendung erzeugt EmDash ein Vorschau-Secret pro Site und speichert es in der Datenbank, sodass der Normalfall keine Konfiguration erfordert.

Setzen Sie EMDASH_PREVIEW_SECRET in Ihrer Umgebung nur dann, wenn Sie Folgendes benötigen:

  • Das Secret über mehrere Prozesse hinweg teilen (z. B. ein separater Vorschau-Worker, der URLs signiert und zur Verifizierung an Ihre Haupt-Site sendet)
  • Das Secret aus Compliance- oder Audit-Gründen auf einen von Ihnen kontrollierten Wert festlegen
  • Beim Wiederherstellen aus einem Backup auf einen bekannten Wert migrieren
# Optional: override the auto-generated secret
EMDASH_PREVIEW_SECRET="your-random-secret-key-here"

Wenn der Wert gesetzt ist, hat der Wert aus der Umgebung Vorrang vor dem in der Datenbank gespeicherten Wert.

Bestehende Templates funktionieren automatisch mit der Vorschau, wie in der folgenden Seite:

---
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>

Das Flag isPreview ist true, wenn ein gültiges Vorschau-Token zu dem von der Abfrage zurückgegebenen Eintrag passt. Ungültige, abgelaufene und nicht passende Tokens legen keine Entwurfsinhalte offen.

Vorschau-URLs erzeugen

Die meisten Redakteure verwenden im Admin View on site. Diese Aktion ruft den Endpunkt für Vorschau-URLs auf, der das Site-Secret auflöst, ohne es dem Browser preiszugeben.

Verwenden Sie getPreviewUrl(), wenn serverseitiger Anwendungscode einen Link erstellen muss. Der Helper verlangt das Signatur-Secret ausdrücklich. Lesen Sie Secrets aus process.env und schlagen Sie fehl, bevor ein Link erzeugt wird, wenn die Variable fehlt:

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...

Ist EMDASH_PREVIEW_SECRET nicht gesetzt, erzeugt der EmDash-Endpunkt ein Secret pro Site und speichert es in der Datenbank. Der eigenständige Helper kann diesen gespeicherten Wert nicht lesen; konfigurieren Sie daher die Umgebungsvariable, wenn Anwendungscode den Helper aufruft.

Der Wert id identifiziert den autorisierten Inhalt und ersetzt außerdem {id} im URL-Pfad. Er kann eine Datenbank-ID oder ein Slug sein. Verwenden Sie denselben Bezeichner, den die Zielseite an getEmDashEntry() übergibt.

Übergeben Sie baseUrl, um eine absolute URL zu erzeugen:

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...

Übergeben Sie pathPattern, um eine URL mit einem benutzerdefinierten Pfad zu erzeugen:

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

Locale-bewusste Pfade

pathPattern unterstützt außerdem den Platzhalter {locale}. Übergeben Sie ein leeres locale, wenn der Eintrag in der Standard-Locale vorliegt und prefixDefaultLocale den Wert false hat; durch den leeren Wert entstehende benachbarte Schrägstriche werden automatisch zusammengeführt.

Das folgende Beispiel baut eine Vorschau-URL mit Locale-Präfix:

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=...

Der Link View on site im Admin läuft über POST /_emdash/api/content/{collection}/{id}/preview-url. Der Endpunkt verwendet die Datenbank-ID des Eintrags und liefert dessen Locale automatisch mit. Setzen Sie EMDASH_PREVIEW_PATH_PATTERN, wenn Ihre öffentliche Route vom Standardmuster /{collection}/{id} abweicht. Zum Beispiel erzeugt /{locale}/posts/{id} für einen portugiesischen Eintrag /pt-br/posts/01ABC... und für einen Eintrag in der Standard-Locale ohne Präfix /posts/01ABC.... Die Zielroute kann diese Datenbank-ID an getEmDashEntry() übergeben. Ein pathPattern im Anfragekörper überschreibt den Wert aus der Umgebung.

Der Endpunkt kann den Slug des Eintrags nicht getrennt von seiner Datenbank-ID einsetzen. Muss eine Vorschau-URL den Slug verwenden, erzeugen Sie sie im serverseitigen Anwendungscode mit getPreviewUrl() und übergeben Sie den Slug als id.

Der Platzhalter {locale} erhält den konfigurierten Locale-Code. Die benutzerdefinierten path-Zuordnungen für Locales von Astro werden nicht angewendet. Verwendet die öffentliche Route ein anderes Segment, rufen Sie den Helper mit diesem Segment auf oder stellen Sie eine Route bereit, die den vom Admin-Endpunkt verwendeten Locale-Code akzeptiert.

Die Token-Gültigkeit festlegen

Steuern Sie, wie lange Vorschau-Links gültig bleiben:

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 });

Unterstützte Einheiten: s (Sekunden), m (Minuten), h (Stunden), d (Tage), w (Wochen).

Tokens in einem benutzerdefinierten Ablauf verifizieren

Normale Astro-Seiten rufen verifyPreviewToken() nicht auf; die EmDash-Middleware verifiziert _preview bereits. Verwenden Sie diesen Helper nur, wenn Code außerhalb dieser Middleware ein Token prüfen muss:

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,
});

Das Ergebnis zeigt an, ob das Token gültig ist:

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
}

Einen Vorschau-Indikator anzeigen

Verwenden Sie das von getEmDashEntry zurückgegebene Flag isPreview, um anzuzeigen, dass die Seite aus einer Vorschau-Anfrage stammt:

{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>
)}

Vorschau-URLs untersuchen

isPreviewRequest(url)

Prüft, ob eine URL einen Parameter _preview enthält. Das Token wird dabei nicht verifiziert:

import { isPreviewRequest } from "emdash";

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

getPreviewToken(url)

Extrahiert den Token-String aus einer URL:

import { getPreviewToken } from "emdash";

const token = getPreviewToken(Astro.url);
// Returns the token string or null

parseContentId(contentId)

Zerlegt eine Inhalts-ID in Sammlung und ID:

import { parseContentId } from "emdash";

const { collection, id } = parseContentId("posts:my-draft-post");
// { collection: "posts", id: "my-draft-post" }

Token-Sicherheit

Vorschau-Tokens sind signiert und zeitlich begrenzt. Der Admin-Endpunkt und die Helper-Funktionen erzeugen und verifizieren sie für Sie; Sie bauen oder parsen sie nicht von Hand. Ein Token identifiziert einen Eintrag und funktioniert nach seinem Ablauf nicht mehr.

Vollständiges Beispiel

Die folgende Seite kombiniert Vorschau und Unterstützung der visuellen Bearbeitung in einem vollständigen Blogbeitrags-Template:

---
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>

Beachten Sie die Spreads {...entry.edit} und {...entry.edit.title} – sie fügen data-emdash-ref-Attribute hinzu, die die visuelle Bearbeitung für authentifizierte Redakteure ermöglichen. In der Produktion erzeugen sie keine Ausgabe.

API-Referenz

getPreviewUrl(options)

Erzeugt eine Vorschau-URL mit einem signierten Token.

Optionen:

  • collection — Sammlungs-Slug (String)
  • id — Inhalts-ID oder Slug (String)
  • secret — Signatur-Secret (String)
  • expiresIn — Gültigkeitsdauer des Tokens (Standard: "1h")
  • baseUrl — Optionale Basis-URL für absolute Links
  • pathPattern — URL-Muster mit den Platzhaltern {collection}, {id} und {locale} (Standard: "/{collection}/{id}")
  • locale — Wert, der für {locale} eingesetzt wird. Ein leerer String lässt das Locale-Segment weg (Schrägstriche werden zusammengeführt).

Rückgabewert: Promise<string>

verifyPreviewToken(options)

Verifiziert ein Vorschau-Token.

Optionen:

  • secret — Verifizierungs-Secret (String)
  • url — URL, aus der das Token extrahiert wird, ODER
  • token — Token-String direkt

Rückgabewert: Promise<VerifyPreviewTokenResult>

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

generatePreviewToken(options)

Erzeugt ein Token, ohne eine URL zu bauen.

Optionen:

  • contentId — Inhalts-ID im Format collection:id
  • expiresIn — Gültigkeitsdauer des Tokens (Standard: "1h")
  • secret — Signatur-Secret

Rückgabewert: Promise<string>