Skip to content

Commit

Permalink
feat(next-drupal): add example-router-migration
Browse files Browse the repository at this point in the history
  • Loading branch information
JohnAlbin committed Jan 30, 2024
1 parent 083cebb commit 31958b5
Show file tree
Hide file tree
Showing 41 changed files with 1,175 additions and 17 deletions.
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

0 comments on commit 31958b5

Please sign in to comment.