diff --git a/README.md b/README.md index 6f76559a4a..6677e945ad 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,8 @@ If you are using Nx, then you will need to point `publish` to the folder inside The Next.js Runtime fully supports ISR on Netlify. For more details see [the ISR docs](https://github.com/netlify/next-runtime/blob/main/docs/isr.md). +Note that Netlify has a minimum TTL of 60 seconds for revalidation. + ## Use with `next export` If you are using `next export` to generate a static site, you do not need most of the functionality of this Next.js diff --git a/cypress/integration/default/dynamic-routes.spec.ts b/cypress/integration/default/dynamic-routes.spec.ts index f01dca8948..07375209dd 100644 --- a/cypress/integration/default/dynamic-routes.spec.ts +++ b/cypress/integration/default/dynamic-routes.spec.ts @@ -1,22 +1,23 @@ +/* eslint-disable max-lines-per-function */ describe('Static Routing', () => { it('renders correct page via SSR on a static route', () => { - cy.request('/getServerSideProps/static/').then((res) => { + cy.request({ url: '/getServerSideProps/static/', headers: { 'x-nf-debug-logging': '1' } }).then((res) => { expect(res.status).to.eq(200) - expect(res.headers).to.have.property('x-render-mode', 'ssr') + expect(res.headers).to.have.property('x-nf-render-mode', 'ssr') expect(res.body).to.contain('Sleepy Hollow') }) }) it('serves correct static file on a static route', () => { - cy.request('/getStaticProps/static/').then((res) => { + cy.request({ url: '/getStaticProps/static/', headers: { 'x-nf-debug-logging': '1' } }).then((res) => { expect(res.status).to.eq(200) - expect(res.headers).to.not.have.property('x-render-mode') + expect(res.headers).to.not.have.property('x-nf-render-mode') expect(res.body).to.contain('Dancing with the Stars') }) }) it('renders correct page via ODB on a static route', () => { - cy.request('/getStaticProps/with-revalidate/').then((res) => { + cy.request({ url: '/getStaticProps/with-revalidate/', headers: { 'x-nf-debug-logging': '1' } }).then((res) => { expect(res.status).to.eq(200) - expect(res.headers).to.have.property('x-render-mode', 'isr') + expect(res.headers).to.have.property('x-nf-render-mode', 'odb ttl=1') expect(res.body).to.contain('Dancing with the Stars') }) }) @@ -24,104 +25,125 @@ describe('Static Routing', () => { describe('Dynamic Routing', () => { it('renders correct page via SSR on a dynamic route', () => { - cy.request('/getServerSideProps/1/').then((res) => { + cy.request({ url: '/getServerSideProps/1/', headers: { 'x-nf-debug-logging': '1' } }).then((res) => { expect(res.status).to.eq(200) - expect(res.headers).to.have.property('x-render-mode', 'ssr') + expect(res.headers).to.have.property('x-nf-render-mode', 'ssr') expect(res.body).to.contain('Under the Dome') }) }) it('renders correct page via SSR on a dynamic catch-all route', () => { - cy.request('/getServerSideProps/all/1/').then((res) => { + cy.request({ url: '/getServerSideProps/all/1/', headers: { 'x-nf-debug-logging': '1' } }).then((res) => { expect(res.status).to.eq(200) - expect(res.headers).to.have.property('x-render-mode', 'ssr') + expect(res.headers).to.have.property('x-nf-render-mode', 'ssr') expect(res.body).to.contain('Under the Dome') }) }) it('serves correct static file on a prerendered dynamic route with fallback: false', () => { - cy.request('/getStaticProps/1/').then((res) => { + cy.request({ url: '/getStaticProps/1/', headers: { 'x-nf-debug-logging': '1' } }).then((res) => { expect(res.status).to.eq(200) - expect(res.headers).to.not.have.property('x-render-mode') + expect(res.headers).to.not.have.property('x-nf-render-mode') expect(res.body).to.contain('Under the Dome') }) }) it('renders custom 404 on a non-prerendered dynamic route with fallback: false', () => { - cy.request({ url: '/getStaticProps/3/', failOnStatusCode: false }).then((res) => { - expect(res.status).to.eq(404) - expect(res.headers).to.have.property('x-render-mode', 'odb') - expect(res.body).to.contain('Custom 404') - }) + cy.request({ url: '/getStaticProps/3/', headers: { 'x-nf-debug-logging': '1' }, failOnStatusCode: false }).then( + (res) => { + expect(res.status).to.eq(404) + expect(res.headers).to.have.property('x-nf-render-mode', 'odb') + expect(res.body).to.contain('Custom 404') + }, + ) }) it('serves correct static file on a prerendered dynamic route with fallback: true', () => { - cy.request('/getStaticProps/withFallback/1/').then((res) => { + cy.request({ url: '/getStaticProps/withFallback/1/', headers: { 'x-nf-debug-logging': '1' } }).then((res) => { expect(res.status).to.eq(200) - expect(res.headers).to.not.have.property('x-render-mode') + expect(res.headers).to.not.have.property('x-nf-render-mode') expect(res.body).to.contain('Under the Dome') }) }) it('renders fallback page via ODB on a non-prerendered dynamic route with fallback: true', () => { - cy.request('/getStaticProps/withFallback/3/').then((res) => { + cy.request({ url: '/getStaticProps/withFallback/3/', headers: { 'x-nf-debug-logging': '1' } }).then((res) => { expect(res.status).to.eq(200) // expect 'odb' until https://github.com/netlify/pillar-runtime/issues/438 is fixed - expect(res.headers).to.have.property('x-render-mode', 'odb') + expect(res.headers).to.have.property('x-nf-render-mode', 'odb') // expect 'Bitten' until the above is fixed and we can test for fallback 'Loading...' message expect(res.body).to.contain('Bitten') }) }) it('serves correct static file on a prerendered dynamic route with fallback: blocking', () => { - cy.request('/getStaticProps/withFallbackBlocking/1/').then((res) => { - expect(res.status).to.eq(200) - expect(res.headers).to.not.have.property('x-render-mode') - expect(res.body).to.contain('Under the Dome') - }) + cy.request({ url: '/getStaticProps/withFallbackBlocking/1/', headers: { 'x-nf-debug-logging': '1' } }).then( + (res) => { + expect(res.status).to.eq(200) + expect(res.headers).to.not.have.property('x-nf-render-mode') + expect(res.body).to.contain('Under the Dome') + }, + ) }) it('renders correct page via ODB on a non-prerendered dynamic route with fallback: blocking', () => { - cy.request('/getStaticProps/withFallbackBlocking/3/').then((res) => { - expect(res.status).to.eq(200) - expect(res.headers).to.have.property('x-render-mode', 'odb') - expect(res.body).to.contain('Bitten') - }) + cy.request({ url: '/getStaticProps/withFallbackBlocking/3/', headers: { 'x-nf-debug-logging': '1' } }).then( + (res) => { + expect(res.status).to.eq(200) + expect(res.headers).to.have.property('x-nf-render-mode', 'odb') + expect(res.body).to.contain('Bitten') + }, + ) }) it('renders correct page via ODB on a prerendered dynamic route with revalidate and fallback: false', () => { - cy.request('/getStaticProps/withRevalidate/1/').then((res) => { + cy.request({ url: '/getStaticProps/withRevalidate/1/', headers: { 'x-nf-debug-logging': '1' } }).then((res) => { expect(res.status).to.eq(200) - expect(res.headers).to.have.property('x-render-mode', 'isr') + expect(res.headers).to.have.property('x-nf-render-mode', 'odb ttl=60') expect(res.body).to.contain('Under the Dome') }) }) it('renders custom 404 on a non-prerendered dynamic route with revalidate and fallback: false', () => { - cy.request({ url: '/getStaticProps/withRevalidate/3/', failOnStatusCode: false }).then((res) => { + cy.request({ + url: '/getStaticProps/withRevalidate/3/', + headers: { 'x-nf-debug-logging': '1' }, + failOnStatusCode: false, + }).then((res) => { expect(res.status).to.eq(404) - expect(res.headers).to.have.property('x-render-mode', 'odb') + expect(res.headers).to.have.property('x-nf-render-mode', 'odb') expect(res.body).to.contain('Custom 404') }) }) it('renders correct page via ODB on a prerendered dynamic route with revalidate and fallback: true', () => { - cy.request('/getStaticProps/withRevalidate/withFallback/1/').then((res) => { - expect(res.status).to.eq(200) - expect(res.headers).to.have.property('x-render-mode', 'isr') - expect(res.body).to.contain('Under the Dome') - }) + cy.request({ url: '/getStaticProps/withRevalidate/withFallback/1/', headers: { 'x-nf-debug-logging': '1' } }).then( + (res) => { + expect(res.status).to.eq(200) + expect(res.headers).to.have.property('x-nf-render-mode', 'odb ttl=60') + expect(res.body).to.contain('Under the Dome') + }, + ) }) it('renders fallback page via ODB on a non-prerendered dynamic route with revalidate and fallback: true', () => { - cy.request('/getStaticProps/withRevalidate/withFallback/3/').then((res) => { - expect(res.status).to.eq(200) - expect(res.headers).to.have.property('x-render-mode', 'isr') - // expect 'Bitten' until https://github.com/netlify/pillar-runtime/issues/438 is fixed - expect(res.body).to.contain('Bitten') - }) + cy.request({ url: '/getStaticProps/withRevalidate/withFallback/3/', headers: { 'x-nf-debug-logging': '1' } }).then( + (res) => { + expect(res.status).to.eq(200) + expect(res.headers).to.have.property('x-nf-render-mode', 'odb ttl=60') + // expect 'Bitten' until https://github.com/netlify/pillar-runtime/issues/438 is fixed + expect(res.body).to.contain('Bitten') + }, + ) }) it('renders correct page via ODB on a prerendered dynamic route with revalidate and fallback: blocking', () => { - cy.request('/getStaticProps/withRevalidate/withFallbackBlocking/1/').then((res) => { + cy.request({ + url: '/getStaticProps/withRevalidate/withFallbackBlocking/1/', + headers: { 'x-nf-debug-logging': '1' }, + }).then((res) => { expect(res.status).to.eq(200) - expect(res.headers).to.have.property('x-render-mode', 'isr') + expect(res.headers).to.have.property('x-nf-render-mode', 'odb ttl=60') expect(res.body).to.contain('Under the Dome') }) }) it('renders correct page via ODB on a non-prerendered dynamic route with revalidate and fallback: blocking', () => { - cy.request('/getStaticProps/withRevalidate/withFallbackBlocking/3/').then((res) => { + cy.request({ + url: '/getStaticProps/withRevalidate/withFallbackBlocking/3/', + headers: { 'x-nf-debug-logging': '1' }, + }).then((res) => { expect(res.status).to.eq(200) - expect(res.headers).to.have.property('x-render-mode', 'isr') + expect(res.headers).to.have.property('x-nf-render-mode', 'odb ttl=60') expect(res.body).to.contain('Bitten') }) }) }) +/* eslint-enable max-lines-per-function */ diff --git a/demos/default/pages/getStaticProps/with-revalidate.js b/demos/default/pages/getStaticProps/with-revalidate.js index d3731601cb..dda98ce051 100644 --- a/demos/default/pages/getStaticProps/with-revalidate.js +++ b/demos/default/pages/getStaticProps/with-revalidate.js @@ -25,6 +25,7 @@ export async function getStaticProps(context) { props: { show: data, }, + // ODB handler will use the minimum TTL=60s revalidate: 1, } } diff --git a/packages/runtime/src/templates/getHandler.ts b/packages/runtime/src/templates/getHandler.ts index 6a08e1c226..c6fbff2bb5 100644 --- a/packages/runtime/src/templates/getHandler.ts +++ b/packages/runtime/src/templates/getHandler.ts @@ -129,15 +129,15 @@ const makeHandler = (conf: NextConfig, app, pageRoot, staticManifest: Array<[str // Long-expiry TTL is basically no TTL, so we'll skip it if (ttl > 0 && ttl < ONE_YEAR_IN_SECONDS) { result.ttl = ttl - requestMode = 'isr' + requestMode = `odb ttl=${ttl}` } } multiValueHeaders['cache-control'] = ['public, max-age=0, must-revalidate'] } - multiValueHeaders['x-render-mode'] = [requestMode] - console.log( - `[${event.httpMethod}] ${event.path} (${requestMode?.toUpperCase()}${result.ttl > 0 ? ` ${result.ttl}s` : ''})`, - ) + multiValueHeaders['x-nf-render-mode'] = [requestMode] + + console.log(`[${event.httpMethod}] ${event.path} (${requestMode?.toUpperCase()})`) + return { ...result, multiValueHeaders,