Skip to content

Commit 2bdbd5b

Browse files
committed
feat: next/image improvements (sharp + on demand builders)
1 parent ba04239 commit 2bdbd5b

14 files changed

+700
-1413
lines changed

package-lock.json

+596-1,364
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@netlify/plugin-nextjs",
3-
"version": "3.1.0",
3+
"version": "3.1.0-experimental-odb.1",
44
"description": "Run Next.js seamlessly on Netlify",
55
"main": "index.js",
66
"files": [
@@ -43,6 +43,7 @@
4343
},
4444
"homepage": "https://github.com/netlify/netlify-plugin-nextjs#readme",
4545
"dependencies": {
46+
"@netlify/functions": "^0.6.0",
4647
"@sls-next/lambda-at-edge": "^1.5.2",
4748
"adm-zip": "^0.5.4",
4849
"chalk": "^4.1.0",
@@ -52,10 +53,12 @@
5253
"find-cache-dir": "^3.3.1",
5354
"find-up": "^5.0.0",
5455
"fs-extra": "^9.1.0",
55-
"jimp": "^0.16.1",
5656
"make-dir": "^3.1.0",
57+
"mime-types": "^2.1.30",
5758
"moize": "^6.0.0",
58-
"semver": "^7.3.2"
59+
"node-fetch": "^2.6.1",
60+
"semver": "^7.3.2",
61+
"sharp": "^0.28.1"
5962
},
6063
"devDependencies": {
6164
"@netlify/eslint-config-node": "^2.6.2",

src/lib/config.js

+4
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ const TEMPLATES_DIR = join(__dirname, 'templates')
2525
// This is the Netlify Function template that wraps all SSR pages
2626
const FUNCTION_TEMPLATE_PATH = join(TEMPLATES_DIR, 'netlifyFunction.js')
2727

28+
// This is the Netlify Builder template that wraps ISR pages
29+
const BUILDER_TEMPLATE_PATH = join(TEMPLATES_DIR, 'netlifyOnDemandBuilder.js')
30+
2831
// This is the file where custom redirects can be configured
2932
const CUSTOM_REDIRECTS_PATH = join('.', '_redirects')
3033

@@ -44,6 +47,7 @@ module.exports = {
4447
NEXT_CONFIG_PATH,
4548
TEMPLATES_DIR,
4649
FUNCTION_TEMPLATE_PATH,
50+
BUILDER_TEMPLATE_PATH,
4751
CUSTOM_REDIRECTS_PATH,
4852
CUSTOM_HEADERS_PATH,
4953
NEXT_IMAGE_FUNCTION_NAME,

src/lib/helpers/setupNetlifyFunctionForPage.js

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
const { copySync } = require('fs-extra')
22
const { join } = require('path')
3-
const { TEMPLATES_DIR, FUNCTION_TEMPLATE_PATH } = require('../config')
3+
const { TEMPLATES_DIR, FUNCTION_TEMPLATE_PATH, BUILDER_TEMPLATE_PATH } = require('../config')
44
const getNextDistDir = require('./getNextDistDir')
55
const getNetlifyFunctionName = require('./getNetlifyFunctionName')
66
const copyDynamicImportChunks = require('./copyDynamicImportChunks')
77
const { logItem } = require('./logger')
88

99
// Create a Netlify Function for the page with the given file path
10-
const setupNetlifyFunctionForPage = async ({ filePath, functionsPath, isApiPage }) => {
10+
const setupNetlifyFunctionForPage = async ({ filePath, functionsPath, isApiPage, isISR }) => {
1111
// Set function name based on file path
1212
const functionName = getNetlifyFunctionName(filePath, isApiPage)
1313
const functionDirectory = join(functionsPath, functionName)
@@ -18,13 +18,14 @@ const setupNetlifyFunctionForPage = async ({ filePath, functionsPath, isApiPage
1818

1919
// Copy function templates
2020
const functionTemplateCopyPath = join(functionDirectory, `${functionName}.js`)
21-
copySync(FUNCTION_TEMPLATE_PATH, functionTemplateCopyPath, {
21+
const srcTemplatePath = isISR ? BUILDER_TEMPLATE_PATH : FUNCTION_TEMPLATE_PATH
22+
copySync(srcTemplatePath, functionTemplateCopyPath, {
2223
overwrite: false,
2324
errorOnExist: true,
2425
})
2526

2627
// Copy function helpers
27-
const functionHelpers = ['renderNextPage.js', 'createRequestObject.js', 'createResponseObject.js']
28+
const functionHelpers = ['functionBase.js', 'renderNextPage.js', 'createRequestObject.js', 'createResponseObject.js']
2829
functionHelpers.forEach((helper) => {
2930
copySync(join(TEMPLATES_DIR, helper), join(functionDirectory, helper), {
3031
overwrite: false,

src/lib/pages/getStaticPropsWithFallback/setup.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const setup = async (functionsPath) => {
1616
const relativePath = getFilePathForRoute(route, 'js')
1717
const filePath = join('pages', relativePath)
1818
logItem(filePath)
19-
await setupNetlifyFunctionForPage({ filePath, functionsPath })
19+
await setupNetlifyFunctionForPage({ filePath, functionsPath, isISR: true })
2020
})
2121
}
2222

src/lib/steps/setupRedirects.js

+8-2
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,16 @@ const setupRedirects = async (publishPath) => {
4545
const staticRedirects = nextRedirects.filter(({ route }) => !isDynamicRoute(removeFileExtension(route)))
4646
const dynamicRedirects = nextRedirects.filter(({ route }) => isDynamicRoute(removeFileExtension(route)))
4747

48-
// Add next/image redirect to our image function
48+
// Add necessary next/image redirects for our image function
4949
dynamicRedirects.push({
5050
route: '/_next/image* url=:url w=:width q=:quality',
51-
target: `/.netlify/functions/${NEXT_IMAGE_FUNCTION_NAME}?url=:url&w=:width&q=:quality`,
51+
target: `/nextimg/:url/:width/:quality`,
52+
statusCode: '301',
53+
force: true,
54+
})
55+
dynamicRedirects.push({
56+
route: '/nextimg/*',
57+
target: `/.netlify/functions/${NEXT_IMAGE_FUNCTION_NAME}`,
5258
})
5359

5460
const sortedStaticRedirects = getSortedRedirects(staticRedirects)

src/lib/templates/functionBase.js

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// TEMPLATE: This file will be copied to the Netlify functions directory when
2+
// running next-on-netlify
3+
4+
// Render function for the Next.js page
5+
const renderNextPage = require('./renderNextPage')
6+
7+
const base = async (event, context, callback) => {
8+
// x-forwarded-host is undefined on Netlify for proxied apps that need it
9+
// fixes https://github.com/netlify/next-on-netlify/issues/46
10+
if (!event.multiValueHeaders.hasOwnProperty('x-forwarded-host')) {
11+
event.multiValueHeaders['x-forwarded-host'] = [event.headers['host']]
12+
}
13+
14+
// Get the request URL
15+
const { path } = event
16+
console.log('[request]', path)
17+
18+
// Render the Next.js page
19+
const response = await renderNextPage({ event, context })
20+
21+
// Convert header values to string. Netlify does not support integers as
22+
// header values. See: https://github.com/netlify/cli/issues/451
23+
Object.keys(response.multiValueHeaders).forEach((key) => {
24+
response.multiValueHeaders[key] = response.multiValueHeaders[key].map((value) => String(value))
25+
})
26+
27+
response.multiValueHeaders['Cache-Control'] = ['no-cache']
28+
29+
callback(null, response)
30+
}
31+
32+
module.exports = base

src/lib/templates/imageFunction.js

+30-9
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,45 @@
1-
const jimp = require('jimp')
1+
const path = require('path')
2+
const { builder } = require('@netlify/functions')
3+
const sharp = require('sharp')
4+
const fetch = require('node-fetch')
25

36
// Function used to mimic next/image and sharp
4-
exports.handler = async (event) => {
5-
const { url, w = 500, q = 75 } = event.queryStringParameters
7+
const handler = async (event) => {
8+
const [,, url, w = 500, q = 75] = event.path.split('/')
9+
const parsedUrl = decodeURIComponent(url)
610
const width = parseInt(w)
711
const quality = parseInt(q)
812

9-
const imageUrl = url.startsWith('/') ? `${process.env.DEPLOY_URL || `http://${event.headers.host}`}${url}` : url
10-
const image = await jimp.read(imageUrl)
11-
12-
image.resize(width, jimp.AUTO).quality(quality)
13+
const imageUrl = parsedUrl.startsWith('/') ? `${process.env.DEPLOY_URL || `http://${event.headers.host}`}${parsedUrl}` : parsedUrl
14+
const imageData = await fetch(imageUrl)
15+
const bufferData = await imageData.buffer()
16+
const ext = path.extname(imageUrl)
17+
const mimeType = ext === 'jpg' ? `image/jpeg` : `image/${ext}`
18+
let image = await sharp(bufferData)
19+
if (mimeType === 'image/webp') {
20+
image = image.webp({ quality })
21+
} else if (mimeType === 'image/jpeg') {
22+
image = image.jpeg({ quality })
23+
} else if (mimeType === 'image/png') {
24+
image = image.png({ quality })
25+
} else if (mimeType === 'image/avif') {
26+
image = image.avif({ quality })
27+
} else if (mimeType === 'image/tiff') {
28+
image = image.tiff({ quality })
29+
} else if (mimeType === 'image/heif') {
30+
image = image.heif({ quality })
31+
}
1332

14-
const imageBuffer = await image.getBufferAsync(image.getMIME())
33+
const imageBuffer = await image.resize(width).toBuffer()
1534

1635
return {
1736
statusCode: 200,
1837
headers: {
19-
'Content-Type': image.getMIME(),
38+
'Content-Type': mimeType,
2039
},
2140
body: imageBuffer.toString('base64'),
2241
isBase64Encoded: true,
2342
}
2443
}
44+
45+
exports.handler = builder(handler)

src/lib/templates/netlifyFunction.js

+2-24
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,6 @@
33

44
// Render function for the Next.js page
55
const renderNextPage = require('./renderNextPage')
6+
const functionBase = require('./functionBase')
67

7-
exports.handler = async (event, context, callback) => {
8-
// x-forwarded-host is undefined on Netlify for proxied apps that need it
9-
// fixes https://github.com/netlify/next-on-netlify/issues/46
10-
if (!event.multiValueHeaders.hasOwnProperty('x-forwarded-host')) {
11-
event.multiValueHeaders['x-forwarded-host'] = [event.headers['host']]
12-
}
13-
14-
// Get the request URL
15-
const { path } = event
16-
console.log('[request]', path)
17-
18-
// Render the Next.js page
19-
const response = await renderNextPage({ event, context })
20-
21-
// Convert header values to string. Netlify does not support integers as
22-
// header values. See: https://github.com/netlify/cli/issues/451
23-
Object.keys(response.multiValueHeaders).forEach((key) => {
24-
response.multiValueHeaders[key] = response.multiValueHeaders[key].map((value) => String(value))
25-
})
26-
27-
response.multiValueHeaders['Cache-Control'] = ['no-cache']
28-
29-
callback(null, response)
30-
}
8+
exports.handler = functionBase
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// TEMPLATE: This file will be copied to the Netlify functions directory when
2+
// running next-on-netlify
3+
4+
// Render on demand builder for the Next.js page
5+
const { builder } = require('@netlify/functions')
6+
const renderNextPage = require('./renderNextPage')
7+
const functionBase = require('./functionBase')
8+
9+
exports.handler = builder(functionBase)

src/tests/__snapshots__/defaults.test.js.snap

+2-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ exports[`Routing creates Netlify redirects 1`] = `
4646
/_next/data/%BUILD_ID%/getStaticProps/withFallback/:slug/* /.netlify/functions/next_getStaticProps_withFallback_slug 200
4747
/_next/data/%BUILD_ID%/getStaticProps/withFallbackBlocking/:id.json /.netlify/functions/next_getStaticProps_withFallbackBlocking_id 200
4848
/_next/data/%BUILD_ID%/getStaticProps/withRevalidate/withFallback/:id.json /.netlify/functions/next_getStaticProps_withRevalidate_withFallback_id 200
49-
/_next/image* url=:url w=:width q=:quality /.netlify/functions/next_image?url=:url&w=:width&q=:quality 200
49+
/_next/image* url=:url w=:width q=:quality /nextimg/:url/:w/:q 301!
5050
/api/shows/:id /.netlify/functions/next_api_shows_id 200
5151
/api/shows/:params/* /.netlify/functions/next_api_shows_params 200
5252
/getServerSideProps/all /.netlify/functions/next_getServerSideProps_all_slug 200
@@ -56,6 +56,7 @@ exports[`Routing creates Netlify redirects 1`] = `
5656
/getStaticProps/withFallback/:slug/* /.netlify/functions/next_getStaticProps_withFallback_slug 200
5757
/getStaticProps/withFallbackBlocking/:id /.netlify/functions/next_getStaticProps_withFallbackBlocking_id 200
5858
/getStaticProps/withRevalidate/withFallback/:id /.netlify/functions/next_getStaticProps_withRevalidate_withFallback_id 200
59+
/nextimg/* /.netlify/functions/next_image 200
5960
/shows/:id /.netlify/functions/next_shows_id 200
6061
/shows/:params/* /.netlify/functions/next_shows_params 200
6162
/static/:id /static/[id].html 200"

src/tests/__snapshots__/i18n.test.js.snap

+2-1
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ exports[`Routing creates Netlify redirects 1`] = `
9292
/_next/data/%BUILD_ID%/getStaticProps/withFallback/:slug/* /.netlify/functions/next_getStaticProps_withFallback_slug 200
9393
/_next/data/%BUILD_ID%/getStaticProps/withFallbackBlocking/:id.json /.netlify/functions/next_getStaticProps_withFallbackBlocking_id 200
9494
/_next/data/%BUILD_ID%/getStaticProps/withRevalidate/withFallback/:id.json /.netlify/functions/next_getStaticProps_withRevalidate_withFallback_id 200
95-
/_next/image* url=:url w=:width q=:quality /.netlify/functions/next_image?url=:url&w=:width&q=:quality 200
95+
/_next/image* url=:url w=:width q=:quality /nextimg/:url/:w/:q 301!
9696
/api/shows/:id /.netlify/functions/next_api_shows_id 200
9797
/api/shows/:params/* /.netlify/functions/next_api_shows_params 200
9898
/en/getServerSideProps/all /.netlify/functions/next_getServerSideProps_all_slug 200
@@ -122,6 +122,7 @@ exports[`Routing creates Netlify redirects 1`] = `
122122
/getStaticProps/withFallback/:slug/* /.netlify/functions/next_getStaticProps_withFallback_slug 200
123123
/getStaticProps/withFallbackBlocking/:id /.netlify/functions/next_getStaticProps_withFallbackBlocking_id 200
124124
/getStaticProps/withRevalidate/withFallback/:id /.netlify/functions/next_getStaticProps_withRevalidate_withFallback_id 200
125+
/nextimg/* /.netlify/functions/next_image 200
125126
/shows/:id /.netlify/functions/next_shows_id 200
126127
/shows/:params/* /.netlify/functions/next_shows_params 200
127128
/static/:id /en/static/[id].html 200"

src/tests/__snapshots__/optionalCatchAll.test.js.snap

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ exports[`Routing creates Netlify redirects 1`] = `
66
/page /.netlify/functions/next_page 200
77
/_next/data/%BUILD_ID%/index.json /.netlify/functions/next_all 200
88
/_next/data/%BUILD_ID%/* /.netlify/functions/next_all 200
9-
/_next/image* url=:url w=:width q=:quality /.netlify/functions/next_image?url=:url&w=:width&q=:quality 200
9+
/_next/image* url=:url w=:width q=:quality /nextimg/:url/:w/:q 301!
10+
/nextimg/* /.netlify/functions/next_image 200
1011
/ /.netlify/functions/next_all 200
1112
/_next/* /_next/:splat 200
1213
/* /.netlify/functions/next_all 200"

src/tests/preRenderedIndexPages.test.js

+1-3
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,6 @@ describe('Routing', () => {
6868

6969
// Check that no redirects are present
7070
expect(redirects[0]).toEqual('# Next-on-Netlify Redirects')
71-
expect(redirects[1]).toEqual(
72-
'/_next/image* url=:url w=:width q=:quality /.netlify/functions/next_image?url=:url&w=:width&q=:quality 200',
73-
)
71+
expect(redirects[1]).toEqual('/_next/image* url=:url w=:width q=:quality /nextimg/:url/:w/:q 301!')
7472
})
7573
})

0 commit comments

Comments
 (0)