Modalità Anteprima

In questa pagina

Il sistema di anteprima di EmDash permette agli editor di visualizzare contenuti non pubblicati tramite URL sicuri e a tempo limitato. I link di anteprima usano token firmati con HMAC-SHA256 che puoi condividere con i revisori senza esporre tutti i tuoi contenuti in bozza.

Come funzionano le richieste di anteprima

  1. L’amministrazione genera un URL di anteprima per un articolo in bozza
  2. L’URL contiene un parametro di query _preview firmato con una scadenza
  3. Il middleware di EmDash verifica automaticamente il token e imposta il contesto della richiesta
  4. Il codice del tuo template chiama getEmDashEntry() come al solito: il contenuto in bozza viene servito automaticamente

Il middleware verifica il token prima di renderizzare la pagina e registra quale voce di contenuto autorizza. getEmDashEntry() legge automaticamente quel contesto della richiesta. Lo stesso template serve quindi la bozza per un link di anteprima valido e corrispondente e la voce pubblicata per una richiesta ordinaria. Un token per un’altra collezione o un’altra voce non concede mai l’accesso alla bozza della pagina richiesta.

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 richiede alcuna configurazione.

Imposta EMDASH_PREVIEW_SECRET nel tuo ambiente solo se hai bisogno di:

  • Condividere il segreto tra più processi (ad esempio, un Worker di anteprima separato che firma gli URL e li invia al tuo sito principale per la verifica)
  • Fissare il segreto a un valore che controlli per motivi di conformità o di audit
  • Migrare a un valore noto durante il ripristino da un backup
# Optional: override the auto-generated secret
EMDASH_PREVIEW_SECRET="your-random-secret-key-here"

Se impostato, il valore dell’ambiente ha la precedenza su quello memorizzato nel database.

I template esistenti funzionano automaticamente con l’anteprima, come nella pagina seguente:

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

Il flag isPreview è true quando un token di anteprima valido corrisponde alla voce restituita dalla query. I token non validi, scaduti e non corrispondenti non espongono il contenuto in bozza.

Generare URL di anteprima

La maggior parte degli editor usa View on site nell’amministrazione. Quell’azione chiama l’endpoint dell’URL di anteprima, che risolve il segreto del sito senza esporlo al browser.

Usa getPreviewUrl() quando il codice applicativo lato server deve creare un link. L’helper richiede esplicitamente il segreto di firma. Leggi i segreti da process.env e fai fallire l’operazione prima di generare un link se la variabile manca:

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

Quando EMDASH_PREVIEW_SECRET non è impostato, l’endpoint di EmDash genera e memorizza nel database un segreto per sito. L’helper autonomo non può leggere quel valore memorizzato, quindi configura la variabile d’ambiente se il codice dell’applicazione chiama l’helper.

Il valore id identifica il contenuto autorizzato e sostituisce anche {id} nel percorso dell’URL. Può essere un ID del database o uno slug. Usa lo stesso identificatore che la pagina di destinazione passa a getEmDashEntry().

Passa baseUrl per generare un URL assoluto:

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

Passa pathPattern per generare un URL con un percorso personalizzato:

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

Percorsi consapevoli delle impostazioni locali

pathPattern supporta anche un segnaposto {locale}. Passa un locale vuoto quando la voce è nelle impostazioni locali predefinite e prefixDefaultLocale è false; gli slash adiacenti lasciati dal valore vuoto vengono compressi automaticamente.

L’esempio seguente costruisce un URL di anteprima con il prefisso delle impostazioni locali:

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

Il link View on site dell’amministrazione passa attraverso POST /_emdash/api/content/{collection}/{id}/preview-url. L’endpoint usa l’ID del database della voce e ne fornisce automaticamente le impostazioni locali. Imposta EMDASH_PREVIEW_PATH_PATTERN quando la tua route pubblica differisce dal pattern predefinito /{collection}/{id}. Ad esempio, /{locale}/posts/{id} produce /pt-br/posts/01ABC... per una voce in portoghese e /posts/01ABC... per una voce senza prefisso nelle impostazioni locali predefinite. La route di destinazione può passare quell’ID del database a getEmDashEntry(). Un pathPattern nel corpo della richiesta sostituisce il valore dell’ambiente.

L’endpoint non può sostituire lo slug della voce separatamente dal suo ID del database. Se un URL di anteprima deve usare lo slug, generalo nel codice applicativo lato server con getPreviewUrl() e passa lo slug come id.

Il segnaposto {locale} riceve il codice delle impostazioni locali configurato. Non applica le mappature path personalizzate delle impostazioni locali di Astro. Se la route pubblica usa un segmento diverso, chiama l’helper con quel segmento oppure fornisci una route che accetti il codice delle impostazioni locali usato dall’endpoint dell’amministrazione.

Impostare la scadenza del token

Controlla per quanto tempo i link di anteprima restano validi:

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

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

Verificare i token in un flusso personalizzato

Le normali pagine Astro non chiamano verifyPreviewToken(); il middleware di EmDash verifica già _preview. Usa questo helper solo quando il codice esterno a quel middleware deve convalidare un token:

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

Il risultato indica se il token è valido:

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
}

Mostrare un indicatore di anteprima

Usa il flag isPreview restituito da getEmDashEntry per mostrare che la pagina proviene da una richiesta di anteprima:

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

Ispezionare gli URL di anteprima

isPreviewRequest(url)

Controlla se un URL contiene un parametro _preview. Questo non verifica il token:

import { isPreviewRequest } from "emdash";

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

getPreviewToken(url)

Estrae la stringa del token da un URL:

import { getPreviewToken } from "emdash";

const token = getPreviewToken(Astro.url);
// Returns the token string or 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 a tempo limitato. L’endpoint dell’amministrazione e le funzioni helper li generano e li verificano per te; non li costruisci né li analizzi a mano. Un token identifica una sola voce e smette di funzionare alla scadenza.

Esempio completo

La pagina seguente combina l’anteprima e il supporto alla modifica visuale in un template completo per un articolo di blog:

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

Nota gli spread {...entry.edit} e {...entry.edit.title}: aggiungono attributi data-emdash-ref che abilitano la modifica visuale per gli editor autenticati. In produzione non producono alcun output.

Riferimento API

getPreviewUrl(options)

Genera un URL di anteprima con un token firmato.

Opzioni:

  • collection — Slug della collezione (stringa)
  • id — ID del contenuto o slug (stringa)
  • secret — Segreto di firma (stringa)
  • expiresIn — Durata di validità del token (predefinito: "1h")
  • baseUrl — URL di base facoltativo per i link assoluti
  • pathPattern — Pattern di URL con i segnaposto {collection}, {id} e {locale} (predefinito: "/{collection}/{id}")
  • locale — Valore che sostituisce {locale}. Una stringa vuota omette il segmento delle impostazioni locali (gli slash vengono compressi).

Restituisce: Promise<string>

verifyPreviewToken(options)

Verifica un token di anteprima.

Opzioni:

  • secret — Segreto di verifica (stringa)
  • url — URL da cui estrarre il token, OPPURE
  • token — La 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 — Segreto di firma

Restituisce: Promise<string>