From f8c1ceaa0e2429d4ae68411df79f707854814b02 Mon Sep 17 00:00:00 2001 From: Joshua Scott Date: Thu, 25 Jul 2024 10:59:40 +0300 Subject: [PATCH 1/7] NEX-101: Start fetching page props ASAP for faster page generation --- next/pages/[...slug].tsx | 6 ++++-- next/pages/all-articles/[[...page]].tsx | 4 +++- next/pages/dashboard/index.tsx | 4 +++- .../webforms/[webformName]/[webformSubmissionUuid].tsx | 4 +++- next/pages/index.tsx | 4 +++- 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/next/pages/[...slug].tsx b/next/pages/[...slug].tsx index e61a8b64..1bb69a8b 100644 --- a/next/pages/[...slug].tsx +++ b/next/pages/[...slug].tsx @@ -93,13 +93,15 @@ interface PageProps extends CommonPageProps { } export const getStaticProps: GetStaticProps = async (context) => { + const commonPageProps = getCommonPageProps(context); + // Get the path from the context: const path = Array.isArray(context.params.slug) ? `/${context.params.slug?.join("/")}` : context.params.slug; const variables = { - path: path, + path, langcode: context.locale, }; @@ -208,7 +210,7 @@ export const getStaticProps: GetStaticProps = async (context) => { return { props: { - ...(await getCommonPageProps(context)), + ...(await commonPageProps), node: nodeEntity, languageLinks, }, diff --git a/next/pages/all-articles/[[...page]].tsx b/next/pages/all-articles/[[...page]].tsx index 1c237df6..0c3ec9d5 100644 --- a/next/pages/all-articles/[[...page]].tsx +++ b/next/pages/all-articles/[[...page]].tsx @@ -62,6 +62,8 @@ export const getStaticPaths: GetStaticPaths = async () => { export const getStaticProps: GetStaticProps = async ( context, ) => { + const commonPageProps = getCommonPageProps(context); + // Get the page parameter: const page = context.params.page; const currentPage = parseInt(Array.isArray(page) ? page[0] : page || "1"); @@ -96,7 +98,7 @@ export const getStaticProps: GetStaticProps = async ( return { props: { - ...(await getCommonPageProps(context)), + ...(await commonPageProps), articleTeasers: articles, paginationProps: { currentPage, diff --git a/next/pages/dashboard/index.tsx b/next/pages/dashboard/index.tsx index bffc901a..952f4bdc 100644 --- a/next/pages/dashboard/index.tsx +++ b/next/pages/dashboard/index.tsx @@ -82,6 +82,8 @@ export const getServerSideProps: GetServerSideProps< submissions: WebformSubmissionsListItem[]; } > = async (context) => { + const commonPageProps = getCommonPageProps(context); + const { locale, resolvedUrl } = context; const session = await getServerSession(context.req, context.res, authOptions); @@ -112,7 +114,7 @@ export const getServerSideProps: GetServerSideProps< return { props: { - ...(await getCommonPageProps(context)), + ...(await commonPageProps), submissions, session, }, diff --git a/next/pages/dashboard/webforms/[webformName]/[webformSubmissionUuid].tsx b/next/pages/dashboard/webforms/[webformName]/[webformSubmissionUuid].tsx index 301332cc..385286da 100644 --- a/next/pages/dashboard/webforms/[webformName]/[webformSubmissionUuid].tsx +++ b/next/pages/dashboard/webforms/[webformName]/[webformSubmissionUuid].tsx @@ -58,6 +58,8 @@ export const getServerSideProps: GetServerSideProps< submission: WebformSubmission; } > = async (context) => { + const commonPageProps = getCommonPageProps(context); + const { locale, params, resolvedUrl } = context; const session = await getServerSession(context.req, context.res, authOptions); @@ -89,7 +91,7 @@ export const getServerSideProps: GetServerSideProps< return { props: { - ...(await getCommonPageProps(context)), + ...(await commonPageProps), submission, }, }; diff --git a/next/pages/index.tsx b/next/pages/index.tsx index 960f2218..c10715a2 100644 --- a/next/pages/index.tsx +++ b/next/pages/index.tsx @@ -54,6 +54,8 @@ export default function IndexPage({ export const getStaticProps: GetStaticProps = async ( context, ) => { + const commonPageProps = getCommonPageProps(context); + const variables = { // This works because it matches the pathauto pattern for the Frontpage content type defined in Drupal: path: `frontpage-${context.locale}`, @@ -100,7 +102,7 @@ export const getStaticProps: GetStaticProps = async ( return { props: { - ...(await getCommonPageProps(context)), + ...(await commonPageProps), frontpage, stickyArticleTeasers: articles, }, From 077e62d1ba024cecb011af552e1c545a09e60e3f Mon Sep 17 00:00:00 2001 From: Joshua Scott Date: Thu, 25 Jul 2024 11:11:57 +0300 Subject: [PATCH 2/7] NEX-101: Start fetching article teasers ASAP for faster page generation --- next/pages/index.tsx | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/next/pages/index.tsx b/next/pages/index.tsx index c10715a2..9fcf22dc 100644 --- a/next/pages/index.tsx +++ b/next/pages/index.tsx @@ -62,14 +62,19 @@ export const getStaticProps: GetStaticProps = async ( langcode: context.locale, }; - const data = await drupalClientViewer.doGraphQlRequest( - GET_ENTITY_AT_DRUPAL_PATH, - variables, - ); + const [frontpageData, stickyArticleTeasers] = await Promise.all([ + drupalClientViewer.doGraphQlRequest(GET_ENTITY_AT_DRUPAL_PATH, variables), + drupalClientViewer.doGraphQlRequest(LISTING_ARTICLES, { + langcode: context.locale, + sticky: true, + page: 0, + pageSize: 3, + }), + ]); - const frontpage = extractEntityFromRouteQueryResult(data); + const frontpage = extractEntityFromRouteQueryResult(frontpageData); - if (!frontpage || !(frontpage.__typename === "NodeFrontpage")) { + if (!frontpage || frontpage.__typename !== "NodeFrontpage") { return { notFound: true, revalidate: 10, @@ -84,17 +89,6 @@ export const getStaticProps: GetStaticProps = async ( }; } - // Get the last 3 sticky articles in the current language: - const stickyArticleTeasers = await drupalClientViewer.doGraphQlRequest( - LISTING_ARTICLES, - { - langcode: context.locale, - sticky: true, - page: 0, - pageSize: 3, - }, - ); - // We cast the results as the ListingArticle type to get type safety: const articles = (stickyArticleTeasers.articlesView From 363415d1b1b4ad3c533330300fa64a320f743ca5 Mon Sep 17 00:00:00 2001 From: Joshua Scott Date: Thu, 25 Jul 2024 15:18:31 +0300 Subject: [PATCH 3/7] NEX-101: Cleanup data fetching; prepare for better error handling --- next/lib/contexts/language-links-context.tsx | 4 +- next/lib/get-common-page-props.ts | 12 ++- next/pages/404.tsx | 8 +- next/pages/500.tsx | 8 +- next/pages/[...slug].tsx | 89 +++++++------------ next/pages/all-articles/[[...page]].tsx | 16 ++-- next/pages/auth/login.tsx | 4 +- next/pages/auth/register.tsx | 4 +- next/pages/dashboard/index.tsx | 7 +- .../[webformName]/[webformSubmissionUuid].tsx | 7 +- next/pages/index.tsx | 29 +++--- next/pages/search.tsx | 8 +- 12 files changed, 90 insertions(+), 106 deletions(-) diff --git a/next/lib/contexts/language-links-context.tsx b/next/lib/contexts/language-links-context.tsx index 634c0750..d51715fa 100644 --- a/next/lib/contexts/language-links-context.tsx +++ b/next/lib/contexts/language-links-context.tsx @@ -37,10 +37,10 @@ export const getStandardLanguageLinks = () => */ export function createLanguageLinksForNextOnlyPage( path: string, - context: GetStaticPropsContext, + locales: GetStaticPropsContext["locales"], ): LanguageLinks { const languageLinks = getStandardLanguageLinks(); - context.locales.forEach((locale) => { + locales.forEach((locale) => { languageLinks[locale].path = languageLinks[locale].path === "/" ? path diff --git a/next/lib/get-common-page-props.ts b/next/lib/get-common-page-props.ts index 4941a34e..d0b65be2 100644 --- a/next/lib/get-common-page-props.ts +++ b/next/lib/get-common-page-props.ts @@ -3,12 +3,18 @@ import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { getMenus } from "@/lib/drupal/get-menus"; +import siteConfig from "@/site.config"; + export type CommonPageProps = Awaited>; -export async function getCommonPageProps(context: GetStaticPropsContext) { +export async function getCommonPageProps({ + locale = siteConfig.defaultLocale, +}: { + locale: GetStaticPropsContext["locale"]; +}) { const [translations, menus] = await Promise.all([ - serverSideTranslations(context.locale), - getMenus(context), + serverSideTranslations(locale), + getMenus({ locale }), ]); return { diff --git a/next/pages/404.tsx b/next/pages/404.tsx index 8035c88a..a91238eb 100644 --- a/next/pages/404.tsx +++ b/next/pages/404.tsx @@ -25,12 +25,12 @@ export default function NotFoundPage() { ); } -export const getStaticProps: GetStaticProps = async ( - context, -) => { +export const getStaticProps: GetStaticProps = async ({ + locale, +}) => { return { props: { - ...(await getCommonPageProps(context)), + ...(await getCommonPageProps({ locale })), }, revalidate: 60, }; diff --git a/next/pages/500.tsx b/next/pages/500.tsx index 58c7152e..fb1e1086 100644 --- a/next/pages/500.tsx +++ b/next/pages/500.tsx @@ -25,12 +25,12 @@ export default function NotFoundPage() { ); } -export const getStaticProps: GetStaticProps = async ( - context, -) => { +export const getStaticProps: GetStaticProps = async ({ + locale, +}) => { return { props: { - ...(await getCommonPageProps(context)), + ...(await getCommonPageProps({ locale })), }, revalidate: 60, }; diff --git a/next/pages/[...slug].tsx b/next/pages/[...slug].tsx index 1bb69a8b..8c8a034b 100644 --- a/next/pages/[...slug].tsx +++ b/next/pages/[...slug].tsx @@ -1,4 +1,4 @@ -import type { PreviewData, Redirect } from "next"; +import type { GetStaticPathsResult, Redirect } from "next"; import { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from "next"; import { Meta } from "@/components/meta"; @@ -45,13 +45,8 @@ export default function CustomPage({ ); } -export const getStaticPaths: GetStaticPaths = async (context) => { - // We want to generate static paths for all locales: - const locales = context.locales || []; - - const staticPaths: ReturnType< - typeof drupalClientViewer.buildStaticPathsParamsFromPaths - > = []; +export const getStaticPaths: GetStaticPaths = async ({ locales }) => { + const staticPaths: GetStaticPathsResult["paths"] = []; for (const locale of locales) { // Get the defined paths via graphql for the current locale: @@ -92,32 +87,27 @@ interface PageProps extends CommonPageProps { languageLinks: LanguageLinks; } -export const getStaticProps: GetStaticProps = async (context) => { - const commonPageProps = getCommonPageProps(context); - - // Get the path from the context: - const path = Array.isArray(context.params.slug) - ? `/${context.params.slug?.join("/")}` - : context.params.slug; - - const variables = { - path, - langcode: context.locale, - }; +export const getStaticProps: GetStaticProps = async ({ + locale, + params, + preview, + previewData, +}) => { + const commonPageProps = getCommonPageProps({ locale }); - // Are we in Next.js preview mode? - const isPreview = context.preview || false; - const drupalClient = isPreview ? drupalClientPreviewer : drupalClientViewer; + const drupalClient = preview ? drupalClientPreviewer : drupalClientViewer; + const path = "/" + (params.slug as string[]).join("/"); - // Get the page data with Graphql. - // We want to use a different client if we are in preview mode: - const data = await drupalClient.doGraphQlRequest( + const nodeByPathResult = await drupalClient.doGraphQlRequest( GET_ENTITY_AT_DRUPAL_PATH, - variables, + { + path, + langcode: locale, + }, ); // If the data contains a RedirectResponse, we redirect to the path: - const redirect = extractRedirectFromRouteQueryResult(data); + const redirect = extractRedirectFromRouteQueryResult(nodeByPathResult); if (redirect) { return { @@ -130,11 +120,10 @@ export const getStaticProps: GetStaticProps = async (context) => { }; } - // Get the entity from the response: - let nodeEntity = extractEntityFromRouteQueryResult(data); + let node = extractEntityFromRouteQueryResult(nodeByPathResult); // If there's no node, return 404: - if (!nodeEntity) { + if (!node) { return { notFound: true, revalidate: 60, @@ -142,10 +131,10 @@ export const getStaticProps: GetStaticProps = async (context) => { } // If node is a frontpage, redirect to / for the current locale: - if (nodeEntity.__typename === "NodeFrontpage") { + if (node.__typename === "NodeFrontpage") { return { redirect: { - destination: `/${context.locale}`, + destination: `/${locale}`, permanent: false, } satisfies Redirect, }; @@ -155,34 +144,24 @@ export const getStaticProps: GetStaticProps = async (context) => { // In this case, the previewData will contain the resourceVersion property, // we can use that in combination with the node id to fetch the correct revision // This means that we will need to do a second request to Drupal. - const { previewData } = context as { - previewData: PreviewData & { resourceVersion?: string }; - }; if ( - isPreview && - previewData && + preview && typeof previewData === "object" && - previewData.resourceVersion && - // If the resourceVersion is "rel:latest-version", we don't need to fetch the revision: + "resourceVersion" in previewData && + typeof previewData.resourceVersion === "string" && previewData.resourceVersion !== "rel:latest-version" ) { - // Get the node id from the entity we already have: - const nodeId = nodeEntity.id; - // the revision will be in the format "id:[id]": - const revisionId = previewData.resourceVersion.split(":").slice(1); - // To fetch the entity at a specific revision, we need to call a specific path: - const revisionPath = `/node/${nodeId}/revisions/${revisionId}/view`; - - // Get the node at the specific data with Graphql: + const [nodeId, revisionId] = previewData.resourceVersion.split(":"); + const path = `/node/${nodeId}/revisions/${revisionId}/view`; const revisionRoutedata = await drupalClient.doGraphQlRequest( GET_ENTITY_AT_DRUPAL_PATH, - { path: revisionPath, langcode: context.locale }, + { path, langcode: locale }, ); // Instead of the entity at the current revision, we want now to // display the entity at the requested revision: - nodeEntity = extractEntityFromRouteQueryResult(revisionRoutedata); - if (!nodeEntity) { + node = extractEntityFromRouteQueryResult(revisionRoutedata); + if (!node) { return { notFound: true, revalidate: 60, @@ -191,7 +170,7 @@ export const getStaticProps: GetStaticProps = async (context) => { } // Unless we are in preview, return 404 if the node is set to unpublished: - if (!isPreview && nodeEntity.status !== true) { + if (!preview && node.status !== true) { return { notFound: true, revalidate: 60, @@ -202,8 +181,8 @@ export const getStaticProps: GetStaticProps = async (context) => { let languageLinks; // Not all node types necessarily have translations enabled, // if so, only show the standard language links. - if ("translations" in nodeEntity) { - languageLinks = createLanguageLinks(nodeEntity.translations); + if ("translations" in node) { + languageLinks = createLanguageLinks(node.translations); } else { languageLinks = getStandardLanguageLinks(); } @@ -211,7 +190,7 @@ export const getStaticProps: GetStaticProps = async (context) => { return { props: { ...(await commonPageProps), - node: nodeEntity, + node: node, languageLinks, }, revalidate: 60, diff --git a/next/pages/all-articles/[[...page]].tsx b/next/pages/all-articles/[[...page]].tsx index 0c3ec9d5..d596cdf9 100644 --- a/next/pages/all-articles/[[...page]].tsx +++ b/next/pages/all-articles/[[...page]].tsx @@ -59,13 +59,15 @@ export const getStaticPaths: GetStaticPaths = async () => { }; }; -export const getStaticProps: GetStaticProps = async ( - context, -) => { - const commonPageProps = getCommonPageProps(context); +export const getStaticProps: GetStaticProps = async ({ + locale, + locales, + params, +}) => { + const commonPageProps = getCommonPageProps({ locale }); // Get the page parameter: - const page = context.params.page; + const page = params.page; const currentPage = parseInt(Array.isArray(page) ? page[0] : page || "1"); // This has to match one of the allowed values in the article listing view // in Drupal. @@ -74,7 +76,7 @@ export const getStaticProps: GetStaticProps = async ( const { totalPages, articles } = await getLatestArticlesItems({ limit: PAGE_SIZE, offset: currentPage ? PAGE_SIZE * (currentPage - 1) : 0, - locale: context.locale, + locale, }); // Create pagination props. @@ -94,7 +96,7 @@ export const getStaticProps: GetStaticProps = async ( // Create language links for this page. // Note: the links will always point to the first page, because we cannot guarantee that // the other pages will exist in all languages. - const languageLinks = createLanguageLinksForNextOnlyPage(pageRoot, context); + const languageLinks = createLanguageLinksForNextOnlyPage(pageRoot, locales); return { props: { diff --git a/next/pages/auth/login.tsx b/next/pages/auth/login.tsx index 9c13d699..e2a64643 100644 --- a/next/pages/auth/login.tsx +++ b/next/pages/auth/login.tsx @@ -133,10 +133,10 @@ export default function LogIn() { ); } -export async function getStaticProps(context: GetStaticPropsContext) { +export async function getStaticProps({ locale }: GetStaticPropsContext) { return { props: { - ...(await getCommonPageProps(context)), + ...(await getCommonPageProps({ locale })), }, }; } diff --git a/next/pages/auth/register.tsx b/next/pages/auth/register.tsx index 21ea5744..6b29359c 100644 --- a/next/pages/auth/register.tsx +++ b/next/pages/auth/register.tsx @@ -118,10 +118,10 @@ export default function Register() { ); } -export async function getStaticProps(context: GetStaticPropsContext) { +export async function getStaticProps({ locale }: GetStaticPropsContext) { return { props: { - ...(await getCommonPageProps(context)), + ...(await getCommonPageProps({ locale })), }, }; } diff --git a/next/pages/dashboard/index.tsx b/next/pages/dashboard/index.tsx index 952f4bdc..6f237f5d 100644 --- a/next/pages/dashboard/index.tsx +++ b/next/pages/dashboard/index.tsx @@ -81,11 +81,10 @@ export const getServerSideProps: GetServerSideProps< CommonPageProps & { submissions: WebformSubmissionsListItem[]; } -> = async (context) => { - const commonPageProps = getCommonPageProps(context); +> = async ({ locale, resolvedUrl, req, res }) => { + const commonPageProps = getCommonPageProps({ locale }); - const { locale, resolvedUrl } = context; - const session = await getServerSession(context.req, context.res, authOptions); + const session = await getServerSession(req, res, authOptions); if (!session) { return redirectExpiredSessionToLoginPage(locale, resolvedUrl); diff --git a/next/pages/dashboard/webforms/[webformName]/[webformSubmissionUuid].tsx b/next/pages/dashboard/webforms/[webformName]/[webformSubmissionUuid].tsx index 385286da..3ade07c7 100644 --- a/next/pages/dashboard/webforms/[webformName]/[webformSubmissionUuid].tsx +++ b/next/pages/dashboard/webforms/[webformName]/[webformSubmissionUuid].tsx @@ -57,11 +57,10 @@ export const getServerSideProps: GetServerSideProps< CommonPageProps & { submission: WebformSubmission; } -> = async (context) => { - const commonPageProps = getCommonPageProps(context); +> = async ({ locale, params, resolvedUrl, req, res }) => { + const commonPageProps = getCommonPageProps({ locale }); - const { locale, params, resolvedUrl } = context; - const session = await getServerSession(context.req, context.res, authOptions); + const session = await getServerSession(req, res, authOptions); if (!session) { return redirectExpiredSessionToLoginPage(locale, resolvedUrl); diff --git a/next/pages/index.tsx b/next/pages/index.tsx index 9fcf22dc..3cbe9746 100644 --- a/next/pages/index.tsx +++ b/next/pages/index.tsx @@ -51,28 +51,27 @@ export default function IndexPage({ ); } -export const getStaticProps: GetStaticProps = async ( - context, -) => { - const commonPageProps = getCommonPageProps(context); +export const getStaticProps: GetStaticProps = async ({ + locale, + preview, +}) => { + const commonPageProps = getCommonPageProps({ locale }); - const variables = { - // This works because it matches the pathauto pattern for the Frontpage content type defined in Drupal: - path: `frontpage-${context.locale}`, - langcode: context.locale, - }; - - const [frontpageData, stickyArticleTeasers] = await Promise.all([ - drupalClientViewer.doGraphQlRequest(GET_ENTITY_AT_DRUPAL_PATH, variables), + const [nodeByPathResult, stickyArticleTeasers] = await Promise.all([ + drupalClientViewer.doGraphQlRequest(GET_ENTITY_AT_DRUPAL_PATH, { + // This works because it matches the pathauto pattern for the Frontpage content type defined in Drupal: + path: `frontpage-${locale}`, + langcode: locale, + }), drupalClientViewer.doGraphQlRequest(LISTING_ARTICLES, { - langcode: context.locale, + langcode: locale, sticky: true, page: 0, pageSize: 3, }), ]); - const frontpage = extractEntityFromRouteQueryResult(frontpageData); + const frontpage = extractEntityFromRouteQueryResult(nodeByPathResult); if (!frontpage || frontpage.__typename !== "NodeFrontpage") { return { @@ -82,7 +81,7 @@ export const getStaticProps: GetStaticProps = async ( } // Unless we are in preview, return 404 if the node is set to unpublished: - if (!context.preview && frontpage.status !== true) { + if (!preview && frontpage.status !== true) { return { notFound: true, revalidate: 10, diff --git a/next/pages/search.tsx b/next/pages/search.tsx index e195b82b..604fbc63 100644 --- a/next/pages/search.tsx +++ b/next/pages/search.tsx @@ -136,12 +136,12 @@ export default function SearchPage() { ); } -export const getStaticProps: GetStaticProps = async ( - context, -) => { +export const getStaticProps: GetStaticProps = async ({ + locale, +}) => { return { props: { - ...(await getCommonPageProps(context)), + ...(await getCommonPageProps({ locale })), }, revalidate: 60, }; From fe04cc23452634d5f9d641321815df3525198444 Mon Sep 17 00:00:00 2001 From: Joshua Scott Date: Thu, 25 Jul 2024 15:46:12 +0300 Subject: [PATCH 4/7] NEX-101: Use constants for revalidation periods --- next/lib/constants.ts | 2 ++ next/pages/404.tsx | 3 ++- next/pages/500.tsx | 3 ++- next/pages/[...slug].tsx | 9 +++++---- next/pages/all-articles/[[...page]].tsx | 3 ++- next/pages/index.tsx | 7 ++++--- next/pages/search.tsx | 3 ++- 7 files changed, 19 insertions(+), 11 deletions(-) create mode 100644 next/lib/constants.ts diff --git a/next/lib/constants.ts b/next/lib/constants.ts new file mode 100644 index 00000000..f244dfdd --- /dev/null +++ b/next/lib/constants.ts @@ -0,0 +1,2 @@ +export const REVALIDATE_SHORT = 10; // 10 seconds +export const REVALIDATE_LONG = 60 * 10; // 10 minutes diff --git a/next/pages/404.tsx b/next/pages/404.tsx index a91238eb..083edb06 100644 --- a/next/pages/404.tsx +++ b/next/pages/404.tsx @@ -4,6 +4,7 @@ import { useTranslation } from "next-i18next"; import { HeadingPage } from "@/components/heading--page"; import { Meta } from "@/components/meta"; +import { REVALIDATE_LONG } from "@/lib/constants"; import { CommonPageProps, getCommonPageProps, @@ -32,6 +33,6 @@ export const getStaticProps: GetStaticProps = async ({ props: { ...(await getCommonPageProps({ locale })), }, - revalidate: 60, + revalidate: REVALIDATE_LONG, }; }; diff --git a/next/pages/500.tsx b/next/pages/500.tsx index fb1e1086..41968e9a 100644 --- a/next/pages/500.tsx +++ b/next/pages/500.tsx @@ -4,6 +4,7 @@ import { useTranslation } from "next-i18next"; import { HeadingPage } from "@/components/heading--page"; import { Meta } from "@/components/meta"; +import { REVALIDATE_LONG } from "@/lib/constants"; import { CommonPageProps, getCommonPageProps, @@ -32,6 +33,6 @@ export const getStaticProps: GetStaticProps = async ({ props: { ...(await getCommonPageProps({ locale })), }, - revalidate: 60, + revalidate: REVALIDATE_LONG, }; }; diff --git a/next/pages/[...slug].tsx b/next/pages/[...slug].tsx index 8c8a034b..35e7b039 100644 --- a/next/pages/[...slug].tsx +++ b/next/pages/[...slug].tsx @@ -3,6 +3,7 @@ import { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from "next"; import { Meta } from "@/components/meta"; import { Node } from "@/components/node"; +import { REVALIDATE_LONG } from "@/lib/constants"; import { createLanguageLinks, LanguageLinks, @@ -126,7 +127,7 @@ export const getStaticProps: GetStaticProps = async ({ if (!node) { return { notFound: true, - revalidate: 60, + revalidate: REVALIDATE_LONG, }; } @@ -164,7 +165,7 @@ export const getStaticProps: GetStaticProps = async ({ if (!node) { return { notFound: true, - revalidate: 60, + revalidate: REVALIDATE_LONG, }; } } @@ -173,7 +174,7 @@ export const getStaticProps: GetStaticProps = async ({ if (!preview && node.status !== true) { return { notFound: true, - revalidate: 60, + revalidate: REVALIDATE_LONG, }; } @@ -193,6 +194,6 @@ export const getStaticProps: GetStaticProps = async ({ node: node, languageLinks, }, - revalidate: 60, + revalidate: REVALIDATE_LONG, }; }; diff --git a/next/pages/all-articles/[[...page]].tsx b/next/pages/all-articles/[[...page]].tsx index d596cdf9..df2f8742 100644 --- a/next/pages/all-articles/[[...page]].tsx +++ b/next/pages/all-articles/[[...page]].tsx @@ -7,6 +7,7 @@ import { HeadingPage } from "@/components/heading--page"; import { LayoutProps } from "@/components/layout"; import { Meta } from "@/components/meta"; import { Pagination, PaginationProps } from "@/components/pagination"; +import { REVALIDATE_LONG } from "@/lib/constants"; import { createLanguageLinksForNextOnlyPage, LanguageLinks, @@ -112,6 +113,6 @@ export const getStaticProps: GetStaticProps = async ({ }, languageLinks, }, - revalidate: 60, + revalidate: REVALIDATE_LONG, }; }; diff --git a/next/pages/index.tsx b/next/pages/index.tsx index 3cbe9746..053d26ac 100644 --- a/next/pages/index.tsx +++ b/next/pages/index.tsx @@ -8,6 +8,7 @@ import { LayoutProps } from "@/components/layout"; import { LogoStrip } from "@/components/logo-strip"; import { Meta } from "@/components/meta"; import { Node } from "@/components/node"; +import { REVALIDATE_LONG, REVALIDATE_SHORT } from "@/lib/constants"; import { drupalClientViewer } from "@/lib/drupal/drupal-client"; import { getCommonPageProps } from "@/lib/get-common-page-props"; import type { FragmentArticleTeaserFragment } from "@/lib/gql/graphql"; @@ -76,7 +77,7 @@ export const getStaticProps: GetStaticProps = async ({ if (!frontpage || frontpage.__typename !== "NodeFrontpage") { return { notFound: true, - revalidate: 10, + revalidate: REVALIDATE_SHORT, }; } @@ -84,7 +85,7 @@ export const getStaticProps: GetStaticProps = async ({ if (!preview && frontpage.status !== true) { return { notFound: true, - revalidate: 10, + revalidate: REVALIDATE_SHORT, }; } @@ -99,6 +100,6 @@ export const getStaticProps: GetStaticProps = async ({ frontpage, stickyArticleTeasers: articles, }, - revalidate: 60, + revalidate: REVALIDATE_LONG, }; }; diff --git a/next/pages/search.tsx b/next/pages/search.tsx index 604fbc63..f015b184 100644 --- a/next/pages/search.tsx +++ b/next/pages/search.tsx @@ -20,6 +20,7 @@ import { MultiCheckboxFacet } from "@/components/search/search-multicheckbox-fac import { Pagination } from "@/components/search/search-pagination"; import { PagingInfoView } from "@/components/search/search-paging-info"; import { SearchResult } from "@/components/search/search-result"; +import { REVALIDATE_LONG } from "@/lib/constants"; import { CommonPageProps, getCommonPageProps, @@ -143,6 +144,6 @@ export const getStaticProps: GetStaticProps = async ({ props: { ...(await getCommonPageProps({ locale })), }, - revalidate: 60, + revalidate: REVALIDATE_LONG, }; }; From e70a17ad2a940324c9301639780a3643aaaad104 Mon Sep 17 00:00:00 2001 From: Joshua Scott Date: Thu, 25 Jul 2024 15:50:54 +0300 Subject: [PATCH 5/7] NEX-101: Do not retry GraphQL errors; throw AbortError instead --- next/lib/drupal/drupal-client.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/next/lib/drupal/drupal-client.ts b/next/lib/drupal/drupal-client.ts index 492fdabd..05109096 100644 --- a/next/lib/drupal/drupal-client.ts +++ b/next/lib/drupal/drupal-client.ts @@ -1,13 +1,19 @@ import { DrupalClient } from "next-drupal"; import { type TypedDocumentNode } from "@graphql-typed-document-node/core"; import { request, type RequestDocument, type Variables } from "graphql-request"; -import pRetry, { type Options } from "p-retry"; +import pRetry, { AbortError, type Options } from "p-retry"; import { env } from "@/env"; const RETRY_OPTIONS: Options = { retries: env.NODE_ENV === "development" ? 1 : 5, - onFailedAttempt: ({ attemptNumber, retriesLeft }) => { + onFailedAttempt: ({ attemptNumber, retriesLeft, message }) => { + // Don't retry GraphQL errors: + if (message.startsWith("GraphQL Error")) { + // Throw the relevant part of the error message (e.g. "GraphQL Error (Code: 500)") + throw new AbortError(message.slice(0, 25)); + } + console.log( `Fetch ${attemptNumber} failed (${retriesLeft} retries remaining)`, ); From 68948163ba0b0256f41617e8ed082866191c0dec Mon Sep 17 00:00:00 2001 From: Joshua Scott Date: Thu, 25 Jul 2024 16:02:20 +0300 Subject: [PATCH 6/7] NEX-101: Throw error if frontpage generation fails This ensures that: 1. The build fails if frontpage cannot be built 2. Frontpage would never serve a 404 if CMS has an issue (the last version of the page would be shown instead) --- next/pages/index.tsx | 58 ++++++++++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/next/pages/index.tsx b/next/pages/index.tsx index 053d26ac..c4380301 100644 --- a/next/pages/index.tsx +++ b/next/pages/index.tsx @@ -1,5 +1,6 @@ import { GetStaticProps, InferGetStaticPropsType } from "next"; import { useTranslation } from "next-i18next"; +import { AbortError } from "p-retry"; import { ArticleTeasers } from "@/components/article/article-teasers"; import { ContactList } from "@/components/contact-list"; @@ -8,8 +9,11 @@ import { LayoutProps } from "@/components/layout"; import { LogoStrip } from "@/components/logo-strip"; import { Meta } from "@/components/meta"; import { Node } from "@/components/node"; -import { REVALIDATE_LONG, REVALIDATE_SHORT } from "@/lib/constants"; -import { drupalClientViewer } from "@/lib/drupal/drupal-client"; +import { REVALIDATE_LONG } from "@/lib/constants"; +import { + drupalClientPreviewer, + drupalClientViewer, +} from "@/lib/drupal/drupal-client"; import { getCommonPageProps } from "@/lib/get-common-page-props"; import type { FragmentArticleTeaserFragment } from "@/lib/gql/graphql"; import { FragmentMetaTagFragment } from "@/lib/gql/graphql"; @@ -20,6 +24,7 @@ import { import { extractEntityFromRouteQueryResult } from "@/lib/graphql/utils"; import type { FrontpageType } from "@/types/graphql"; +import { env } from "@/env"; import { Divider } from "@/ui/divider"; interface HomepageProps extends LayoutProps { @@ -58,47 +63,58 @@ export const getStaticProps: GetStaticProps = async ({ }) => { const commonPageProps = getCommonPageProps({ locale }); + const drupalClient = preview ? drupalClientPreviewer : drupalClientViewer; + + // This works because it matches the pathauto pattern for the Frontpage content type defined in Drupal: + const path = `frontpage-${locale}`; + const [nodeByPathResult, stickyArticleTeasers] = await Promise.all([ - drupalClientViewer.doGraphQlRequest(GET_ENTITY_AT_DRUPAL_PATH, { - // This works because it matches the pathauto pattern for the Frontpage content type defined in Drupal: - path: `frontpage-${locale}`, + drupalClient.doGraphQlRequest(GET_ENTITY_AT_DRUPAL_PATH, { + path, langcode: locale, }), - drupalClientViewer.doGraphQlRequest(LISTING_ARTICLES, { + drupalClient.doGraphQlRequest(LISTING_ARTICLES, { langcode: locale, sticky: true, page: 0, pageSize: 3, }), - ]); + ]).catch((error: unknown) => { + const type = + error instanceof AbortError + ? "GraphQL" + : error instanceof TypeError + ? "Network" + : "Unknown"; + + const moreInfo = + type === "GraphQL" + ? `Check graphql_compose logs: ${env.NEXT_PUBLIC_DRUPAL_BASE_URL}/admin/reports` + : ""; + + throw new Error( + `${type} Error during GetNodeByPath query with $path: "${path}" and $langcode: "${locale}". ${moreInfo}`, + ); + }); const frontpage = extractEntityFromRouteQueryResult(nodeByPathResult); if (!frontpage || frontpage.__typename !== "NodeFrontpage") { - return { - notFound: true, - revalidate: REVALIDATE_SHORT, - }; + throw new Error("Frontpage not found for locale " + locale); } // Unless we are in preview, return 404 if the node is set to unpublished: if (!preview && frontpage.status !== true) { - return { - notFound: true, - revalidate: REVALIDATE_SHORT, - }; + throw new Error("Frontpage not published for locale " + locale); } - // We cast the results as the ListingArticle type to get type safety: - const articles = - (stickyArticleTeasers.articlesView - ?.results as FragmentArticleTeaserFragment[]) ?? []; - return { props: { ...(await commonPageProps), frontpage, - stickyArticleTeasers: articles, + stickyArticleTeasers: + (stickyArticleTeasers.articlesView + ?.results as FragmentArticleTeaserFragment[]) ?? [], }, revalidate: REVALIDATE_LONG, }; From 586757f03cda11bbc06f563a37d084cc9504f60e Mon Sep 17 00:00:00 2001 From: Joshua Scott Date: Thu, 25 Jul 2024 16:12:21 +0300 Subject: [PATCH 7/7] NEX-101: Improve error handling in [...slug] page - If 404 encountered during a build, throw an error to abort the build (paths returned from getStaticPaths should always exist) - If fetching node data fails: 1. Provide a link to GraphQL logs for debugging purposes 2. Rethrow the error to prevent page becoming 404, so website is more resilient to CMS/networking issues. This also means that, in most cases, feature environments can be accessed after silta puts the containers to sleep, without having to wake up BOTH the CMS and Next.js containers (just the Next.js one). --- next/pages/[...slug].tsx | 79 ++++++++++++++++++++++++++++------------ 1 file changed, 55 insertions(+), 24 deletions(-) diff --git a/next/pages/[...slug].tsx b/next/pages/[...slug].tsx index 35e7b039..a7c087da 100644 --- a/next/pages/[...slug].tsx +++ b/next/pages/[...slug].tsx @@ -1,9 +1,10 @@ import type { GetStaticPathsResult, Redirect } from "next"; import { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from "next"; +import { AbortError } from "p-retry"; import { Meta } from "@/components/meta"; import { Node } from "@/components/node"; -import { REVALIDATE_LONG } from "@/lib/constants"; +import { REVALIDATE_LONG, REVALIDATE_SHORT } from "@/lib/constants"; import { createLanguageLinks, LanguageLinks, @@ -28,6 +29,8 @@ import { } from "@/lib/graphql/utils"; import { TypedRouteEntity } from "@/types/graphql"; +import { env } from "@/env"; + export default function CustomPage({ node, }: InferGetStaticPropsType) { @@ -90,26 +93,43 @@ interface PageProps extends CommonPageProps { export const getStaticProps: GetStaticProps = async ({ locale, + defaultLocale, params, preview, previewData, + revalidateReason, }) => { const commonPageProps = getCommonPageProps({ locale }); const drupalClient = preview ? drupalClientPreviewer : drupalClientViewer; const path = "/" + (params.slug as string[]).join("/"); - const nodeByPathResult = await drupalClient.doGraphQlRequest( - GET_ENTITY_AT_DRUPAL_PATH, - { + const nodeByPathResult = await drupalClient + .doGraphQlRequest(GET_ENTITY_AT_DRUPAL_PATH, { path, langcode: locale, - }, - ); + }) + .catch((error: unknown) => { + const type = + error instanceof AbortError + ? "GraphQL" + : error instanceof TypeError + ? "Network" + : "Unknown"; + + const moreInfo = + type === "GraphQL" + ? `Check graphql_compose logs: ${env.NEXT_PUBLIC_DRUPAL_BASE_URL}/admin/reports` + : ""; + + throw new Error( + `${type} Error during GetNodeByPath query with $path: "${path}" and $langcode: "${locale}". ${moreInfo}`, + ); + }); - // If the data contains a RedirectResponse, we redirect to the path: + // The response will contain either a redirect or node data. + // If it's a redirect, redirect to the new path: const redirect = extractRedirectFromRouteQueryResult(nodeByPathResult); - if (redirect) { return { redirect: { @@ -123,28 +143,47 @@ export const getStaticProps: GetStaticProps = async ({ let node = extractEntityFromRouteQueryResult(nodeByPathResult); - // If there's no node, return 404: + // Node not found: if (!node) { + switch (revalidateReason) { + case "build": + // Pages returned from getStaticPaths should always exist. Abort the build: + throw new Error( + `Node not found in GetNodeByPath query response with $path: "${path}" and $langcode: "${locale}".`, + ); + case "stale": + case "on-demand": + default: + // Not an error, the requested node just doesn't exist. Return 404: + return { + notFound: true, + revalidate: REVALIDATE_LONG, + }; + } + } + + // Node is not published: + if (!preview && node.status !== true) { return { notFound: true, revalidate: REVALIDATE_LONG, }; } - // If node is a frontpage, redirect to / for the current locale: - if (node.__typename === "NodeFrontpage") { + // Node is actually a frontpage: + if (!preview && node.__typename === "NodeFrontpage") { return { redirect: { - destination: `/${locale}`, + destination: locale === defaultLocale ? "/" : `/${locale}`, permanent: false, } satisfies Redirect, + revalidate: REVALIDATE_LONG, }; } // When in preview, we could be requesting a specific revision. // In this case, the previewData will contain the resourceVersion property, - // we can use that in combination with the node id to fetch the correct revision - // This means that we will need to do a second request to Drupal. + // which we can use to fetch the correct revision: if ( preview && typeof previewData === "object" && @@ -165,19 +204,11 @@ export const getStaticProps: GetStaticProps = async ({ if (!node) { return { notFound: true, - revalidate: REVALIDATE_LONG, + revalidate: REVALIDATE_SHORT, }; } } - // Unless we are in preview, return 404 if the node is set to unpublished: - if (!preview && node.status !== true) { - return { - notFound: true, - revalidate: REVALIDATE_LONG, - }; - } - // Add information about possible other language versions of this node. let languageLinks; // Not all node types necessarily have translations enabled, @@ -191,7 +222,7 @@ export const getStaticProps: GetStaticProps = async ({ return { props: { ...(await commonPageProps), - node: node, + node, languageLinks, }, revalidate: REVALIDATE_LONG,