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
- L’amministrazione genera un URL di anteprima per un articolo in bozza
- L’URL include il parametro di query firmato
_previewcon scadenza - Il middleware di EmDash verifica automaticamente il token e prepara il contesto della richiesta
- 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 contenutocollection:idexp— 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)
secreteurloppuretoken
Restituisce: Promise<VerifyPreviewTokenResult>
type VerifyPreviewTokenResult =
| { valid: true; payload: PreviewTokenPayload }
| { valid: false; error: "invalid" | "expired" | "malformed" | "none" };
generatePreviewToken(options)
contentId,expiresIn,secret
Restituisce: Promise<string>