From a6cd01625b6ac2cfa3551a4da8c9d398b096a0e8 Mon Sep 17 00:00:00 2001 From: Magdalena Maislinger Date: Thu, 21 Mar 2024 10:38:38 +0100 Subject: [PATCH] Create rewrites for redirect loop (#1787) Creates rewrites for redirect loop, as case sensitivity in Next.js is inconsistent (see also https://github.com/vercel/next.js/issues/21498#issuecomment-848315717). --- Background: Sometimes you want a redirect from an uppercase to a lowercase URL (e.g. `/Example` to `/example`) for SEO purposes. This can be created in our admin without problems. The casing is saved correctly in the DB and transferred to Next. The problem is that this creates a redirect loop in the site. The reason is that the case sensitivity of Next.js is inconsistent. The delivery of pages is case sensitive. The redirects are case insensitive: Without redirect: http://localhost:3000/example -> page is delivered http://localhost:3000/Example -> 404 With redirect: http://localhost:3000/example -> Redirect to /example -> Loop http://localhost:3000/Example -> Redirect to /example -> Loop Also described here: https://github.com/vercel/next.js/issues/21498 --------- Co-authored-by: Thomas Dax --- demo/site/next.config.js | 6 + demo/site/preBuild/src/createRedirects.ts | 148 +++++++++++++--------- demo/site/preBuild/src/createRewrites.ts | 27 ++++ 3 files changed, 121 insertions(+), 60 deletions(-) create mode 100644 demo/site/preBuild/src/createRewrites.ts diff --git a/demo/site/next.config.js b/demo/site/next.config.js index bab71efbe6..ae6de57691 100644 --- a/demo/site/next.config.js +++ b/demo/site/next.config.js @@ -29,7 +29,13 @@ if (process.env.SITE_IS_PREVIEW !== "true") { * @type {import('next').NextConfig} **/ const nextConfig = { + rewrites: async () => { + if (process.env.NEXT_PUBLIC_SITE_IS_PREVIEW === "true") return []; + var rewrites = await require("./preBuild/build/preBuild/src/createRewrites").createRewrites(); + return rewrites; + }, redirects: async () => { + if (process.env.NEXT_PUBLIC_SITE_IS_PREVIEW === "true") return []; var redirects = await require("./preBuild/build/preBuild/src/createRedirects").createRedirects(); return redirects; }, diff --git a/demo/site/preBuild/src/createRedirects.ts b/demo/site/preBuild/src/createRedirects.ts index a44cba79fb..9c83855f34 100644 --- a/demo/site/preBuild/src/createRedirects.ts +++ b/demo/site/preBuild/src/createRedirects.ts @@ -10,6 +10,91 @@ const createRedirects = async () => { return [...(await createApiRedirects()), ...(await createInternalRedirects())]; }; +const redirectsQuery = gql` + query Redirects($scope: RedirectScopeInput!, $filter: RedirectFilter, $sort: [RedirectSort!], $offset: Int!, $limit: Int!) { + paginatedRedirects(scope: $scope, filter: $filter, sort: $sort, offset: $offset, limit: $limit) { + nodes { + sourceType + source + target + } + totalCount + } + } +`; + +function replaceRegexCharacters(value: string): string { + // escape ":" and "?", otherwise it is used for next.js regex path matching (https://nextjs.org/docs/pages/api-reference/next-config-js/redirects#regex-path-matching) + return value.replace(/[:?]/g, "\\$&"); +} + +export async function* getRedirects() { + let offset = 0; + const limit = 100; + + while (true) { + const { paginatedRedirects } = await createGraphQLClient().request(redirectsQuery, { + filter: { active: { equal: true } }, + sort: { field: "createdAt", direction: "DESC" }, + offset, + limit, + scope: { domain }, + }); + + yield* paginatedRedirects.nodes.map((redirect) => { + let source: string | undefined; + let destination: string | undefined; + let has: Redirect["has"]; + + if (redirect.sourceType === "path") { + // query parameters have to be defined with has, see: https://nextjs.org/docs/pages/api-reference/next-config-js/redirects#header-cookie-and-query-matching + if (redirect.source?.includes("?")) { + const searchParamsString = redirect.source.split("?").slice(1).join("?"); + const searchParams = new URLSearchParams(searchParamsString); + has = []; + + searchParams.forEach((value, key) => { + if (has) { + has.push({ type: "query", key, value: replaceRegexCharacters(value) }); + } + }); + source = replaceRegexCharacters(redirect.source.replace(searchParamsString, "")); + } else { + source = replaceRegexCharacters(redirect.source); + } + } + + const target = redirect.target as RedirectsLinkBlockData; + + if (target.block !== undefined) { + switch (target.block.type) { + case "internal": + destination = (target.block.props as InternalLinkBlockData).targetPage?.path; + break; + + case "external": + destination = (target.block.props as ExternalLinkBlockData).targetUrl; + break; + case "news": + if ((target.block.props as NewsLinkBlockData).id !== undefined) { + destination = `/news/${(target.block.props as NewsLinkBlockData).id}`; + } + + break; + } + } + + return { ...redirect, source, destination, has }; + }); + + if (offset + limit >= paginatedRedirects.totalCount) { + break; + } + + offset += limit; + } +} + const createInternalRedirects = async (): Promise => { if (process.env.ADMIN_URL === undefined) { console.error(`Cannot create "/admin" redirect: Missing ADMIN_URL environment variable`); @@ -25,74 +110,17 @@ const createInternalRedirects = async (): Promise => { ]; }; const createApiRedirects = async (): Promise => { - const query = gql` - query Redirects($scope: RedirectScopeInput!) { - redirects(scope: $scope, active: true) { - sourceType - source - target - } - } - `; const apiUrl = process.env.API_URL_INTERNAL; if (!apiUrl) { console.error("No Environment Variable API_URL_INTERNAL available. Can not perform redirect config"); return []; } - const response = await createGraphQLClient().request(query, { scope: { domain } }); - const redirects: Redirect[] = []; - function replaceRegexCharacters(value: string): string { - // escape ":" and "?", otherwise it is used for next.js regex path matching (https://nextjs.org/docs/pages/api-reference/next-config-js/redirects#regex-path-matching) - return value.replace(/[:?]/g, "\\$&"); - } - - for (const redirect of response.redirects) { - let source: string | undefined; - let destination: string | undefined; - let has: Redirect["has"]; - - if (redirect.sourceType === "path") { - // query parameters have to be defined with has, see: https://nextjs.org/docs/pages/api-reference/next-config-js/redirects#header-cookie-and-query-matching - if (redirect.source?.includes("?")) { - const searchParamsString = redirect.source.split("?").slice(1).join("?"); - const searchParams = new URLSearchParams(searchParamsString); - has = []; - - searchParams.forEach((value, key) => { - if (has) { - has.push({ type: "query", key, value: replaceRegexCharacters(value) }); - } - }); - source = replaceRegexCharacters(redirect.source.replace(searchParamsString, "")); - } else { - source = replaceRegexCharacters(redirect.source); - } - } - - const target = redirect.target as RedirectsLinkBlockData; - - if (target.block !== undefined) { - switch (target.block.type) { - case "internal": - destination = (target.block.props as InternalLinkBlockData).targetPage?.path; - break; - - case "external": - destination = (target.block.props as ExternalLinkBlockData).targetUrl; - break; - case "news": - if ((target.block.props as NewsLinkBlockData).id !== undefined) { - destination = `/news/${(target.block.props as NewsLinkBlockData).id}`; - } - - break; - } - } - - if (source === destination) { + for await (const redirect of getRedirects()) { + const { source, destination, has } = redirect; + if (source?.toLowerCase() === destination?.toLowerCase()) { console.warn(`Skipping redirect loop ${source} -> ${destination}`); continue; } diff --git a/demo/site/preBuild/src/createRewrites.ts b/demo/site/preBuild/src/createRewrites.ts new file mode 100644 index 0000000000..2e5f3f2fe7 --- /dev/null +++ b/demo/site/preBuild/src/createRewrites.ts @@ -0,0 +1,27 @@ +import { Rewrite } from "next/dist/lib/load-custom-routes"; + +import { getRedirects } from "./createRedirects"; + +const createRewrites = async () => { + const apiUrl = process.env.API_URL_INTERNAL; + if (!apiUrl) { + console.error("No Environment Variable API_URL_INTERNAL available. Can not perform redirect config"); + return { redirects: [], rewrites: [] }; + } + + const rewrites: Rewrite[] = []; + + for await (const redirect of getRedirects()) { + const { source, destination } = redirect; + + // A rewrite is created for each redirect where the source and destination differ only by casing (otherwise, this causes a redirection loop). + // For instance, a rewrite is created for the redirect /Example -> /example. + if (source && destination && source.toLowerCase() === destination.toLowerCase()) { + rewrites.push({ source, destination }); + } + } + + return rewrites; +}; + +export { createRewrites };