Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add draft mode support #670

Merged
merged 6 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion examples/example-graphql/lib/drupal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ export const drupal = new DrupalClient(
clientSecret: process.env.DRUPAL_CLIENT_SECRET,
},
previewSecret: process.env.DRUPAL_PREVIEW_SECRET,
forceIframeSameSiteCookie: true,
}
)

Expand Down
12 changes: 12 additions & 0 deletions examples/example-router-migration/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# See https://next-drupal.org/docs/environment-variables

# Required
NEXT_PUBLIC_DRUPAL_BASE_URL=https://site.example.com
NEXT_IMAGE_DOMAIN=site.example.com

# Authentication
DRUPAL_CLIENT_ID=Retrieve this from /admin/config/services/consumer
DRUPAL_CLIENT_SECRET=Retrieve this from /admin/config/services/consumer

# Required for On-demand Revalidation
DRUPAL_REVALIDATE_SECRET=Retrieve this from /admin/config/services/next
4 changes: 4 additions & 0 deletions examples/example-router-migration/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "next/core-web-vitals",
"root": true
}
40 changes: 40 additions & 0 deletions examples/example-router-migration/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# IDE files
/.idea
/.vscode

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
1 change: 1 addition & 0 deletions examples/example-router-migration/.nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v20
18 changes: 18 additions & 0 deletions examples/example-router-migration/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Ignore everything.
/*

# Format most files in the root directory.
!/*.js
!/*.ts
!/*.md
!/*.json
# But ignore some.
/package.json
/package-lock.json
/CHANGELOG.md

# Don't ignore these nested directories.
!/app
!/components
!/lib
!/pages
4 changes: 4 additions & 0 deletions examples/example-router-migration/.prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"semi": false,
"trailingComma": "es5"
}
4 changes: 4 additions & 0 deletions examples/example-router-migration/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Change Log

All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
43 changes: 43 additions & 0 deletions examples/example-router-migration/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# example-router-migration

Next.js recommends using their new App Router over the legacy Pages Router. The [full router migration guide](https://nextjs.org/docs/app/building-your-application/upgrading/app-router-migration) is available in the Next.js documentation.

The new App Router is also designed to facilitate sites that need to migrate from the Pages Router in a piecemeal fashion rather than all at once.

This codebase is an example of a `next-drupal` site that is in the middle of a Next.js Pages to App Router migration.

## Piecemeal router migration steps

### Initial migration

1. Update the `next-drupal` package to the latest 2.x version.
2. Update the `next` module on your Drupal site to the latest 2.x version.
1. The most recent version is available at https://www.drupal.org/project/next
2. Run your Drupal site’s /update.php script.
3. Migrate from Preview Mode to Draft mode. Preview mode only works with the legacy Pages Router. Draft mode works with both routers.
1. Update the `/pages/api/preview.ts` file to match the one in this Git repo.
2. Update the `/pages/api/exit-preview.ts` file to match the one in this Git repo.
3. Delete your `/pages/api/revalidate.ts` file.
4. Create a `/app/api` directory and add all the files from this Git repo’s `/app/api` directory.

### Piecemeal migration

Follow [Next.js’ router migration guide](https://nextjs.org/docs/app/building-your-application/upgrading/app-router-migration).

Over time, you will be moving all the files from `/pages` to `/app`. However, these JavaScript files should remain in the `/pages` directory to prevent Preview/Draft Mode from breaking:

- `/pages/api/exit-preview.ts`
- `/pages/api/preview.ts`

### Final migration steps

1. Turn off the legacy Preview Mode.
1. Go to the Next.js site configuration on your Drupal site at `/admin/config/services/next`.
2. For each Next.js configuration, change the end of the URL in the “Draft URL (or Preview URL)” setting from `preview` to `draft`, e.g. `https://example.com/api/preview` to `https://example.com/api/draft`.
2. Delete the last files in your `/pages` directory:
- `/pages/api/exit-preview.ts`
- `/pages/api/preview.ts`

## License

Licensed under the [MIT license](https://github.com/chapter-three/next-drupal/blob/master/LICENSE).
115 changes: 115 additions & 0 deletions examples/example-router-migration/app/[...slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { draftMode } from "next/headers"
import { notFound } from "next/navigation"
import { getDraftData } from "next-drupal/draft"
import { Article } from "@/components/drupal/Article"
import { BasicPage } from "@/components/drupal/BasicPage"
import { drupal } from "@/lib/drupal"
import type { Metadata, ResolvingMetadata } from "next"
import type { DrupalNode, JsonApiParams } from "next-drupal"

async function getNode(slug: string[]) {
const path = slug.join("/")

const params: JsonApiParams = {}

const draftData = getDraftData()

if (draftData.slug === `/${path}`) {
params.resourceVersion = draftData.resourceVersion
}

// Translating the path also allows us to discover the entity type.
const translatedPath = await drupal.translatePath(path)

if (!translatedPath) {
throw new Error("Resource not found", { cause: "NotFound" })
}

const type = translatedPath.jsonapi?.resourceName!
const uuid = translatedPath.entity.uuid

if (type === "node--article") {
params.include = "field_image,uid"
}

const resource = await drupal.getResource<DrupalNode>(type, uuid, {
params,
})

if (!resource) {
throw new Error(
`Failed to fetch resource: ${translatedPath?.jsonapi?.individual}`,
{
cause: "DrupalError",
}
)
}

return resource
}

type NodePageParams = {
slug: string[]
}
type NodePageProps = {
params: NodePageParams
searchParams: { [key: string]: string | string[] | undefined }
}

export async function generateMetadata(
{ params: { slug } }: NodePageProps,
parent: ResolvingMetadata
): Promise<Metadata> {
let node
try {
node = await getNode(slug)
} catch (e) {
// If we fail to fetch the node, don't return any metadata.
return {}
}

return {
title: node.title,
}
}

const RESOURCE_TYPES = ["node--page", "node--article"]

export async function generateStaticParams(): Promise<NodePageParams[]> {
// TODO: Replace getStaticPathsFromContext() usage since there is no context.
const paths = await drupal.getStaticPathsFromContext(RESOURCE_TYPES, {})
// console.log(
// "generateStaticParams",
// paths.map(({ params }) => params)
// )
return paths.map((path: string | { params: NodePageParams }) =>
typeof path === "string" ? { slug: [] } : path?.params
)
}

export default async function NodePage({
params: { slug },
searchParams,
}: NodePageProps) {
const isDraftMode = draftMode().isEnabled

let node
try {
node = await getNode(slug)
} catch (error) {
// If getNode throws an error, tell Next.js the path is 404.
notFound()
}

// If we're not in draft mode and the resource is not published, return a 404.
if (!isDraftMode && node?.status === false) {
notFound()
}

return (
<>
{node.type === "node--page" && <BasicPage node={node} />}
{node.type === "node--article" && <Article node={node} />}
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { disableDraftMode } from "next-drupal/draft"
import type { NextRequest } from "next/server"

export async function GET(request: NextRequest) {
return disableDraftMode()
}
7 changes: 7 additions & 0 deletions examples/example-router-migration/app/api/draft/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { drupal } from "@/lib/drupal"
import { enableDraftMode } from "next-drupal/draft"
import type { NextRequest } from "next/server"

export async function GET(request: NextRequest): Promise<Response | never> {
return enableDraftMode(request, drupal)
}
28 changes: 28 additions & 0 deletions examples/example-router-migration/app/api/revalidate/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { revalidatePath } from "next/cache"
import type { NextRequest } from "next/server"

async function handler(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
const slug = searchParams.get("slug")
const secret = searchParams.get("secret")

// Validate secret.
if (secret !== process.env.DRUPAL_REVALIDATE_SECRET) {
return new Response("Invalid secret.", { status: 401 })
}

// Validate slug.
if (!slug) {
return new Response("Invalid slug.", { status: 400 })
}

try {
revalidatePath(slug)

return new Response("Revalidated.")
} catch (error) {
return new Response((error as Error).message, { status: 500 })
}
}

export { handler as GET, handler as POST }
37 changes: 37 additions & 0 deletions examples/example-router-migration/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { DraftAlert } from "@/components/misc/DraftAlert"
import { HeaderNav } from "@/components/navigation/HeaderNav"
import type { Metadata } from "next"
import type { ReactNode } from "react"

import "@/styles/globals.css"

export const metadata: Metadata = {
title: {
default: "Next.js for Drupal",
template: "%s | Next.js for Drupal",
},
description: "A Next.js site powered by a Drupal backend.",
icons: {
icon: "/favicon.ico",
},
}

export default function RootLayout({
// Layouts must accept a children prop.
// This will be populated with nested layouts or pages
children,
}: {
children: ReactNode
}) {
return (
<html lang="en">
<body>
<DraftAlert />
<div className="max-w-screen-md px-6 mx-auto">
<HeaderNav />
<main className="container py-10 mx-auto">{children}</main>
</div>
</body>
</html>
)
}
51 changes: 51 additions & 0 deletions examples/example-router-migration/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { ArticleTeaser } from "@/components/drupal/ArticleTeaser"
import { Link } from "@/components/navigation/Link"
import { drupal } from "@/lib/drupal"
import type { Metadata } from "next"
import type { DrupalNode } from "next-drupal"

export const metadata: Metadata = {
description: "A Next.js site powered by a Drupal backend.",
}

export default async function Home() {
const nodes = await drupal.getResourceCollection<DrupalNode[]>(
"node--article",
{
params: {
"filter[status]": 1,
"fields[node--article]": "title,path,field_image,uid,created",
include: "field_image,uid",
sort: "-created",
},
}
)

return (
<>
<h1 className="mb-2 text-6xl font-black leading-6">
Latest Articles.
<br />
<small className="text-xl">
<em>Using the App Router</em>
</small>
</h1>
<p className="mb-10">
Switch to{" "}
<Link href="/pages-router" className="underline hover:text-blue-600">
Pages Router
</Link>
</p>
{nodes?.length ? (
nodes.map((node) => (
<div key={node.id}>
<ArticleTeaser node={node} />
<hr className="my-20" />
</div>
))
) : (
<p className="py-4">No nodes found</p>
)}
</>
)
}
Loading
Loading