Skip to content

Commit

Permalink
Faster 404 response with less drupal fetching
Browse files Browse the repository at this point in the history
  • Loading branch information
pookmish committed Jan 5, 2024
1 parent fdbdb67 commit f693a7f
Show file tree
Hide file tree
Showing 12 changed files with 7,887 additions and 4,498 deletions.
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
.idea
.vscode
tsconfig.tsbuildinfo

# Yarn files. See https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored.
.yarn/*
!.yarn/cache
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions


# dependencies
/node_modules
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v18
v20
893 changes: 893 additions & 0 deletions .yarn/releases/yarn-4.0.2.cjs

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
nodeLinker: node-modules

yarnPath: .yarn/releases/yarn-4.0.2.cjs
67 changes: 22 additions & 45 deletions app/(public)/[...slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import {getResourceFromContext} from "@/lib/drupal/get-resource";
import {getPathsFromContext} from "@/lib/drupal/get-paths";
import {getAllDrupalPaths, getPathsFromContext, pathIsValid} from "@/lib/drupal/get-paths";
import NodePageDisplay from "@/components/node";
import {notFound, redirect} from "next/navigation";
import {translatePathFromContext} from "@/lib/drupal/translate-path";
import {DrupalMenuLinkContent} from "next-drupal";
import {GetStaticPathsResult, Metadata} from "next";
import {Metadata} from "next";
import {getNodeMetadata} from "./metadata";
import LibraryHeader from "@/components/node/sul-library/library-header";
import {PageProps, Params, StanfordNode} from "@/lib/drupal/drupal";
import InternalHeaderBanner from "@/components/patterns/internal-header-banner";
import SecondaryMenu from "@/components/menu/secondary-menu";
import {getMenu} from "@/lib/drupal/get-menu";
import {DrupalJsonApiParams} from "drupal-jsonapi-params";
import {isDraftMode} from "@/lib/drupal/is-draft-mode";
import UnpublishedBanner from "@/components/patterns/unpublished-banner";
import {getPathFromContext} from "@/lib/drupal/utils";

export const revalidate = 2592000;

Expand All @@ -23,29 +23,34 @@ class RedirectError extends Error {
}
}

const fetchNodeData = async (params: Params) => {
const fetchNodeData = async (params: Params): Promise<{ node: StanfordNode, fullWidth: boolean }> => {
const draftMode = isDraftMode();
const path = await translatePathFromContext({params}, {draftMode});
const path = getPathFromContext({params});
if (!pathIsValid(path)) throw new Error();

const pathInfo = await translatePathFromContext({params}, {draftMode});

// Check for redirect.
if (path?.redirect?.[0].to) {
if (pathInfo?.redirect?.[0].to) {
const currentPath = '/' + (typeof params.slug === 'object' ? params.slug.join('/') : params.slug);
const [destination] = path.redirect;
const [destination] = pathInfo.redirect;

if (destination.to != currentPath) {
throw new RedirectError(destination.to);
}
}

if (!path || !path.jsonapi) {
if (!pathInfo || !pathInfo.jsonapi) {
throw new Error('Unable to translate path');
}

if (params?.slug?.[0] === 'node' && path?.entity?.path) {
throw new RedirectError(path.entity.path);
if (params?.slug?.[0] === 'node' && pathInfo?.entity?.path) {
throw new RedirectError(pathInfo.entity.path);
}

const node = await getResourceFromContext<StanfordNode>(path.jsonapi.resourceName, {params}, {draftMode})
const node = await getResourceFromContext<StanfordNode>(pathInfo.jsonapi.resourceName, {params}, {draftMode})
if (!node) throw new Error();

const fullWidth: boolean = (node?.type === 'node--stanford_page' && node.layout_selection?.resourceIdObjMeta?.drupal_internal__target_id === 'stanford_basic_page_full') ||
(node?.type === 'node--sul_library' && node.layout_selection?.resourceIdObjMeta?.drupal_internal__target_id === 'sul_library_full_width');

Expand Down Expand Up @@ -144,40 +149,12 @@ const NodePage = async ({params}: PageProps) => {
export default NodePage;

export const generateStaticParams = async () => {
const completeBuild = process.env.BUILD_COMPLETE === 'true'
const params = new DrupalJsonApiParams();
params.addPageLimit(50);
let paths: GetStaticPathsResult["paths"] = [];

try {
paths = await getPathsFromContext([
'node--stanford_page',
'node--stanford_event',
'node--stanford_news',
'node--stanford_person',
'node--sul_library'
], {params: params.getQueryObject()});

let fetchMore = completeBuild;
let fetchedData: GetStaticPathsResult["paths"] = []
let page = 1;
while (fetchMore) {
console.log('Fetching page ' + page);
params.addPageOffset(page * 50);

fetchedData = await getPathsFromContext([
'node--stanford_page',
'node--stanford_event',
'node--stanford_news',
'node--stanford_person',
'node--sul_library'
], {params: params.getQueryObject()})
paths = [...paths, ...fetchedData];
fetchMore = fetchedData.length > 0;
page++;
}
} catch (e) {
const allPaths = await getAllDrupalPaths();
const nodePaths = allPaths.get('node');

let params: Params[] = [];
if (nodePaths) {
params = nodePaths.map(path => ({slug: path.split('/')}))
}
return paths.map(path => typeof path !== "string" ? path?.params : path).slice(0, (completeBuild ? -1 : 5));
return process.env.BUILD_COMPLETE === 'true' ? params : params.slice(0, 1);
}
19 changes: 15 additions & 4 deletions eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
{
"extends": "next",
"root": true,
"extends": [
"next/core-web-vitals",
"plugin:storybook/recommended"
],
"rules": {
"react-hooks/exhaustive-deps": "off"
}
"react-hooks/exhaustive-deps": "off",
"react-hooks/rules-of-hooks": "off",
"@typescript-eslint/no-unused-vars": "off",
"unused-imports/no-unused-imports": "error",
"unused-imports/no-unused-vars": [
"warn",
{ "vars": "all", "varsIgnorePattern": "^_", "args": "after-used", "argsIgnorePattern": "^_" }
]
},
"plugins": ["unused-imports"],
"ignorePatterns": ["**/__generated__/**/*"]
}
64 changes: 33 additions & 31 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,48 +11,50 @@
"lint": "next lint && tsc"
},
"dependencies": {
"@formkit/auto-animate": "^0.8.0",
"@heroicons/react": "^2.0.12",
"@mui/base": "^5.0.0-beta.5",
"@formkit/auto-animate": "^0.8.1",
"@heroicons/react": "^2.1.1",
"@mui/base": "^5.0.0-beta.29",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/typography": "^0.5.2",
"@tanstack/react-query": "^5.0.0",
"@tailwindcss/typography": "^0.5.10",
"@tanstack/react-query": "^5.17.0",
"@uidotdev/usehooks": "^2.4.1",
"axios": "^1.1.3",
"axios": "^1.6.3",
"critters": "^0.0.20",
"decanter": "^7.0.0-rc.2",
"drupal-jsonapi-params": "^2.0.0",
"html-react-parser": "^5.0.6",
"jsona": "^1.11.0",
"next": "^13.0",
"next-drupal": "^1.2.1",
"decanter": "^7.1.2",
"drupal-jsonapi-params": "^2.3.1",
"html-react-parser": "^5.0.11",
"jsona": "^1.12.1",
"next": "^13.5.6",
"next-drupal": "^1.6.0",
"nextjs-google-analytics": "^2.3.3",
"react": "^18.2.0",
"react-aria": "^3.25.0",
"react-aria": "^3.31.0",
"react-dom": "^18.2.0",
"react-error-boundary": "^4.0.2",
"react-focus-lock": "^2.9.4",
"react-intersection-observer": "^9.4.3",
"react-error-boundary": "^4.0.12",
"react-focus-lock": "^2.9.6",
"react-intersection-observer": "^9.5.3",
"react-obfuscate": "^3.6.9",
"react-obfuscate-email": "^1.1.2",
"react-resize-detector": "^9.1.0",
"react-stately": "^3.23.0",
"react-resize-detector": "^9.1.1",
"react-stately": "^3.29.0",
"react-tiny-oembed": "^1.1.0",
"server-only": "^0.0.1",
"sharp": "^0.33.0",
"tailwind-merge": "^2.0.0",
"sharp": "^0.33.1",
"tailwind-merge": "^2.2.0",
"usehooks-ts": "^2.9.1",
"xml2js": "^0.6.0"
"xml2js": "^0.6.2"
},
"devDependencies": {
"@types/node": "^20.1.1",
"@types/qs": "^6.9.10",
"@types/react": "^18.0.21",
"autoprefixer": "^10.4.4",
"eslint": "^8.12.0",
"eslint-config-next": "^13.0",
"postcss": "^8.4.12",
"tailwindcss": "^3.0.23",
"typescript": "^5.0.4"
}
"@types/node": "^20.10.6",
"@types/qs": "^6.9.11",
"@types/react": "^18.2.46",
"autoprefixer": "^10.4.16",
"eslint": "^8.56.0",
"eslint-config-next": "^13.5.6",
"eslint-plugin-unused-imports": "^3.0.0",
"postcss": "^8.4.32",
"tailwindcss": "^3.4.0",
"typescript": "^5.3.3"
},
"packageManager": "[email protected]"
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const HomePageBanner = async () => {
.addInclude(['su_library__contact_img.field_media_image'])
.addSort('title', 'ASC');

const libraries = await getResourceCollection<Library[]>('node--sul_library', {params: params.getQueryObject()});
const libraries = await getResourceCollection<Library>('node--sul_library', {params: params.getQueryObject()});

// Trim all the fat.
const trimmedLibraries = libraries.map(library => (
Expand Down
15 changes: 15 additions & 0 deletions src/lib/drupal/drupal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -520,4 +520,19 @@ export type Params = {
export type PageProps = {
params: Params
searchParams?: Record<string, string | string[] | undefined>
}

export type DrupalRedirect = JsonApiResource & {
redirect_source: {
path: string,
query: []
},
redirect_redirect: {
uri: string,
title: string,
options: [],
target_uuid: string,
url: string
},
status_code: number
}
81 changes: 79 additions & 2 deletions src/lib/drupal/get-paths.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,88 @@
import {GetStaticPathsResult} from "next";
import {AccessToken, JsonApiParams, JsonApiResourceWithPath} from "next-drupal";
import {getResourceCollection} from "@/lib/drupal/get-resource";
import {isDraftMode} from "@/lib/drupal/is-draft-mode";
import {DrupalJsonApiParams} from "drupal-jsonapi-params";
import {DrupalRedirect, PageProps} from "@/lib/drupal/drupal";
import {getPathFromContext} from "@/lib/drupal/utils";


export const pathIsValid = async (path: string, type?: 'node' | 'redirect') => {
if (isDraftMode()) return true;
const drupalPaths = await getAllDrupalPaths();
if (type) {
return drupalPaths.get(type)?.includes(path);
}
let allPaths: string[] = [];
drupalPaths.forEach(typePaths => allPaths = [...allPaths, ...typePaths])
return allPaths.includes(path);
}

export const getAllDrupalPaths = async (): Promise<Map<string, string[]>> => {
const paths = new Map();
paths.set('node', await getNodePaths())
paths.set('redirect', await getRedirectPaths())
return paths;
}

const getNodePaths = async (): Promise<string[]> => {
const params = new DrupalJsonApiParams();
// Add a simple include so that it doesn't fetch all the data right now. The full node data comes later, we only need
// the node paths.
params.addInclude(['node_type']);
params.addPageLimit(50);

const contentTypes = [
'node--stanford_page',
'node--stanford_event',
'node--stanford_news',
'node--stanford_person',
'node--sul_library'
]

let paths: PageProps[] = [];

let fetchMore = true;
let fetchedData: PageProps[] = []
let page = 0;
while (fetchMore) {
params.addPageOffset(page * 50);

// Use JSON API to fetch the list of all node paths on the site.
fetchedData = await getPathsFromContext(contentTypes, {params: params.getQueryObject()})
paths = [...paths, ...fetchedData];
fetchMore = fetchedData.length > 0;
page++;
}
return paths.map(pagePath => getPathFromContext(pagePath)).filter(path => !!path);
}

const getRedirectPaths = async (): Promise<string[]> => {
const params = new DrupalJsonApiParams();
params.addPageLimit(50);

let redirects: DrupalRedirect[] = []
let fetchMore = true;
let fetchedData: DrupalRedirect[] = []
let page = 0;

while (fetchMore) {
params.addPageOffset(page * 50);

// Use JSON API to fetch the list of all node paths on the site.
fetchedData = await getResourceCollection<DrupalRedirect>('redirect--redirect', {params: params.getQueryObject()})
redirects = [...redirects, ...fetchedData];

fetchMore = fetchedData.length === 50;
page++;
}
return redirects.map(redirect => redirect.redirect_source.path)
}

export const getPathsFromContext = async (
types: string | string[],
options: { params?: JsonApiParams,accessToken?: AccessToken } = {}
): Promise<GetStaticPathsResult["paths"]> => {
): Promise<PageProps[]> => {
if (typeof types === "string") {
types = [types]
}
Expand All @@ -16,7 +93,7 @@ export const getPathsFromContext = async (
// Use sparse fieldset to expand max size.
options.params = {[`fields[${type}]`]: "path", ...options?.params,}

const resources = await getResourceCollection<JsonApiResourceWithPath[]>(type, {
const resources = await getResourceCollection<JsonApiResourceWithPath>(type, {
deserialize: true,
...options,
})
Expand Down
6 changes: 3 additions & 3 deletions src/lib/drupal/get-resource.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,15 +123,15 @@ export const getResourceByPath = async <T extends JsonApiResource>(
return options.deserialize ? deserialize(data) : data
}

export const getResourceCollection = async <T = JsonApiResource[]>(
export const getResourceCollection = async <T extends JsonApiResource>(
type: string,
options?: {
deserialize?: boolean,
accessToken?: AccessToken,
draftMode?: boolean,
next?: NextFetchRequestConfig
} & JsonApiWithLocaleOptions,
): Promise<T> => {
): Promise<T[]> => {
options = {deserialize: true, draftMode: false, ...options}

const apiPath = await getJsonApiPathForResourceType(type)
Expand Down Expand Up @@ -184,7 +184,7 @@ export const getConfigPageResource = async <T extends JsonApiResource>(

let response;
try {
response = await getResourceCollection<JsonApiResource>(`config_pages--${name}`, options);
response = await getResourceCollection<T>(`config_pages--${name}`, options);
if (response.length === 0) return;
} catch (e) {
return;
Expand Down
Loading

0 comments on commit f693a7f

Please sign in to comment.