Directus Content Versioning for Draft Previews
This guide belongs to Directus Data Layer Patterns and solves a common Directus editing problem: how to prepare and preview changes to an item that is already published, without affecting the live site until the changes are ready. Directus content versioning lets editors create a version of an item, edit it freely, preview it, and promote it to the main item when approved. The guide covers enabling versions, configuring preview URLs, fetching a version in the frontend’s draft mode and promoting it with revalidation.
With only a status field, a published item has one state: editing it changes the live page as soon as it is saved. Teams work around this by duplicating items or saving changes elsewhere, which breaks references and history. Content versions keep the main item live and untouched while one or more named versions hold the pending changes.
The Problem
A university’s course pages were published items in Directus. When a department prepared next year’s course descriptions, editors either changed the live pages months early or copied items into a “drafts” collection and pasted content back later, losing relations to modules and instructors. Preview only worked for items in draft status, so there was no way to see next year’s version of a live course page before publishing it.
How Content Versioning Works
Enable versioning per collection. In the collection settings, turn on content versioning. Editors then see a version menu on each item.
Versions hold changes, not copies. A version stores the changes relative to the main item. Reading an item with a version key returns the main item merged with the version’s changes, so the frontend receives the same shape as usual.
Preview URLs per collection. Configure a preview URL template on the collection, including the item key and the version key, pointing at the frontend’s draft route. The studio opens it from the item view.
Promotion. When the version is ready, editors promote it, which applies its changes to the main item. That is a normal update event, so the revalidation flow updates the public page.
Implementation
Configure the collection’s preview URL in the studio with placeholders for the key and version, for example:
https://www.example.com/api/draft?collection=courses&key={{id}}&version={{$version}}&secret=PREVIEW_SECRET
The secret here is a short preview secret that only enables draft mode; it is not a Directus token. The draft route checks it, enables draft mode, stores the version key in a cookie and redirects to the page.
// app/api/draft/route.ts
import { draftMode, cookies } from "next/headers";
import { redirect } from "next/navigation";
import { pathForItem } from "@/lib/routes";
export async function GET(req: Request) {
const url = new URL(req.url);
if (url.searchParams.get("secret") !== process.env.PREVIEW_SECRET) return new Response("invalid", { status: 401 });
const collection = url.searchParams.get("collection")!;
const key = url.searchParams.get("key")!;
const version = url.searchParams.get("version");
(await draftMode()).enable();
const jar = await cookies();
if (version && version !== "main") jar.set("directus_version", version, { httpOnly: true, secure: true, sameSite: "none", path: "/" });
else jar.delete("directus_version");
const path = await pathForItem(collection, key); // from the route manifest
if (!path?.startsWith("/")) return new Response("unknown item", { status: 404 });
redirect(path);
}
The data layer reads the version from the cookie in draft mode and passes it to Directus with the preview token.
// lib/directus/course.ts
import { draftMode, cookies } from "next/headers";
import { createDirectus, rest, staticToken, readItems } from "@directus/sdk";
export async function getCourse(slug: string) {
const isDraft = (await draftMode()).isEnabled;
const version = isDraft ? (await cookies()).get("directus_version")?.value : undefined;
const token = isDraft ? process.env.DIRECTUS_PREVIEW_TOKEN! : process.env.DIRECTUS_READ_TOKEN!;
const client = createDirectus(process.env.DIRECTUS_URL!).with(staticToken(token)).with(rest());
const [course] = await client.request(readItems("courses", {
filter: { slug: { _eq: slug } },
fields: ["id", "title", "slug", "description", { modules: ["id", "title"] }],
limit: 1,
...(version ? { version } : {}), // merged view of main item + version
}));
return course ?? null;
}
Draft-mode requests bypass all caches, so editors see changes to the version as soon as they save and refresh. Published requests use the read-only token, never a version, and stay cached.
Promotion and revalidation
Promoting a version updates the main item, which fires items.update and runs the revalidation flow described in Directus flows for revalidation. Nothing special is needed on the frontend. Restrict promotion to roles that are allowed to publish, since promoting is effectively publishing.
Exiting draft mode
Editors move between preview and the public site, and a sticky draft mode causes confusion: an editor who previewed a version yesterday still sees that version today and assumes it is live. Add a visible banner in draft mode that names the version being shown, with a link to leave draft mode, which clears the draft cookie and the version cookie and returns to the public page. Give the draft cookies a short lifetime, a few hours, so they expire on their own. When the editor opens a different item’s preview URL, the draft route replaces the version cookie, so the previous version never leaks into another item’s preview. These small details matter more than they seem: most reports of “preview shows the wrong thing” come from stale draft cookies rather than from bugs in fetching.
For teams that use live preview inside the studio, the same route works in the preview pane. Make sure the frontend allows being framed by the Directus studio’s origin through its content security policy, and nothing else, so preview works without opening the site to framing by arbitrary pages.
Configuration Reference
| Setting | Recommendation | Why |
|---|---|---|
| Versioning | enabled on collections with live edits | Changes stay separate until promoted. |
| Preview URL | draft route with key, version and preview secret | One click from the studio. |
| Version storage | httpOnly cookie in draft mode | Persists across preview navigation. |
| Token | preview token only in draft mode | Versions are not public. |
| Caching | bypass in draft mode | Editors see saves immediately. |
| Promotion | restricted to publishers | Promotion equals publishing. |
Gotchas & Edge Cases
- Relations inside versions. Changes to related items are not part of the version unless they are changes to the relation itself. Preview shows current related items.
- Several versions. Editors can create more than one version; the preview shows whichever version key the URL carries. Name versions clearly.
- Cookies in the studio iframe. Live preview inside the studio uses an iframe on another origin; cookies need
SameSite=None; Secure, as in the example. - Stale versions. A version prepared months ago may conflict with later changes to the main item. Review the merged result before promoting.
Worked Example
The university enabled versioning on course and programme collections. Departments created a version per course for the coming academic year in spring, previewed the full new pages through the draft route, and promoted all versions on the day the new catalogue went live. Relations to modules and instructors stayed intact, the live pages remained correct until the switch, and the promotion day required no content copying.
Combining Versions with Scheduled Publishing
Many version workflows end on a known date, such as the start of a season or a product launch. Directus does not promote versions on a schedule by itself, but a scheduled flow can: a flow with a schedule trigger runs at the chosen time, reads items whose version is marked ready and whose release date has passed, and promotes them through the API. Each promotion fires the usual update events and revalidations. Keep the list of scheduled promotions visible to editors, for example as a field on the item or a small dashboard, and send a notification after the run with successes and failures, so a problem on launch morning is noticed immediately rather than by readers.
Rollout Checklist
- Enable content versioning on collections where published items change.
- Configure preview URLs with key, version and a preview secret.
- Store the version in an httpOnly cookie in draft mode and fetch with the preview token.
- Bypass caches for draft requests.
- Let promotion trigger the normal revalidation flow.
- Restrict promotion to publishing roles.
Frequently Asked Questions
Do versions replace the status field?
No. Status still controls whether new items are published at all. Versions handle changes to items that are already live.
Can the public site ever show a version?
Not directly. Versions are visible only through the preview token in draft mode; the public site always shows the main item until a version is promoted.
What happens to a version after promotion?
It is applied to the main item and removed or kept according to Directus’s behaviour for your version; the main item’s revision history records the change.
Does versioning affect API performance?
Reading with a version key merges changes on the fly, which is fast for single items. It is only used in draft mode anyway, so public traffic never pays any cost for it.