Il sistema di anteprima di EmDash consente agli editor di visualizzare contenuti non pubblicati tramite URL sicuri e a tempo limitato. I link di anteprima usano token firmati HMAC-SHA256 che puoi condividere con i revisori senza esporre tutti i tuoi contenuti in bozza.
Come funziona
- L’admin genera un URL di anteprima per un post in bozza
- L’URL contiene un parametro di query
_previewfirmato con un tempo di scadenza - Il middleware di EmDash verifica automaticamente il token e configura il contesto della richiesta
- Il tuo codice template chiama
getEmDashEntry()come di consueto — il contenuto in bozza viene servito automaticamente
L’anteprima è implicita. Il middleware verifica il token e le funzioni di query lo leggono tramite AsyncLocalStorage, così lo stesso codice template serve contenuti in bozza durante un’anteprima e contenuti pubblicati altrimenti.
Configurare l’anteprima
L’anteprima funziona non appena EmDash è installato. Al primo utilizzo, EmDash genera un segreto di anteprima per sito e lo memorizza nel database, quindi il caso comune non necessita di configurazione.
Imposta EMDASH_PREVIEW_SECRET nel tuo ambiente solo se hai bisogno di:
- Condividere il segreto tra più processi (es. un Worker di anteprima separato che firma URL e li invia al tuo sito principale per la verifica)
- Fissare il segreto a un valore che controlli per ragioni di conformità/audit
- Migrare a un valore noto durante il ripristino da un backup
# Opzionale: sovrascrivere il segreto generato automaticamente
EMDASH_PREVIEW_SECRET="your-random-secret-key-here"
Se impostato, il valore dell’ambiente prevale sul valore memorizzato nel DB.
I template esistenti funzionano automaticamente con l’anteprima, come nella seguente pagina:
---
import { getEmDashEntry } from "emdash";
const { slug } = Astro.params;
const { entry, isPreview, error } = await getEmDashEntry("posts", slug);
if (error) {
return new Response("Errore del server", { 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>
Il flag isPreview è true quando il contenuto in bozza viene servito tramite un token di anteprima valido.
Generare URL di anteprima
Usa getPreviewUrl() per creare link di anteprima. La funzione prende il segreto come argomento esplicito:
import { getPreviewUrl } from "emdash";
const previewUrl = await getPreviewUrl({
collection: "posts",
id: "my-draft-post",
secret: import.meta.env.EMDASH_PREVIEW_SECRET,
expiresIn: "1h",
});
Quando EMDASH_PREVIEW_SECRET non è impostato, EmDash auto-genera e memorizza un segreto per sito nel database per la verifica dei token. L’helper template getPreviewUrl() richiede ancora che tu passi il segreto esplicitamente — fissa la tua variabile d’ambiente se lo chiami dai template di pagina. La maggior parte dei siti usa il pulsante “Genera link di anteprima” dell’UI admin, che passa attraverso l’API e usa il segreto risolto automaticamente.
Passa baseUrl per generare un URL assoluto:
const fullUrl = await getPreviewUrl({
collection: "posts",
id: "my-draft-post",
secret: import.meta.env.EMDASH_PREVIEW_SECRET,
baseUrl: "https://example.com",
});
Passa pathPattern per generare un URL con un percorso personalizzato:
const blogUrl = await getPreviewUrl({
collection: "posts",
id: "my-draft-post",
secret: import.meta.env.EMDASH_PREVIEW_SECRET,
pathPattern: "/blog/{id}",
});
Percorsi consapevoli del locale
pathPattern supporta anche un placeholder {locale}. Passa un locale vuoto quando l’entry è nel locale predefinito e prefixDefaultLocale è false; le barre adiacenti lasciate dal valore vuoto vengono automaticamente compresse.
await getPreviewUrl({
collection: "posts",
id: "hello",
secret,
pathPattern: "/{locale}/{id}",
locale: "pt-br",
});
// Restituisce: /pt-br/hello?_preview=...
await getPreviewUrl({
collection: "posts",
id: "hello",
secret,
pathPattern: "/{locale}/{id}",
locale: "",
});
// Restituisce: /hello?_preview=...
Il link “Visualizza sul sito” dell’admin passa attraverso POST /_emdash/api/content/{collection}/{id}/preview-url, che legge il locale dell’entry, cerca la configurazione i18n del sito e fornisce il locale automaticamente. Per cambiare il pattern predefinito usato da quell’endpoint, imposta EMDASH_PREVIEW_PATH_PATTERN (es. /{locale}/{id}).
Scadenza del token
Controlla per quanto tempo i link di anteprima restano validi:
await getPreviewUrl({ ..., expiresIn: "1h" }); // 1 ora (predefinito)
await getPreviewUrl({ ..., expiresIn: "30m" }); // 30 minuti
await getPreviewUrl({ ..., expiresIn: "1d" }); // 1 giorno
await getPreviewUrl({ ..., expiresIn: "2w" }); // 2 settimane
await getPreviewUrl({ ..., expiresIn: 3600 }); // 3600 secondi
Unità supportate: s (secondi), m (minuti), h (ore), d (giorni), w (settimane).
Verificare i token
Usa verifyPreviewToken() per validare le richieste di anteprima in arrivo:
import { verifyPreviewToken } from "emdash";
const result = await verifyPreviewToken({
url: Astro.url,
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);
// "none" | "malformed" | "invalid" | "expired"
}
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 helper
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");
Sicurezza dei token
I token di anteprima sono firmati e a tempo limitato. La CLI e le funzioni helper li generano e verificano per te; non li costruisci o li parsi manualmente. Un token identifica un’entry e smette di funzionare dopo la scadenza.
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("Errore del server", { 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.status === "draft" && (
<span class="draft-indicator">Bozza</span>
)}
</header>
<div class="content" {...entry.edit.content}>
<PortableText value={entry.data.content} />
</div>
</article>
</BaseLayout>
Nota gli spread {...entry.edit} e {...entry.edit.title} — aggiungono attributi data-emdash-ref che abilitano l’editing visuale per gli editor autenticati. In produzione, non producono output.
Riferimento API
getPreviewUrl(options)
collection— Slug della collezione (string)id— ID del contenuto o slug (string)secret— Segreto di firma (string)expiresIn— Durata di validità del token (predefinito:"1h")baseUrl— URL base opzionale per link assolutipathPattern— Pattern URL con placeholder{collection},{id}e{locale}(predefinito:"/{collection}/{id}")locale— Valore sostituito per{locale}. Stringa vuota omette il segmento locale.
Restituisce: Promise<string>
verifyPreviewToken(options)
secret— Segreto di verifica (string)url— URL da cui estrarre il token, OPPUREtoken— Stringa del token direttamente
Restituisce: Promise<VerifyPreviewTokenResult>
type VerifyPreviewTokenResult =
| { valid: true; payload: PreviewTokenPayload }
| { valid: false; error: "invalid" | "expired" | "malformed" | "none" };
generatePreviewToken(options)
contentId— ID del contenuto nel formatocollection:idexpiresIn— Durata di validità del token (predefinito:"1h")secret— Segreto di firma
Restituisce: Promise<string>