diff --git a/src/cypress/fixtures/next.config.js-with-basePath b/src/cypress/fixtures/next.config.js-with-basePath new file mode 100644 index 0000000000..6ad9e1249d --- /dev/null +++ b/src/cypress/fixtures/next.config.js-with-basePath @@ -0,0 +1,4 @@ +module.exports = { + target: "experimental-serverless-trace", + basePath: "/foo", +}; diff --git a/src/cypress/fixtures/pages/getServerSideProps/[id].js b/src/cypress/fixtures/pages/getServerSideProps/[id].js index 3487e968de..39e1d862ac 100644 --- a/src/cypress/fixtures/pages/getServerSideProps/[id].js +++ b/src/cypress/fixtures/pages/getServerSideProps/[id].js @@ -11,7 +11,7 @@ const Show = ({ errorCode, show }) => { return (
- This page uses getInitialProps() to fetch the show with the ID provided in the URL: /shows/:id
+ This page uses getServerSideProps() to fetch the show with the ID provided in the URL: /shows/:id
Refresh the page to see server-side rendering in action.
diff --git a/src/cypress/fixtures/pages/getServerSideProps/static.js b/src/cypress/fixtures/pages/getServerSideProps/static.js
index dfbce6da56..4c4faea824 100644
--- a/src/cypress/fixtures/pages/getServerSideProps/static.js
+++ b/src/cypress/fixtures/pages/getServerSideProps/static.js
@@ -3,7 +3,7 @@ import Link from 'next/link'
const Show = ({ show }) => (
- This page uses getInitialProps() to fetch the show with the ID provided in the URL: /shows/:id
+ This page uses getServerSideProps() to fetch the show with the ID provided in the URL: /shows/:id
Refresh the page to see server-side rendering in action.
diff --git a/src/cypress/integration/basePath_spec.js b/src/cypress/integration/basePath_spec.js
new file mode 100644
index 0000000000..00a3c014bf
--- /dev/null
+++ b/src/cypress/integration/basePath_spec.js
@@ -0,0 +1,747 @@
+const project = 'basePath'
+
+before(() => {
+ // When changing the base URL within a spec file, Cypress runs the spec twice
+ // To avoid rebuilding and redeployment on the second run, we check if the
+ // project has already been deployed.
+ cy.task('isDeployed').then((isDeployed) => {
+ // Cancel setup, if already deployed
+ if (isDeployed) return
+
+ // Clear project folder
+ cy.task('clearProject', { project })
+
+ // Copy NextJS files
+ cy.task('copyFixture', {
+ project,
+ from: 'pages',
+ to: 'pages',
+ })
+ cy.task('copyFixture', {
+ project,
+ from: 'next.config.js-with-basePath',
+ to: 'next.config.js',
+ })
+
+ // Copy package.json file
+ cy.task('copyFixture', {
+ project,
+ from: 'package.json',
+ to: 'package.json',
+ })
+
+ // Copy Netlify settings
+ cy.task('copyFixture', {
+ project,
+ from: 'netlify.toml',
+ to: 'netlify.toml',
+ })
+ cy.task('copyFixture', {
+ project,
+ from: '.netlify',
+ to: '.netlify',
+ })
+
+ // Build
+ cy.task('buildProject', { project })
+
+ // Deploy
+ cy.task('deployProject', { project }, { timeout: 480 * 1000 })
+ })
+
+ // Set base URL
+ cy.task('getBaseUrl', { project }).then((url) => {
+ Cypress.config('baseUrl', url)
+ })
+})
+
+after(() => {
+ // While the before hook runs twice (it's re-run when the base URL changes),
+ // the after hook only runs once.
+ cy.task('clearDeployment')
+})
+
+describe('getInitialProps', () => {
+ context('with static route', () => {
+ it('loads TV shows', () => {
+ cy.visit('/')
+
+ cy.get('ul').first().children().should('have.length', 5)
+ })
+
+ it('loads TV shows when SSR-ing', () => {
+ cy.ssr('/')
+
+ cy.get('ul').first().children().should('have.length', 5)
+ })
+
+ it('loads TV shows w basePath', () => {
+ cy.visit('/foo')
+
+ cy.get('ul').first().children().should('have.length', 5)
+ })
+
+ it('loads TV shows when SSR-ing w basePath', () => {
+ cy.ssr('/foo')
+
+ cy.get('ul').first().children().should('have.length', 5)
+ })
+ })
+
+ context('with dynamic route', () => {
+ it('loads TV show', () => {
+ cy.visit('/foo/shows/24251')
+
+ cy.get('h1').should('contain', 'Show #24251')
+ cy.get('p').should('contain', 'Animal Science')
+ })
+
+ it('loads TV show when SSR-ing', () => {
+ cy.ssr('/foo/shows/24251')
+
+ cy.get('h1').should('contain', 'Show #24251')
+ cy.get('p').should('contain', 'Animal Science')
+ })
+ })
+
+ context('with catch-all route', () => {
+ it('displays all URL parameters, including query string parameters', () => {
+ cy.visit('/foo/shows/94/this-is-all/being/captured/yay?search=dog&custom-param=cat')
+
+ // path parameters
+ cy.get('p').should('contain', '[0]: 94')
+ cy.get('p').should('contain', '[1]: this-is-all')
+ cy.get('p').should('contain', '[2]: being')
+ cy.get('p').should('contain', '[3]: captured')
+ cy.get('p').should('contain', '[4]: yay')
+
+ // query string parameters
+ cy.get('p').should('contain', '[search]: dog')
+ cy.get('p').should('contain', '[custom-param]: cat')
+
+ cy.get('h1').should('contain', 'Show #94')
+ cy.get('p').should('contain', 'Defiance')
+ })
+
+ it('displays all URL parameters when SSR-ing, including query string parameters', () => {
+ cy.visit('/foo/shows/94/this-is-all/being/captured/yay?search=dog&custom-param=cat')
+
+ // path parameters
+ cy.get('p').should('contain', '[0]: 94')
+ cy.get('p').should('contain', '[1]: this-is-all')
+ cy.get('p').should('contain', '[2]: being')
+ cy.get('p').should('contain', '[3]: captured')
+ cy.get('p').should('contain', '[4]: yay')
+
+ // query string parameters
+ cy.get('p').should('contain', '[search]: dog')
+ cy.get('p').should('contain', '[custom-param]: cat')
+
+ cy.get('h1').should('contain', 'Show #94')
+ cy.get('p').should('contain', 'Defiance')
+ })
+ })
+})
+
+describe('getServerSideProps', () => {
+ it('exposes function context on the req object', () => {
+ cy.visit('/foo/getServerSideProps/context')
+
+ cy.get('pre')
+ .first()
+ .then((json) => {
+ const {
+ req: {
+ netlifyFunctionParams: { event, context },
+ },
+ } = JSON.parse(json.html())
+
+ expect(event).to.have.property('path', '/foo/getServerSideProps/context')
+ expect(event).to.have.property('httpMethod', 'GET')
+ expect(event).to.have.property('headers')
+ expect(event).to.have.property('multiValueHeaders')
+ expect(event).to.have.property('isBase64Encoded')
+ expect(context.done).to.be.undefined
+ expect(context.getRemainingTimeInMillis).to.be.undefined
+ expect(context).to.have.property('awsRequestId')
+ expect(context).to.have.property('callbackWaitsForEmptyEventLoop')
+ expect(context).to.have.property('clientContext')
+ })
+ })
+
+ context('with static route', () => {
+ it('loads TV shows', () => {
+ cy.visit('/foo/getServerSideProps/static')
+
+ cy.get('h1').should('contain', 'Show #42')
+ cy.get('p').should('contain', 'Sleepy Hollow')
+ })
+
+ it('loads TV shows when SSR-ing', () => {
+ cy.ssr('/foo/getServerSideProps/static')
+
+ cy.get('h1').should('contain', 'Show #42')
+ cy.get('p').should('contain', 'Sleepy Hollow')
+ })
+
+ it('loads page props from data .json file when navigating to it', () => {
+ cy.visit('/foo')
+ cy.window().then((w) => (w.noReload = true))
+
+ // Navigate to page and test that no reload is performed
+ // See: https://glebbahmutov.com/blog/detect-page-reload/
+ cy.contains('getServerSideProps/static').click()
+ cy.get('h1').should('contain', 'Show #42')
+ cy.get('p').should('contain', 'Sleepy Hollow')
+ cy.window().should('have.property', 'noReload', true)
+ })
+ })
+
+ context('with dynamic route', () => {
+ it('loads TV show', () => {
+ cy.visit('/foo/getServerSideProps/1337')
+
+ cy.get('h1').should('contain', 'Show #1337')
+ cy.get('p').should('contain', 'Whodunnit?')
+ })
+
+ it('loads TV show when SSR-ing', () => {
+ cy.ssr('/foo/getServerSideProps/1337')
+
+ cy.get('h1').should('contain', 'Show #1337')
+ cy.get('p').should('contain', 'Whodunnit?')
+ })
+
+ it('loads page props from data .json file when navigating to it', () => {
+ cy.visit('/')
+ cy.window().then((w) => (w.noReload = true))
+
+ // Navigate to page and test that no reload is performed
+ // See: https://glebbahmutov.com/blog/detect-page-reload/
+ cy.contains('getServerSideProps/1337').click()
+
+ cy.get('h1').should('contain', 'Show #1337')
+ cy.get('p').should('contain', 'Whodunnit?')
+
+ cy.contains('Go back home').click()
+ cy.contains('getServerSideProps/1338').click()
+
+ cy.get('h1').should('contain', 'Show #1338')
+ cy.get('p').should('contain', 'The Whole Truth')
+
+ cy.window().should('have.property', 'noReload', true)
+ })
+ })
+
+ context('with catch-all route', () => {
+ it('does not match base path (without params)', () => {
+ cy.request({
+ url: '/foo/getServerSideProps/catch/all',
+ failOnStatusCode: false,
+ }).then((response) => {
+ expect(response.status).to.eq(404)
+ cy.state('document').write(response.body)
+ })
+
+ cy.get('h2').should('contain', 'This page could not be found.')
+ })
+
+ it('loads TV show with one param', () => {
+ cy.visit('/foo/getServerSideProps/catch/all/1337')
+
+ cy.get('h1').should('contain', 'Show #1337')
+ cy.get('p').should('contain', 'Whodunnit?')
+ })
+
+ it('loads TV show with multiple params', () => {
+ cy.visit('/foo/getServerSideProps/catch/all/1337/multiple/params')
+
+ cy.get('h1').should('contain', 'Show #1337')
+ cy.get('p').should('contain', 'Whodunnit?')
+ })
+
+ it('loads page props from data .json file when navigating to it', () => {
+ cy.visit('/')
+ cy.window().then((w) => (w.noReload = true))
+
+ // Navigate to page and test that no reload is performed
+ // See: https://glebbahmutov.com/blog/detect-page-reload/
+ cy.contains('getServerSideProps/catch/all/1337').click()
+
+ cy.get('h1').should('contain', 'Show #1337')
+ cy.get('p').should('contain', 'Whodunnit?')
+
+ cy.contains('Go back home').click()
+ cy.contains('getServerSideProps/catch/all/1338').click()
+
+ cy.get('h1').should('contain', 'Show #1338')
+ cy.get('p').should('contain', 'The Whole Truth')
+
+ cy.window().should('have.property', 'noReload', true)
+ })
+ })
+})
+
+describe('getStaticProps', () => {
+ context('with static route', () => {
+ it('loads TV show', () => {
+ cy.visit('/foo/getStaticProps/static')
+
+ cy.get('h1').should('contain', 'Show #71')
+ cy.get('p').should('contain', 'Dancing with the Stars')
+ })
+
+ it('loads page props from data .json file when navigating to it', () => {
+ cy.visit('/foo')
+ cy.window().then((w) => (w.noReload = true))
+
+ // Navigate to page and test that no reload is performed
+ // See: https://glebbahmutov.com/blog/detect-page-reload/
+ cy.contains('getStaticProps/static').click()
+ cy.get('h1').should('contain', 'Show #71')
+ cy.get('p').should('contain', 'Dancing with the Stars')
+ cy.window().should('have.property', 'noReload', true)
+ })
+
+ context('with revalidate', () => {
+ it('loads TV show', () => {
+ cy.visit('/foo/getStaticProps/with-revalidate')
+
+ cy.get('h1').should('contain', 'Show #71')
+ cy.get('p').should('contain', 'Dancing with the Stars')
+ })
+
+ it('loads TV shows when SSR-ing', () => {
+ cy.ssr('/foo/getStaticProps/with-revalidate')
+
+ cy.get('h1').should('contain', 'Show #71')
+ cy.get('p').should('contain', 'Dancing with the Stars')
+ })
+ })
+ })
+
+ context('with dynamic route', () => {
+ context('without fallback', () => {
+ it('loads shows 1 and 2', () => {
+ cy.visit('/foo/getStaticProps/1')
+ cy.get('h1').should('contain', 'Show #1')
+ cy.get('p').should('contain', 'Under the Dome')
+
+ cy.visit('/foo/getStaticProps/2')
+ cy.get('h1').should('contain', 'Show #2')
+ cy.get('p').should('contain', 'Person of Interest')
+ })
+
+ it('loads page props from data .json file when navigating to it', () => {
+ cy.visit('/foo')
+ cy.window().then((w) => (w.noReload = true))
+
+ // Navigate to page and test that no reload is performed
+ // See: https://glebbahmutov.com/blog/detect-page-reload/
+ cy.contains('getStaticProps/1').click()
+
+ cy.get('h1').should('contain', 'Show #1')
+ cy.get('p').should('contain', 'Under the Dome')
+
+ cy.contains('Go back home').click()
+ cy.contains('getStaticProps/2').click()
+
+ cy.get('h1').should('contain', 'Show #2')
+ cy.get('p').should('contain', 'Person of Interest')
+
+ cy.window().should('have.property', 'noReload', true)
+ })
+
+ it('returns 404 when trying to access non-defined path', () => {
+ cy.request({
+ url: '/foo/getStaticProps/3',
+ failOnStatusCode: false,
+ }).then((response) => {
+ expect(response.status).to.eq(404)
+ cy.state('document').write(response.body)
+ })
+
+ cy.get('h2').should('contain', 'This page could not be found.')
+ })
+ })
+
+ context('with fallback', () => {
+ it('loads pre-rendered TV shows 3 and 4', () => {
+ cy.visit('/foo/getStaticProps/withFallback/3')
+ cy.get('h1').should('contain', 'Show #3')
+ cy.get('p').should('contain', 'Bitten')
+
+ cy.visit('/foo/getStaticProps/withFallback/4')
+ cy.get('h1').should('contain', 'Show #4')
+ cy.get('p').should('contain', 'Arrow')
+ })
+
+ it('loads non-pre-rendered TV show', () => {
+ cy.visit('/foo/getStaticProps/withFallback/75')
+
+ cy.get('h1').should('contain', 'Show #75')
+ cy.get('p').should('contain', 'The Mindy Project')
+ })
+
+ it('loads non-pre-rendered TV shows when SSR-ing', () => {
+ cy.ssr('/foo/getStaticProps/withFallback/75')
+
+ cy.get('h1').should('contain', 'Show #75')
+ cy.get('p').should('contain', 'The Mindy Project')
+ })
+
+ it('loads page props from data .json file when navigating to it', () => {
+ cy.visit('/foo')
+ cy.window().then((w) => (w.noReload = true))
+
+ // Navigate to page and test that no reload is performed
+ // See: https://glebbahmutov.com/blog/detect-page-reload/
+ cy.contains('getStaticProps/withFallback/3').click()
+
+ cy.get('h1').should('contain', 'Show #3')
+ cy.get('p').should('contain', 'Bitten')
+
+ cy.contains('Go back home').click()
+ cy.contains('getStaticProps/withFallback/4').click()
+
+ cy.get('h1').should('contain', 'Show #4')
+ cy.get('p').should('contain', 'Arrow')
+
+ cy.contains('Go back home').click()
+ cy.contains('getStaticProps/withFallback/75').click()
+
+ cy.get('h1').should('contain', 'Show #75')
+ cy.get('p').should('contain', 'The Mindy Project')
+
+ cy.window().should('have.property', 'noReload', true)
+ })
+ })
+
+ context('with revalidate', () => {
+ it('loads TV show', () => {
+ cy.visit('/foo/getStaticProps/withRevalidate/75')
+
+ cy.get('h1').should('contain', 'Show #75')
+ cy.get('p').should('contain', 'The Mindy Project')
+ })
+
+ it('loads TV shows when SSR-ing', () => {
+ cy.ssr('/foo/getStaticProps/withRevalidate/75')
+
+ cy.get('h1').should('contain', 'Show #75')
+ cy.get('p').should('contain', 'The Mindy Project')
+ })
+
+ it('loads page props from data .json file when navigating to it', () => {
+ cy.visit('/')
+ cy.window().then((w) => (w.noReload = true))
+
+ // Navigate to page and test that no reload is performed
+ // See: https://glebbahmutov.com/blog/detect-page-reload/
+ cy.contains('getStaticProps/withRevalidate/3').click()
+
+ cy.get('h1').should('contain', 'Show #3')
+ cy.get('p').should('contain', 'Bitten')
+
+ cy.contains('Go back home').click()
+ cy.contains('getStaticProps/withRevalidate/4').click()
+
+ cy.get('h1').should('contain', 'Show #4')
+ cy.get('p').should('contain', 'Arrow')
+
+ cy.window().should('have.property', 'noReload', true)
+ })
+ })
+ })
+
+ context('with catch-all route', () => {
+ context('with fallback', () => {
+ it('loads pre-rendered shows 1 and 2', () => {
+ cy.visit('/foo/getStaticProps/withFallback/my/path/1')
+ cy.get('h1').should('contain', 'Show #1')
+ cy.get('p').should('contain', 'Under the Dome')
+
+ cy.visit('/foo/getStaticProps/withFallback/my/path/2')
+ cy.get('h1').should('contain', 'Show #2')
+ cy.get('p').should('contain', 'Person of Interest')
+ })
+
+ it('loads non-pre-rendered TV show', () => {
+ cy.visit('/foo/getStaticProps/withFallback/undefined/catch/all/path/75')
+
+ cy.get('h1').should('contain', 'Show #75')
+ cy.get('p').should('contain', 'The Mindy Project')
+ })
+
+ it('loads page props from data .json file when navigating to it', () => {
+ cy.visit('/')
+ cy.window().then((w) => (w.noReload = true))
+
+ // Navigate to page and test that no reload is performed
+ // See: https://glebbahmutov.com/blog/detect-page-reload/
+ cy.contains('getStaticProps/withFallback/my/path/1').click()
+
+ cy.get('h1').should('contain', 'Show #1')
+ cy.get('p').should('contain', 'Under the Dome')
+
+ cy.contains('Go back home').click()
+ cy.contains('getStaticProps/withFallback/my/path/2').click()
+
+ cy.get('h1').should('contain', 'Show #2')
+ cy.get('p').should('contain', 'Person of Interest')
+
+ cy.contains('Go back home').click()
+ cy.contains('getStaticProps/withFallback/my/undefined/path/75').click()
+
+ cy.get('h1').should('contain', 'Show #75')
+ cy.get('p').should('contain', 'The Mindy Project')
+
+ cy.window().should('have.property', 'noReload', true)
+ })
+ })
+ })
+})
+
+describe('API endpoint', () => {
+ context('with static route', () => {
+ it('returns hello world, with all response headers', () => {
+ cy.request('/foo/api/static').then((response) => {
+ expect(response.headers['content-type']).to.include('application/json')
+ expect(response.headers['my-custom-header']).to.include('header123')
+
+ expect(response.body).to.have.property('message', 'hello world :)')
+ })
+ })
+ })
+
+ context('with dynamic route', () => {
+ it('returns TV show', () => {
+ cy.request('/foo/api/shows/305').then((response) => {
+ expect(response.headers['content-type']).to.include('application/json')
+
+ expect(response.body).to.have.property('show')
+ expect(response.body.show).to.have.property('id', 305)
+ expect(response.body.show).to.have.property('name', 'Black Mirror')
+ })
+ })
+ })
+
+ context('with catch-all route', () => {
+ it('returns all URL paremeters, including query string parameters', () => {
+ cy.request('/foo/api/shows/590/this/path/is/captured?metric=dog&p2=cat').then((response) => {
+ expect(response.headers['content-type']).to.include('application/json')
+
+ // Params
+ expect(response.body).to.have.property('params')
+ expect(response.body.params).to.deep.eq(['590', 'this', 'path', 'is', 'captured'])
+
+ // Query string parameters
+ expect(response.body).to.have.property('queryStringParams')
+ expect(response.body.queryStringParams).to.deep.eq({
+ metric: 'dog',
+ p2: 'cat',
+ })
+
+ // Show
+ expect(response.body).to.have.property('show')
+ expect(response.body.show).to.have.property('id', 590)
+ expect(response.body.show).to.have.property('name', 'Pokémon')
+ })
+ })
+ })
+
+ it('redirects with res.redirect', () => {
+ cy.visit('/foo/api/redirect?to=999')
+
+ cy.url().should('include', '/shows/999')
+ cy.get('h1').should('contain', 'Show #999')
+ cy.get('p').should('contain', 'Flash Gordon')
+ })
+
+ it('exposes function context on the req object', () => {
+ cy.request('/foo/api/context').then((response) => {
+ const {
+ req: {
+ netlifyFunctionParams: { event, context },
+ },
+ } = response.body
+
+ expect(event).to.have.property('path', '/foo/api/context')
+ expect(event).to.have.property('httpMethod', 'GET')
+ expect(event).to.have.property('headers')
+ expect(event).to.have.property('multiValueHeaders')
+ expect(event).to.have.property('isBase64Encoded')
+ expect(context.done).to.be.undefined
+ expect(context.getRemainingTimeInMillis).to.be.undefined
+ expect(context).to.have.property('awsRequestId')
+ expect(context).to.have.property('callbackWaitsForEmptyEventLoop')
+ expect(context).to.have.property('clientContext')
+ })
+ })
+})
+
+describe('Preview Mode', () => {
+ it('redirects to preview test page with dynamic route', () => {
+ cy.visit('/foo/api/enterPreview?id=999')
+
+ cy.url().should('include', '/previewTest/999')
+ })
+
+ it('redirects to static preview test page', () => {
+ cy.visit('/foo/api/enterPreviewStatic')
+
+ cy.url().should('include', '/previewTest/static')
+ })
+
+ it('sets cookies on client', () => {
+ Cypress.Cookies.debug(true)
+ cy.getCookie('__prerender_bypass').should('not.exist')
+ cy.getCookie('__next_preview_data').should('not.exist')
+
+ cy.visit('/api/enterPreview?id=999')
+
+ cy.getCookie('__prerender_bypass').should('not.be', null)
+ cy.getCookie('__next_preview_data').should('not.be', null)
+ })
+
+ it('sets cookies on client with static redirect', () => {
+ Cypress.Cookies.debug(true)
+ cy.getCookie('__prerender_bypass').should('not.exist')
+ cy.getCookie('__next_preview_data').should('not.exist')
+
+ cy.visit('/api/enterPreviewStatic')
+
+ cy.getCookie('__prerender_bypass').should('not.be', null)
+ cy.getCookie('__next_preview_data').should('not.be', null)
+ })
+
+ it('renders serverSideProps page in preview mode', () => {
+ cy.visit('/foo/api/enterPreview?id=999')
+
+ if (Cypress.env('DEPLOY') === 'local') {
+ cy.makeCookiesWorkWithHttpAndReload()
+ }
+
+ cy.get('h1').should('contain', 'Person #999')
+ cy.get('p').should('contain', 'Sebastian Lacause')
+ })
+
+ it('renders staticProps page in preview mode', () => {
+ // cypress local (aka netlify dev) doesn't support cookie-based redirects
+ if (Cypress.env('DEPLOY') !== 'local') {
+ cy.visit('/foo/api/enterPreviewStatic')
+ cy.get('h1').should('contain', 'Number: 3')
+ }
+ })
+
+ it('can move in and out of preview mode for SSRed page', () => {
+ cy.visit('/foo/api/enterPreview?id=999')
+
+ if (Cypress.env('DEPLOY') === 'local') {
+ cy.makeCookiesWorkWithHttpAndReload()
+ }
+
+ cy.get('h1').should('contain', 'Person #999')
+ cy.get('p').should('contain', 'Sebastian Lacause')
+
+ cy.contains('Go back home').click()
+
+ // Verify that we're still in preview mode
+ cy.contains('previewTest/222').click()
+ cy.get('h1').should('contain', 'Person #222')
+ cy.get('p').should('contain', 'Corey Lof')
+
+ // Exit preview mode
+ cy.visit('/foo/api/exitPreview')
+
+ // Verify that we're no longer in preview mode
+ cy.contains('previewTest/222').click()
+ cy.get('h1').should('contain', 'Show #222')
+ cy.get('p').should('contain', 'Happyland')
+ })
+
+ it('can move in and out of preview mode for static page', () => {
+ if (Cypress.env('DEPLOY') !== 'local') {
+ cy.visit('/foo/api/enterPreviewStatic')
+ cy.window().then((w) => (w.noReload = true))
+
+ cy.get('h1').should('contain', 'Number: 3')
+
+ cy.contains('Go back home').click()
+
+ // Verify that we're still in preview mode
+ cy.contains('previewTest/static').click()
+ cy.get('h1').should('contain', 'Number: 3')
+
+ cy.window().should('have.property', 'noReload', true)
+
+ // Exit preview mode
+ cy.visit('/foo/api/exitPreview')
+
+ // TO-DO: test if this is the static html?
+ // Verify that we're no longer in preview mode
+ cy.contains('previewTest/static').click()
+ cy.get('h1').should('contain', 'Number: 4')
+ }
+ })
+
+ it('hits the prerendered html out of preview mode and netlify function in preview mode', () => {
+ if (Cypress.env('DEPLOY') !== 'local') {
+ cy.request('/foo/previewTest/static').then((response) => {
+ expect(response.headers['cache-control']).to.include('public')
+ })
+
+ cy.visit('/foo/api/enterPreviewStatic')
+
+ cy.request('/foo/previewTest/static').then((response) => {
+ expect(response.headers['cache-control']).to.include('private')
+ })
+ }
+ })
+})
+
+describe('pre-rendered HTML pages', () => {
+ context('with static route', () => {
+ it('renders', () => {
+ cy.visit('/foo/static')
+
+ cy.get('p').should('contain', 'It is a static page.')
+ })
+
+ it('renders when SSR-ing', () => {
+ cy.visit('/foo/static')
+
+ cy.get('p').should('contain', 'It is a static page.')
+ })
+ })
+
+ context('with dynamic route', () => {
+ it('renders', () => {
+ cy.visit('/foo/static/superdynamic')
+
+ cy.get('p').should('contain', 'It is a static page.')
+ cy.get('p').should('contain', 'it has a dynamic URL parameter: /static/:id.')
+ })
+
+ it('renders when SSR-ing', () => {
+ cy.visit('/foo/static/superdynamic')
+
+ cy.get('p').should('contain', 'It is a static page.')
+ cy.get('p').should('contain', 'it has a dynamic URL parameter: /static/:id.')
+ })
+ })
+})
+
+describe('404 page', () => {
+ it('renders', () => {
+ cy.request({
+ url: '/foo/this-page-does-not-exist',
+ failOnStatusCode: false,
+ }).then((response) => {
+ expect(response.status).to.eq(404)
+ cy.state('document').write(response.body)
+ })
+
+ cy.get('h2').should('contain', 'This page could not be found.')
+ })
+})
diff --git a/src/lib/config.js b/src/lib/config.js
index b828bf00b2..59ca693aae 100644
--- a/src/lib/config.js
+++ b/src/lib/config.js
@@ -1,6 +1,5 @@
const { join } = require('path')
-const getNextDistDir = require('./helpers/getNextDistDir')
const getNextSrcDirs = require('./helpers/getNextSrcDir')
// This is where next-on-netlify will place all static files.
diff --git a/src/lib/helpers/convertToBasePathRedirects.js b/src/lib/helpers/convertToBasePathRedirects.js
new file mode 100644
index 0000000000..e154bb8f53
--- /dev/null
+++ b/src/lib/helpers/convertToBasePathRedirects.js
@@ -0,0 +1,73 @@
+// This helper converts the collection of redirects for all page types into
+// the necessary redirects for a basePath-generated site
+// NOTE: /withoutProps/redirects.js has some of its own contained basePath logic
+
+const getBasePathDefaultRedirects = ({ basePath, nextRedirects }) => {
+ if (basePath === '') return []
+ // In a basePath-configured site, all _next assets are fetched with the prepended basePath
+ return [
+ {
+ route: `${basePath}/_next/*`,
+ target: '/_next/:splat',
+ statusCode: '301',
+ force: true,
+ },
+ ]
+}
+
+const convertToBasePathRedirects = ({ basePath, nextRedirects }) => {
+ if (basePath === '') return nextRedirects
+ const basePathRedirects = getBasePathDefaultRedirects({ basePath, nextRedirects })
+ nextRedirects.forEach((r) => {
+ if (r.route === '/') {
+ // On Vercel, a basePath configured site 404s on /, but we can ensure it redirects to /basePath
+ const indexRedirects = [
+ {
+ route: '/',
+ target: basePath,
+ statusCode: '301',
+ force: true,
+ },
+ {
+ route: basePath,
+ target: r.target,
+ },
+ ]
+ basePathRedirects.push(...indexRedirects)
+ } else if (!r.route.includes('_next/') && r.target.includes('/.netlify/functions') && r.conditions) {
+ // If this is a preview mode redirect, we need different behavior than other function targets below
+ // because the conditions prevent us from doing a route -> basePath/route force
+ basePathRedirects.push({
+ route: `${basePath}${r.route}`,
+ target: r.target,
+ force: true,
+ conditions: r.conditions,
+ })
+ basePathRedirects.push({
+ route: `${basePath}${r.route}`,
+ target: r.route,
+ })
+ } else if (!r.route.includes('_next/') && r.target.includes('/.netlify/functions')) {
+ // This force redirect is necessary for non-preview mode function targets because the serverless lambdas
+ // try to strip basePath and redirect to the plain route per https://github.com/vercel/next.js/blob/5bff9eac084b69affe3560c4f4cfd96724aa5e49/packages/next/next-server/lib/router/router.ts#L974
+ const functionRedirects = [
+ {
+ route: r.route,
+ target: `${basePath}${r.route}`,
+ statusCode: '301',
+ force: true,
+ },
+ {
+ route: `${basePath}${r.route}`,
+ target: r.target,
+ },
+ ]
+ basePathRedirects.push(...functionRedirects)
+ } else {
+ basePathRedirects.push(r)
+ }
+ })
+ return basePathRedirects
+}
+
+module.exports = convertToBasePathRedirects
diff --git a/src/lib/helpers/formatRedirectTarget.js b/src/lib/helpers/formatRedirectTarget.js
new file mode 100644
index 0000000000..ed8d3c7c09
--- /dev/null
+++ b/src/lib/helpers/formatRedirectTarget.js
@@ -0,0 +1,9 @@
+// Returns formatted redirect target
+const { DYNAMIC_PARAMETER_REGEX } = require('../constants/regex')
+
+const formatRedirectTarget = ({ basePath, target }) =>
+ basePath !== '' && target.includes(basePath)
+ ? target.replace(DYNAMIC_PARAMETER_REGEX, '/:$1').replace('[', '').replace(']', '').replace('...', '')
+ : target
+
+module.exports = formatRedirectTarget
diff --git a/src/lib/pages/withoutProps/redirects.js b/src/lib/pages/withoutProps/redirects.js
index f48ba3ad58..b85eafb6b3 100644
--- a/src/lib/pages/withoutProps/redirects.js
+++ b/src/lib/pages/withoutProps/redirects.js
@@ -1,3 +1,4 @@
+const getNextConfig = require('../../../../helpers/getNextConfig')
const addDefaultLocaleRedirect = require('../../helpers/addDefaultLocaleRedirect')
const asyncForEach = require('../../helpers/asyncForEach')
const isDynamicRoute = require('../../helpers/isDynamicRoute')
@@ -20,12 +21,22 @@ const getPages = require('./pages')
const getRedirects = async () => {
const redirects = []
const pages = await getPages()
+ const { basePath } = await getNextConfig()
await asyncForEach(pages, async ({ route, filePath }) => {
const target = filePath.replace(/pages/, '')
await addDefaultLocaleRedirect(redirects, route, target)
+ // For sites that use basePath, manually add necessary redirects here specific
+ // only to this page type (which excludes static route redirects by default)
+ if (basePath !== '') {
+ redirects.push({
+ route: `${basePath}${route}`,
+ target: route,
+ })
+ }
+
// Only create normal redirects for pages with dynamic routing
if (!isDynamicRoute(route)) return
diff --git a/src/lib/steps/setupRedirects.js b/src/lib/steps/setupRedirects.js
index c86b48acf3..50ff238d09 100644
--- a/src/lib/steps/setupRedirects.js
+++ b/src/lib/steps/setupRedirects.js
@@ -2,7 +2,10 @@ const { join } = require('path')
const { existsSync, readFileSync, writeFileSync } = require('fs-extra')
+const getNextConfig = require('../../../helpers/getNextConfig')
const { CUSTOM_REDIRECTS_PATH, NEXT_IMAGE_FUNCTION_NAME } = require('../config')
+const convertToBasePathRedirects = require('../helpers/convertToBasePathRedirects')
+const formatRedirectTarget = require('../helpers/formatRedirectTarget')
const getNetlifyRoutes = require('../helpers/getNetlifyRoutes')
const getSortedRedirects = require('../helpers/getSortedRedirects')
const isDynamicRoute = require('../helpers/isDynamicRoute')
@@ -31,7 +34,7 @@ const setupRedirects = async (publishPath) => {
const getSPRevalidateRedirects = require('../pages/getStaticPropsWithRevalidate/redirects')
const getWithoutPropsRedirects = require('../pages/withoutProps/redirects')
- const nextRedirects = [
+ let nextRedirects = [
...(await getApiRedirects()),
...(await getInitialPropsRedirects()),
...(await getServerSidePropsRedirects()),
@@ -44,12 +47,17 @@ const setupRedirects = async (publishPath) => {
// Add _redirect section heading
redirects.push('# Next-on-Netlify Redirects')
+ const { basePath } = await getNextConfig()
+ if (basePath !== '') {
+ nextRedirects = convertToBasePathRedirects({ basePath, nextRedirects })
+ }
+
const staticRedirects = nextRedirects.filter(({ route }) => !isDynamicRoute(removeFileExtension(route)))
const dynamicRedirects = nextRedirects.filter(({ route }) => isDynamicRoute(removeFileExtension(route)))
// Add necessary next/image redirects for our image function
dynamicRedirects.push({
- route: '/_next/image* url=:url w=:width q=:quality',
+ route: `${basePath || ''}/_next/image* url=:url w=:width q=:quality`,
target: `/nextimg/:url/:width/:quality`,
statusCode: '301',
force: true,
@@ -68,7 +76,12 @@ const setupRedirects = async (publishPath) => {
// require two Netlify routes in the _redirects file
getNetlifyRoutes(nextRedirect.route).forEach((netlifyRoute) => {
const { conditions = [], force = false, statusCode = '200', target } = nextRedirect
- const redirectPieces = [netlifyRoute, target, `${statusCode}${force ? '!' : ''}`, conditions.join(' ')]
+ const redirectPieces = [
+ netlifyRoute,
+ formatRedirectTarget({ basePath, target }),
+ `${statusCode}${force ? '!' : ''}`,
+ conditions.join(' '),
+ ]
const redirect = redirectPieces.join(' ').trim()
logItem(redirect)
redirects.push(redirect)
diff --git a/src/tests/__snapshots__/basePath.test.js.snap b/src/tests/__snapshots__/basePath.test.js.snap
new file mode 100644
index 0000000000..ee4b58d4a6
--- /dev/null
+++ b/src/tests/__snapshots__/basePath.test.js.snap
@@ -0,0 +1,86 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Routing creates Netlify redirects 1`] = `
+"# Next-on-Netlify Redirects
+/ /foo 301!
+/_next/data/%BUILD_ID%/getServerSideProps/static.json /.netlify/functions/next_getServerSideProps_static 200
+/_next/data/%BUILD_ID%/getStaticProps/1.json /.netlify/functions/next_getStaticProps_id 200! Cookie=__prerender_bypass,__next_preview_data
+/_next/data/%BUILD_ID%/getStaticProps/2.json /.netlify/functions/next_getStaticProps_id 200! Cookie=__prerender_bypass,__next_preview_data
+/_next/data/%BUILD_ID%/getStaticProps/static.json /.netlify/functions/next_getStaticProps_static 200! Cookie=__prerender_bypass,__next_preview_data
+/_next/data/%BUILD_ID%/getStaticProps/with-revalidate.json /.netlify/functions/next_getStaticProps_withrevalidate 200
+/_next/data/%BUILD_ID%/getStaticProps/withFallback/3.json /.netlify/functions/next_getStaticProps_withFallback_id 200! Cookie=__prerender_bypass,__next_preview_data
+/_next/data/%BUILD_ID%/getStaticProps/withFallback/4.json /.netlify/functions/next_getStaticProps_withFallback_id 200! Cookie=__prerender_bypass,__next_preview_data
+/_next/data/%BUILD_ID%/getStaticProps/withFallback/my/path/1.json /.netlify/functions/next_getStaticProps_withFallback_slug 200! Cookie=__prerender_bypass,__next_preview_data
+/_next/data/%BUILD_ID%/getStaticProps/withFallback/my/path/2.json /.netlify/functions/next_getStaticProps_withFallback_slug 200! Cookie=__prerender_bypass,__next_preview_data
+/_next/data/%BUILD_ID%/getStaticProps/withFallbackBlocking/3.json /.netlify/functions/next_getStaticProps_withFallbackBlocking_id 200! Cookie=__prerender_bypass,__next_preview_data
+/_next/data/%BUILD_ID%/getStaticProps/withFallbackBlocking/4.json /.netlify/functions/next_getStaticProps_withFallbackBlocking_id 200! Cookie=__prerender_bypass,__next_preview_data
+/_next/data/%BUILD_ID%/getStaticProps/withRevalidate/1.json /.netlify/functions/next_getStaticProps_withRevalidate_id 200
+/_next/data/%BUILD_ID%/getStaticProps/withRevalidate/2.json /.netlify/functions/next_getStaticProps_withRevalidate_id 200
+/api/hello-background /foo/api/hello-background 301!
+/api/static /foo/api/static 301!
+/foo /.netlify/functions/next_index 200
+/foo/404 /404 200
+/foo/_next/* /_next/:splat 301!
+/foo/api/hello-background /.netlify/functions/next_api_hello-background 200
+/foo/api/static /.netlify/functions/next_api_static 200
+/foo/getServerSideProps/static /.netlify/functions/next_getServerSideProps_static 200
+/foo/getStaticProps/1 /.netlify/functions/next_getStaticProps_id 200! Cookie=__prerender_bypass,__next_preview_data
+/foo/getStaticProps/1 /getStaticProps/1 200
+/foo/getStaticProps/2 /.netlify/functions/next_getStaticProps_id 200! Cookie=__prerender_bypass,__next_preview_data
+/foo/getStaticProps/2 /getStaticProps/2 200
+/foo/getStaticProps/static /.netlify/functions/next_getStaticProps_static 200! Cookie=__prerender_bypass,__next_preview_data
+/foo/getStaticProps/static /getStaticProps/static 200
+/foo/getStaticProps/with-revalidate /.netlify/functions/next_getStaticProps_withrevalidate 200
+/foo/getStaticProps/withFallback/3 /.netlify/functions/next_getStaticProps_withFallback_id 200! Cookie=__prerender_bypass,__next_preview_data
+/foo/getStaticProps/withFallback/3 /getStaticProps/withFallback/3 200
+/foo/getStaticProps/withFallback/4 /.netlify/functions/next_getStaticProps_withFallback_id 200! Cookie=__prerender_bypass,__next_preview_data
+/foo/getStaticProps/withFallback/4 /getStaticProps/withFallback/4 200
+/foo/getStaticProps/withFallback/my/path/1 /.netlify/functions/next_getStaticProps_withFallback_slug 200! Cookie=__prerender_bypass,__next_preview_data
+/foo/getStaticProps/withFallback/my/path/1 /getStaticProps/withFallback/my/path/1 200
+/foo/getStaticProps/withFallback/my/path/2 /.netlify/functions/next_getStaticProps_withFallback_slug 200! Cookie=__prerender_bypass,__next_preview_data
+/foo/getStaticProps/withFallback/my/path/2 /getStaticProps/withFallback/my/path/2 200
+/foo/getStaticProps/withFallbackBlocking/3 /.netlify/functions/next_getStaticProps_withFallbackBlocking_id 200! Cookie=__prerender_bypass,__next_preview_data
+/foo/getStaticProps/withFallbackBlocking/3 /getStaticProps/withFallbackBlocking/3 200
+/foo/getStaticProps/withFallbackBlocking/4 /.netlify/functions/next_getStaticProps_withFallbackBlocking_id 200! Cookie=__prerender_bypass,__next_preview_data
+/foo/getStaticProps/withFallbackBlocking/4 /getStaticProps/withFallbackBlocking/4 200
+/foo/getStaticProps/withRevalidate/1 /.netlify/functions/next_getStaticProps_withRevalidate_id 200
+/foo/getStaticProps/withRevalidate/2 /.netlify/functions/next_getStaticProps_withRevalidate_id 200
+/foo/static /static 200
+/getServerSideProps/static /foo/getServerSideProps/static 301!
+/getStaticProps/with-revalidate /foo/getStaticProps/with-revalidate 301!
+/getStaticProps/withRevalidate/1 /foo/getStaticProps/withRevalidate/1 301!
+/getStaticProps/withRevalidate/2 /foo/getStaticProps/withRevalidate/2 301!
+/_next/data/%BUILD_ID%/getServerSideProps/all.json /.netlify/functions/next_getServerSideProps_all_slug 200
+/_next/data/%BUILD_ID%/getServerSideProps/all/* /.netlify/functions/next_getServerSideProps_all_slug 200
+/_next/data/%BUILD_ID%/getServerSideProps/:id.json /.netlify/functions/next_getServerSideProps_id 200
+/_next/data/%BUILD_ID%/getStaticProps/withFallback/:id.json /.netlify/functions/next_getStaticProps_withFallback_id 200
+/_next/data/%BUILD_ID%/getStaticProps/withFallback/:slug/* /.netlify/functions/next_getStaticProps_withFallback_slug 200
+/_next/data/%BUILD_ID%/getStaticProps/withFallbackBlocking/:id.json /.netlify/functions/next_getStaticProps_withFallbackBlocking_id 200
+/_next/data/%BUILD_ID%/getStaticProps/withRevalidate/withFallback/:id.json /.netlify/functions/next_getStaticProps_withRevalidate_withFallback_id 200
+/api/shows/:id /foo/api/shows/:id 301!
+/api/shows/:params/* /foo/api/shows/:params 301!
+/foo/_next/image* url=:url w=:width q=:quality /nextimg/:url/:width/:quality 301!
+/foo/api/shows/:id /.netlify/functions/next_api_shows_id 200
+/foo/api/shows/:params/* /.netlify/functions/next_api_shows_params 200
+/foo/getServerSideProps/all /.netlify/functions/next_getServerSideProps_all_slug 200
+/foo/getServerSideProps/all/* /.netlify/functions/next_getServerSideProps_all_slug 200
+/foo/getServerSideProps/:id /.netlify/functions/next_getServerSideProps_id 200
+/foo/getStaticProps/withFallback/:id /.netlify/functions/next_getStaticProps_withFallback_id 200
+/foo/getStaticProps/withFallback/:slug/* /.netlify/functions/next_getStaticProps_withFallback_slug 200
+/foo/getStaticProps/withFallbackBlocking/:id /.netlify/functions/next_getStaticProps_withFallbackBlocking_id 200
+/foo/getStaticProps/withRevalidate/withFallback/:id /.netlify/functions/next_getStaticProps_withRevalidate_withFallback_id 200
+/foo/shows/:id /.netlify/functions/next_shows_id 200
+/foo/shows/:params/* /.netlify/functions/next_shows_params 200
+/foo/static/:id /static/[id] 200
+/getServerSideProps/all /foo/getServerSideProps/all/:slug 301!
+/getServerSideProps/all/* /foo/getServerSideProps/all/:slug 301!
+/getServerSideProps/:id /foo/getServerSideProps/:id 301!
+/getStaticProps/withFallback/:id /foo/getStaticProps/withFallback/:id 301!
+/getStaticProps/withFallback/:slug/* /foo/getStaticProps/withFallback/:slug 301!
+/getStaticProps/withFallbackBlocking/:id /foo/getStaticProps/withFallbackBlocking/:id 301!
+/getStaticProps/withRevalidate/withFallback/:id /foo/getStaticProps/withRevalidate/withFallback/:id 301!
+/nextimg/* /.netlify/functions/next_image 200
+/shows/:id /foo/shows/:id 301!
+/shows/:params/* /foo/shows/:params 301!
+/static/:id /static/[id].html 200"
+`;
diff --git a/src/tests/basePath.test.js b/src/tests/basePath.test.js
new file mode 100644
index 0000000000..0c2a02aeec
--- /dev/null
+++ b/src/tests/basePath.test.js
@@ -0,0 +1,51 @@
+// Test next-on-netlify when a custom distDir is set in next.config.js
+
+const { EOL } = require('os')
+const { parse, join } = require('path')
+const { readFileSync } = require('fs-extra')
+const buildNextApp = require('./helpers/buildNextApp')
+
+// The name of this test file (without extension)
+const FILENAME = parse(__filename).name
+
+// The directory which will be used for testing.
+// We simulate a NextJS app within that directory, with pages, and a
+// package.json file.
+const PROJECT_PATH = join(__dirname, 'builds', FILENAME)
+
+// Capture the output to verify successful build
+let buildOutput
+
+beforeAll(
+ async () => {
+ buildOutput = await buildNextApp()
+ .forTest(__filename)
+ .withPages('pages')
+ .withNextConfig('next.config.js-with-basePath.js')
+ .withPackageJson('package.json')
+ .build()
+ },
+ // time out after 180 seconds
+ 180 * 1000,
+)
+
+describe('next-on-netlify', () => {
+ test('builds successfully', () => {
+ expect(buildOutput).toMatch('Next on Netlify')
+ expect(buildOutput).toMatch('Success! All done!')
+ })
+})
+
+describe('Routing', () => {
+ test('creates Netlify redirects', async () => {
+ // Read _redirects file
+ const contents = readFileSync(join(PROJECT_PATH, 'out_publish', '_redirects'))
+ let redirects = contents.toString()
+
+ // Replace non-persistent build ID with placeholder
+ redirects = redirects.replace(/\/_next\/data\/[^\/]+\//g, '/_next/data/%BUILD_ID%/')
+
+ // Check that redirects match
+ expect(redirects).toMatchSnapshot()
+ })
+})
diff --git a/src/tests/fixtures/next.config.js-with-basePath.js b/src/tests/fixtures/next.config.js-with-basePath.js
new file mode 100644
index 0000000000..133213593d
--- /dev/null
+++ b/src/tests/fixtures/next.config.js-with-basePath.js
@@ -0,0 +1,4 @@
+module.exports = {
+ target: 'serverless',
+ basePath: '/foo',
+}