diff --git a/README.md b/README.md index 52284fb088..5c6d561c27 100644 --- a/README.md +++ b/README.md @@ -15,17 +15,23 @@ commands separately will not work, because the Next.js Runtime will not generate If you use [`next/image`](https://nextjs.org/docs/basic-features/image-optimization), your images will be automatically optimized at runtime, ensuring that they are served at the best size and format. The image will be processed on the -first request which means it may take longer to load, but the generated image is then cached at the edge and served as a -static file to future visitors. By default, Next.js will deliver WebP images if the browser supports it. WebP is a new -image format with wide browser support that will usually generate smaller files than png or jpg. You can additionally -enable the AVIF format, which is often even smaller in filesize than WebP. The drawback is that with particularly large -images AVIF may take too long to generate, meaning the function times-out. You can configure +first request which means it may take longer to load, but the generated image is then cached and served as a static file +to future visitors. By default, Next.js will deliver WebP images if the browser supports it. WebP is a modern image format +with wide browser support that will usually generate smaller files than PNG or JPG. Additionally, you can enable AVIF +format, which is often even smaller in filesize than WebP. The drawback is that with particularly large images, AVIF images may +take too long to generate, and the function times-out. You can configure [the supported image formats](https://nextjs.org/docs/api-reference/next/image#acceptable-formats) in your `next.config.js` file. +### Enabling Edge Images + +It is possible to run image content negotiation on the edge. This allows images to be processed on the first request, +and then, in future loads, served from cache on the edge. + In order to deliver the correct format to a visitor's browser, this uses a Netlify Edge Function. In some cases your -site may not support Edge Functions, in which case it will instead fall back to delivering the original file format. You -may also manually disable the Edge Function by setting the environment variable `NEXT_DISABLE_EDGE_IMAGES` to `true`. +site may not support Edge Functions, in which case it will instead fall back to delivering the original file format. + +To turn on Edge image handling for Next/Image, set the environment variable `NEXT_FORCE_EDGE_IMAGES` to `true` ## Returning custom response headers on images handled by `ipx` @@ -44,11 +50,14 @@ by targeting the `/_next/image/*` route: ## Disabling included image loader -If you wish to disable the use of the image loader which is bundled into the runtime by default, set the `DISABLE_IPX` environment variable to `true`. +If you wish to disable the use of the image loader which is bundled into the runtime by default, set the `DISABLE_IPX` +environment variable to `true`. -This should only be done if the site is not using `next/image` or is using a different loader (such as Cloudinary or Imgix). +This should only be done if the site is not using `next/image` or is using a different loader (such as Cloudinary or +Imgix). -See the [Next.js documentation](https://nextjs.org/docs/api-reference/next/image#built-in-loaders) for image loader options. +See the [Next.js documentation](https://nextjs.org/docs/api-reference/next/image#built-in-loaders) for image loader +options. ## Next.js Middleware on Netlify @@ -56,11 +65,13 @@ Next.js Middleware works out of the box on Netlify. By default, middleware runs support for running Middleware at the origin, set the environment variable `NEXT_DISABLE_NETLIFY_EDGE` to `true`. Be aware that this will result in slower performance, as all pages that match middleware must use SSR. -For more details on Next.js Middleware with Netlify, see the [middleware docs](https://docs.netlify.com/integrations/frameworks/next-js/middleware/). +For more details on Next.js Middleware with Netlify, see the +[middleware docs](https://docs.netlify.com/integrations/frameworks/next-js/middleware/). ### Limitations -Due to how the site configuration is handled when it's run using Netlify Edge Functions, data such as `locale` and `defaultLocale` will be missing on the `req.nextUrl` object when running `netlify dev`. +Due to how the site configuration is handled when it's run using Netlify Edge Functions, data such as `locale` and +`defaultLocale` will be missing on the `req.nextUrl` object when running `netlify dev`. However, this data is available on `req.nextUrl` in a production environment. @@ -112,8 +123,9 @@ following ways: ### From the UI (Recommended): -You can go to the [UI](https://app.netlify.com/plugins/@netlify/plugin-nextjs/install) and choose the site to install the Next.js Runtime on. This method -is recommended because you will benefit from auto-upgrades to important fixes and feature updates. +You can go to the [UI](https://app.netlify.com/plugins/@netlify/plugin-nextjs/install) and choose the site to install +the Next.js Runtime on. This method is recommended because you will benefit from auto-upgrades to important fixes and +feature updates. ### From `npm`: @@ -139,9 +151,9 @@ If you previously set these values, they're no longer needed and should be remov - `external_node_modules` in `netlify.toml` - The environment variable `NEXT_USE_NETLIFY_EDGE` can be removed as this is now the default -The `serverless` and `experimental-serverless-trace` targets are deprecated in Next.js 12, and all builds with this Next.js -Runtime will now use the default `server` target. If you previously set the target in your `next.config.js`, you should -remove it. +The `serverless` and `experimental-serverless-trace` targets are deprecated in Next.js 12, and all builds with this +Next.js Runtime will now use the default `server` target. If you previously set the target in your `next.config.js`, you +should remove it. If you currently use redirects or rewrites on your site, see [the Rewrites and Redirects guide](https://docs.netlify.com/integrations/frameworks/next-js/redirects-and-rewrites/) for @@ -149,8 +161,10 @@ information on changes to how they are handled in this version. In particular, n files must be placed in `public`, not in the root of the site. ## Using with pnpm -If your site uses pnpm to manage dependencies, currently you must [enable public hoisting](https://pnpm.io/npmrc#public-hoist-pattern). -The simplest way to do this is to create a `.npmrc` file in the root of your project with the content: + +If your site uses pnpm to manage dependencies, currently you must +[enable public hoisting](https://pnpm.io/npmrc#public-hoist-pattern). The simplest way to do this is to create a +`.npmrc` file in the root of your project with the content: ```ini public-hoist-pattern[]=* diff --git a/packages/runtime/src/helpers/edge.ts b/packages/runtime/src/helpers/edge.ts index 86c081a524..b77526642e 100644 --- a/packages/runtime/src/helpers/edge.ts +++ b/packages/runtime/src/helpers/edge.ts @@ -358,11 +358,106 @@ export const writeEdgeFunctions = async ({ await writeJSON(join(edgeFunctionRoot, 'edge-shared', 'nextConfig.json'), nextConfig) await copy(join(publish, 'prerender-manifest.json'), join(edgeFunctionRoot, 'edge-shared', 'prerender-manifest.json')) + // early return if edge is disabled + if (destr(process.env.NEXT_DISABLE_NETLIFY_EDGE)) { + console.log('Environment variable NEXT_DISABLE_NETLIFY_EDGE has been set, skipping Netlify Edge Function creation.') + return + } + + const rscFunctions = await writeRscDataEdgeFunction({ + prerenderManifest: await loadPrerenderManifest(netlifyConfig), + appPathRoutesManifest: await loadAppPathRoutesManifest(netlifyConfig), + }) + + manifest.functions.push(...rscFunctions) + + const middlewareManifest = await loadMiddlewareManifest(netlifyConfig) + if (!middlewareManifest) { + console.error("Couldn't find the middleware manifest") + return + } + + let usesEdge = false + + for (const middleware of middlewareManifest.sortedMiddleware) { + usesEdge = true + const edgeFunctionDefinition = middlewareManifest.middleware[middleware] + const functionName = sanitizeName(edgeFunctionDefinition.name) + const matchers = generateEdgeFunctionMiddlewareMatchers({ + edgeFunctionDefinition, + edgeFunctionRoot, + nextConfig, + }) + await writeEdgeFunction({ + edgeFunctionDefinition, + edgeFunctionRoot, + netlifyConfig, + functionName, + matchers, + middleware: true, + }) + + manifest.functions.push( + ...matchers.map((matcher) => middlewareMatcherToEdgeFunctionDefinition(matcher, functionName)), + ) + } + // Functions (i.e. not middleware, but edge SSR and API routes) + if (typeof middlewareManifest.functions === 'object') { + // When using the app dir, we also need to check if the EF matches a page + const appPathRoutesManifest = await loadAppPathRoutesManifest(netlifyConfig) + + // A map of all route pages to their page regex. This is used for pages dir and appDir. + const pageRegexMap = new Map( + [...(routesManifest.dynamicRoutes || []), ...(routesManifest.staticRoutes || [])].map((route) => [ + route.page, + route.regex, + ]), + ) + // Create a map of pages-dir routes to their data route regex (appDir uses the same route as the HTML) + const dataRoutesMap = new Map( + [...(routesManifest.dataRoutes || [])].map((route) => [route.page, route.dataRouteRegex]), + ) + + for (const edgeFunctionDefinition of Object.values(middlewareManifest.functions)) { + usesEdge = true + const functionName = sanitizeName(edgeFunctionDefinition.name) + await writeEdgeFunction({ + edgeFunctionDefinition, + edgeFunctionRoot, + netlifyConfig, + functionName, + }) + const pattern = getEdgeFunctionPatternForPage({ + edgeFunctionDefinition, + pageRegexMap, + appPathRoutesManifest, + }) + manifest.functions.push({ + function: functionName, + name: edgeFunctionDefinition.name, + pattern, + // cache: "manual" is currently experimental, so we restrict it to sites that use experimental appDir + cache: usesAppDir ? 'manual' : undefined, + }) + // pages-dir page routes also have a data route. If there's a match, add an entry mapping that to the function too + const dataRoute = dataRoutesMap.get(edgeFunctionDefinition.page) + if (dataRoute) { + manifest.functions.push({ + function: functionName, + name: edgeFunctionDefinition.name, + pattern: dataRoute, + cache: usesAppDir ? 'manual' : undefined, + }) + } + } + } + if ( + destr(process.env.NEXT_FORCE_EDGE_IMAGES) && !destr(process.env.NEXT_DISABLE_EDGE_IMAGES) && - !destr(process.env.NEXT_DISABLE_NETLIFY_EDGE) && !destr(process.env.DISABLE_IPX) ) { + usesEdge = true console.log( 'Using Netlify Edge Functions for image format detection. Set env var "NEXT_DISABLE_EDGE_IMAGES=true" to disable.', ) @@ -378,101 +473,18 @@ export const writeEdgeFunctions = async ({ name: 'next/image handler', path: '/_next/image*', }) + } else { + console.log( + 'You are not using Netlify Edge Functions for image format detection. Set env var "NEXT_FORCE_EDGE_IMAGES=true" to enable.', + ) } - if (!destr(process.env.NEXT_DISABLE_NETLIFY_EDGE)) { - const rscFunctions = await writeRscDataEdgeFunction({ - prerenderManifest: await loadPrerenderManifest(netlifyConfig), - appPathRoutesManifest: await loadAppPathRoutesManifest(netlifyConfig), - }) - - manifest.functions.push(...rscFunctions) - - const middlewareManifest = await loadMiddlewareManifest(netlifyConfig) - if (!middlewareManifest) { - console.error("Couldn't find the middleware manifest") - return - } - - let usesEdge = false - for (const middleware of middlewareManifest.sortedMiddleware) { - usesEdge = true - const edgeFunctionDefinition = middlewareManifest.middleware[middleware] - const functionName = sanitizeName(edgeFunctionDefinition.name) - const matchers = generateEdgeFunctionMiddlewareMatchers({ - edgeFunctionDefinition, - edgeFunctionRoot, - nextConfig, - }) - await writeEdgeFunction({ - edgeFunctionDefinition, - edgeFunctionRoot, - netlifyConfig, - functionName, - matchers, - middleware: true, - }) - - manifest.functions.push( - ...matchers.map((matcher) => middlewareMatcherToEdgeFunctionDefinition(matcher, functionName)), - ) - } - // Functions (i.e. not middleware, but edge SSR and API routes) - if (typeof middlewareManifest.functions === 'object') { - // When using the app dir, we also need to check if the EF matches a page - const appPathRoutesManifest = await loadAppPathRoutesManifest(netlifyConfig) - - // A map of all route pages to their page regex. This is used for pages dir and appDir. - const pageRegexMap = new Map( - [...(routesManifest.dynamicRoutes || []), ...(routesManifest.staticRoutes || [])].map((route) => [ - route.page, - route.regex, - ]), - ) - // Create a map of pages-dir routes to their data route regex (appDir uses the same route as the HTML) - const dataRoutesMap = new Map( - [...(routesManifest.dataRoutes || [])].map((route) => [route.page, route.dataRouteRegex]), - ) - - for (const edgeFunctionDefinition of Object.values(middlewareManifest.functions)) { - usesEdge = true - const functionName = sanitizeName(edgeFunctionDefinition.name) - await writeEdgeFunction({ - edgeFunctionDefinition, - edgeFunctionRoot, - netlifyConfig, - functionName, - }) - const pattern = getEdgeFunctionPatternForPage({ - edgeFunctionDefinition, - pageRegexMap, - appPathRoutesManifest, - }) - manifest.functions.push({ - function: functionName, - name: edgeFunctionDefinition.name, - pattern, - // cache: "manual" is currently experimental, so we restrict it to sites that use experimental appDir - cache: usesAppDir ? 'manual' : undefined, - }) - // pages-dir page routes also have a data route. If there's a match, add an entry mapping that to the function too - const dataRoute = dataRoutesMap.get(edgeFunctionDefinition.page) - if (dataRoute) { - manifest.functions.push({ - function: functionName, - name: edgeFunctionDefinition.name, - pattern: dataRoute, - cache: usesAppDir ? 'manual' : undefined, - }) - } - } - } - if (usesEdge) { - console.log(outdent` - ✨ Deploying middleware and functions to ${greenBright`Netlify Edge Functions`} ✨ - This feature is in beta. Please share your feedback here: https://ntl.fyi/next-netlify-edge - `) - } + if (usesEdge) { + console.log(outdent` + ✨ Deploying middleware and functions to ${greenBright`Netlify Edge Functions`} ✨ + This feature is in beta. Please share your feedback here: https://ntl.fyi/next-netlify-edge + `) } + await writeJson(join(edgeFunctionRoot, 'manifest.json'), manifest) } diff --git a/test/index.spec.js b/test/index.spec.js index 1979e81407..ba6438dbd6 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -690,6 +690,20 @@ describe('onBuild()', () => { expect(existsSync(path.join('.netlify', 'functions-internal', '_ipx', '_ipx.js'))).toBeTruthy() }) + // Enabled while edge images are off by default + test('does not generate an ipx edge function by default', async () => { + await moveNextDist() + await nextRuntime.onBuild(defaultArgs) + expect(existsSync(path.join('.netlify', 'edge-functions', 'ipx', 'index.ts'))).toBeFalsy() + }) + + test('generates an ipx edge function if force is set', async () => { + process.env.NEXT_FORCE_EDGE_IMAGES = '1' + await moveNextDist() + await nextRuntime.onBuild(defaultArgs) + expect(existsSync(path.join('.netlify', 'edge-functions', 'ipx', 'index.ts'))).toBeTruthy() + }) + test('does not generate an ipx function when DISABLE_IPX is set', async () => { process.env.DISABLE_IPX = '1' await moveNextDist()