Draft mode in Makeswift

How Makeswift uses Draft Mode

Miguel Oller

Arvin Poddar

Updated September 18, 2024

When using the Makeswift builder to edit pages, you can view your draft page content at the same URL as your live pages. But how is it possible to display different versions of a page at the same URL?



Under the hood, Makeswift is leveraging Next.js Draft Mode. Draft Mode is a mechanism provided by Next.js for bypassing the cache of statically generated pages to view draft content from a headless CMS. With this feature, a user can preview draft content at request time even though the page was generated at build time. In our case, Makeswift is acting as the headless CMS. Each page in Makeswift has two variants:



  • The “draft” page — The version of the page that is being edited in the builder.
  • The “live” page — The version of the page that people see when they visit your site. This version is only updated when you publish changes from the builder.



Makeswift takes advantage of Draft Mode to display the draft version of your page while editing or previewing with our builder. These pages are Draft Mode pages — the builder displays the draft page, while the live pages are the built pages.



Let’s walk through how this all works.

💡 Note: Prior to Next.js introducing App Router, the comparable equivalent of Draft Mode was Preview Mode. We still use Preview Mode for sites that are built on Pages Router, but in this post, we’ll only discuss Draft Mode — the implementations are largely similar.

The DraftModeScript

During manual installation, one of the steps involves modifying the root layout to include a <DraftModeScript /> in the layout head — meaning that it’s included in each of your pages.



When your page is requested, we use the draftMode()function from 'next/headers' to determine if the request is for a draft page. We discuss how the draftMode() function works in a later section. We then set a flag in the <DraftModeScript> that will tell us if the draft page was requested and inject it into the layout.



The primary responsibility of this script is to ensure that the page is running in the correct mode (”Live” or “Draft”) depending on whether it’s loaded in the builder or not. This is done via a postMessage protocol between the builder window and the iframe where the site is rendered inside the builder. Each message being exchanged in this protocol has a type field and some payload data, if applicable.



  1. First, the script checks if the page is running inside an iframe. If it isn’t, don’t do anything else, since the page must not be running in the builder.
  2. Add an event listener for a postMessage event.
    1. If the received message came from the builder and has a typeequal to "makeswift_draft_mode", get the secret contained in the message payload. This secret is your Makeswift site API key!
    2. Recall that the script has a flag that is set if the draft page was requested. The script also checks if the current URL of the page contains a query param corresponding to Draft Mode. If both of these are not true, we replace the location of the script window to include this query param. Basically, we rewrite our URL to be a Draft Mode URL in order to “force” the page to enter Draft Mode.
    3. If either of these are true, we modify the global fetch method to add a X-Makeswift-Draft-Modeheader for any request to our host origin. This trick helps keep us in Draft Mode as we client-side navigate in an already loaded draft page!
  3. Send a message to the parent window with a type of "makeswift_draft_mode".



The Builder

Meanwhile, the Makeswift builder is listening for messages from the iframe. When the builder receives a message of type "makeswift_draft_mode", it verifies that the message came from your site origin. If so, the builder sends a message back to the iframe containing the secret API key and the same message type. This corresponds to the <DraftModeScript> awaiting messages from the builder in Step 2 above.



How does the builder know what your site origin is?



The builder uses the current page ID to look up the page’s path and its site host origin. With the origin of the site, the builder can initiate a request to the host at /api/makeswift/manifest to retrieve the site manifest, attaching the API key as a query param. The MakeswiftAPIHandler, which is exported from your site under /api/makeswift, receives this request and returns the site manifest, an object containing various host settings (one of which is draftMode).



If draftMode is enabled in the manifest, the builder constructs the URL for the page and attaches the x-makeswift-draft-mode query param. Ultimately, this URL is what is shown in the iframe, allowing us to see the draft version of the page in the builder.



Makeswift Next.js Plugin

In Next.js, enabling Draft Mode means settings a __prerender_bypass cookie that contains a unique token value that is set at build time. Every call to draftMode() from 'next/headers' is checking if this cookie is present on the request.



If this cookie makes it back to your browser, it means that all of your requests to your site will show you draft content until the cookie is cleared. This isn’t what we want: we only want to see draft content in the builder, but visiting a live page directly should only show published content. So, how do we see draft content without setting this cookie in our browser?



Makeswift handles this using rewrite rules that pass your request through a proxy. In your site’s next.config.js file, your config is wrapped with a withMakeswift enhancer from @makeswift/runtime/next/plugin. This enhancer includes rewrite rules so that any request to your site containing a x-makeswift-draft-mode header or query param gets rewritten to your site’s /api/makeswift/proxy-draft-mode route.



This route acts as a proxy. First, it creates a clone of the inbound request, removing the Draft Mode query param so that the cloned request doesn’t return to the proxy. Then, it enables Draft Mode on the current response and reads the unique token value. We now attach this cookie as part of the cookies of the cloned request. We also attach another cookie (called the draft data cookie) to the cloned request, which contains a static object:



