Skip to content

Commit

Permalink
Create rewrites for redirect loop (#1787)
Browse files Browse the repository at this point in the history
Creates rewrites for redirect loop, as case sensitivity in Next.js is
inconsistent (see also
vercel/next.js#21498 (comment)).

--- 

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: vercel/next.js#21498

---------

Co-authored-by: Thomas Dax <[email protected]>
  • Loading branch information
magdalenaxm and thomasdax98 authored Mar 21, 2024
1 parent ddd798e commit a6cd016
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 60 deletions.
6 changes: 6 additions & 0 deletions demo/site/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
},
Expand Down
148 changes: 88 additions & 60 deletions demo/site/preBuild/src/createRedirects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<GQLRedirectsQuery, GQLRedirectsQueryVariables>(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<Redirect[]> => {
if (process.env.ADMIN_URL === undefined) {
console.error(`Cannot create "/admin" redirect: Missing ADMIN_URL environment variable`);
Expand All @@ -25,74 +110,17 @@ const createInternalRedirects = async (): Promise<Redirect[]> => {
];
};
const createApiRedirects = async (): Promise<Redirect[]> => {
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<GQLRedirectsQuery, GQLRedirectsQueryVariables>(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;
}
Expand Down
27 changes: 27 additions & 0 deletions demo/site/preBuild/src/createRewrites.ts
Original file line number Diff line number Diff line change
@@ -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 };

0 comments on commit a6cd016

Please sign in to comment.