Skip to content

NEX-179: Move the redirect logic to middleware #290

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 2 additions & 24 deletions next/src/app/[locale]/(dynamic)/[...slug]/page.tsx
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically here the idea is that once we get here we dont' need to check if there's a redirect, because that is handled by the middleware.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Metadata } from "next";
import { draftMode } from "next/headers";
import { notFound, permanentRedirect, redirect } from "next/navigation";
import { notFound } from "next/navigation";
import { getDraftData } from "next-drupal/draft";
import { setRequestLocale } from "next-intl/server";

Expand All @@ -9,10 +9,7 @@ import { REVALIDATE_LONG } from "@/lib/constants";
import { getNodeByPathQuery } from "@/lib/drupal/get-node";
import { getNodeMetadata } from "@/lib/drupal/get-node-metadata";
import { getNodeStaticParams } from "@/lib/drupal/get-node-static-params";
import {
extractEntityFromRouteQueryResult,
extractRedirectFromRouteQueryResult,
} from "@/lib/graphql/utils";
import { extractEntityFromRouteQueryResult } from "@/lib/graphql/utils";

type NodePageParams = {
params: { slug: string[]; locale: string };
Expand Down Expand Up @@ -55,20 +52,6 @@ export default async function NodePage({
// in the getNodeByPathQuery function.
const nodeByPathResult = await getNodeByPathQuery(path, locale, isDraftMode);

// The response will contain either a redirect or node data.
// If it's a redirect, redirect to the new path:
const redirectResult = extractRedirectFromRouteQueryResult(nodeByPathResult);

if (redirectResult) {
// Set to temporary redirect for 302 and 307 status codes,
// and permanent for all others.
if (redirectResult.status === 307 || redirectResult.status === 302) {
redirect(redirectResult.url);
} else {
permanentRedirect(redirectResult.url);
}
}

// Extract the node entity from the query result:
let node = extractEntityFromRouteQueryResult(nodeByPathResult);

Expand All @@ -77,11 +60,6 @@ export default async function NodePage({
notFound();
}

// If the node is a frontpage, redirect to the frontpage:
if (!isDraftMode && node.__typename === "NodeFrontpage") {
redirect(`/${locale}`);
}

Comment on lines -80 to -84
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, also this we would need to handle in middleware maybe

// When in draftMode, we could be requesting a specific revision.
// In this case, the draftData will contain the resourceVersion property,
// which we can use to fetch the correct revision:
Expand Down
2 changes: 1 addition & 1 deletion next/src/app/[locale]/(static)/nodepreview/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { setRequestLocale } from "next-intl/server";

import { Node } from "@/components/node";
import NotFoundPage from "@/components/not-found-page";
import { fetchNodeByPathQuery } from "@/lib/drupal/get-node";
import { fetchNodeByPathQuery } from "@/lib/drupal/get-node-nocache";
import { extractEntityFromRouteQueryResult } from "@/lib/graphql/utils";

async function DrupalPreviewPage({ searchParams, params: { locale } }) {
Expand Down
31 changes: 31 additions & 0 deletions next/src/lib/drupal/get-node-nocache.ts
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to move this function to its own file, because we could not import an export from this file in middleware (the neshka/cache package causes webpack errors there)

Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {
drupalClientPreviewer,
drupalClientViewer,
} from "@/lib/drupal/drupal-client";
import { GET_ENTITY_AT_DRUPAL_PATH } from "@/lib/graphql/queries";

/**
* Function to directly fetch a node from Drupal by its path and locale.
*
* This function is used to fetch the node data without caching, and
* can be imported in middleware or other server-side code.
*
* @param path The path of the node.
* @param locale The locale of the node.
* @param revision The revision of the node.
* @param isDraftMode If true, fetches the draft version of the node.
* @returns The fetched node data or null if not found.
*/
export async function fetchNodeByPathQuery(
path: string,
locale: string,
isDraftMode: boolean,
revision: string = null,
) {
const drupalClient = isDraftMode ? drupalClientPreviewer : drupalClientViewer;
return await drupalClient.doGraphQlRequest(GET_ENTITY_AT_DRUPAL_PATH, {
path,
langcode: locale,
revision,
});
}
30 changes: 2 additions & 28 deletions next/src/lib/drupal/get-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,10 @@ import { neshCache } from "@neshca/cache-handler/functions";
import { AbortError } from "p-retry";

import { REVALIDATE_LONG } from "@/lib/constants";
import {
drupalClientPreviewer,
drupalClientViewer,
} from "@/lib/drupal/drupal-client";
import { GET_ENTITY_AT_DRUPAL_PATH } from "@/lib/graphql/queries";

import { env } from "@/env";
import { fetchNodeByPathQuery } from "./get-node-nocache";

/**
* Function to directly fetch a node from Drupal by its path and locale.
*
* @param path The path of the node.
* @param locale The locale of the node.
* @param revision The revision of the node.
* @param isDraftMode If true, fetches the draft version of the node.
* @returns The fetched node data or null if not found.
*/
export async function fetchNodeByPathQuery(
path: string,
locale: string,
isDraftMode: boolean,
revision: string = null,
) {
const drupalClient = isDraftMode ? drupalClientPreviewer : drupalClientViewer;
return await drupalClient.doGraphQlRequest(GET_ENTITY_AT_DRUPAL_PATH, {
path,
langcode: locale,
revision,
});
}
Comment on lines -14 to -35
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to move this function to its own file, because we could not import an export from this file in middleware (the neshka/cache package causes webpack errors there)

import { env } from "@/env";

// Here we wrap the function in react cache and nesh cache avoiding unnecessary requests.
const cachedFetchNodeByPathQuery = neshCache(cache(fetchNodeByPathQuery));
Expand Down
84 changes: 83 additions & 1 deletion next/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { NextRequest, NextResponse } from "next/server";
import createMiddleware from "next-intl/middleware";

import { fetchNodeByPathQuery } from "@/lib/drupal/get-node-nocache";
import { extractRedirectFromRouteQueryResult } from "@/lib/graphql/utils";

import { routing } from "./i18n/routing";
import { auth, DEFAULT_LOGIN_REDIRECT_URL, DEFAULT_LOGIN_URL } from "./auth";

Expand All @@ -25,12 +28,85 @@ interface AppRouteHandlerFnContext {
params?: Record<string, string | string[]>;
}

// Handle redirects from Drupal
async function handleDrupalRedirects(request: NextRequest) {
const pathname = request.nextUrl.pathname;
console.log("Handling Drupal redirects for path:", pathname);

// We need to check if the path includes a locale.
// Get the default locale from the routing configuration
const defaultLocale = routing.defaultLocale;
const pathnameSegments = pathname.split("/").filter(Boolean);

// Handle locale determination
let locale;
let slugSegments;

if (pathnameSegments.length === 0) {
// Root path - use default locale
locale = defaultLocale;
slugSegments = [];
} else {
// Check if first segment is a valid locale
const firstSegment = pathnameSegments[0];
const isLocale = routing.locales.includes(
firstSegment as (typeof routing.locales)[number],
);

if (isLocale) {
// Path includes locale: /en/about
locale = firstSegment;
slugSegments = pathnameSegments.slice(1);
} else {
// Path doesn't include locale: /about
// Assume default locale
locale = defaultLocale;
slugSegments = pathnameSegments;
}
}

// If there are no slug segments, it's likely the homepage
if (slugSegments.length === 0) {
return null;
}

// Construct path for query
const path = "/" + slugSegments.join("/");
console.log("Constructed path for query:", path);
console.log("Locale for query:", locale);

try {
// Check if this path should redirect
const nodeByPathResult = await fetchNodeByPathQuery(path, locale, false);
const redirectResult =
extractRedirectFromRouteQueryResult(nodeByPathResult);

if (redirectResult) {
// Apply the appropriate redirect
const status =
redirectResult.status === 307 || redirectResult.status === 302
? redirectResult.status
: 308; // Permanent redirect

return NextResponse.redirect(
new URL(redirectResult.url, request.url),
status,
);
}
} catch (error) {
// If there's an error, continue to the page component
console.error("Middleware error:", error);
}

return null;
}

Comment on lines +31 to +103
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we handle the logic to understand if the path has the language in it en/my-drupal-page, and if it does not we assume the default locale (so /drupal-page is the same as en/drupal-page

also we ignore the frontpage

const intlMiddleware = createMiddleware(routing, {
alternateLinks: false,
});

const authMiddleware = (request: NextRequest, ctx: AppRouteHandlerFnContext) =>
auth((req) => {
auth(async (req) => {
const isLoggedIn = req.auth?.user;
const isProtectedRoute = PROTECTED_ROUTES.some((route) =>
req.nextUrl.pathname.startsWith(route),
Expand Down Expand Up @@ -60,6 +136,12 @@ const authMiddleware = (request: NextRequest, ctx: AppRouteHandlerFnContext) =>
}
}

// Check if there are redirects from Drupal for this request:
const redirectResponse = await handleDrupalRedirects(request);
if (redirectResponse) {
Comment on lines +139 to +141
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this here, after the checks for auth pages, and before the middleware for next-intl

(not sure this is great)

return redirectResponse;
}

return intlMiddleware(request);
})(request, ctx);

Expand Down