{
	makeswift: true,
	// The "Working" const is a legacy term we use to identify a "Draft" page
	siteVersion: MakeswiftSiteVersion.Working
}



Now, we can disable Draft Mode on the current response, kick off the cloned request, and return the response of the cloned request. Because we disabled Draft Mode on the original response, but still attached a __prerender_bypass cookie to the proxied request, the final response is a Makeswift page with draft data, and we never actually have to set the Draft Mode cookie on your browser!



Using the Draft Data

In our site, we have a app/[[...path]]/page.tsx file which defines a catch-all route. This file contains our Page component:



import { notFound } from 'next/navigation'
import { Page as MakeswiftPage } from '@makeswift/runtime/next'
import { getSiteVersion } from '@makeswift/runtime/next/server'
import { client } from '@/lib/makeswift/client'

export default async function Page({ params }: { params: ParsedUrlQuery }) {
  const path = '/' + (params?.path ?? []).join('/')
  const snapshot = await client.getPageSnapshot(path, {
    siteVersion: getSiteVersion(),
  })

  if (snapshot == null) return notFound()

  return <MakeswiftPage snapshot={snapshot} />
}



The getSiteVersion function checks the cookies on the incoming request. We have two cookies that we’re looking for: a Draft Mode cookie and a draft data cookie. The Draft Mode cookie will tell us if the request is for a draft page, and the draft data cookie will tell us the site version (either live or draft) to request.



We can now see how Makeswift is serving as a headless CMS: we use the Makeswift client to fetch a different version of the page based on whether we’re in Draft Mode or not.



Putting it all together

Now that we are aware of how the parts link together, we can construct an order of events for our page.



  1. When manually installing Makeswift, we included a <DraftModeScript> in the head of our root layout. This script will run whenever any of your pages load.
  2. During Makeswift installation, we wrapped your next.config.js file with the withMakeswift enhancer (from the Makeswift Next.js plugin). This wrapper adds rewrites to our site for any request with either:
    1. A "x-makeswift-draft-mode" query param (and associated secret site API key)
    2. A "X-Makeswift-Draft-Mode"header (and associated secret site API key). These page requests are rewritten to /api/makeswift/proxy-draft-mode, which is an API route that was added to your site with the MakeswiftAPIHandler.
  3. We load a page from our site in the Makeswift builder.
  4. The builder looks up our page to find our site origin, which it uses to fetch the site manifest via the /api/makeswift/manifest route of our site. If your manifest has draftMode enabled, the builder construct a URL for the page, attaching a x-makeswift-draft-mode query param to the URL. We then load this URL in the builder iframe.
  5. With the source of the iframe set, the page is requested from our host. Because the x-makeswift-draft-mode query param is set, the request is rewritten to the /api/makeswift/proxy-draft-mode route on our site.
  6. At this route, the Draft Mode proxy creates a cloned request with the addition of a __prerender_bypass cookie and a draft data cookie. The proxy kicks off this cloned request.
  7. Now, the proxied request arrives at the hands of the catch-all [[...path]].tsx route. In the Page component, where we fetch the page content with the Makeswift runtime client, we use the getSiteVersion function to read the cookies on the inbound request, allowing us to retrieve the draft version of your page, which we then pass to the <MakeswiftPage> component to render.
  8. Recall that the delivered page content includes the <DraftModeScript> in the page head.
  9. Now, the page is finally loaded in the iframe. At this point, the <DraftModeScript> runs in the iframe.
  10. The <DraftModeScript> checks if the page is running in an iframe. If it is, and the script page has a query param for Draft Mode attached, it attaches an event listener for a postMessage from the builder. Specifically, we’re waiting for a postMessage with a type of "makeswift_draft_mode" and an attached secret API key.
    1. If for some reason the query param wasn’t set, it re-navigates the window location to include the query param.
  11. Regardless, the Draft Mode script sends a postMessage to its parent window (in this case, the builder iframe window). This message has a type of "makeswift_draft_mode". Also, the script removes the secret param from the iframe URL.
  12. Meanwhile, the builder has already registered a listener for a post message of type "makeswift_draft_mode".
  13. When the builder gets this message from our page in the iframe (from Step 11), the builder responds with a "makeswift_draft_mode" message containing the site API key. This will trigger the functionality described in the listener declared in Step 10, completing the postMessage handshake. This confirms to the builder that the page in the iframe is running in Draft Mode.



And that’s how Makeswift leverages Next.js Draft Mode to display the draft versions of your page in the builder, while showing the live version everywhere else!

If you’re interested in taking a deeper look under the hood, check out out our open source runtime repo at https://github.com/makeswift/makeswift.