Skip to content

Commit

Permalink
feat(next-drupal): add draft mode for app router pages
Browse files Browse the repository at this point in the history
Issue #502
  • Loading branch information
JohnAlbin committed Jan 29, 2024
1 parent 06fd733 commit 0a1fbf9
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 12 deletions.
15 changes: 15 additions & 0 deletions modules/next/next.post_update.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

/**
* @file
* Post update functions for Next.
*
* All empty post-update hooks ensure the cache is cleared.
* @see https://www.drupal.org/node/2960601
*/

/**
* Add new route for validating draft URLs.
*/
function next_post_update_add_draft_route() {
}
9 changes: 9 additions & 0 deletions modules/next/next.routing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ next.settings:
options:
_admin_route: TRUE

next.validate_draft_url:
path: '/next/draft-url'
defaults:
_controller: 'Drupal\next\Controller\NextPreviewUrlController::validate'
methods: [POST]
requirements:
_access: 'TRUE'
_format: 'json'

next.validate_preview_url:
path: '/next/preview-url'
defaults:
Expand Down
12 changes: 11 additions & 1 deletion packages/next-drupal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@
"default": "./dist/index.cjs"
}
},
"./draft": {
"import": {
"types": "./dist/draft.d.ts",
"default": "./dist/draft.js"
},
"require": {
"types": "./dist/draft.d.cts",
"default": "./dist/draft.cjs"
}
},
"./navigation": {
"import": {
"types": "./dist/navigation.d.ts",
Expand Down Expand Up @@ -66,7 +76,7 @@
},
"dependencies": {
"jsona": "^1.12.1",
"next": "^12.2.0 || ^13 || ^14",
"next": "^13.4 || ^14",
"node-cache": "^5.1.2",
"qs": "^6.11.2",
"react": "^17.0.2 || ^18",
Expand Down
50 changes: 40 additions & 10 deletions packages/next-drupal/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ import type {
const DEFAULT_API_PREFIX = "/jsonapi"
const DEFAULT_FRONT_PAGE = "/home"
const DEFAULT_WITH_AUTH = false
export const DRAFT_DATA_COOKIE_NAME = "draftData"
// See https://vercel.com/docs/workflow-collaboration/draft-mode
export const DRAFT_MODE_COOKIE_NAME = "__prerender_bypass"

// From simple_oauth.
const DEFAULT_AUTH_URL = "/oauth/token"
Expand Down Expand Up @@ -1047,9 +1050,41 @@ export class DrupalClient {
return href
}

async validateDraftUrl(searchParams: URLSearchParams): Promise<Response> {
const slug = searchParams.get("slug")

this.debug(`Fetching draft url validation for ${slug}.`)

// Fetch the headless CMS to check if the provided `slug` exists
let response: Response
try {
// Validate the draft url.
const validateUrl = this.buildUrl("/next/draft-url").toString()
response = await this.fetch(validateUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(Object.fromEntries(searchParams.entries())),
})
} catch (error) {
response = new Response(JSON.stringify({ message: error.message }), {
status: 401,
})
}

this.debug(
response.status !== 200
? `Could not validate slug, ${slug}`
: `Validated slug, ${slug}`
)

return response
}

async preview(
request?: NextApiRequest,
response?: NextApiResponse,
request: NextApiRequest,
response: NextApiResponse,
options?: PreviewOptions
) {
const { slug, resourceVersion, plugin } = request.query
Expand All @@ -1059,14 +1094,9 @@ export class DrupalClient {
response.clearPreviewData()

// Validate the preview url.
const validateUrl = this.buildUrl("/next/preview-url")
const result = await this.fetch(validateUrl.toString(), {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(request.query),
})
const result = await this.validateDraftUrl(
new URL(request.url).searchParams
)

if (!result.ok) {
response.statusCode = result.status
Expand Down
73 changes: 73 additions & 0 deletions packages/next-drupal/src/draft.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { cookies, draftMode } from "next/headers"
import { redirect } from "next/navigation"
import { DRAFT_DATA_COOKIE_NAME, DRAFT_MODE_COOKIE_NAME } from "./client"
import type { NextRequest } from "next/server"
import type { DrupalClient } from "./client"

export async function enableDraftMode(
request: NextRequest,
drupal: DrupalClient
): Promise<Response | never> {
// Validate the draft request.
const response = await drupal.validateDraftUrl(request.nextUrl.searchParams)

// If validation fails, don't enable draft mode.
if (!response.ok) {
return response
}

const searchParams = request.nextUrl.searchParams
const slug = searchParams.get("slug")

// Enable Draft Mode by setting the cookie
draftMode().enable()

// Override the default SameSite=lax.
// See https://github.com/vercel/next.js/issues/49927
const draftModeCookie = cookies().get(DRAFT_MODE_COOKIE_NAME)
if (draftModeCookie) {
cookies().set({
...draftModeCookie,
sameSite: "none",
secure: true,
})
}

// Send Drupal's data to the draft-mode page.
const { secret, scope, plugin, ...draftData } = Object.fromEntries(
searchParams.entries()
)
cookies().set({
...draftModeCookie,
name: DRAFT_DATA_COOKIE_NAME,
sameSite: "none",
secure: true,
value: JSON.stringify(draftData),
})

// Redirect to the path from the fetched post. We can safely redirect to the
// slug since this has been validated on the server.
redirect(slug)
}

export function disableDraftMode() {
cookies().delete(DRAFT_DATA_COOKIE_NAME)
draftMode().disable()

return new Response("Draft mode is disabled")
}

export interface DraftData {
slug?: string
resourceVersion?: string
}

export function getDraftData() {
let data: DraftData = {}

if (draftMode().isEnabled && cookies().has(DRAFT_DATA_COOKIE_NAME)) {
data = JSON.parse(cookies().get(DRAFT_DATA_COOKIE_NAME)?.value || "{}")
}

return data
}
2 changes: 1 addition & 1 deletion packages/next-drupal/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { defineConfig } from "tsup"

export const tsup = defineConfig({
entry: ["src/index.ts", "src/navigation.ts"],
entry: ["src/index.ts", "src/draft.ts", "src/navigation.ts"],
// Enable experimental code splitting support in CommonJS.
// splitting: true,
// Use Rollup for tree shaking.
Expand Down

0 comments on commit 0a1fbf9

Please sign in to comment.