Skip to content

Commit b539450

Browse files
committed
feat(basic-starter): upgrade starters to App Router
Fixes #601
1 parent 7efff75 commit b539450

File tree

31 files changed

+447
-413
lines changed

31 files changed

+447
-413
lines changed

starters/basic-starter/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Basic Starter
22

3-
A simple starter for building your site with Next.js' Pages Router and Drupal.
3+
A simple starter for building your site with Next.js and Drupal.
44

55
## How to use
66

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { draftMode } from "next/headers"
2+
import { notFound } from "next/navigation"
3+
import { getDraftData } from "next-drupal/draft"
4+
import { Article } from "@/components/drupal/Article"
5+
import { BasicPage } from "@/components/drupal/BasicPage"
6+
import { drupal } from "@/lib/drupal"
7+
import type { Metadata, ResolvingMetadata } from "next"
8+
import type { DrupalNode, JsonApiParams } from "next-drupal"
9+
10+
async function getNode(slug: string[]) {
11+
const path = `/${slug.join("/")}`
12+
13+
const params: JsonApiParams = {}
14+
15+
const draftData = getDraftData()
16+
17+
if (draftData.path === path) {
18+
params.resourceVersion = draftData.resourceVersion
19+
}
20+
21+
// Translating the path also allows us to discover the entity type.
22+
const translatedPath = await drupal.translatePath(path)
23+
24+
if (!translatedPath) {
25+
throw new Error("Resource not found", { cause: "NotFound" })
26+
}
27+
28+
const type = translatedPath.jsonapi?.resourceName!
29+
const uuid = translatedPath.entity.uuid
30+
31+
if (type === "node--article") {
32+
params.include = "field_image,uid"
33+
}
34+
35+
const resource = await drupal.getResource<DrupalNode>(type, uuid, {
36+
params,
37+
})
38+
39+
if (!resource) {
40+
throw new Error(
41+
`Failed to fetch resource: ${translatedPath?.jsonapi?.individual}`,
42+
{
43+
cause: "DrupalError",
44+
}
45+
)
46+
}
47+
48+
return resource
49+
}
50+
51+
type NodePageParams = {
52+
slug: string[]
53+
}
54+
type NodePageProps = {
55+
params: NodePageParams
56+
searchParams: { [key: string]: string | string[] | undefined }
57+
}
58+
59+
export async function generateMetadata(
60+
{ params: { slug } }: NodePageProps,
61+
parent: ResolvingMetadata
62+
): Promise<Metadata> {
63+
let node
64+
try {
65+
node = await getNode(slug)
66+
} catch (e) {
67+
// If we fail to fetch the node, don't return any metadata.
68+
return {}
69+
}
70+
71+
return {
72+
title: node.title,
73+
}
74+
}
75+
76+
const RESOURCE_TYPES = ["node--page", "node--article"]
77+
78+
export async function generateStaticParams(): Promise<NodePageParams[]> {
79+
const resources = await drupal.getResourceCollectionPathSegments(
80+
RESOURCE_TYPES,
81+
{
82+
// The pathPrefix will be removed from the returned path segments array.
83+
// pathPrefix: "/blog",
84+
// The list of locales to return.
85+
// locales: ["en", "es"],
86+
// The default locale.
87+
// defaultLocale: "en",
88+
}
89+
)
90+
91+
return resources.map((resource) => {
92+
// resources is an array containing objects like: {
93+
// path: "/blog/some-category/a-blog-post",
94+
// type: "node--article",
95+
// locale: "en", // or `undefined` if no `locales` requested.
96+
// segments: ["blog", "some-category", "a-blog-post"],
97+
// }
98+
return {
99+
slug: resource.segments,
100+
}
101+
})
102+
}
103+
104+
export default async function NodePage({
105+
params: { slug },
106+
searchParams,
107+
}: NodePageProps) {
108+
const isDraftMode = draftMode().isEnabled
109+
110+
let node
111+
try {
112+
node = await getNode(slug)
113+
} catch (error) {
114+
// If getNode throws an error, tell Next.js the path is 404.
115+
notFound()
116+
}
117+
118+
// If we're not in draft mode and the resource is not published, return a 404.
119+
if (!isDraftMode && node?.status === false) {
120+
notFound()
121+
}
122+
123+
return (
124+
<>
125+
{node.type === "node--page" && <BasicPage node={node} />}
126+
{node.type === "node--article" && <Article node={node} />}
127+
</>
128+
)
129+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { disableDraftMode } from "next-drupal/draft"
2+
import type { NextRequest } from "next/server"
3+
4+
export async function GET(request: NextRequest) {
5+
return disableDraftMode()
6+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { drupal } from "@/lib/drupal"
2+
import { enableDraftMode } from "next-drupal/draft"
3+
import type { NextRequest } from "next/server"
4+
5+
export async function GET(request: NextRequest): Promise<Response | never> {
6+
return enableDraftMode(request, drupal)
7+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { revalidatePath } from "next/cache"
2+
import type { NextRequest } from "next/server"
3+
4+
async function handler(request: NextRequest) {
5+
const searchParams = request.nextUrl.searchParams
6+
const path = searchParams.get("path")
7+
const secret = searchParams.get("secret")
8+
9+
// Validate secret.
10+
if (secret !== process.env.DRUPAL_REVALIDATE_SECRET) {
11+
return new Response("Invalid secret.", { status: 401 })
12+
}
13+
14+
// Validate path.
15+
if (!path) {
16+
return new Response("Invalid path.", { status: 400 })
17+
}
18+
19+
try {
20+
revalidatePath(path)
21+
22+
return new Response("Revalidated.")
23+
} catch (error) {
24+
return new Response((error as Error).message, { status: 500 })
25+
}
26+
}
27+
28+
export { handler as GET, handler as POST }

starters/basic-starter/app/layout.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { DraftAlert } from "@/components/misc/DraftAlert"
2+
import { HeaderNav } from "@/components/navigation/HeaderNav"
3+
import type { Metadata } from "next"
4+
import type { ReactNode } from "react"
5+
6+
import "@/styles/globals.css"
7+
8+
export const metadata: Metadata = {
9+
title: {
10+
default: "Next.js for Drupal",
11+
template: "%s | Next.js for Drupal",
12+
},
13+
description: "A Next.js site powered by a Drupal backend.",
14+
icons: {
15+
icon: "/favicon.ico",
16+
},
17+
}
18+
19+
export default function RootLayout({
20+
// Layouts must accept a children prop.
21+
// This will be populated with nested layouts or pages
22+
children,
23+
}: {
24+
children: ReactNode
25+
}) {
26+
return (
27+
<html lang="en">
28+
<body>
29+
<DraftAlert />
30+
<div className="max-w-screen-md px-6 mx-auto">
31+
<HeaderNav />
32+
<main className="container py-10 mx-auto">{children}</main>
33+
</div>
34+
</body>
35+
</html>
36+
)
37+
}
Lines changed: 9 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
import Head from "next/head"
21
import { ArticleTeaser } from "@/components/drupal/ArticleTeaser"
3-
import { Layout } from "@/components/Layout"
42
import { drupal } from "@/lib/drupal"
5-
import type { InferGetStaticPropsType, GetStaticProps } from "next"
3+
import type { Metadata } from "next"
64
import type { DrupalNode } from "next-drupal"
75

8-
export const getStaticProps = (async (context) => {
9-
const nodes = await drupal.getResourceCollectionFromContext<DrupalNode[]>(
6+
export const metadata: Metadata = {
7+
description: "A Next.js site powered by a Drupal backend.",
8+
}
9+
10+
export default async function Home() {
11+
const nodes = await drupal.getResourceCollection<DrupalNode[]>(
1012
"node--article",
11-
context,
1213
{
1314
params: {
1415
"filter[status]": 1,
@@ -19,28 +20,8 @@ export const getStaticProps = (async (context) => {
1920
}
2021
)
2122

22-
return {
23-
props: {
24-
nodes,
25-
},
26-
}
27-
}) satisfies GetStaticProps<{
28-
nodes: DrupalNode[]
29-
}>
30-
31-
export default function Home({
32-
nodes,
33-
}: InferGetStaticPropsType<typeof getStaticProps>) {
3423
return (
35-
<Layout>
36-
<Head>
37-
<title>Next.js for Drupal</title>
38-
<meta
39-
name="description"
40-
content="A Next.js site powered by a Drupal backend."
41-
key="description"
42-
/>
43-
</Head>
24+
<>
4425
<h1 className="mb-10 text-6xl font-black">Latest Articles.</h1>
4526
{nodes?.length ? (
4627
nodes.map((node) => (
@@ -52,6 +33,6 @@ export default function Home({
5233
) : (
5334
<p className="py-4">No nodes found</p>
5435
)}
55-
</Layout>
36+
</>
5637
)
5738
}

starters/basic-starter/components/Layout.tsx

Lines changed: 0 additions & 15 deletions
This file was deleted.

starters/basic-starter/components/misc/PreviewAlert.tsx renamed to starters/basic-starter/components/misc/DraftAlert/Client.tsx

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,36 @@
1+
"use client"
2+
13
import { useEffect, useState } from "react"
2-
import { useRouter } from "next/router"
34

4-
export function PreviewAlert() {
5-
const router = useRouter()
6-
const isPreview = router.isPreview
7-
const [showPreviewAlert, setShowPreviewAlert] = useState<boolean>(false)
5+
export function DraftAlertClient({
6+
isDraftEnabled,
7+
}: {
8+
isDraftEnabled: boolean
9+
}) {
10+
const [showDraftAlert, setShowDraftAlert] = useState<boolean>(false)
811

912
useEffect(() => {
10-
setShowPreviewAlert(isPreview && window.top === window.self)
11-
}, [isPreview])
13+
setShowDraftAlert(isDraftEnabled && window.top === window.self)
14+
}, [isDraftEnabled])
1215

13-
if (!showPreviewAlert) {
16+
if (!showDraftAlert) {
1417
return null
1518
}
1619

1720
function buttonHandler() {
18-
void fetch("/api/exit-preview")
19-
setShowPreviewAlert(false)
21+
void fetch("/api/disable-draft")
22+
setShowDraftAlert(false)
2023
}
2124

2225
return (
2326
<div className="sticky top-0 left-0 z-50 w-full px-2 py-1 text-center text-white bg-black">
2427
<p className="mb-0">
25-
This page is a preview.{" "}
28+
This page is a draft.
2629
<button
2730
className="inline-block ml-3 rounded border px-1.5 hover:bg-white hover:text-black active:bg-gray-200 active:text-gray-500"
2831
onClick={buttonHandler}
2932
>
30-
Exit preview mode
33+
Exit draft mode
3134
</button>
3235
</p>
3336
</div>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Suspense } from "react"
2+
import { draftMode } from "next/headers"
3+
import { DraftAlertClient } from "./Client"
4+
5+
export function DraftAlert() {
6+
const isDraftEnabled = draftMode().isEnabled
7+
8+
return (
9+
<Suspense fallback={null}>
10+
<DraftAlertClient isDraftEnabled={isDraftEnabled} />
11+
</Suspense>
12+
)
13+
}

starters/basic-starter/lib/drupal.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
import { NextDrupalPages } from "next-drupal"
1+
import { NextDrupal } from "next-drupal"
22

33
const baseUrl = process.env.NEXT_PUBLIC_DRUPAL_BASE_URL as string
44
const clientId = process.env.DRUPAL_CLIENT_ID as string
55
const clientSecret = process.env.DRUPAL_CLIENT_SECRET as string
66

7-
export const drupal = new NextDrupalPages(baseUrl, {
7+
export const drupal = new NextDrupal(baseUrl, {
88
auth: {
99
clientId,
1010
clientSecret,
1111
},
12-
useDefaultEndpoints: true,
1312
// debug: true,
1413
})

0 commit comments

Comments
 (0)