Skip to content

Commit e704edf

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

File tree

14 files changed

+200
-195
lines changed

14 files changed

+200
-195
lines changed
Lines changed: 85 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,14 @@
1-
import Head from "next/head"
1+
import { draftMode } from "next/headers"
2+
import { notFound } from "next/navigation"
3+
import { getDraftData } from "next-drupal/draft"
24
import { Article } from "@/components/drupal/Article"
35
import { BasicPage } from "@/components/drupal/BasicPage"
4-
import { Layout } from "@/components/Layout"
56
import { drupal } from "@/lib/drupal"
6-
import type {
7-
GetStaticPaths,
8-
GetStaticProps,
9-
InferGetStaticPropsType,
10-
} from "next"
7+
import type { Metadata, ResolvingMetadata } from "next"
118
import type { DrupalArticle, DrupalPage, NodesPath } from "@/types"
129

13-
export const getStaticPaths = (async (context) => {
14-
// Fetch the paths for the first 50 articles and pages.
15-
// We'll fall back to on-demand generation for the rest.
16-
const data = await drupal.query<{
17-
nodeArticles: NodesPath
18-
nodePages: NodesPath
19-
}>({
20-
query: `query {
21-
nodeArticles(first: 50) {
22-
nodes {
23-
path,
24-
}
25-
}
26-
nodePages(first: 50) {
27-
nodes {
28-
path,
29-
}
30-
}
31-
}`,
32-
})
33-
34-
// Build static paths.
35-
const paths = [
36-
...(data?.nodeArticles?.nodes as { path: string }[]),
37-
...(data?.nodePages?.nodes as { path: string }[]),
38-
].map(({ path }) => ({ params: { slug: path.split("/").filter(Boolean) } }))
39-
40-
return {
41-
paths,
42-
fallback: "blocking",
43-
}
44-
}) satisfies GetStaticPaths
45-
46-
export const getStaticProps = (async (context) => {
47-
if (!context?.params?.slug) {
48-
return {
49-
notFound: true,
50-
}
51-
}
10+
async function getNode(slug: string[]) {
11+
const path = `/${slug.join("/")}`
5212

5313
const data = await drupal.query<{
5414
route: { entity: DrupalArticle | DrupalPage }
@@ -92,46 +52,96 @@ export const getStaticProps = (async (context) => {
9252
}
9353
}`,
9454
variables: {
95-
path: `/${(context.params.slug as []).join("/")}`,
55+
path,
9656
},
9757
})
9858

9959
const resource = data?.route?.entity
10060

101-
// If we're not in preview mode and the resource is not published,
102-
// Return page not found.
103-
if (!resource || (!context.preview && resource?.status === false)) {
104-
return {
105-
notFound: true,
106-
}
61+
if (!resource) {
62+
throw new Error(`Failed to fetch resource: ${path}`, {
63+
cause: "DrupalError",
64+
})
65+
}
66+
67+
return resource
68+
}
69+
70+
type NodePageParams = {
71+
slug: string[]
72+
}
73+
type NodePageProps = {
74+
params: NodePageParams
75+
searchParams: { [key: string]: string | string[] | undefined }
76+
}
77+
78+
export async function generateMetadata(
79+
{ params: { slug } }: NodePageProps,
80+
parent: ResolvingMetadata
81+
): Promise<Metadata> {
82+
let node
83+
try {
84+
node = await getNode(slug)
85+
} catch (e) {
86+
// If we fail to fetch the node, don't return any metadata.
87+
return {}
10788
}
10889

10990
return {
110-
props: {
111-
resource,
112-
},
91+
title: node.title,
92+
}
93+
}
94+
95+
export async function generateStaticParams(): Promise<NodePageParams[]> {
96+
// Fetch the paths for the first 50 articles and pages.
97+
// We'll fall back to on-demand generation for the rest.
98+
const data = await drupal.query<{
99+
nodeArticles: NodesPath
100+
nodePages: NodesPath
101+
}>({
102+
query: `query {
103+
nodeArticles(first: 50) {
104+
nodes {
105+
path,
106+
}
107+
}
108+
nodePages(first: 50) {
109+
nodes {
110+
path,
111+
}
112+
}
113+
}`,
114+
})
115+
116+
return [
117+
...(data?.nodeArticles?.nodes as { path: string }[]),
118+
...(data?.nodePages?.nodes as { path: string }[]),
119+
].map(({ path }) => ({ slug: path.split("/").filter(Boolean) }))
120+
}
121+
122+
export default async function Page({
123+
params: { slug },
124+
searchParams,
125+
}: NodePageProps) {
126+
const isDraftMode = draftMode().isEnabled
127+
128+
let node
129+
try {
130+
node = await getNode(slug)
131+
} catch (error) {
132+
// If getNode throws an error, tell Next.js the path is 404.
133+
notFound()
113134
}
114-
}) satisfies GetStaticProps<{
115-
resource: DrupalArticle | DrupalPage
116-
}>
117135

118-
export default function Page({
119-
resource,
120-
}: InferGetStaticPropsType<typeof getStaticProps>) {
121-
if (!resource) return null
136+
// If we're not in draft mode and the resource is not published, return a 404.
137+
if (!isDraftMode && node?.status === false) {
138+
notFound()
139+
}
122140

123141
return (
124-
<Layout>
125-
<Head>
126-
<title>{resource.title}</title>
127-
<meta
128-
name="description"
129-
content="A Next.js site powered by Drupal."
130-
key="description"
131-
/>
132-
</Head>
133-
{resource.__typename === "NodePage" && <BasicPage node={resource} />}
134-
{resource.__typename === "NodeArticle" && <Article node={resource} />}
135-
</Layout>
142+
<>
143+
{node.__typename === "NodePage" && <BasicPage node={node} />}
144+
{node.__typename === "NodeArticle" && <Article node={node} />}
145+
</>
136146
)
137147
}

app/api/disable-draft/route.ts

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+
}

app/api/draft/route.ts

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+
}

app/api/revalidate/route.ts

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 }

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+
}

pages/index.tsx renamed to app/page.tsx

Lines changed: 9 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
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 { DrupalArticle } from "@/types"
75

8-
export const getStaticProps = (async (context) => {
6+
export const metadata: Metadata = {
7+
description: "A Next.js site powered by a Drupal backend.",
8+
}
9+
10+
export default async function Home() {
911
// Fetch the first 10 articles.
1012
const data = await drupal.query<{
1113
nodeArticles: {
@@ -38,29 +40,10 @@ export const getStaticProps = (async (context) => {
3840
}
3941
`,
4042
})
43+
const nodes = data?.nodeArticles?.nodes ?? []
4144

42-
return {
43-
props: {
44-
nodes: data?.nodeArticles?.nodes ?? [],
45-
},
46-
}
47-
}) satisfies GetStaticProps<{
48-
nodes: DrupalArticle[]
49-
}>
50-
51-
export default function Home({
52-
nodes,
53-
}: InferGetStaticPropsType<typeof getStaticProps>) {
5445
return (
55-
<Layout>
56-
<Head>
57-
<title>Next.js for Drupal</title>
58-
<meta
59-
name="description"
60-
content="A Next.js site powered by a Drupal backend."
61-
key="description"
62-
/>
63-
</Head>
46+
<>
6447
<h1 className="mb-10 text-6xl font-black">Latest Articles.</h1>
6548
{nodes?.length ? (
6649
nodes.map((node) => (
@@ -72,6 +55,6 @@ export default function Home({
7255
) : (
7356
<p className="py-4">No nodes found</p>
7457
)}
75-
</Layout>
58+
</>
7659
)
7760
}

components/layout.tsx

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

components/misc/PreviewAlert.tsx renamed to 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>

0 commit comments

Comments
 (0)