Modalità anteprima

In questa pagina

Il sistema di anteprima di EmDash consente alle persone che modificano i contenuti di visualizzare materiale non pubblicato tramite URL sicuri e a tempo limitato. I link usano token firmati con HMAC-SHA256 che puoi condividere con i revisori senza esporre l’intera bozza.

Come funziona

  1. L’amministrazione genera un URL di anteprima per un articolo in bozza
  2. L’URL include il parametro di query firmato _preview con scadenza
  3. Il middleware di EmDash verifica automaticamente il token e prepara il contesto della richiesta
  4. Il template chiama getEmDashEntry() come sempre: la bozza viene servita automaticamente

L’anteprima è implicita. Non serve gestire token o opzioni nel template: middleware e funzioni di query usano AsyncLocalStorage.

Configurare l’anteprima

Aggiungi un segreto di anteprima all’ambiente:

EMDASH_PREVIEW_SECRET="your-random-secret-key-here"

Genera una stringa casuale sicura. Questo segreto firma e verifica i token.

Fatto. I template esistenti funzionano subito con l’anteprima:

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

const { slug } = Astro.params;

// Nessuna logica speciale: il middleware
// rileva i token _preview e serve la bozza automaticamente
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">
    Stai visualizzando un’anteprima. Questo contenuto non è pubblicato.
  </div>
)}

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

isPreview è true quando viene servita una bozza con un token valido.

Generare URL di anteprima

Usa getPreviewUrl():

import { getPreviewUrl } from "emdash";

const previewUrl = await getPreviewUrl({
	collection: "posts",
	id: "my-draft-post",
	secret: import.meta.env.EMDASH_PREVIEW_SECRET,
	expiresIn: "1h",
});

Con URL di base per link assoluti:

const fullUrl = await getPreviewUrl({
	collection: "posts",
	id: "my-draft-post",
	secret: import.meta.env.EMDASH_PREVIEW_SECRET,
	baseUrl: "https://example.com",
});

Con pattern di percorso personalizzato:

const blogUrl = await getPreviewUrl({
	collection: "posts",
	id: "my-draft-post",
	secret: import.meta.env.EMDASH_PREVIEW_SECRET,
	pathPattern: "/blog/{id}",
});

Scadenza del token

await getPreviewUrl({ ..., expiresIn: "1h" });
await getPreviewUrl({ ..., expiresIn: "30m" });
await getPreviewUrl({ ..., expiresIn: "1d" });
await getPreviewUrl({ ..., expiresIn: "2w" });
await getPreviewUrl({ ..., expiresIn: 3600 });

Unità: s, m, h, d, w.

Verificare i token

Usa verifyPreviewToken():

import { verifyPreviewToken } from "emdash";

const result = await verifyPreviewToken({
	url: Astro.url,
	secret: import.meta.env.EMDASH_PREVIEW_SECRET,
});

const result2 = await verifyPreviewToken({
	token: someTokenString,
	secret: import.meta.env.EMDASH_PREVIEW_SECRET,
});
if (result.valid) {
	console.log(result.payload.cid);
	console.log(result.payload.exp);
	console.log(result.payload.iat);
} else {
	console.log(result.error);
}

Indicatore di anteprima

{isPreview && (
  <div class="preview-banner" role="alert">
    <strong>Anteprima</strong> — Stai visualizzando contenuto non pubblicato.
    <a href={Astro.url.pathname}>Esci dall’anteprima</a>
  </div>
)}

Funzioni di supporto

isPreviewRequest(url)

import { isPreviewRequest } from "emdash";

if (isPreviewRequest(Astro.url)) {
}

getPreviewToken(url)

import { getPreviewToken } from "emdash";

const token = getPreviewToken(Astro.url);

parseContentId(contentId)

import { parseContentId } from "emdash";

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

Formato del token

base64url(payload).base64url(signature)

  • cid — ID contenuto collection:id
  • exp — Scadenza (secondi da epoch)
  • iat — Emissione (secondi da epoch)

Firmato con HMAC-SHA256.

Esempio completo

---
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.redirect("/404");
}
---

<BaseLayout title={entry.data.title}>
  {isPreview && (
    <div class="preview-banner" role="alert">
      <strong>Anteprima</strong> — Questo contenuto non è pubblicato.
    </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">Bozza</span>
      )}
    </header>

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

Riferimento API

getPreviewUrl(options)

  • collection, id, secret, expiresIn (predefinito "1h"), baseUrl, pathPattern (predefinito "/{collection}/{id}")

Restituisce: Promise<string>

verifyPreviewToken(options)

  • secret e url oppure token

Restituisce: Promise<VerifyPreviewTokenResult>

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

generatePreviewToken(options)

  • contentId, expiresIn, secret

Restituisce: Promise<string>