Skip to content

Commit

Permalink
feat(basic-starter): upgrade starters to App Router
Browse files Browse the repository at this point in the history
Fixes #601
  • Loading branch information
JohnAlbin committed Mar 11, 2024
1 parent 96c136f commit d01e59c
Show file tree
Hide file tree
Showing 34 changed files with 497 additions and 448 deletions.
2 changes: 1 addition & 1 deletion starters/basic-starter/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Basic Starter

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

## How to use

Expand Down
129 changes: 129 additions & 0 deletions starters/basic-starter/app/[...slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
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.path === 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[]> {
const resources = await drupal.getResourceCollectionPathSegments(
RESOURCE_TYPES,
{
// The pathPrefix will be removed from the returned path segments array.
// pathPrefix: "/blog",
// The list of locales to return.
// locales: ["en", "es"],
// The default locale.
// defaultLocale: "en",
}
)

return resources.map((resource) => {
// resources is an array containing objects like: {
// path: "/blog/some-category/a-blog-post",
// type: "node--article",
// locale: "en", // or `undefined` if no `locales` requested.
// segments: ["blog", "some-category", "a-blog-post"],
// }
return {
slug: resource.segments,
}
})
}

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} />}
</>
)
}
6 changes: 6 additions & 0 deletions starters/basic-starter/app/api/disable-draft/route.ts
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 starters/basic-starter/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 starters/basic-starter/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 path = searchParams.get("path")
const secret = searchParams.get("secret")

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

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

try {
revalidatePath(path)

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 starters/basic-starter/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>
)
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import Head from "next/head"
import { ArticleTeaser } from "@/components/drupal/ArticleTeaser"
import { Layout } from "@/components/Layout"
import { drupal } from "@/lib/drupal"
import type { InferGetStaticPropsType, GetStaticProps } from "next"
import type { Metadata } from "next"
import type { DrupalNode } from "next-drupal"

export const getStaticProps = (async (context) => {
const nodes = await drupal.getResourceCollectionFromContext<DrupalNode[]>(
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",
context,
{
params: {
"filter[status]": 1,
Expand All @@ -19,28 +20,8 @@ export const getStaticProps = (async (context) => {
}
)

return {
props: {
nodes,
},
}
}) satisfies GetStaticProps<{
nodes: DrupalNode[]
}>

export default function Home({
nodes,
}: InferGetStaticPropsType<typeof getStaticProps>) {
return (
<Layout>
<Head>
<title>Next.js for Drupal</title>
<meta
name="description"
content="A Next.js site powered by a Drupal backend."
key="description"
/>
</Head>
<>
<h1 className="mb-10 text-6xl font-black">Latest Articles.</h1>
{nodes?.length ? (
nodes.map((node) => (
Expand All @@ -52,6 +33,6 @@ export default function Home({
) : (
<p className="py-4">No nodes found</p>
)}
</Layout>
</>
)
}
15 changes: 0 additions & 15 deletions starters/basic-starter/components/Layout.tsx

This file was deleted.

38 changes: 38 additions & 0 deletions starters/basic-starter/components/misc/DraftAlert/Client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"use client"

import { useEffect, useState } from "react"

export function DraftAlertClient({
isDraftEnabled,
}: {
isDraftEnabled: boolean
}) {
const [showDraftAlert, setShowDraftAlert] = useState<boolean>(false)

useEffect(() => {
setShowDraftAlert(isDraftEnabled && window.top === window.self)
}, [isDraftEnabled])

if (!showDraftAlert) {
return null
}

function buttonHandler() {
void fetch("/api/disable-draft")
setShowDraftAlert(false)
}

return (
<div className="sticky top-0 left-0 z-50 w-full px-2 py-1 text-center text-white bg-black">
<p className="mb-0">
This page is a draft.
<button
className="inline-block ml-3 rounded border px-1.5 hover:bg-white hover:text-black active:bg-gray-200 active:text-gray-500"
onClick={buttonHandler}
>
Exit draft mode
</button>
</p>
</div>
)
}
13 changes: 13 additions & 0 deletions starters/basic-starter/components/misc/DraftAlert/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Suspense } from "react"
import { draftMode } from "next/headers"
import { DraftAlertClient } from "./Client"

export function DraftAlert() {
const isDraftEnabled = draftMode().isEnabled

return (
<Suspense fallback={null}>
<DraftAlertClient isDraftEnabled={isDraftEnabled} />
</Suspense>
)
}
30 changes: 0 additions & 30 deletions starters/basic-starter/components/misc/PreviewAlert.tsx

This file was deleted.

4 changes: 2 additions & 2 deletions starters/basic-starter/lib/drupal.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { DrupalClient } from "next-drupal"
import { NextDrupal } from "next-drupal"

const baseUrl = process.env.NEXT_PUBLIC_DRUPAL_BASE_URL as string
const clientId = process.env.DRUPAL_CLIENT_ID as string
const clientSecret = process.env.DRUPAL_CLIENT_SECRET as string

export const drupal = new DrupalClient(baseUrl, {
export const drupal = new NextDrupal(baseUrl, {
auth: {
clientId,
clientSecret,
Expand Down
Loading

0 comments on commit d01e59c

Please sign in to comment.