diff --git a/packages/gatsby-plugin-nginx/src/nginx-generator.ts b/packages/gatsby-plugin-nginx/src/nginx-generator.ts index 68e19e0496..b13258620a 100644 --- a/packages/gatsby-plugin-nginx/src/nginx-generator.ts +++ b/packages/gatsby-plugin-nginx/src/nginx-generator.ts @@ -176,6 +176,12 @@ function generateNginxConfiguration({ ], }, + // Remove trailing slash if present + { + cmd: ['location', '~', '^(?.+)/$'], + children: [{ cmd: ['rewrite', '.+', '$no_slash'] }], + }, + ...prependLocations, ...locations, ...appendLocations, @@ -201,6 +207,7 @@ function stringify(directives: NginxDirective[]): string { const wildcard = /\*/g const namedSegment = /:[^/]+/g +const catchAll = '/(.*)' // Converts a gatsby path to nginx location path // Ex: @@ -208,10 +215,17 @@ const namedSegment = /:[^/]+/g // '/:splat' => '^/([^/]+)$' // '/foo/bar/:splat' => '^/foo/bar/([^/]+)$' export function convertToRegExp(path: string) { - const converted = path + let converted = path .replace(wildcard, '(.*)') // replace * with (.*) .replace(namedSegment, '([^/]+)') // replace :param like with url component like regex ([^/]+) + if (converted.endsWith(catchAll) && converted.length > catchAll.length) { + // allows '/*' to serve '' (without trailing slash) + // this is necessary because we are now removing trailing slashes from incoming requests + // so exact matches can work when there is a trailing slash + converted = converted.slice(0, -catchAll.length) + '(?:/(.*))?' + } + const noTrailingSlashes = normalizePath(converted) return `^${noTrailingSlashes}$` diff --git a/packages/gatsby-plugin-nginx/test/nginx-generator.test.ts b/packages/gatsby-plugin-nginx/test/nginx-generator.test.ts index 78220433a2..927a6f0090 100644 --- a/packages/gatsby-plugin-nginx/test/nginx-generator.test.ts +++ b/packages/gatsby-plugin-nginx/test/nginx-generator.test.ts @@ -57,7 +57,8 @@ describe('convert Gatsby paths into nginx RegExp', () => { it('handles wildcard (*)', () => { expect(convertToRegExp('/*')).toEqual('^/(.*)$') expect(convertToRegExp('/*/')).toEqual('^/(.*)$') - expect(convertToRegExp('/pt/*')).toEqual('^/pt/(.*)$') + expect(convertToRegExp('/*/*')).toEqual('^/(.*)(?:/(.*))?$') + expect(convertToRegExp('/pt/*')).toEqual('^/pt(?:/(.*))?$') }) }) @@ -109,7 +110,7 @@ describe('generateRewrites', () => { }, { children: [{ cmd: ['rewrite', '.+', '/pt/__client-side-search__'] }], - cmd: ['location', '~*', '"^/pt/(.*)$"'], + cmd: ['location', '~*', '"^/pt(?:/(.*))?$"'], }, { children: [{ cmd: ['rewrite', '.+', '/foo-path'] }], @@ -138,7 +139,7 @@ describe('generateRedirects', () => { it('correctly translates into NginxDirectives', () => { const expected = [ { - cmd: ['location', '~*', '"^/api/(.*)$"'], + cmd: ['location', '~*', '"^/api(?:/(.*))?$"'], children: [ { cmd: [ @@ -150,7 +151,7 @@ describe('generateRedirects', () => { ], }, { - cmd: ['location', '~*', '"^/graphql/(.*)$"'], + cmd: ['location', '~*', '"^/graphql(?:/(.*))?$"'], children: [ { cmd: [ @@ -279,6 +280,9 @@ describe('generateNginxConfiguration', () => { deny all; return 404; } + location ~ ^(?.+)/$ { + rewrite .+ $no_slash; + } location = /foo { add_header a \\"b\\"; try_files /foo/index.html =404;