Modalità Anteprima

In questa pagina

Il sistema di anteprima di EmDash consente agli editori di visualizzare contenuti non pubblicati tramite URL sicuri con limite di tempo. I link di anteprima utilizzano token firmati HMAC-SHA256 che puoi condividere con i revisori senza esporre l’intero contenuto in bozza.

Come Funziona

  1. L’amministratore genera un URL di anteprima per un post in bozza
  2. L’URL contiene un parametro query _preview firmato con un tempo di scadenza
  3. Il middleware di EmDash verifica automaticamente il token e configura il contesto della richiesta
  4. Il tuo codice template chiama getEmDashEntry() normalmente — il contenuto in bozza viene servito automaticamente

L’anteprima è implicita. Il middleware verifica il token e le funzioni di query lo leggono tramite AsyncLocalStorage, quindi lo stesso codice template serve contenuto in bozza durante un’anteprima e contenuto pubblicato altrimenti.

Configurazione dell’Anteprima

L’anteprima funziona non appena EmDash è installato. Al primo utilizzo, EmDash genera un secret di anteprima per sito e lo memorizza nel database, quindi il caso comune non richiede configurazione.

Imposta EMDASH_PREVIEW_SECRET nel tuo ambiente solo se hai bisogno di:

  • Condividere il secret tra più processi (ad es. un Worker di anteprima separato che firma gli URL e li invia al tuo sito principale per la verifica)
  • Fissare il secret a un valore che controlli per motivi di conformità/audit
  • Migrare a un valore noto durante il ripristino da un backup
# Facoltativo: sostituisce il secret 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 pagina seguente:

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

const { slug } = Astro.params;

// Non è necessaria una gestione speciale dell'anteprima — il middleware
// rileva i token _preview e serve automaticamente il contenuto in bozza
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>

Il flag isPreview è true quando il contenuto in bozza viene servito tramite un token di anteprima valido.

Generazione di URL di Anteprima

Usa getPreviewUrl() per creare link di anteprima. La funzione prende il secret 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",
});
// Restituisce: /posts/my-draft-post?_preview=eyJjaWQ...

Quando EMDASH_PREVIEW_SECRET non è impostato, EmDash genera e memorizza automaticamente un secret per sito nel database per la verifica dei token. L’helper template getPreviewUrl() richiede comunque che tu passi il secret esplicitamente — fissa la tua variabile di ambiente se lo chiami dai template di pagina. La maggior parte dei siti utilizza invece il pulsante “Genera link di anteprima” dell’interfaccia di amministrazione, che passa attraverso l’API e utilizza automaticamente il secret risolto.

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",
});
// Restituisce: https://example.com/posts/my-draft-post?_preview=eyJjaWQ...

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}",
});
// Restituisce: /blog/my-draft-post?_preview=eyJjaWQ...

Percorsi consapevoli della locale

pathPattern supporta anche un segnaposto {locale}. Passa una locale vuota quando la voce è nella locale predefinita e prefixDefaultLocale è false; le barre adiacenti lasciate dal valore vuoto vengono automaticamente compresse.

L’esempio seguente costruisce un URL di anteprima con prefisso locale:

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: "", // locale predefinita, nessun prefisso
});
// Restituisce: /hello?_preview=...

Il link “Visualizza sul sito” dell’amministratore passa attraverso POST /_emdash/api/content/{collection}/{id}/preview-url, che legge la locale della voce, cerca la configurazione i18n del sito e fornisce la locale automaticamente. Per modificare il pattern predefinito utilizzato da quell’endpoint, imposta EMDASH_PREVIEW_PATH_PATTERN (ad es. /{locale}/{id}) — i corpi delle richieste hanno ancora la precedenza quando includono il proprio pathPattern.

Scadenza dei Token

Controlla per quanto tempo i link di anteprima rimangono validi:

// Valido per 1 ora (predefinito)
await getPreviewUrl({ ..., expiresIn: "1h" });

// Valido per 30 minuti
await getPreviewUrl({ ..., expiresIn: "30m" });

