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 }) { <>
-
-
- - Next.js for Drupal - - - Read the docs - -
-
+
{children}
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 ( +
+
+ + Next.js for Drupal + + + Read the docs + +
+
+ ) +} 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 }) { <>
-
-
- - Next.js for Drupal - - - Read the docs - -
-
+
{children}
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 ( +
+
+ + Next.js for Drupal + + + Read the docs + +
+
+ ) +} 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 }