Keep your public Astro blog static and add a separate server-rendered route for draft previews. With genko, that route uses an entry's draftKey and a server-side delivery API key to call getOrNull(id, { draftKey }). An adapter supplies the runtime for previews without changing how public articles are generated.
This article was written by the operator of genko.me. The draft preview documentation, Astro tutorial, SDK, and editor implementation were checked on October 21, 2026.
Give previews their own route
Build /blog/[id] from published entries. Render /preview/[id] when an editor requests it. Draft IDs do not need to appear in getStaticPaths(), and draft HTML does not need to enter your public build.
Route | Execution | Content |
|---|---|---|
| Build time | Published listing |
| Build time | Published article |
| Request time | Saved content matching the key |
Astro's on-demand rendering supports opting individual routes out of prerendering with export const prerender = false when an adapter is configured. Use that for the preview route. A host that only serves files cannot execute it.
Share the article layout between public and preview routes so typography, image sizes, and spacing match. Keep fetching and the preview notice outside that shared presentation component. For the public blog setup, see Building an Astro blog with a headless CMS and the Astro getting-started guide.
genko's preview token is called draftKey
The token in this integration is an entry-specific draftKey. It is used alongside a regular delivery API key, rather than replacing that credential. Preview works for an individual entry in a list API or for an object API; it does not provide a list of all drafts.
These are the SDK calls, where client is createGenko(...).apis:
// One entry in a list APIconst post = await client.blog.getOrNull(id, { draftKey });
// An object API does not take an entry IDconst page = await client.profile.getOrNull({ draftKey });getOrNull returns null for a 404. Authentication failures and network errors still throw. If you prefer get, remember that a 404 also throws and needs its own handling.
The checked SDK sends draftKey through the X-Draft-Key header and the delivery credential through X-API-KEY. The editor-facing preview URL and the server-to-API request are different parts of the flow. The delivery API reference also recommends X-Draft-Key for direct requests.
Previews show saved content. They can include saved unpublished changes to an already published article, but cannot read text that exists only in an unsaved editor session. Previewing a parent entry does not expose unpublished referenced entries.
Add the Node adapter
In your existing Astro project, install the SDK and adapter:
npm install @genko-me/sdk @astrojs/node// astro.config.mjsimport { defineConfig } from "astro/config";import node from "@astrojs/node";
export default defineConfig({ output: "static", adapter: node({ mode: "standalone" }),});Merge these options with existing configuration rather than discarding your integrations. This example targets a Node runtime. Other adapters need their own runtime secret access and deployment setup.
Set GENKO_API_KEY in the server environment, using a key with read access to blog. Do not put it in a PUBLIC_ variable or pass it as a browser component prop. See Astro's environment-variable guide when adapting secret handling to your host.
Define the schema and a preview client
Create a list API called blog in genko, with a text title and rich-text body. TypeScript declarations describe those fields; they do not create them in the CMS.
// src/lib/schema.tsimport { listApi } from "@genko-me/sdk";
export type Blog = { title: string; body: string };
export const apis = { blog: listApi<Blog>({ fields: [ { id: "title", type: "text" }, { id: "body", type: "richtext" }, ], }),};// src/lib/genko-preview.tsimport { createGenko } from "@genko-me/sdk";import { apis } from "./schema";
export function createPreviewClient() { const apiKey = process.env.GENKO_API_KEY; if (!apiKey) throw new Error("GENKO_API_KEY is required");
return createGenko({ workspace: "your-workspace", apiKey, apis, fetch: (input, init) => fetch(input, { ...init, cache: "no-store" }), }).apis;}Replace your-workspace and call this factory only on the server. It reads the Node runtime's environment and explicitly disables fetch caching. A dedicated preview client also avoids inheriting caching options from a public-page client.
Implement the server-rendered preview page
This route has no getStaticPaths(). It receives the ID and key from the request, rejects missing, duplicate, or oversized keys, and keeps every response out of shared caches.
---// src/pages/preview/[id].astroexport const prerender = false;
import { createPreviewClient } from "../../lib/genko-preview";import type { Blog } from "../../lib/schema";
Astro.response.headers.set("Cache-Control", "private, no-store");Astro.response.headers.set("X-Robots-Tag", "noindex, nofollow");Astro.response.headers.set("Referrer-Policy", "no-referrer");
const id = Astro.params.id;const keys = Astro.url.searchParams.getAll("draftKey");let post: (Blog & { id: string }) | null = null;let message = "Preview unavailable.";
if (!id || keys.length !== 1 || !keys[0] || keys[0].length > 64) { Astro.response.status = 400; message = "Open the preview again from the editor.";} else { try { const client = createPreviewClient(); post = await client.blog.getOrNull(id, { draftKey: keys[0] }); if (!post) { Astro.response.status = 404; message = "Check the entry and its preview key."; } } catch { Astro.response.status = 502; message = "Could not fetch the preview. Contact the site administrator."; }}---<html lang="en"> <head> <meta charset="utf-8" /> <meta name="robots" content="noindex, nofollow" /> <meta name="referrer" content="no-referrer" /> <title>Draft preview</title> </head> <body> <aside>You are previewing saved content.</aside> <main> {post ? ( <article> <h1>{post.title}</h1> <div set:html={post.body} /> </article> ) : <p>{message}</p>} </main> <a href="/blog">Return to published articles</a> </body></html>An invalid key never falls back to a public fetch. Such a fallback can make a broken preview look successful when the article already has a published version. For the same reason, do not quietly convert an empty key to undefined and request public content.
The example distinguishes missing content from a failed fetch. In production, use internal monitoring to separate configuration errors, API authentication failures, and network problems. Do not expose raw exceptions, request URLs, credentials, or draft bodies in the response or logs.
set:html here renders genko's rich-text body. Do not reuse it as an unrestricted sink for unrelated HTML input. Replace the article markup with your shared article component when matching the public design.
Connect the genko editor
The complete preview URL template for this route is:
https://your-site.example/preview/{id}?draftKey={key}In the current settings UI, draftKey={key} appears as a fixed suffix. If the editable field contains only the URL body, enter https://your-site.example/preview/{id} and check that the completed URL contains the key once. An interface that accepts the complete template can use the full form above. Existing query parameters require &draftKey={key}. Either form is normalized so the key appears once.
The dashboard replaces {id} with the entry ID and {key} with that entry's preview key. The editor's Preview action obtains the key, builds the URL, and opens a new tab. Editors do not need the delivery API key or a developer to generate each preview link.
The workflow is save, open Preview, inspect the result, edit, and save again. Refreshing the preview fetches the saved changes. For a live entry, save unpublished changes with the editor's draft-saving action: the preview shows those changes while the public delivery API continues returning the published content.
Protect both the response and the URL
Keeping the API key on the server is only part of the setup. The preview URL carries draftKey, and anyone who can use that URL can reach this preview route. Treat the URL as sensitive and avoid pasting it into public channels or support messages.
The example disables caching for both the API fetch and the HTML response. It also sets search-indexing and referrer directives. Exclude /preview/* from shared CDN or reverse-proxy caches, and configure access logging and analytics so they do not retain the query string. noindex is not access control.
Keep analytics, ads, and sharing buttons off preview pages. If previews must be restricted to signed-in staff, add site authentication or an access gateway. After publishing, regenerate the preview key in the entry's settings to invalidate older preview URLs.
Because this is a separate route, its exit link returns to the published listing. Removing only draftKey would leave the browser on /preview/[id], where the example returns 400. If you want a direct public-detail link, add /blog/[id] when a published version is available.
Handle languages and public deployment separately
A multilingual preview template can also include {lang}:
https://your-site.example/preview/{id}?lang={lang}&draftKey={key}Read and validate the supported language in the server route, then pass it with getOrNull(id, { draftKey, lang }). Each language version has its own key. Adding lang to the URL alone does not update the single-language implementation above. The multilingual content guide describes language-specific delivery conditions.
A current preview does not mean the public site has deployed the changes. The preview fetches at request time; static public pages still need a rebuild after publishing. Test that second path independently. The Astro tutorial covers webhook-triggered updates.
Check failure cases before handing it to editors
Start with an unpublished saved entry and open it from the dashboard. Then try an empty key, a wrong key, duplicate key parameters, and an old URL after key regeneration. None should show the public article as a substitute.
For an already published entry, save an unpublished change and confirm that only the preview shows it. Finally, test an adapter-backed production build and confirm the CDN does not store preview HTML. A successful development-server preview does not prove that a static-only deployment can execute the route.
FAQ
Can I build this preview on static-only hosting?
The preview route needs a server runtime. Public pages can remain static, but the deployment must execute the adapter's server output.
Is the genko API key enough to fetch drafts?
You also need the target entry's draftKey. Adding one to a list request does not turn it into a list of drafts.
Does preview show unsaved editor changes?
It shows saved content. Save in the editor before opening or refreshing the preview.
Do I need Next.js Draft Mode for Astro?
This implementation uses an Astro server route and passes draftKey to the SDK. It does not use Next.js Draft Mode.
Does a working preview mean my public page has updated?
Preview fetching and static publication are separate paths. Check the publishing webhook and the public-site build as well.
