From 4b3b39e14901247981e16e9ba5fa8f10789d9c4a Mon Sep 17 00:00:00 2001 From: JohnAlbin Date: Mon, 22 Apr 2024 22:52:38 +0800 Subject: [PATCH] feat(basic-starter): upgrade starters to NextDrupal classes Issue #601 --- starters/basic-starter/.gitignore | 3 +- starters/basic-starter/README.md | 2 +- starters/basic-starter/components/Layout.tsx | 20 +---- .../components/drupal/ArticleTeaser.tsx | 2 +- .../components/{ => misc}/PreviewAlert.tsx | 0 .../components/navigation/HeaderNav.tsx | 21 +++++ .../components/navigation/Link.tsx | 23 +++++ starters/basic-starter/lib/drupal.ts | 12 +-- starters/basic-starter/tsconfig.json | 7 +- starters/graphql-starter/.gitignore | 3 +- starters/graphql-starter/README.md | 4 + .../graphql-starter/components/Layout.tsx | 20 +---- .../components/drupal/Article.tsx | 11 ++- .../components/drupal/ArticleTeaser.tsx | 13 ++- .../components/drupal/BasicPage.tsx | 4 +- .../components/{ => misc}/PreviewAlert.tsx | 0 .../components/navigation/HeaderNav.tsx | 21 +++++ .../components/navigation/Link.tsx | 23 +++++ starters/graphql-starter/lib/drupal.ts | 50 ++--------- .../lib/next-drupal-graphql.ts | 66 ++++++++++++++ starters/graphql-starter/pages/[...slug].tsx | 86 ++++++++++--------- starters/graphql-starter/pages/index.tsx | 16 ++-- starters/graphql-starter/tsconfig.json | 7 +- starters/graphql-starter/types/index.d.ts | 10 ++- 24 files changed, 266 insertions(+), 158 deletions(-) rename starters/basic-starter/components/{ => misc}/PreviewAlert.tsx (100%) create mode 100644 starters/basic-starter/components/navigation/HeaderNav.tsx create mode 100644 starters/basic-starter/components/navigation/Link.tsx rename starters/graphql-starter/components/{ => misc}/PreviewAlert.tsx (100%) create mode 100644 starters/graphql-starter/components/navigation/HeaderNav.tsx create mode 100644 starters/graphql-starter/components/navigation/Link.tsx create mode 100644 starters/graphql-starter/lib/next-drupal-graphql.ts 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..9b2f40c0 100644 --- a/starters/basic-starter/lib/drupal.ts +++ b/starters/basic-starter/lib/drupal.ts @@ -1,12 +1,14 @@ -import { DrupalClient } from "next-drupal" +import { NextDrupalPages } 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, { +export const drupal = new NextDrupalPages(baseUrl, { auth: { clientId, clientSecret, }, + useDefaultEndpoints: true, + // debug: 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/README.md b/starters/graphql-starter/README.md index 37621e7a..1e754465 100644 --- a/starters/graphql-starter/README.md +++ b/starters/graphql-starter/README.md @@ -6,6 +6,10 @@ A next-drupal starter for building your site with Next.js and GraphQL. `npx create-next-app -e https://github.com/chapter-three/next-drupal-graphql-starter` +## Deploy to Vercel + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fchapter-three%2Fnext-drupal-graphql-starter&env=NEXT_PUBLIC_DRUPAL_BASE_URL,NEXT_IMAGE_DOMAIN,DRUPAL_CLIENT_ID,DRUPAL_CLIENT_SECRET&envDescription=Learn%20more%20about%20environment%20variables&envLink=https%3A%2F%2Fnext-drupal.org%2Fdocs%2Fenvironment-variables&project-name=next-drupal&demo-title=Next.js%20for%20Drupal&demo-description=A%20next-generation%20front-end%20for%20your%20Drupal%20site.&demo-url=https%3A%2F%2Fdemo.next-drupal.org&demo-image=https%3A%2F%2Fnext-drupal.org%2Fimages%2Fdemo-screenshot.jpg) + ## Documentation See https://next-drupal.org 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..b6f5d80a --- /dev/null +++ b/starters/graphql-starter/lib/next-drupal-graphql.ts @@ -0,0 +1,66 @@ +// This is an example GraphQL implementation using NextDrupalBase, a lower-level +// class that contains helper methods and no JSON:API methods. + +import { NextDrupalBase } from "next-drupal" +import type { + BaseUrl, + EndpointSearchParams, + NextDrupalBaseOptions, +} from "next-drupal" + +const DEFAULT_API_PREFIX = "/graphql" + +export class NextDrupalGraphQL extends NextDrupalBase { + 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. + }) + + 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 + } + + // Since the endpoint doesn't change (even with different locales), there's + // no need to use this method; use NextDrupalGraphQL.query() directly. + async buildEndpoint({ + searchParams, + }: { + searchParams?: EndpointSearchParams + } = {}): Promise { + return this.buildUrl(this.apiPrefix, searchParams).toString() + } +} + +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/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 }