Skip to content

Commit 0bac0e0

Browse files
authored
feat: use edge functions for content negotiation by default (#1438)
* feat: use edge functions for content negotiation by default * chore: remove console.log
1 parent 65ffdb2 commit 0bac0e0

File tree

7 files changed

+153
-95
lines changed

7 files changed

+153
-95
lines changed

README.md

+53-37
Original file line numberDiff line numberDiff line change
@@ -37,56 +37,39 @@ If you build on Netlify, this plugin will work with no additional configuration.
3737
deploying locally using the Netlify CLI, you must deploy using `netlify deploy --build`. Running the build and deploy
3838
commands separately will not work, because the plugin will not generate the required configuration.
3939

40-
## Migrating from an older version of the plugin
41-
42-
You can manually upgrade from the previous version of the plugin by running the following command:
43-
44-
```shell
45-
npm install -D @netlify/plugin-nextjs@latest
46-
```
47-
48-
Change the `publish` directory to `.next`:
49-
50-
```toml
51-
[build]
52-
publish = ".next"
53-
```
54-
55-
If you previously set these values, they're no longer needed and can be removed:
56-
57-
- `distDir` in your `next.config.js`
58-
- `node_bundler = "esbuild"` in `netlify.toml`
59-
- `external_node_modules` in `netlify.toml`
60-
61-
The `serverless` and `experimental-serverless-trace` targets are deprecated in Next 12, and all builds with this plugin
62-
will now use the default `server` target. If you previously set the target in your `next.config.js`, you should remove
63-
it.
64-
65-
If you currently use redirects or rewrites on your site, see
66-
[the Rewrites and Redirects guide](https://github.com/netlify/netlify-plugin-nextjs/blob/main/docs/redirects-rewrites.md)
67-
for information on changes to how they are handled in this version. In particular, note that `_redirects` and `_headers`
68-
files must be placed in `public`, not in the root of the site.
40+
## Using `next/image`
41+
42+
If you use [`next/image`](https://nextjs.org/docs/basic-features/image-optimization), your images will be automatically
43+
optimized at runtime, ensuring that they are served at the best size and format. The image will be processed on the
44+
first request which means it may take longer to load, but the generated image is then cached at the edge and served as a
45+
static file to future visitors. By default, Next will deliver WebP images if the browser supports it. WebP is a new
46+
image format with wide browser support that will usually generate smaller files than png or jpg. You can additionally
47+
enable the AVIF format, which is often even smaller in filesize than WebP. The drawback is that with particularly large
48+
images AVIF may take too long to generate, meaning the function times-out. You can configure
49+
[the supported image formats](https://nextjs.org/docs/api-reference/next/image#acceptable-formats) in your
50+
`next.config.js` file.
51+
52+
In order to deliver the correct format to a visitor's browser, this uses a Netlify Edge Function. In some cases your
53+
site may not support Edge Functions, in which case it will instead fall back to delivering the original file format. You
54+
may also manually disable the Edge Function by setting the environment variable `NEXT_DISABLE_EDGE_IMAGES` to `true`.
6955

7056
## Next.js Middleware on Netlify
7157

7258
Next.js Middleware works out of the box on Netlify, but check out the
7359
[docs on some caveats](https://github.com/netlify/netlify-plugin-nextjs/blob/main/docs/middleware.md). By default,
7460
middleware runs using SSR. For better results, you should enable [Netlify Edge Functions](#netlify-edge-functions),
75-
which ensures middleware runs at the edge.
61+
which ensures middleware runs at the edge. To use Netlify Edge Functions for middleware or to enable
62+
[edge server rendering](https://nextjs.org/blog/next-12-2#edge-server-rendering-experimental), set the environment
63+
variable `NEXT_USE_NETLIFY_EDGE` to `true`.
7664

7765
### No nested middleware in Next 12.2.0
7866

7967
In Next 12.2.0, nested middleware [has been deprecated](https://nextjs.org/docs/messages/middleware-upgrade-guide) in
8068
favor of root level middleware. If you are not using edge functions then this means that you won't get the benefits of
8169
using a CDN, and ISR will not work.
8270

83-
To fix this issue, you can run your middleware on [Netlify Edge Functions](#netlify-edge-functions).
84-
85-
## Netlify Edge Functions
86-
87-
To use Netlify Edge Functions for middleware or to enable
88-
[edge server rendering](https://nextjs.org/blog/next-12-2#edge-server-rendering-experimental), set the environment
89-
variable `NEXT_USE_NETLIFY_EDGE` to `true`.
71+
To fix this issue, you can run your middleware on [Netlify Edge Functions](#netlify-edge-functions) by setting the
72+
environment variable `NEXT_USE_NETLIFY_EDGE` to `true`.
9073

9174
## Monorepos
9275

@@ -123,6 +106,39 @@ images). You can see the requests for these in [the function logs](https://docs.
123106
and fallback routes you will not see any requests that are served from the edge cache, just actual rendering requests.
124107
These are all internal functions, so you won't find them in your site's own functions directory.
125108

109+
The plugin will also generate a Netlify Edge Function called 'ipx' to handle image content negotiation, and if Edge
110+
runtime or middleware is enabled it will also generate edge functions for middleware and edge routes.
111+
112+
## Migrating from an older version of the plugin
113+
114+
You can manually upgrade from the previous version of the plugin by running the following command:
115+
116+
```shell
117+
npm install -D @netlify/plugin-nextjs@latest
118+
```
119+
120+
Change the `publish` directory to `.next`:
121+
122+
```toml
123+
[build]
124+
publish = ".next"
125+
```
126+
127+
If you previously set these values, they're no longer needed and can be removed:
128+
129+
- `distDir` in your `next.config.js`
130+
- `node_bundler = "esbuild"` in `netlify.toml`
131+
- `external_node_modules` in `netlify.toml`
132+
133+
The `serverless` and `experimental-serverless-trace` targets are deprecated in Next 12, and all builds with this plugin
134+
will now use the default `server` target. If you previously set the target in your `next.config.js`, you should remove
135+
it.
136+
137+
If you currently use redirects or rewrites on your site, see
138+
[the Rewrites and Redirects guide](https://github.com/netlify/netlify-plugin-nextjs/blob/main/docs/redirects-rewrites.md)
139+
for information on changes to how they are handled in this version. In particular, note that `_redirects` and `_headers`
140+
files must be placed in `public`, not in the root of the site.
141+
126142
## Feedback
127143

128144
If you think you have found a bug in the plugin,

demos/canary/next.config.js

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ const nextConfig = {
66
// your project has ESLint errors.
77
ignoreDuringBuilds: true,
88
},
9+
images: {
10+
formats: ['image/avif', 'image/webp'],
11+
},
912
experimental: {
1013
images: {
1114
remotePatterns: [

docs/isr.md

+19-7
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,15 @@ request is made for stale content, the page will be regenerated in the backgroun
2121
but it can take up to 60 seconds before the new content is then updated in all CDN nodes if they already had a cached
2222
copy.
2323

24-
If the static regeneration relies on local files in your repository they need to be bundled with the handler functions.
25-
This can be done by modifying your [file based configuration](https://docs.netlify.com/configure-builds/file-based-configuration).
26-
An entry to the `included_files` option needs to be added under the `functions` option. You should be careful to not include unnecessary files, particularly large files such as images or videos, because there is a 50MB size limit for each handler function.
27-
See [Functions Configuration Docs](https://docs.netlify.com/configure-builds/file-based-configuration/#functions) for more info.
28-
Update your `netlify.toml` file to include the following (assuming local content is located in the /content directory):
24+
If the static regeneration relies on local files in your repository they need to be bundled with the handler functions.
25+
This can be done by modifying your
26+
[file based configuration](https://docs.netlify.com/configure-builds/file-based-configuration). An entry to the
27+
`included_files` option needs to be added under the `functions` option. You should be careful to not include unnecessary
28+
files, particularly large files such as images or videos, because there is a 50MB size limit for each handler function.
29+
See [Functions Configuration Docs](https://docs.netlify.com/configure-builds/file-based-configuration/#functions) for
30+
more info. Update your `netlify.toml` file to include the following (assuming local content is located in the /content
31+
directory):
32+
2933
```toml
3034
[functions]
3135
included_files = ["content/**"]
@@ -37,30 +41,38 @@ If you only need the content for DSG pages, then you can target only that functi
3741
[functions.__dsg]
3842
included_files = ["content/**"]
3943
```
44+
4045
or, for SSR pages:
4146

4247
```toml
4348
[functions.__ssr]
4449
included_files = ["content/**"]
4550
```
51+
4652
If a new deploy is made, all persisted pages and CDN cached pages will be invalidated so that conflicts are avoided. If
4753
this did not happen, a stale HTML page might make a request for an asset that no longer exists in the new deploy. By
4854
invalidating all persisted pages, you can be confident that this will never happen and that deploys remain atomic.
4955

56+
### On-demand ISR
57+
58+
On-Demand ISR (where a path is manually revalidated) is not currently supported on Netlify.
59+
[Please let us know](https://github.com/netlify/netlify-plugin-nextjs/discussions/1228) if this feature would be useful
60+
to you, and if so how you would plan to use it.
61+
5062
### Alternatives to ISR
5163

5264
ISR is best for situations where there are regular updates to content throughout the day, particularly you don't have
5365
control over when it happens. It is less ideal in situations such as a CMS with incremental updates where you can have
5466
the CMS trigger a deploy when a page is added or edited. This offers the best performance and avoids unnecesary
5567
rebuilds.
5668

57-
### Static site generation
69+
#### Static site generation
5870

5971
For high-traffic pages you can use
6072
[static generation](https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation) without
6173
`revalidate`, which deploys static files directly to the CDN for maximum performance.
6274

63-
### Distributed persistent rendering
75+
#### Distributed persistent rendering
6476

6577
For less commonly-accessed content you can use return `fallback: "blocking"` from
6678
[`getStaticPaths`](https://nextjs.org/docs/basic-features/data-fetching#getstaticpaths-static-generation) and defer

plugin/src/helpers/edge.ts

+31-25
Original file line numberDiff line numberDiff line change
@@ -125,12 +125,6 @@ const writeEdgeFunction = async ({
125125
* Writes Edge Functions for the Next middleware
126126
*/
127127
export const writeEdgeFunctions = async (netlifyConfig: NetlifyConfig) => {
128-
const middlewareManifest = await loadMiddlewareManifest(netlifyConfig)
129-
if (!middlewareManifest) {
130-
console.error("Couldn't find the middleware manifest")
131-
return
132-
}
133-
134128
const manifest: FunctionManifest = {
135129
functions: [],
136130
version: 1,
@@ -139,35 +133,47 @@ export const writeEdgeFunctions = async (netlifyConfig: NetlifyConfig) => {
139133
const edgeFunctionRoot = resolve('.netlify', 'edge-functions')
140134
await emptyDir(edgeFunctionRoot)
141135

142-
await copyEdgeSourceFile({ edgeFunctionDir: edgeFunctionRoot, file: 'ipx.ts' })
143-
144-
manifest.functions.push({
145-
function: 'ipx',
146-
path: '/_next/image*',
147-
})
148-
149-
for (const middleware of middlewareManifest.sortedMiddleware) {
150-
const edgeFunctionDefinition = middlewareManifest.middleware[middleware]
151-
const functionDefinition = await writeEdgeFunction({
152-
edgeFunctionDefinition,
153-
edgeFunctionRoot,
154-
netlifyConfig,
136+
if (!process.env.NEXT_DISABLE_EDGE_IMAGES) {
137+
if (!process.env.NEXT_USE_NETLIFY_EDGE) {
138+
console.log(
139+
'Using Netlify Edge Functions for image format detection. Set env var "NEXT_DISABLE_EDGE_IMAGES=true" to disable.',
140+
)
141+
}
142+
await copyEdgeSourceFile({ edgeFunctionDir: edgeFunctionRoot, file: 'ipx.ts' })
143+
manifest.functions.push({
144+
function: 'ipx',
145+
path: '/_next/image*',
155146
})
156-
manifest.functions.push(functionDefinition)
157147
}
158-
// Older versions of the manifest format don't have the functions field
159-
// No, the version field was not incremented
160-
if (typeof middlewareManifest.functions === 'object') {
161-
for (const edgeFunctionDefinition of Object.values(middlewareManifest.functions)) {
148+
if (process.env.NEXT_USE_NETLIFY_EDGE) {
149+
const middlewareManifest = await loadMiddlewareManifest(netlifyConfig)
150+
if (!middlewareManifest) {
151+
console.error("Couldn't find the middleware manifest")
152+
return
153+
}
154+
155+
for (const middleware of middlewareManifest.sortedMiddleware) {
156+
const edgeFunctionDefinition = middlewareManifest.middleware[middleware]
162157
const functionDefinition = await writeEdgeFunction({
163158
edgeFunctionDefinition,
164159
edgeFunctionRoot,
165160
netlifyConfig,
166161
})
167162
manifest.functions.push(functionDefinition)
168163
}
164+
// Older versions of the manifest format don't have the functions field
165+
// No, the version field was not incremented
166+
if (typeof middlewareManifest.functions === 'object') {
167+
for (const edgeFunctionDefinition of Object.values(middlewareManifest.functions)) {
168+
const functionDefinition = await writeEdgeFunction({
169+
edgeFunctionDefinition,
170+
edgeFunctionRoot,
171+
netlifyConfig,
172+
})
173+
manifest.functions.push(functionDefinition)
174+
}
175+
}
169176
}
170-
171177
await writeJson(join(edgeFunctionRoot, 'manifest.json'), manifest)
172178
}
173179

plugin/src/helpers/functions.ts

+8-9
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,14 @@ export const setupImageFunction = async ({
8181

8282
const imagePath = imageconfig.path || '/_next/image'
8383

84-
// If we have edge, we use content negotiation instead of the redirect
85-
if (!process.env.NEXT_USE_NETLIFY_EDGE) {
86-
netlifyConfig.redirects.push({
87-
from: `${imagePath}*`,
88-
query: { url: ':url', w: ':width', q: ':quality' },
89-
to: `${basePath}/${IMAGE_FUNCTION_NAME}/w_:width,q_:quality/:url`,
90-
status: 301,
91-
})
92-
}
84+
// If we have edge functions then the request will have already been rewritten
85+
// so this won't match. This is matched if edge is disabled or unavailable.
86+
netlifyConfig.redirects.push({
87+
from: `${imagePath}*`,
88+
query: { url: ':url', w: ':width', q: ':quality' },
89+
to: `${basePath}/${IMAGE_FUNCTION_NAME}/w_:width,q_:quality/:url`,
90+
status: 301,
91+
})
9392

9493
netlifyConfig.redirects.push({
9594
from: `${basePath}/${IMAGE_FUNCTION_NAME}/*`,

plugin/src/index.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -137,12 +137,14 @@ const plugin: NetlifyPlugin = {
137137
buildId,
138138
})
139139

140+
// We call this even if we don't have edge functions enabled because we still use it for images
141+
await writeEdgeFunctions(netlifyConfig)
142+
140143
if (process.env.NEXT_USE_NETLIFY_EDGE) {
141144
console.log(outdent`
142145
✨ Deploying to ${greenBright`Netlify Edge Functions`}
143146
This feature is in beta. Please share your feedback here: https://ntl.fyi/next-netlify-edge
144147
`)
145-
await writeEdgeFunctions(netlifyConfig)
146148
await updateConfig(publish)
147149
}
148150

plugin/src/templates/edge/ipx.ts

+36-16
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,52 @@
1-
import { Accepts } from 'https://deno.land/x/accepts/mod.ts'
2-
import type { Context } from 'netlify:edge'
1+
import { Accepts } from "https://deno.land/x/[email protected]/mod.ts";
2+
import type { Context } from "netlify:edge";
3+
import imageconfig from "../functions-internal/_ipx/imageconfig.json" assert {
4+
type: "json",
5+
};
6+
7+
const defaultFormat = "webp"
38

49
/**
510
* Implement content negotiation for images
611
*/
712

13+
// deno-lint-ignore require-await
814
const handler = async (req: Request, context: Context) => {
9-
const { searchParams } = new URL(req.url)
10-
const accept = new Accepts(req.headers)
11-
const type = accept.types(['avif', 'webp'])
15+
const { searchParams } = new URL(req.url);
16+
const accept = new Accepts(req.headers);
17+
const { formats = [defaultFormat] } = imageconfig;
18+
if (formats.length === 0) {
19+
formats.push(defaultFormat);
20+
}
21+
let type = accept.types(formats) || defaultFormat;
22+
if(Array.isArray(type)) {
23+
type = type[0];
24+
}
1225

13-
const source = searchParams.get('url')
14-
const width = searchParams.get('w')
15-
const quality = searchParams.get('q') ?? 75
26+
27+
const source = searchParams.get("url");
28+
const width = searchParams.get("w");
29+
const quality = searchParams.get("q") ?? 75;
1630

1731
if (!source || !width) {
18-
return new Response('Invalid request', {
32+
return new Response("Invalid request", {
1933
status: 400,
20-
})
34+
});
2135
}
2236

23-
const modifiers = [`w_${width}`, `q_${quality}`]
37+
const modifiers = [`w_${width}`, `q_${quality}`];
2438

2539
if (type) {
26-
modifiers.push(`f_${type}`)
40+
if(type.includes('/')) {
41+
// If this is a mimetype, strip "image/"
42+
type = type.split('/')[1];
43+
}
44+
modifiers.push(`f_${type}`);
2745
}
46+
const target = `/_ipx/${modifiers.join(",")}/${encodeURIComponent(source)}`;
47+
return context.rewrite(
48+
target,
49+
);
50+
};
2851

29-
return context.rewrite(`/_ipx/${modifiers.join(',')}/${encodeURIComponent(source)}`)
30-
}
31-
32-
export default handler
52+
export default handler;

0 commit comments

Comments
 (0)