diff --git a/starters/basic-starter/.gitignore b/starters/basic-starter/.gitignore
index 83ec79b6..081b7c17 100644
--- a/starters/basic-starter/.gitignore
+++ b/starters/basic-starter/.gitignore
@@ -6,8 +6,7 @@
.pnp.js
.yarn/install-state.gz
-# build/test artifacts
-/.turbo
+# testing
/coverage
# next.js
diff --git a/starters/basic-starter/README.md b/starters/basic-starter/README.md
index 03dfcfb4..0075ec73 100644
--- a/starters/basic-starter/README.md
+++ b/starters/basic-starter/README.md
@@ -1,6 +1,6 @@
# Basic Starter
-A simple starter for building your site with Next.js and Drupal.
+A simple starter for building your site with Next.js' Pages Router and Drupal.
## How to use
diff --git a/starters/basic-starter/components/Layout.tsx b/starters/basic-starter/components/Layout.tsx
index 672d0b48..cb1f4012 100644
--- a/starters/basic-starter/components/Layout.tsx
+++ b/starters/basic-starter/components/Layout.tsx
@@ -1,5 +1,5 @@
-import Link from "next/link"
-import { PreviewAlert } from "@/components/PreviewAlert"
+import { HeaderNav } from "@/components/navigation/HeaderNav"
+import { PreviewAlert } from "@/components/misc/PreviewAlert"
import type { ReactNode } from "react"
export function Layout({ children }: { children: ReactNode }) {
@@ -7,21 +7,7 @@ export function Layout({ children }: { children: ReactNode }) {
<>
>
diff --git a/starters/basic-starter/components/drupal/ArticleTeaser.tsx b/starters/basic-starter/components/drupal/ArticleTeaser.tsx
index 4909b4a1..8efeac62 100644
--- a/starters/basic-starter/components/drupal/ArticleTeaser.tsx
+++ b/starters/basic-starter/components/drupal/ArticleTeaser.tsx
@@ -1,5 +1,5 @@
import Image from "next/image"
-import Link from "next/link"
+import { Link } from "@/components/navigation/Link"
import { absoluteUrl, formatDate } from "@/lib/utils"
import type { DrupalNode } from "next-drupal"
diff --git a/starters/basic-starter/components/PreviewAlert.tsx b/starters/basic-starter/components/misc/PreviewAlert.tsx
similarity index 100%
rename from starters/basic-starter/components/PreviewAlert.tsx
rename to starters/basic-starter/components/misc/PreviewAlert.tsx
diff --git a/starters/basic-starter/components/navigation/HeaderNav.tsx b/starters/basic-starter/components/navigation/HeaderNav.tsx
new file mode 100644
index 00000000..bccb4e4c
--- /dev/null
+++ b/starters/basic-starter/components/navigation/HeaderNav.tsx
@@ -0,0 +1,21 @@
+import { Link } from "@/components/navigation/Link"
+
+export function HeaderNav() {
+ return (
+
+ )
+}
diff --git a/starters/basic-starter/components/navigation/Link.tsx b/starters/basic-starter/components/navigation/Link.tsx
new file mode 100644
index 00000000..23dbc4c0
--- /dev/null
+++ b/starters/basic-starter/components/navigation/Link.tsx
@@ -0,0 +1,23 @@
+import { forwardRef } from "react"
+import NextLink from "next/link"
+import type { AnchorHTMLAttributes, ReactNode } from "react"
+import type { LinkProps as NextLinkProps } from "next/link"
+
+type LinkProps = NextLinkProps &
+ Omit, keyof NextLinkProps> & {
+ children?: ReactNode
+ }
+
+export const Link = forwardRef(
+ function LinkWithRef(
+ {
+ // Turn next/link prefetching off by default.
+ // @see https://github.com/vercel/next.js/discussions/24009
+ prefetch = false,
+ ...rest
+ },
+ ref
+ ) {
+ return
+ }
+)
diff --git a/starters/basic-starter/lib/drupal.ts b/starters/basic-starter/lib/drupal.ts
index 513b3bf2..3c23e79e 100644
--- a/starters/basic-starter/lib/drupal.ts
+++ b/starters/basic-starter/lib/drupal.ts
@@ -1,12 +1,14 @@
import { DrupalClient } from "next-drupal"
-const baseUrl: string = process.env.NEXT_PUBLIC_DRUPAL_BASE_URL || ""
-const clientId = process.env.DRUPAL_CLIENT_ID || ""
-const clientSecret = process.env.DRUPAL_CLIENT_SECRET || ""
+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, {
auth: {
clientId,
clientSecret,
},
+ // debug: true,
+ // useDefaultResourceTypeEntry: true,
})
diff --git a/starters/basic-starter/pages/api/exit-preview.ts b/starters/basic-starter/pages/api/exit-preview.ts
index a8cf12e8..f8847b39 100644
--- a/starters/basic-starter/pages/api/exit-preview.ts
+++ b/starters/basic-starter/pages/api/exit-preview.ts
@@ -1,10 +1,9 @@
+import { drupal } from "@/lib/drupal"
import type { NextApiRequest, NextApiResponse } from "next"
export default async function exit(
- _: NextApiRequest,
+ request: NextApiRequest,
response: NextApiResponse
) {
- response.clearPreviewData()
- response.writeHead(307, { Location: "/" })
- response.end()
+ await drupal.previewDisable(request, response)
}
diff --git a/starters/basic-starter/pages/api/preview.ts b/starters/basic-starter/pages/api/preview.ts
index 7660eb64..a0733440 100644
--- a/starters/basic-starter/pages/api/preview.ts
+++ b/starters/basic-starter/pages/api/preview.ts
@@ -1,9 +1,10 @@
import { drupal } from "@/lib/drupal"
import type { NextApiRequest, NextApiResponse } from "next"
-export default async function handler(
+export default async function draft(
request: NextApiRequest,
response: NextApiResponse
) {
- await drupal.preview(request, response)
+ // Enables Preview mode and Draft mode.
+ await drupal.preview(request, response, { enable: true })
}
diff --git a/starters/basic-starter/tsconfig.json b/starters/basic-starter/tsconfig.json
index ad54f56d..23ba4fd5 100644
--- a/starters/basic-starter/tsconfig.json
+++ b/starters/basic-starter/tsconfig.json
@@ -14,10 +14,15 @@
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
"paths": {
"@/*": ["./*"]
}
},
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
diff --git a/starters/graphql-starter/.gitignore b/starters/graphql-starter/.gitignore
index 83ec79b6..081b7c17 100644
--- a/starters/graphql-starter/.gitignore
+++ b/starters/graphql-starter/.gitignore
@@ -6,8 +6,7 @@
.pnp.js
.yarn/install-state.gz
-# build/test artifacts
-/.turbo
+# testing
/coverage
# next.js
diff --git a/starters/graphql-starter/components/Layout.tsx b/starters/graphql-starter/components/Layout.tsx
index 672d0b48..cb1f4012 100644
--- a/starters/graphql-starter/components/Layout.tsx
+++ b/starters/graphql-starter/components/Layout.tsx
@@ -1,5 +1,5 @@
-import Link from "next/link"
-import { PreviewAlert } from "@/components/PreviewAlert"
+import { HeaderNav } from "@/components/navigation/HeaderNav"
+import { PreviewAlert } from "@/components/misc/PreviewAlert"
import type { ReactNode } from "react"
export function Layout({ children }: { children: ReactNode }) {
@@ -7,21 +7,7 @@ export function Layout({ children }: { children: ReactNode }) {
<>
>
diff --git a/starters/graphql-starter/components/drupal/Article.tsx b/starters/graphql-starter/components/drupal/Article.tsx
index 2e9a05a3..e761ec1b 100644
--- a/starters/graphql-starter/components/drupal/Article.tsx
+++ b/starters/graphql-starter/components/drupal/Article.tsx
@@ -1,9 +1,9 @@
import Image from "next/image"
import { formatDate } from "@/lib/utils"
-import type { NodeArticle } from "@/types"
+import type { DrupalArticle } from "@/types"
interface ArticleProps {
- node: NodeArticle
+ node: DrupalArticle
}
export function Article({ node, ...props }: ArticleProps) {
@@ -11,13 +11,12 @@ export function Article({ node, ...props }: ArticleProps) {
{node.title}
- {node.author?.displayName ? (
+ {node.author?.name ? (
- Posted by{" "}
- {node.author.displayName}
+ Posted by {node.author.name}
) : null}
- - {formatDate(node.created)}
+ - {formatDate(node.created.time)}
{node.image && (
diff --git a/starters/graphql-starter/components/drupal/ArticleTeaser.tsx b/starters/graphql-starter/components/drupal/ArticleTeaser.tsx
index ed88aca0..fd200f64 100644
--- a/starters/graphql-starter/components/drupal/ArticleTeaser.tsx
+++ b/starters/graphql-starter/components/drupal/ArticleTeaser.tsx
@@ -1,10 +1,10 @@
import Image from "next/image"
-import Link from "next/link"
+import { Link } from "@/components/navigation/Link"
import { formatDate } from "@/lib/utils"
-import type { NodeArticle } from "@/types"
+import type { DrupalArticle } from "@/types"
interface ArticleTeaserProps {
- node: Partial
+ node: Partial
}
export function ArticleTeaser({ node, ...props }: ArticleTeaserProps) {
@@ -14,13 +14,12 @@ export function ArticleTeaser({ node, ...props }: ArticleTeaserProps) {
{node.title}
- {node.author?.displayName ? (
+ {node.author?.name ? (
- Posted by{" "}
- {node.author.displayName}
+ Posted by {node.author.name}
) : null}
- {node.created && - {formatDate(node.created)} }
+ {node.created && - {formatDate(node.created.time)} }
{node.image && (
diff --git a/starters/graphql-starter/components/drupal/BasicPage.tsx b/starters/graphql-starter/components/drupal/BasicPage.tsx
index b4bc2ed6..a310a15a 100644
--- a/starters/graphql-starter/components/drupal/BasicPage.tsx
+++ b/starters/graphql-starter/components/drupal/BasicPage.tsx
@@ -1,7 +1,7 @@
-import type { NodePage } from "@/types"
+import type { DrupalPage } from "@/types"
interface BasicPageProps {
- node: NodePage
+ node: DrupalPage
}
export function BasicPage({ node, ...props }: BasicPageProps) {
diff --git a/starters/graphql-starter/components/PreviewAlert.tsx b/starters/graphql-starter/components/misc/PreviewAlert.tsx
similarity index 100%
rename from starters/graphql-starter/components/PreviewAlert.tsx
rename to starters/graphql-starter/components/misc/PreviewAlert.tsx
diff --git a/starters/graphql-starter/components/navigation/HeaderNav.tsx b/starters/graphql-starter/components/navigation/HeaderNav.tsx
new file mode 100644
index 00000000..bccb4e4c
--- /dev/null
+++ b/starters/graphql-starter/components/navigation/HeaderNav.tsx
@@ -0,0 +1,21 @@
+import { Link } from "@/components/navigation/Link"
+
+export function HeaderNav() {
+ return (
+
+ )
+}
diff --git a/starters/graphql-starter/components/navigation/Link.tsx b/starters/graphql-starter/components/navigation/Link.tsx
new file mode 100644
index 00000000..23dbc4c0
--- /dev/null
+++ b/starters/graphql-starter/components/navigation/Link.tsx
@@ -0,0 +1,23 @@
+import { forwardRef } from "react"
+import NextLink from "next/link"
+import type { AnchorHTMLAttributes, ReactNode } from "react"
+import type { LinkProps as NextLinkProps } from "next/link"
+
+type LinkProps = NextLinkProps &
+ Omit, keyof NextLinkProps> & {
+ children?: ReactNode
+ }
+
+export const Link = forwardRef(
+ function LinkWithRef(
+ {
+ // Turn next/link prefetching off by default.
+ // @see https://github.com/vercel/next.js/discussions/24009
+ prefetch = false,
+ ...rest
+ },
+ ref
+ ) {
+ return
+ }
+)
diff --git a/starters/graphql-starter/lib/drupal.ts b/starters/graphql-starter/lib/drupal.ts
index 7a7921c2..96c77a55 100644
--- a/starters/graphql-starter/lib/drupal.ts
+++ b/starters/graphql-starter/lib/drupal.ts
@@ -1,51 +1,13 @@
-import { DrupalClient } from "next-drupal"
+import { NextDrupalGraphQL } from "./next-drupal-graphql"
-const baseUrl: string = process.env.NEXT_PUBLIC_DRUPAL_BASE_URL || ""
-const clientId = process.env.DRUPAL_CLIENT_ID || ""
-const clientSecret = process.env.DRUPAL_CLIENT_SECRET || ""
+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 NextDrupalGraphQL(baseUrl, {
auth: {
clientId,
clientSecret,
},
+ // debug: true,
})
-
-export const graphqlEndpoint = drupal.buildUrl("/graphql")
-
-type QueryPayload = {
- query: string
- variables?: Record
-}
-
-type QueryJsonResponse = {
- data?: DataType
- errors?: { message: string }[]
-}
-
-// This is a wrapper around drupal.fetch.
-// Acts as a query helper.
-export async function query(payload: QueryPayload) {
- const response = await drupal.fetch(graphqlEndpoint.toString(), {
- method: "POST",
- body: JSON.stringify(payload),
- withAuth: true, // Make authenticated requests using OAuth.
- headers: {
- "Content-Type": "application/json",
- Accept: "application/json",
- },
- })
-
- if (!response?.ok) {
- throw new Error(response.statusText)
- }
-
- const { data, errors }: QueryJsonResponse = await response.json()
-
- if (errors) {
- console.log(errors)
- throw new Error(errors?.map((e) => e.message).join("\n") ?? "unknown")
- }
-
- return data
-}
diff --git a/starters/graphql-starter/lib/next-drupal-graphql.ts b/starters/graphql-starter/lib/next-drupal-graphql.ts
new file mode 100644
index 00000000..c74a0817
--- /dev/null
+++ b/starters/graphql-starter/lib/next-drupal-graphql.ts
@@ -0,0 +1,57 @@
+// This is an example GraphQL implementation using DrupalClient.
+
+import { DrupalClient } from "next-drupal"
+import type { BaseUrl, NextDrupalBaseOptions } from "next-drupal"
+
+const DEFAULT_API_PREFIX = "/graphql"
+
+export class NextDrupalGraphQL extends DrupalClient {
+ endpoint: string
+
+ constructor(baseUrl: BaseUrl, options: NextDrupalBaseOptions = {}) {
+ super(baseUrl, options)
+
+ const { apiPrefix = DEFAULT_API_PREFIX } = options
+
+ this.apiPrefix = apiPrefix
+
+ this.endpoint = this.buildUrl(this.apiPrefix).toString()
+ }
+
+ async query(payload: QueryPayload) {
+ const response = await this.fetch(this.endpoint, {
+ method: "POST",
+ body: JSON.stringify(payload),
+ withAuth: true, // Make authenticated requests using OAuth.
+ // TODO: Remove headers when switching from extending DrupalClient to
+ // NextDrupalBase, since they will be redundant.
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ },
+ })
+
+ if (!response?.ok) {
+ throw new Error(response.statusText)
+ }
+
+ const { data, errors }: QueryJsonResponse = await response.json()
+
+ if (errors) {
+ this.logger.log(errors)
+ throw new Error(errors?.map((e) => e.message).join("\n") ?? "unknown")
+ }
+
+ return data
+ }
+}
+
+type QueryPayload = {
+ query: string
+ variables?: Record
+}
+
+type QueryJsonResponse = {
+ data?: DataType
+ errors?: { message: string }[]
+}
diff --git a/starters/graphql-starter/pages/[...slug].tsx b/starters/graphql-starter/pages/[...slug].tsx
index 80a20f16..4ca04106 100644
--- a/starters/graphql-starter/pages/[...slug].tsx
+++ b/starters/graphql-starter/pages/[...slug].tsx
@@ -2,18 +2,18 @@ import Head from "next/head"
import { Article } from "@/components/drupal/Article"
import { BasicPage } from "@/components/drupal/BasicPage"
import { Layout } from "@/components/Layout"
-import { drupal, query } from "@/lib/drupal"
+import { drupal } from "@/lib/drupal"
import type {
GetStaticPaths,
GetStaticProps,
InferGetStaticPropsType,
} from "next"
-import type { NodeArticle, NodePage, NodesPath } from "@/types"
+import type { DrupalArticle, DrupalPage, NodesPath } from "@/types"
export const getStaticPaths = (async (context) => {
// Fetch the paths for the first 50 articles and pages.
// We'll fall back to on-demand generation for the rest.
- const data = await query<{
+ const data = await drupal.query<{
nodeArticles: NodesPath
nodePages: NodesPath
}>({
@@ -32,12 +32,10 @@ export const getStaticPaths = (async (context) => {
})
// Build static paths.
- const paths = drupal.buildStaticPathsParamsFromPaths(
- [
- ...(data?.nodeArticles?.nodes as []),
- ...(data?.nodePages?.nodes as []),
- ].map(({ path }) => path)
- )
+ const paths = [
+ ...(data?.nodeArticles?.nodes as { path: string }[]),
+ ...(data?.nodePages?.nodes as { path: string }[]),
+ ].map(({ path }) => ({ params: { slug: path.split("/").filter(Boolean) } }))
return {
paths,
@@ -52,47 +50,53 @@ export const getStaticProps = (async (context) => {
}
}
- const data = await query<{
- nodeByPath: NodeArticle | NodePage
+ const data = await drupal.query<{
+ route: { entity: DrupalArticle | DrupalPage }
}>({
query: `query ($path: String!){
- nodeByPath(path: $path) {
- ... on NodeArticle {
- __typename
- id
- title
- path
- author {
- displayName
- }
- body {
- processed
- }
- status
- created
- image {
- width
- url
- height
- }
- }
- ... on NodePage {
- __typename
- id
- title
- path
- body {
- processed
+ route(path: $path) {
+ ... on RouteInternal {
+ entity {
+ ... on NodeArticle {
+ __typename
+ id
+ title
+ path
+ author {
+ name
+ }
+ body {
+ processed
+ }
+ status
+ created {
+ time
+ }
+ image {
+ width
+ url
+ height
+ }
+ }
+ ... on NodePage {
+ __typename
+ id
+ title
+ path
+ body {
+ processed
+ }
+ }
}
}
}
}`,
variables: {
- path: `/${context.params.slug.join("/")}`,
+ path: `/${(context.params.slug as []).join("/")}`,
},
})
- const resource = data?.nodeByPath
+ const resource = data?.route?.entity
// If we're not in preview mode and the resource is not published,
// Return page not found.
@@ -108,7 +112,7 @@ export const getStaticProps = (async (context) => {
},
}
}) satisfies GetStaticProps<{
- resource: NodeArticle | NodePage
+ resource: DrupalArticle | DrupalPage
}>
export default function Page({
diff --git a/starters/graphql-starter/pages/api/exit-preview.ts b/starters/graphql-starter/pages/api/exit-preview.ts
index a8cf12e8..f8847b39 100644
--- a/starters/graphql-starter/pages/api/exit-preview.ts
+++ b/starters/graphql-starter/pages/api/exit-preview.ts
@@ -1,10 +1,9 @@
+import { drupal } from "@/lib/drupal"
import type { NextApiRequest, NextApiResponse } from "next"
export default async function exit(
- _: NextApiRequest,
+ request: NextApiRequest,
response: NextApiResponse
) {
- response.clearPreviewData()
- response.writeHead(307, { Location: "/" })
- response.end()
+ await drupal.previewDisable(request, response)
}
diff --git a/starters/graphql-starter/pages/api/preview.ts b/starters/graphql-starter/pages/api/preview.ts
index 7660eb64..a0733440 100644
--- a/starters/graphql-starter/pages/api/preview.ts
+++ b/starters/graphql-starter/pages/api/preview.ts
@@ -1,9 +1,10 @@
import { drupal } from "@/lib/drupal"
import type { NextApiRequest, NextApiResponse } from "next"
-export default async function handler(
+export default async function draft(
request: NextApiRequest,
response: NextApiResponse
) {
- await drupal.preview(request, response)
+ // Enables Preview mode and Draft mode.
+ await drupal.preview(request, response, { enable: true })
}
diff --git a/starters/graphql-starter/pages/index.tsx b/starters/graphql-starter/pages/index.tsx
index c5c5bca6..14544955 100644
--- a/starters/graphql-starter/pages/index.tsx
+++ b/starters/graphql-starter/pages/index.tsx
@@ -1,15 +1,15 @@
import Head from "next/head"
import { ArticleTeaser } from "@/components/drupal/ArticleTeaser"
import { Layout } from "@/components/Layout"
-import { query } from "@/lib/drupal"
+import { drupal } from "@/lib/drupal"
import type { InferGetStaticPropsType, GetStaticProps } from "next"
-import type { NodeArticle } from "@/types"
+import type { DrupalArticle } from "@/types"
export const getStaticProps = (async (context) => {
// Fetch the first 10 articles.
- const data = await query<{
+ const data = await drupal.query<{
nodeArticles: {
- nodes: NodeArticle[]
+ nodes: DrupalArticle[]
}
}>({
query: `
@@ -20,12 +20,14 @@ export const getStaticProps = (async (context) => {
title
path
author {
- displayName
+ name
}
body {
processed
}
- created
+ created {
+ time
+ }
image {
width
url
@@ -43,7 +45,7 @@ export const getStaticProps = (async (context) => {
},
}
}) satisfies GetStaticProps<{
- nodes: NodeArticle[]
+ nodes: DrupalArticle[]
}>
export default function Home({
diff --git a/starters/graphql-starter/tsconfig.json b/starters/graphql-starter/tsconfig.json
index ad54f56d..23ba4fd5 100644
--- a/starters/graphql-starter/tsconfig.json
+++ b/starters/graphql-starter/tsconfig.json
@@ -14,10 +14,15 @@
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
"paths": {
"@/*": ["./*"]
}
},
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
diff --git a/starters/graphql-starter/types/index.d.ts b/starters/graphql-starter/types/index.d.ts
index fc52cc6e..95439dc8 100644
--- a/starters/graphql-starter/types/index.d.ts
+++ b/starters/graphql-starter/types/index.d.ts
@@ -15,10 +15,10 @@ export type Image = {
}
export type Author = {
- displayName: string
+ name: string
}
-export type NodePage = {
+export type DrupalPage = {
__typename: "NodePage"
id: string
status: boolean
@@ -29,7 +29,7 @@ export type NodePage = {
}
}
-export type NodeArticle = {
+export type DrupalArticle = {
__typename: "NodeArticle"
id: string
status: boolean
@@ -39,6 +39,8 @@ export type NodeArticle = {
body: {
processed: string
}
- created: string
+ created: {
+ time: string
+ }
image: Image
}