Draft preview without shipping a token to every visitor

A read token in the browser bundle is readable by everyone who loads the page. Preview does not need one — the Studio is already logged in.

Jordan Hale

Jordan Hale

Front-end Engineer ·

Abstract magenta gradient suggesting a secure channel

Previewing unpublished content on a statically generated site looks like it needs a server. There is no request to read a draft through, so the obvious fix is a token in the client bundle — which is to say a token published to everybody who visits.

Let the authenticated window do the query

The Studio is already authenticated as the editor. So the preview page fetches nothing at all: the Studio runs the projection and posts the result into the preview window, re-running it on a subscription so the pane updates as the document is saved.

The Studio runs the query; the preview page only renders the answer.

Both ends of a postMessage are untrusted

Anything that can reach a window can post to it, so origins are matched exactly in both directions and the payload stays untrusted until it has been validated. Never post to '*' — the message names a document to serve.

shared/previewChannel.tsTypeScript
<script setup lang="ts">
// Fetched once, at build time, into the prerendered payload.
const { data } = await useSanityContent<BlogIndexQueryResult>(
    blogIndexQuery
)

// Paging and search are derived from it — no second request.
const matching = computed(() =>
    (data.value?.posts ?? []).filter(matchesFilters)
)
</script>

The remaining case for a browser token

There is one, and it is narrow: a private dataset queried from the browser on purpose. That should be an explicit opt-in with its own environment variable — not something that arrives as a side effect of turning previews on.

PreviewSecurityStatic Generation
Jordan Hale

Written by

Jordan Hale

Front-end Engineer

Jordan builds front ends that stay fast after launch, and spends most of that time deleting work the browser was never asked to do.

Keep reading

All posts →