// Valido per 1 giorno
await getPreviewUrl({ ..., expiresIn: "1d" });

// Valido per 2 settimane
await getPreviewUrl({ ..., expiresIn: "2w" });

// Valido per 3600 secondi
await getPreviewUrl({ ..., expiresIn: 3600 });

Unità supportate: s (secondi), m (minuti), h (ore), d (giorni), w (settimane).

Verifica dei Token

Usa verifyPreviewToken() per convalidare le richieste di anteprima in arrivo:

import { verifyPreviewToken } from "emdash";

// Da un URL (estrae il parametro query _preview)
const result = await verifyPreviewToken({
	url: Astro.url,
	secret: import.meta.env.EMDASH_PREVIEW_SECRET,
});

// Oppure con un token direttamente
const result = await verifyPreviewToken({
	token: someTokenString,
	secret: import.meta.env.EMDASH_PREVIEW_SECRET,
});

Il risultato indica se il token è valido:

if (result.valid) {
	// Il token è valido
	console.log(result.payload.cid); // "posts:my-draft-post"
	console.log(result.payload.exp); // Timestamp di scadenza
	console.log(result.payload.iat); // Timestamp di emissione
} else {
	// Il token non è valido
	console.log(result.error);
	// "none" - nessun token presente
	// "malformed" - struttura del token non valida
	// "invalid" - verifica della firma fallita
	// "expired" - il token è scaduto
}

Indicatore di Anteprima

Puoi mostrare un indicatore visivo quando il contenuto è in anteprima. Il flag isPreview restituito da getEmDashEntry ti indica quando viene servito contenuto in bozza:

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

isPreviewRequest(url)

Verifica se un URL contiene un token di anteprima:

import { isPreviewRequest } from "emdash";

if (isPreviewRequest(Astro.url)) {
	// Gestisci la richiesta di anteprima
}

getPreviewToken(url)

Estrai la stringa del token da un URL:

import { getPreviewToken } from "emdash";

const token = getPreviewToken(Astro.url);
// Restituisce la stringa del token o null

parseContentId(contentId)

Analizza un ID di contenuto in collezione e ID:

import { parseContentId } from "emdash";

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

Sicurezza dei Token

I token di anteprima sono firmati e limitati nel tempo. La CLI e le funzioni di aiuto li generano e li verificano per te; non li costruisci né li analizzi manualmente. Un token identifica una voce e smette di funzionare dopo la scadenza.

Esempio Completo

La pagina seguente combina il supporto per l’anteprima e l’editing visuale in un template completo di post del blog:

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

const { slug } = Astro.params;

// L'anteprima è automatica — il middleware gestisce la verifica dei token
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>

Nota gli spread {...entry.edit} e {...entry.edit.title} — questi aggiungono attributi data-emdash-ref che abilitano l’editing visuale per gli editori autenticati. In produzione, non producono output.

Riferimento API

getPreviewUrl(options)

Genera un URL di anteprima con un token firmato.

Opzioni:

  • collection — Slug della collezione (string)
  • id — ID del contenuto o slug (string)
  • secret — Secret di firma (string)
  • expiresIn — Durata di validità del token (predefinito: "1h")
  • baseUrl — URL base opzionale per link assoluti
  • pathPattern — Pattern URL con segnaposto {collection}, {id} e {locale} (predefinito: "/{collection}/{id}")
  • locale — Valore sostituito per {locale}. Stringa vuota omette il segmento locale (le barre vengono compresse).

Restituisce: Promise<string>

verifyPreviewToken(options)

Verifica un token di anteprima.

Opzioni:

  • secret — Secret di verifica (string)
  • url — URL da cui estrarre il token, OPPURE
  • token — Stringa del token direttamente

Restituisce: Promise<VerifyPreviewTokenResult>

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

generatePreviewToken(options)

Genera un token senza costruire un URL.

Opzioni:

  • contentId — ID del contenuto nel formato collection:id
  • expiresIn — Durata di validità del token (predefinito: "1h")
  • secret — Secret di firma

Restituisce: Promise<string>