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 (
     <div>
       <p>
-        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
         <br />
         Refresh the page to see server-side rendering in action.
         <br />
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 }) => (
   <div>
     <p>
-      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
       <br />
       Refresh the page to see server-side rendering in action.
       <br />
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',
+}