diff --git a/packages/next/build/webpack/loaders/next-image-loader.js b/packages/next/build/webpack/loaders/next-image-loader.js
index e4aff1b76e32f..4a4198b39154e 100644
--- a/packages/next/build/webpack/loaders/next-image-loader.js
+++ b/packages/next/build/webpack/loaders/next-image-loader.js
@@ -17,7 +17,7 @@ function nextImageLoader(content) {
'/static/image/[path][name].[hash].[ext]',
opts
)
- const outputPath = '/_next' + interpolatedName
+ const outputPath = assetPrefix + '/_next' + interpolatedName
let extension = loaderUtils.interpolateName(this, '[ext]', opts)
if (extension === 'jpg') {
@@ -32,7 +32,7 @@ function nextImageLoader(content) {
if (isDev) {
const prefix = 'http://localhost'
const url = new URL('/_next/image', prefix)
- url.searchParams.set('url', assetPrefix + outputPath)
+ url.searchParams.set('url', outputPath)
url.searchParams.set('w', BLUR_IMG_SIZE)
url.searchParams.set('q', BLUR_QUALITY)
blurDataURL = url.href.slice(prefix.length)
diff --git a/packages/next/server/config.ts b/packages/next/server/config.ts
index 5690e59b984d7..24fa19b07bdab 100644
--- a/packages/next/server/config.ts
+++ b/packages/next/server/config.ts
@@ -196,6 +196,13 @@ function assignDefaults(userConfig: { [key: string]: any }) {
)
}
+ // static images are automatically prefixed with assetPrefix
+ // so we need to ensure _next/image allows downloading from
+ // this resource
+ if (config.assetPrefix?.startsWith('http')) {
+ images.domains.push(new URL(config.assetPrefix).hostname)
+ }
+
if (images.domains.length > 50) {
throw new Error(
`Specified images.domains exceeds length of 50, received length (${images.domains.length}), please reduce the length of the array to continue.\nSee more info here: https://nextjs.org/docs/messages/invalid-images-config`
diff --git a/packages/next/server/image-optimizer.ts b/packages/next/server/image-optimizer.ts
index a2d7c6a664d0c..4a4227aeeb1f5 100644
--- a/packages/next/server/image-optimizer.ts
+++ b/packages/next/server/image-optimizer.ts
@@ -135,7 +135,9 @@ export async function imageOptimizer(
}
// Should match output from next-image-loader
- const isStatic = url.startsWith('/_next/static/image')
+ const isStatic = url.startsWith(
+ `${nextConfig.basePath || ''}/_next/static/image`
+ )
const width = parseInt(w, 10)
diff --git a/test/integration/image-component/base-path/components/TallImage.js b/test/integration/image-component/base-path/components/TallImage.js
new file mode 100644
index 0000000000000..c0fbbcfe6d63c
--- /dev/null
+++ b/test/integration/image-component/base-path/components/TallImage.js
@@ -0,0 +1,20 @@
+import React from 'react'
+import Image from 'next/image'
+
+import testTall from './tall.png'
+
+const Page = () => {
+ return (
+
+
+
+
+ )
+}
+
+export default Page
diff --git a/test/integration/image-component/base-path/components/tall.png b/test/integration/image-component/base-path/components/tall.png
new file mode 100644
index 0000000000000..a792dda6c172f
Binary files /dev/null and b/test/integration/image-component/base-path/components/tall.png differ
diff --git a/test/integration/image-component/base-path/pages/static-img.js b/test/integration/image-component/base-path/pages/static-img.js
new file mode 100644
index 0000000000000..29912c6defc24
--- /dev/null
+++ b/test/integration/image-component/base-path/pages/static-img.js
@@ -0,0 +1,54 @@
+import React from 'react'
+import testImg from '../public/foo/test-rect.jpg'
+import Image from 'next/image'
+
+import testJPG from '../public/test.jpg'
+import testPNG from '../public/test.png'
+import testWEBP from '../public/test.webp'
+import testSVG from '../public/test.svg'
+import testGIF from '../public/test.gif'
+import testBMP from '../public/test.bmp'
+import testICO from '../public/test.ico'
+
+import TallImage from '../components/TallImage'
+
+const Page = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default Page
diff --git a/test/integration/image-component/base-path/public/foo/test-rect.jpg b/test/integration/image-component/base-path/public/foo/test-rect.jpg
new file mode 100644
index 0000000000000..68d3a8415f5e6
Binary files /dev/null and b/test/integration/image-component/base-path/public/foo/test-rect.jpg differ
diff --git a/test/integration/image-component/base-path/public/test.ico b/test/integration/image-component/base-path/public/test.ico
new file mode 100644
index 0000000000000..55cce0b4a8547
Binary files /dev/null and b/test/integration/image-component/base-path/public/test.ico differ
diff --git a/test/integration/image-component/base-path/public/test.webp b/test/integration/image-component/base-path/public/test.webp
new file mode 100644
index 0000000000000..4b306cb0898cc
Binary files /dev/null and b/test/integration/image-component/base-path/public/test.webp differ
diff --git a/test/integration/image-component/base-path/test/static.test.js b/test/integration/image-component/base-path/test/static.test.js
new file mode 100644
index 0000000000000..c3b050cb54387
--- /dev/null
+++ b/test/integration/image-component/base-path/test/static.test.js
@@ -0,0 +1,105 @@
+import {
+ findPort,
+ killApp,
+ nextBuild,
+ nextStart,
+ renderViaHTTP,
+ File,
+ waitFor,
+} from 'next-test-utils'
+import webdriver from 'next-webdriver'
+import { join } from 'path'
+
+const appDir = join(__dirname, '../')
+let appPort
+let app
+let browser
+let html
+
+const indexPage = new File(join(appDir, 'pages/static-img.js'))
+
+const runTests = () => {
+ it('Should allow an image with a static src to omit height and width', async () => {
+ expect(await browser.elementById('basic-static')).toBeTruthy()
+ expect(await browser.elementById('blur-png')).toBeTruthy()
+ expect(await browser.elementById('blur-webp')).toBeTruthy()
+ expect(await browser.elementById('blur-jpg')).toBeTruthy()
+ expect(await browser.elementById('static-svg')).toBeTruthy()
+ expect(await browser.elementById('static-gif')).toBeTruthy()
+ expect(await browser.elementById('static-bmp')).toBeTruthy()
+ expect(await browser.elementById('static-ico')).toBeTruthy()
+ expect(await browser.elementById('static-unoptimized')).toBeTruthy()
+ })
+ it('Should use immutable cache-control header for static import', async () => {
+ await browser.eval(
+ `document.getElementById("basic-static").scrollIntoView()`
+ )
+ await waitFor(1000)
+ const url = await browser.eval(
+ `document.getElementById("basic-static").src`
+ )
+ const res = await fetch(url)
+ expect(res.headers.get('cache-control')).toBe(
+ 'public, max-age=315360000, immutable'
+ )
+ })
+ it('Should use immutable cache-control header even when unoptimized', async () => {
+ await browser.eval(
+ `document.getElementById("static-unoptimized").scrollIntoView()`
+ )
+ await waitFor(1000)
+ const url = await browser.eval(
+ `document.getElementById("static-unoptimized").src`
+ )
+ const res = await fetch(url)
+ expect(res.headers.get('cache-control')).toBe(
+ 'public, max-age=31536000, immutable'
+ )
+ })
+ it('Should automatically provide an image height and width', async () => {
+ expect(html).toContain('width:400px;height:300px')
+ })
+ it('Should allow provided width and height to override intrinsic', async () => {
+ expect(html).toContain('width:200px;height:200px')
+ expect(html).not.toContain('width:400px;height:400px')
+ })
+ it('Should add a blurry placeholder to statically imported jpg', async () => {
+ expect(html).toContain(
+ `style="position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;filter:blur(20px);background-size:cover;background-image:url("data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAoKCgoKCgsMDAsPEA4QDxYUExMUFiIYGhgaGCIzICUgICUgMy03LCksNy1RQDg4QFFeT0pPXnFlZXGPiI+7u/sBCgoKCgoKCwwMCw8QDhAPFhQTExQWIhgaGBoYIjMgJSAgJSAzLTcsKSw3LVFAODhAUV5PSk9ecWVlcY+Ij7u7+//CABEIAAgACAMBIgACEQEDEQH/xAAUAAEAAAAAAAAAAAAAAAAAAAAH/9oACAEBAAAAADX/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/9oACAECEAAAAH//xAAUAQEAAAAAAAAAAAAAAAAAAAAA/9oACAEDEAAAAH//xAAdEAABAgcAAAAAAAAAAAAAAAATEhUAAwUUIzLS/9oACAEBAAE/AB0ZlUac43GqMYuo/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAgEBPwB//8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAwEBPwB//9k=");background-position:0% 0%"`
+ )
+ })
+ it('Should add a blurry placeholder to statically imported png', async () => {
+ expect(html).toContain(
+ `style="position:absolute;top:0;left:0;bottom:0;right:0;box-sizing:border-box;padding:0;border:none;margin:auto;display:block;width:0;height:0;min-width:100%;max-width:100%;min-height:100%;max-height:100%;filter:blur(20px);background-size:cover;background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAQAAABuBnYAAAAATklEQVR42i2I0QmAMBQD869Q9K+IsxU6RkfoiA6T55VXDpJLJC9uUJIzcx+XFd2dXMbx8n+QpoeYDpgY66RaDA83jCUfVpK2pER1dcEUP+KfSBtXK+BpAAAAAElFTkSuQmCC");background-position:0% 0%"`
+ )
+ })
+}
+
+describe('Build Error Tests for basePath', () => {
+ it('should throw build error when import statement is used with missing file', async () => {
+ await indexPage.replace(
+ '../public/foo/test-rect.jpg',
+ '../public/foo/test-rect-broken.jpg'
+ )
+
+ const { stderr } = await nextBuild(appDir, undefined, { stderr: true })
+ await indexPage.restore()
+
+ expect(stderr).toContain(
+ "Error: Can't resolve '../public/foo/test-rect-broken.jpg"
+ )
+ })
+})
+describe('Static Image Component Tests for basePath', () => {
+ beforeAll(async () => {
+ await nextBuild(appDir)
+ appPort = await findPort()
+ app = await nextStart(appDir, appPort)
+ html = await renderViaHTTP(appPort, '/docs/static-img')
+ browser = await webdriver(appPort, '/docs/static-img')
+ })
+ afterAll(() => {
+ killApp(app)
+ })
+ runTests()
+})