-- When using `next start` or a custom server image optimization works automatically.
-- [Vercel](https://vercel.com): Works automatically when you deploy on Vercel
-- [Imgix](https://www.imgix.com): `loader: 'imgix'`
-- [Cloudinary](https://cloudinary.com): `loader: 'cloudinary'`
-- [Akamai](https://www.akamai.com): `loader: 'akamai'`
diff --git a/docs/manifest.json b/docs/manifest.json
index ae23677e5b35..696f09bafb5d 100644
--- a/docs/manifest.json
+++ b/docs/manifest.json
@@ -21,6 +21,10 @@
"title": "Built-in CSS Support",
"path": "/docs/basic-features/built-in-css-support.md"
},
+ {
+ "title": "Image Optimization",
+ "path": "/docs/basic-features/image-optimization.md"
+ },
{
"title": "Fast Refresh",
"path": "/docs/basic-features/fast-refresh.md"
@@ -180,6 +184,10 @@
{
"title": "Codemods",
"path": "/docs/advanced-features/codemods.md"
+ },
+ {
+ "title": "Internationalized Routing",
+ "path": "/docs/advanced-features/i18n-routing.md"
}
]
},
@@ -216,6 +224,10 @@
"title": "next/link",
"path": "/docs/api-reference/next/link.md"
},
+ {
+ "title": "next/image",
+ "path": "/docs/api-reference/next/image.md"
+ },
{
"title": "next/head",
"path": "/docs/api-reference/next/head.md"
diff --git a/examples/i18n-routing/.gitignore b/examples/i18n-routing/.gitignore
new file mode 100644
index 000000000000..1437c53f70bc
--- /dev/null
+++ b/examples/i18n-routing/.gitignore
@@ -0,0 +1,34 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+# vercel
+.vercel
diff --git a/examples/i18n-routing/README.md b/examples/i18n-routing/README.md
new file mode 100644
index 000000000000..fa6c4a9394a6
--- /dev/null
+++ b/examples/i18n-routing/README.md
@@ -0,0 +1,23 @@
+# Internationalized Routing
+
+This example shows how to create internationalized pages using Next.js and the i18n routing feature. It shows a normal page, a non-dynamic `getStaticProps` page, a dynamic `getStaticProps` page, and a `getServerSideProps` page.
+
+For further documentation on this feature see the documentation [here](https://nextjs.org/docs/advanced-features/i18n-routing)
+
+## Deploy your own
+
+Deploy the example using [Vercel](https://vercel.com):
+
+[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/vercel/next.js/tree/canary/examples/amp)
+
+## How to use
+
+Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example:
+
+```bash
+npx create-next-app --example i18n-routing i18n-app
+# or
+yarn create next-app --example i18n-routing i18n-app
+```
+
+Deploy it to the cloud with [Vercel](https://vercel.com/import?filter=next.js&utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).
diff --git a/examples/i18n-routing/next.config.js b/examples/i18n-routing/next.config.js
new file mode 100644
index 000000000000..f548199a3b10
--- /dev/null
+++ b/examples/i18n-routing/next.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ i18n: {
+ locales: ['en', 'fr', 'nl'],
+ defaultLocale: 'en',
+ },
+}
diff --git a/examples/i18n-routing/package.json b/examples/i18n-routing/package.json
new file mode 100644
index 000000000000..656226b12945
--- /dev/null
+++ b/examples/i18n-routing/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "i18n-routing",
+ "version": "1.0.0",
+ "scripts": {
+ "dev": "next",
+ "build": "next build",
+ "start": "next start"
+ },
+ "dependencies": {
+ "next": "latest",
+ "react": "^16.7.0",
+ "react-dom": "^16.7.0"
+ },
+ "license": "MIT"
+}
diff --git a/examples/i18n-routing/pages/gsp/[slug].js b/examples/i18n-routing/pages/gsp/[slug].js
new file mode 100644
index 000000000000..896537fe56cf
--- /dev/null
+++ b/examples/i18n-routing/pages/gsp/[slug].js
@@ -0,0 +1,59 @@
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+
+export default function GspPage(props) {
+ const router = useRouter()
+ const { defaultLocale, isFallback, query } = router
+
+ if (isFallback) {
+ return 'Loading...'
+ }
+
+ return (
+
+ )
+}
+
+export const getStaticProps = ({ locale, locales }) => {
+ return {
+ props: {
+ locale,
+ locales,
+ },
+ }
+}
+
+export const getStaticPaths = ({ locales }) => {
+ const paths = []
+
+ for (const locale of locales) {
+ paths.push({ params: { slug: 'first' }, locale })
+ paths.push({ params: { slug: 'second' }, locale })
+ }
+
+ return {
+ paths,
+ fallback: true,
+ }
+}
diff --git a/examples/i18n-routing/pages/gsp/index.js b/examples/i18n-routing/pages/gsp/index.js
new file mode 100644
index 000000000000..4243a6034c46
--- /dev/null
+++ b/examples/i18n-routing/pages/gsp/index.js
@@ -0,0 +1,40 @@
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+
+export default function GspPage(props) {
+ const router = useRouter()
+ const { defaultLocale } = router
+
+ return (
+
+ )
+}
+
+export const getStaticProps = ({ locale, locales }) => {
+ return {
+ props: {
+ locale,
+ locales,
+ },
+ }
+}
diff --git a/examples/i18n-routing/pages/gssp.js b/examples/i18n-routing/pages/gssp.js
new file mode 100644
index 000000000000..94f7158b1202
--- /dev/null
+++ b/examples/i18n-routing/pages/gssp.js
@@ -0,0 +1,40 @@
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+
+export default function GsspPage(props) {
+ const router = useRouter()
+ const { defaultLocale } = router
+
+ return (
+
+ )
+}
+
+export const getServerSideProps = ({ locale, locales }) => {
+ return {
+ props: {
+ locale,
+ locales,
+ },
+ }
+}
diff --git a/examples/i18n-routing/pages/index.js b/examples/i18n-routing/pages/index.js
new file mode 100644
index 000000000000..d3bd67204f26
--- /dev/null
+++ b/examples/i18n-routing/pages/index.js
@@ -0,0 +1,31 @@
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+
+export default function IndexPage(props) {
+ const router = useRouter()
+ const { locale, locales, defaultLocale } = router
+
+ return (
+
+ )
+}
diff --git a/lerna.json b/lerna.json
index 239d9523b350..a0c6944976a2 100644
--- a/lerna.json
+++ b/lerna.json
@@ -17,5 +17,5 @@
"registry": "https://registry.npmjs.org/"
}
},
- "version": "9.5.6-canary.14"
+ "version": "9.5.6-canary.16"
}
diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json
index ce97bcb440be..7757d36353d6 100644
--- a/packages/create-next-app/package.json
+++ b/packages/create-next-app/package.json
@@ -1,6 +1,6 @@
{
"name": "create-next-app",
- "version": "9.5.6-canary.14",
+ "version": "9.5.6-canary.16",
"keywords": [
"react",
"next",
diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json
index 9e951d613ccf..eb85b1a79c23 100644
--- a/packages/eslint-plugin-next/package.json
+++ b/packages/eslint-plugin-next/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/eslint-plugin-next",
- "version": "9.5.6-canary.14",
+ "version": "9.5.6-canary.16",
"description": "ESLint plugin for NextJS.",
"main": "lib/index.js",
"license": "MIT",
diff --git a/packages/next-bundle-analyzer/package.json b/packages/next-bundle-analyzer/package.json
index 930fb377da43..684268d1b806 100644
--- a/packages/next-bundle-analyzer/package.json
+++ b/packages/next-bundle-analyzer/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/bundle-analyzer",
- "version": "9.5.6-canary.14",
+ "version": "9.5.6-canary.16",
"main": "index.js",
"license": "MIT",
"repository": {
diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json
index 748a864e4e1b..95f3075888b7 100644
--- a/packages/next-codemod/package.json
+++ b/packages/next-codemod/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/codemod",
- "version": "9.5.6-canary.14",
+ "version": "9.5.6-canary.16",
"license": "MIT",
"dependencies": {
"chalk": "4.1.0",
diff --git a/packages/next-env/package.json b/packages/next-env/package.json
index ef59ce9697b5..63cc5d7b874e 100644
--- a/packages/next-env/package.json
+++ b/packages/next-env/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/env",
- "version": "9.5.6-canary.14",
+ "version": "9.5.6-canary.16",
"keywords": [
"react",
"next",
diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json
index cd96f126bb93..cae2904e0955 100644
--- a/packages/next-mdx/package.json
+++ b/packages/next-mdx/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/mdx",
- "version": "9.5.6-canary.14",
+ "version": "9.5.6-canary.16",
"main": "index.js",
"license": "MIT",
"repository": {
diff --git a/packages/next-plugin-google-analytics/package.json b/packages/next-plugin-google-analytics/package.json
index 1abe107a3f37..288f382b1d34 100644
--- a/packages/next-plugin-google-analytics/package.json
+++ b/packages/next-plugin-google-analytics/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/plugin-google-analytics",
- "version": "9.5.6-canary.14",
+ "version": "9.5.6-canary.16",
"repository": {
"url": "vercel/next.js",
"directory": "packages/next-plugin-google-analytics"
diff --git a/packages/next-plugin-sentry/package.json b/packages/next-plugin-sentry/package.json
index 63a920806b0e..77fd33118703 100644
--- a/packages/next-plugin-sentry/package.json
+++ b/packages/next-plugin-sentry/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/plugin-sentry",
- "version": "9.5.6-canary.14",
+ "version": "9.5.6-canary.16",
"repository": {
"url": "vercel/next.js",
"directory": "packages/next-plugin-sentry"
diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json
index 13a043a38c69..3ec335749bc7 100644
--- a/packages/next-plugin-storybook/package.json
+++ b/packages/next-plugin-storybook/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/plugin-storybook",
- "version": "9.5.6-canary.14",
+ "version": "9.5.6-canary.16",
"repository": {
"url": "vercel/next.js",
"directory": "packages/next-plugin-storybook"
diff --git a/packages/next-polyfill-module/package.json b/packages/next-polyfill-module/package.json
index 05701a624588..ebe89716be94 100644
--- a/packages/next-polyfill-module/package.json
+++ b/packages/next-polyfill-module/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/polyfill-module",
- "version": "9.5.6-canary.14",
+ "version": "9.5.6-canary.16",
"description": "A standard library polyfill for ES Modules supporting browsers (Edge 16+, Firefox 60+, Chrome 61+, Safari 10.1+)",
"main": "dist/polyfill-module.js",
"license": "MIT",
diff --git a/packages/next-polyfill-nomodule/package.json b/packages/next-polyfill-nomodule/package.json
index 4e96606d16ea..4ab65f6ceec0 100644
--- a/packages/next-polyfill-nomodule/package.json
+++ b/packages/next-polyfill-nomodule/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/polyfill-nomodule",
- "version": "9.5.6-canary.14",
+ "version": "9.5.6-canary.16",
"description": "A polyfill for non-dead, nomodule browsers.",
"main": "dist/polyfill-nomodule.js",
"license": "MIT",
diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts
index 81256b987527..eb9179d6e92f 100644
--- a/packages/next/build/index.ts
+++ b/packages/next/build/index.ts
@@ -594,7 +594,7 @@ export default async function build(
ssgPageRoutes = workerResult.prerenderRoutes
}
- if (workerResult.prerenderFallback === 'unstable_blocking') {
+ if (workerResult.prerenderFallback === 'blocking') {
ssgBlockingFallbackPages.add(page)
} else if (workerResult.prerenderFallback === true) {
ssgStaticFallbackPages.add(page)
@@ -1110,11 +1110,15 @@ export default async function build(
)
}
+ const images = { ...config.images }
+ const { deviceSizes, iconSizes } = images
+ images.sizes = [...deviceSizes, ...iconSizes]
+
await promises.writeFile(
path.join(distDir, IMAGES_MANIFEST),
JSON.stringify({
version: 1,
- images: config.images,
+ images,
}),
'utf8'
)
@@ -1159,7 +1163,7 @@ export default async function build(
printCustomRoutes({ redirects, rewrites, headers })
}
- if (config.experimental.analyticsId) {
+ if (config.analyticsId) {
console.log(
chalk.bold.green('Next.js Analytics') +
' is enabled for this production build. ' +
diff --git a/packages/next/build/utils.ts b/packages/next/build/utils.ts
index e6ad2c1e7103..6a171be27f31 100644
--- a/packages/next/build/utils.ts
+++ b/packages/next/build/utils.ts
@@ -575,7 +575,7 @@ export async function buildStaticPaths(
if (
!(
typeof staticPathsResult.fallback === 'boolean' ||
- staticPathsResult.fallback === 'unstable_blocking'
+ staticPathsResult.fallback === 'blocking'
)
) {
throw new Error(
@@ -700,7 +700,7 @@ export async function isPageStatic(
hasServerProps?: boolean
hasStaticProps?: boolean
prerenderRoutes?: string[] | undefined
- prerenderFallback?: boolean | 'unstable_blocking' | undefined
+ prerenderFallback?: boolean | 'blocking' | undefined
}> {
try {
require('../next-server/lib/runtime-config').setConfig(runtimeEnvConfig)
@@ -775,7 +775,7 @@ export async function isPageStatic(
}
let prerenderRoutes: Array
| undefined
- let prerenderFallback: boolean | 'unstable_blocking' | undefined
+ let prerenderFallback: boolean | 'blocking' | undefined
if (hasStaticProps && hasStaticPaths) {
;({
paths: prerenderRoutes,
diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts
index 272ff411f253..f0d7eed6d91e 100644
--- a/packages/next/build/webpack-config.ts
+++ b/packages/next/build/webpack-config.ts
@@ -991,9 +991,16 @@ export default async function getBaseWebpackConfig(
config.experimental.scrollRestoration
),
'process.env.__NEXT_IMAGE_OPTS': JSON.stringify({
- sizes: config.images.sizes,
+ deviceSizes: config.images.deviceSizes,
+ iconSizes: config.images.iconSizes,
path: config.images.path,
loader: config.images.loader,
+ ...(dev
+ ? {
+ // pass domains in development to allow validating on the client
+ domains: config.images.domains,
+ }
+ : {}),
}),
'process.env.__NEXT_ROUTER_BASEPATH': JSON.stringify(config.basePath),
'process.env.__NEXT_HAS_REWRITES': JSON.stringify(hasRewrites),
@@ -1003,9 +1010,7 @@ export default async function getBaseWebpackConfig(
'process.env.__NEXT_I18N_DOMAINS': JSON.stringify(
config.experimental.i18n.domains
),
- 'process.env.__NEXT_ANALYTICS_ID': JSON.stringify(
- config.experimental.analyticsId
- ),
+ 'process.env.__NEXT_ANALYTICS_ID': JSON.stringify(config.analyticsId),
...(isServer
? {
// Fix bad-actors in the npm ecosystem (e.g. `node-formidable`)
diff --git a/packages/next/build/webpack/loaders/next-serverless-loader.ts b/packages/next/build/webpack/loaders/next-serverless-loader.ts
index 669594fdb2b3..013f63562d0a 100644
--- a/packages/next/build/webpack/loaders/next-serverless-loader.ts
+++ b/packages/next/build/webpack/loaders/next-serverless-loader.ts
@@ -262,33 +262,29 @@ const nextServerlessLoader: loader.Loader = function () {
...parsedUrl,
pathname: localePathResult.pathname,
})
+ req.__nextStrippedLocale = true
parsedUrl.pathname = localePathResult.pathname
+ }
+
+ // If a detected locale is a domain specific locale and we aren't already
+ // on that domain and path prefix redirect to it to prevent duplicate
+ // content from multiple domains
+ if (detectedDomain) {
+ const localeToCheck = localePathResult.detectedLocale
+ ? detectedLocale
+ : acceptPreferredLocale
- // check if the locale prefix matches a domain's defaultLocale
- // and we're on a locale specific domain if so redirect to that domain
- // if (detectedDomain) {
- // const matchedDomain = detectDomainLocale(
- // i18n.domains,
- // undefined,
- // detectedLocale
- // )
-
- // if (matchedDomain) {
- // localeDomainRedirect = \`http\${
- // matchedDomain.http ? '' : 's'
- // }://\${matchedDomain.domain}\`
- // }
- // }
- } else if (detectedDomain) {
const matchedDomain = detectDomainLocale(
i18n.domains,
undefined,
- acceptPreferredLocale
+ localeToCheck
)
if (matchedDomain && matchedDomain.domain !== detectedDomain.domain) {
localeDomainRedirect = \`http\${matchedDomain.http ? '' : 's'}://\${
matchedDomain.domain
+ }/\${
+ localeToCheck === matchedDomain.defaultLocale ? '' : localeToCheck
}\`
}
}
@@ -772,7 +768,7 @@ const nextServerlessLoader: loader.Loader = function () {
if (!renderMode) {
if (_nextData || getStaticProps || getServerSideProps) {
- if (renderOpts.ssgNotFound) {
+ if (renderOpts.isNotFound) {
res.statusCode = 404
const NotFoundComponent = ${
diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx
index 80fb05cd9f34..d938348a338a 100644
--- a/packages/next/client/image.tsx
+++ b/packages/next/client/image.tsx
@@ -14,9 +14,11 @@ const loaders = new Map string>([
type LoaderKey = 'imgix' | 'cloudinary' | 'akamai' | 'default'
type ImageData = {
- sizes: number[]
+ deviceSizes: number[]
+ iconSizes: number[]
loader: LoaderKey
path: string
+ domains?: string[]
}
type ImageProps = Omit<
@@ -34,9 +36,16 @@ type ImageProps = Omit<
)
const imageData: ImageData = process.env.__NEXT_IMAGE_OPTS as any
-const { sizes: configSizes, loader: configLoader, path: configPath } = imageData
-configSizes.sort((a, b) => a - b) // smallest to largest
-const largestSize = configSizes[configSizes.length - 1]
+const {
+ deviceSizes: configDeviceSizes,
+ iconSizes: configIconSizes,
+ loader: configLoader,
+ path: configPath,
+ domains: configDomains,
+} = imageData
+// sort smallest to largest
+configDeviceSizes.sort((a, b) => a - b)
+configIconSizes.sort((a, b) => a - b)
let cachedObserver: IntersectionObserver
const IntersectionObserver =
@@ -74,15 +83,36 @@ function getObserver(): IntersectionObserver | undefined {
))
}
+function getDeviceSizes(width: number | undefined): number[] {
+ if (typeof width !== 'number') {
+ return configDeviceSizes
+ }
+ const smallest = configDeviceSizes[0]
+ if (width < smallest && configIconSizes.includes(width)) {
+ return [width]
+ }
+ const widths: number[] = []
+ for (let size of configDeviceSizes) {
+ widths.push(size)
+ if (size >= width) {
+ break
+ }
+ }
+ return widths
+}
+
function computeSrc(
src: string,
unoptimized: boolean,
+ width: number | undefined,
quality?: string
): string {
if (unoptimized) {
return src
}
- return callLoader({ src, width: largestSize, quality })
+ const widths = getDeviceSizes(width)
+ const largest = widths[widths.length - 1]
+ return callLoader({ src, width: largest, quality })
}
type CallLoaderProps = {
@@ -98,29 +128,39 @@ function callLoader(loaderProps: CallLoaderProps) {
type SrcSetData = {
src: string
- widths: number[]
- quality?: string
+ unoptimized: boolean
+ width: number | undefined
+ quality: string | undefined
}
-function generateSrcSet({ src, widths, quality }: SrcSetData): string {
+function generateSrcSet({
+ src,
+ unoptimized,
+ width,
+ quality,
+}: SrcSetData): string | undefined {
// At each breakpoint, generate an image url using the loader, such as:
// ' www.example.com/foo.jpg?w=480 480w, '
- return widths
- .map((width: number) => `${callLoader({ src, width, quality })} ${width}w`)
+ if (unoptimized) {
+ return undefined
+ }
+
+ return getDeviceSizes(width)
+ .map((w) => `${callLoader({ src, width: w, quality })} ${w}w`)
.join(', ')
}
type PreloadData = {
src: string
- widths: number[]
+ unoptimized: boolean
+ width: number | undefined
sizes?: string
- unoptimized?: boolean
quality?: string
}
function generatePreload({
src,
- widths,
+ width,
unoptimized = false,
sizes,
quality,
@@ -134,15 +174,25 @@ function generatePreload({
)
}
+function getInt(x: unknown): number | undefined {
+ if (typeof x === 'number') {
+ return x
+ }
+ if (typeof x === 'string') {
+ return parseInt(x, 10)
+ }
+ return undefined
+}
+
export default function Image({
src,
sizes,
@@ -159,6 +209,13 @@ export default function Image({
const thisEl = useRef(null)
if (process.env.NODE_ENV !== 'production') {
+ if (!src) {
+ throw new Error(
+ `Image is missing required "src" property. Make sure you pass "src" in props to the \`next/image\` component. Received: ${JSON.stringify(
+ { width, height, quality, unsized }
+ )}`
+ )
+ }
if (!VALID_LOADING_VALUES.includes(loading)) {
throw new Error(
`Image with src "${src}" has invalid "loading" property. Provided "${loading}" should be one of ${VALID_LOADING_VALUES.map(
@@ -194,58 +251,23 @@ export default function Image({
}
}, [thisEl, lazy])
- // Generate attribute values
- const imgSrc = computeSrc(src, unoptimized, quality)
- const imgSrcSet = !unoptimized
- ? generateSrcSet({
- src,
- widths: configSizes,
- quality,
- })
- : undefined
-
- let imgAttributes:
- | {
- src: string
- srcSet?: string
- }
- | {
- 'data-src': string
- 'data-srcset'?: string
- }
- if (!lazy) {
- imgAttributes = {
- src: imgSrc,
- }
- if (imgSrcSet) {
- imgAttributes.srcSet = imgSrcSet
- }
- } else {
- imgAttributes = {
- 'data-src': imgSrc,
- }
- if (imgSrcSet) {
- imgAttributes['data-srcset'] = imgSrcSet
- }
- className = className ? className + ' __lazy' : '__lazy'
- }
-
+ let widthInt = getInt(width)
+ let heightInt = getInt(height)
let divStyle: React.CSSProperties | undefined
let imgStyle: React.CSSProperties | undefined
let wrapperStyle: React.CSSProperties | undefined
if (
- typeof height !== 'undefined' &&
- typeof width !== 'undefined' &&
+ typeof widthInt !== 'undefined' &&
+ typeof heightInt !== 'undefined' &&
!unsized
) {
//
//
- const quotient =
- parseInt(height as string, 10) / parseInt(width as string, 10)
+ const quotient = heightInt / widthInt
const ratio = isNaN(quotient) ? 1 : quotient * 100
wrapperStyle = {
maxWidth: '100%',
- width,
+ width: widthInt,
}
divStyle = {
position: 'relative',
@@ -260,8 +282,8 @@ export default function Image({
width: '100%',
}
} else if (
- typeof height === 'undefined' &&
- typeof width === 'undefined' &&
+ typeof widthInt === 'undefined' &&
+ typeof heightInt === 'undefined' &&
unsized
) {
//
@@ -282,6 +304,41 @@ export default function Image({
}
}
+ // Generate attribute values
+ const imgSrc = computeSrc(src, unoptimized, widthInt, quality)
+ const imgSrcSet = generateSrcSet({
+ src,
+ width: widthInt,
+ unoptimized,
+ quality,
+ })
+
+ let imgAttributes:
+ | {
+ src: string
+ srcSet?: string
+ }
+ | {
+ 'data-src': string
+ 'data-srcset'?: string
+ }
+ if (!lazy) {
+ imgAttributes = {
+ src: imgSrc,
+ }
+ if (imgSrcSet) {
+ imgAttributes.srcSet = imgSrcSet
+ }
+ } else {
+ imgAttributes = {
+ 'data-src': imgSrc,
+ }
+ if (imgSrcSet) {
+ imgAttributes['data-srcset'] = imgSrcSet
+ }
+ className = className ? className + ' __lazy' : '__lazy'
+ }
+
// No need to add preloads on the client side--by the time the application is hydrated,
// it's too late for preloads
const shouldPreload = priority && typeof window === 'undefined'
@@ -292,7 +349,7 @@ export default function Image({
{shouldPreload
? generatePreload({
src,
- widths: configSizes,
+ width: widthInt,
unoptimized,
sizes,
quality,
@@ -349,6 +406,42 @@ function cloudinaryLoader({ root, src, width, quality }: LoaderProps): string {
}
function defaultLoader({ root, src, width, quality }: LoaderProps): string {
+ if (process.env.NODE_ENV !== 'production') {
+ const missingValues = []
+
+ // these should always be provided but make sure they are
+ if (!src) missingValues.push('src')
+ if (!width) missingValues.push('width')
+
+ if (missingValues.length > 0) {
+ throw new Error(
+ `Next Image Optimization requires ${missingValues.join(
+ ', '
+ )} to be provided. Make sure you pass them as props to the \`next/image\` component. Received: ${JSON.stringify(
+ { src, width, quality }
+ )}`
+ )
+ }
+
+ if (src && !src.startsWith('/') && configDomains) {
+ let parsedSrc: URL
+ try {
+ parsedSrc = new URL(src)
+ } catch (err) {
+ console.error(err)
+ throw new Error(
+ `Failed to parse "${src}" if using relative image it must start with a leading slash "/" or be an absolute URL`
+ )
+ }
+
+ if (!configDomains.includes(parsedSrc.hostname)) {
+ throw new Error(
+ `Invalid src prop (${src}) on \`next/image\`, hostname is not configured under images in your \`next.config.js\``
+ )
+ }
+ }
+ }
+
return `${root}?url=${encodeURIComponent(src)}&w=${width}&q=${
quality || '100'
}`
diff --git a/packages/next/export/worker.ts b/packages/next/export/worker.ts
index f13b2c817187..0645fc552972 100644
--- a/packages/next/export/worker.ts
+++ b/packages/next/export/worker.ts
@@ -263,7 +263,7 @@ export default async function exportPage({
html = (result as any).html
}
- if (!html && !(curRenderOpts as any).ssgNotFound) {
+ if (!html && !(curRenderOpts as any).isNotFound) {
throw new Error(`Failed to render serverless page`)
}
} else {
@@ -318,7 +318,7 @@ export default async function exportPage({
html = await renderMethod(req, res, page, query, curRenderOpts)
}
}
- results.ssgNotFound = (curRenderOpts as any).ssgNotFound
+ results.ssgNotFound = (curRenderOpts as any).isNotFound
const validateAmp = async (
rawAmpHtml: string,
diff --git a/packages/next/next-server/lib/i18n/detect-domain-locale.ts b/packages/next/next-server/lib/i18n/detect-domain-locale.ts
index b77e9adca32f..7ad9d9890e57 100644
--- a/packages/next/next-server/lib/i18n/detect-domain-locale.ts
+++ b/packages/next/next-server/lib/i18n/detect-domain-locale.ts
@@ -3,6 +3,7 @@ export function detectDomainLocale(
| Array<{
http?: boolean
domain: string
+ locales?: string[]
defaultLocale: string
}>
| undefined,
@@ -13,17 +14,23 @@ export function detectDomainLocale(
| {
http?: boolean
domain: string
+ locales?: string[]
defaultLocale: string
}
| undefined
if (domainItems) {
+ if (detectedLocale) {
+ detectedLocale = detectedLocale.toLowerCase()
+ }
+
for (const item of domainItems) {
// remove port if present
const domainHostname = item.domain?.split(':')[0].toLowerCase()
if (
hostname === domainHostname ||
- detectedLocale?.toLowerCase() === item.defaultLocale.toLowerCase()
+ detectedLocale === item.defaultLocale.toLowerCase() ||
+ item.locales?.some((locale) => locale.toLowerCase() === detectedLocale)
) {
domainItem = item
break
diff --git a/packages/next/next-server/lib/router/router.ts b/packages/next/next-server/lib/router/router.ts
index 5eeb52da7e5f..d367a16db2eb 100644
--- a/packages/next/next-server/lib/router/router.ts
+++ b/packages/next/next-server/lib/router/router.ts
@@ -813,12 +813,7 @@ export default class Router implements BaseRouter {
this._resolveHref(parsedHref, pages)
if (pages.includes(parsedHref.pathname)) {
- return this.change(
- 'replaceState',
- destination,
- destination,
- options
- )
+ return this.change(method, destination, destination, options)
}
}
diff --git a/packages/next/next-server/server/config.ts b/packages/next/next-server/server/config.ts
index 2890b4a6b818..b3117452e030 100644
--- a/packages/next/next-server/server/config.ts
+++ b/packages/next/next-server/server/config.ts
@@ -23,8 +23,10 @@ const defaultConfig: { [key: string]: any } = {
target: 'server',
poweredByHeader: true,
compress: true,
+ analyticsId: process.env.VERCEL_ANALYTICS_ID || '',
images: {
- sizes: [320, 420, 768, 1024, 1200],
+ deviceSizes: [320, 420, 768, 1024, 1200],
+ iconSizes: [],
domains: [],
path: '/_next/image',
loader: 'default',
@@ -61,7 +63,6 @@ const defaultConfig: { [key: string]: any } = {
optimizeImages: false,
scrollRestoration: false,
i18n: false,
- analyticsId: process.env.VERCEL_ANALYTICS_ID || '',
},
future: {
excludeDefaultMomentLocales: false,
@@ -235,6 +236,13 @@ function assignDefaults(userConfig: { [key: string]: any }) {
`Specified images.domains should be an Array received ${typeof images.domains}`
)
}
+
+ 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`
+ )
+ }
+
const invalid = images.domains.filter(
(d: unknown) => typeof d !== 'string'
)
@@ -246,16 +254,53 @@ function assignDefaults(userConfig: { [key: string]: any }) {
)
}
}
- if (images.sizes) {
- if (!Array.isArray(images.sizes)) {
+ if (images.deviceSizes) {
+ const { deviceSizes } = images
+ if (!Array.isArray(deviceSizes)) {
+ throw new Error(
+ `Specified images.deviceSizes should be an Array received ${typeof deviceSizes}`
+ )
+ }
+
+ if (deviceSizes.length > 25) {
throw new Error(
- `Specified images.sizes should be an Array received ${typeof images.sizes}`
+ `Specified images.deviceSizes exceeds length of 25, received length (${deviceSizes.length}), please reduce the length of the array to continue`
)
}
- const invalid = images.sizes.filter((d: unknown) => typeof d !== 'number')
+
+ const invalid = deviceSizes.filter((d: unknown) => {
+ return typeof d !== 'number' || d < 1 || d > 10000
+ })
+
if (invalid.length > 0) {
throw new Error(
- `Specified images.sizes should be an Array of numbers received invalid values (${invalid.join(
+ `Specified images.deviceSizes should be an Array of numbers that are between 1 and 10000, received invalid values (${invalid.join(
+ ', '
+ )})`
+ )
+ }
+ }
+ if (images.iconSizes) {
+ const { iconSizes } = images
+ if (!Array.isArray(iconSizes)) {
+ throw new Error(
+ `Specified images.iconSizes should be an Array received ${typeof iconSizes}`
+ )
+ }
+
+ if (iconSizes.length > 25) {
+ throw new Error(
+ `Specified images.iconSizes exceeds length of 25, received length (${iconSizes.length}), please reduce the length of the array to continue`
+ )
+ }
+
+ const invalid = iconSizes.filter((d: unknown) => {
+ return typeof d !== 'number' || d < 1 || d > 10000
+ })
+
+ if (invalid.length > 0) {
+ throw new Error(
+ `Specified images.iconSizes should be an Array of numbers that are between 1 and 10000, received invalid values (${invalid.join(
', '
)})`
)
@@ -295,7 +340,26 @@ function assignDefaults(userConfig: { [key: string]: any }) {
if (!item.defaultLocale) return true
if (!item.domain || typeof item.domain !== 'string') return true
- return false
+ let hasInvalidLocale = false
+
+ if (Array.isArray(item.locales)) {
+ for (const locale of item.locales) {
+ if (typeof locale !== 'string') hasInvalidLocale = true
+
+ for (const domainItem of i18n.domains) {
+ if (domainItem === item) continue
+ if (domainItem.locales && domainItem.locales.includes(locale)) {
+ console.warn(
+ `Both ${item.domain} and ${domainItem.domain} configured the locale (${locale}) but only one can. Remove it from one i18n.domains config to continue`
+ )
+ hasInvalidLocale = true
+ break
+ }
+ }
+ }
+ }
+
+ return hasInvalidLocale
})
if (invalidDomainItems.length > 0) {
diff --git a/packages/next/next-server/server/image-optimizer.ts b/packages/next/next-server/server/image-optimizer.ts
index c0c2e023d7b0..d1825f7b2f5e 100644
--- a/packages/next/next-server/server/image-optimizer.ts
+++ b/packages/next/next-server/server/image-optimizer.ts
@@ -23,6 +23,14 @@ const CACHE_VERSION = 1
const ANIMATABLE_TYPES = [WEBP, PNG, GIF]
const VECTOR_TYPES = [SVG]
+type ImageData = {
+ deviceSizes: number[]
+ iconSizes: number[]
+ loader: string
+ path: string
+ domains?: string[]
+}
+
export async function imageOptimizer(
server: Server,
req: IncomingMessage,
@@ -30,7 +38,9 @@ export async function imageOptimizer(
parsedUrl: UrlWithParsedQuery
) {
const { nextConfig, distDir } = server
- const { sizes = [], domains = [], loader } = nextConfig?.images || {}
+ const imageData: ImageData = nextConfig.images
+ const { deviceSizes = [], iconSizes = [], domains = [], loader } = imageData
+ const sizes = [...deviceSizes, ...iconSizes]
if (loader !== 'default') {
await server.render404(req, res, parsedUrl)
diff --git a/packages/next/next-server/server/next-server.ts b/packages/next/next-server/server/next-server.ts
index e478f1da66bc..17ab6495fda1 100644
--- a/packages/next/next-server/server/next-server.ts
+++ b/packages/next/next-server/server/next-server.ts
@@ -342,32 +342,32 @@ export default class Server {
})
;(req as any).__nextStrippedLocale = true
parsedUrl.pathname = localePathResult.pathname
+ }
+
+ // If a detected locale is a domain specific locale and we aren't already
+ // on that domain and path prefix redirect to it to prevent duplicate
+ // content from multiple domains
+ if (detectedDomain && parsedUrl.pathname === '/') {
+ const localeToCheck = acceptPreferredLocale
+ // const localeToCheck = localePathResult.detectedLocale
+ // ? detectedLocale
+ // : acceptPreferredLocale
- // check if the locale prefix matches a domain's defaultLocale
- // and we're on a locale specific domain if so redirect to that domain
- // if (detectedDomain) {
- // const matchedDomain = detectDomainLocale(
- // i18n.domains,
- // undefined,
- // detectedLocale
- // )
-
- // if (matchedDomain) {
- // localeDomainRedirect = `http${matchedDomain.http ? '' : 's'}://${
- // matchedDomain?.domain
- // }`
- // }
- // }
- } else if (detectedDomain) {
const matchedDomain = detectDomainLocale(
i18n.domains,
undefined,
- acceptPreferredLocale
+ localeToCheck
)
- if (matchedDomain && matchedDomain.domain !== detectedDomain.domain) {
+ if (
+ matchedDomain &&
+ (matchedDomain.domain !== detectedDomain.domain ||
+ localeToCheck !== matchedDomain.defaultLocale)
+ ) {
localeDomainRedirect = `http${matchedDomain.http ? '' : 's'}://${
matchedDomain.domain
+ }/${
+ localeToCheck === matchedDomain.defaultLocale ? '' : localeToCheck
}`
}
}
@@ -1351,7 +1351,7 @@ export default class Server {
html = renderResult.html
pageData = renderResult.renderOpts.pageData
sprRevalidate = renderResult.renderOpts.revalidate
- isNotFound = renderResult.renderOpts.ssgNotFound
+ isNotFound = renderResult.renderOpts.isNotFound
} else {
const origQuery = parseUrl(req.url || '', true).query
const resolvedUrl = formatUrl({
@@ -1393,7 +1393,7 @@ export default class Server {
// TODO: change this to a different passing mechanism
pageData = (renderOpts as any).pageData
sprRevalidate = (renderOpts as any).revalidate
- isNotFound = (renderOpts as any).ssgNotFound
+ isNotFound = (renderOpts as any).isNotFound
}
return { html, pageData, sprRevalidate, isNotFound }
diff --git a/packages/next/next-server/server/render.tsx b/packages/next/next-server/server/render.tsx
index 71a53c66d111..d497c70036ac 100644
--- a/packages/next/next-server/server/render.tsx
+++ b/packages/next/next-server/server/render.tsx
@@ -625,8 +625,8 @@ export async function renderToHTML(
(key) =>
key !== 'revalidate' &&
key !== 'props' &&
- key !== 'unstable_redirect' &&
- key !== 'unstable_notFound'
+ key !== 'redirect' &&
+ key !== 'notFound'
)
if (invalidKeys.includes('unstable_revalidate')) {
@@ -637,23 +637,24 @@ export async function renderToHTML(
throw new Error(invalidKeysMsg('getStaticProps', invalidKeys))
}
- if (data.unstable_notFound) {
+ if ('notFound' in data && data.notFound) {
if (pathname === '/404') {
throw new Error(
- `The /404 page can not return unstable_notFound in "getStaticProps", please remove it to continue!`
+ `The /404 page can not return notFound in "getStaticProps", please remove it to continue!`
)
}
- ;(renderOpts as any).ssgNotFound = true
+ ;(renderOpts as any).isNotFound = true
;(renderOpts as any).revalidate = false
return null
}
if (
- data.unstable_redirect &&
- typeof data.unstable_redirect === 'object'
+ 'redirect' in data &&
+ data.redirect &&
+ typeof data.redirect === 'object'
) {
- checkRedirectValues(data.unstable_redirect, req)
+ checkRedirectValues(data.redirect, req)
if (isBuildTimeSSG) {
throw new Error(
@@ -663,18 +664,18 @@ export async function renderToHTML(
}
if (isDataReq) {
- data.props = {
- __N_REDIRECT: data.unstable_redirect.destination,
+ ;(data as any).props = {
+ __N_REDIRECT: data.redirect.destination,
}
} else {
- handleRedirect(res, data.unstable_redirect)
+ handleRedirect(res, data.redirect)
return null
}
}
if (
(dev || isBuildTimeSSG) &&
- !isSerializableProps(pathname, 'getStaticProps', data.props)
+ !isSerializableProps(pathname, 'getStaticProps', (data as any).props)
) {
// this fn should throw an error instead of ever returning `false`
throw new Error(
@@ -682,7 +683,7 @@ export async function renderToHTML(
)
}
- if (typeof data.revalidate === 'number') {
+ if ('revalidate' in data && typeof data.revalidate === 'number') {
if (!Number.isInteger(data.revalidate)) {
throw new Error(
`A page's revalidate option must be seconds expressed as a natural number. Mixed numbers, such as '${data.revalidate}', cannot be used.` +
@@ -703,20 +704,25 @@ export async function renderToHTML(
`\nTo only run getStaticProps at build-time and not revalidate at runtime, you can set \`revalidate\` to \`false\`!`
)
}
- } else if (data.revalidate === true) {
+ } else if ('revalidate' in data && data.revalidate === true) {
// When enabled, revalidate after 1 second. This value is optimal for
// the most up-to-date page possible, but without a 1-to-1
// request-refresh ratio.
data.revalidate = 1
} else {
// By default, we never revalidate.
- data.revalidate = false
+ ;(data as any).revalidate = false
}
- props.pageProps = Object.assign({}, props.pageProps, data.props)
+ props.pageProps = Object.assign(
+ {},
+ props.pageProps,
+ 'props' in data ? data.props : undefined
+ )
// pass up revalidate and props for export
// TODO: change this to a different passing mechanism
- ;(renderOpts as any).revalidate = data.revalidate
+ ;(renderOpts as any).revalidate =
+ 'revalidate' in data ? data.revalidate : undefined
;(renderOpts as any).pageData = props
}
@@ -755,32 +761,55 @@ export async function renderToHTML(
}
const invalidKeys = Object.keys(data).filter(
- (key) => key !== 'props' && key !== 'unstable_redirect'
+ (key) => key !== 'props' && key !== 'redirect' && key !== 'notFound'
)
+ if ((data as any).unstable_notFound) {
+ throw new Error(
+ `unstable_notFound has been renamed to notFound, please update the field to continue. Page: ${pathname}`
+ )
+ }
+ if ((data as any).unstable_redirect) {
+ throw new Error(
+ `unstable_redirect has been renamed to redirect, please update the field to continue. Page: ${pathname}`
+ )
+ }
+
if (invalidKeys.length) {
throw new Error(invalidKeysMsg('getServerSideProps', invalidKeys))
}
- if (
- data.unstable_redirect &&
- typeof data.unstable_redirect === 'object'
- ) {
- checkRedirectValues(data.unstable_redirect, req)
+ if ('notFound' in data) {
+ if (pathname === '/404') {
+ throw new Error(
+ `The /404 page can not return notFound in "getStaticProps", please remove it to continue!`
+ )
+ }
+
+ ;(renderOpts as any).isNotFound = true
+ return null
+ }
+
+ if ('redirect' in data && typeof data.redirect === 'object') {
+ checkRedirectValues(data.redirect, req)
if (isDataReq) {
- data.props = {
- __N_REDIRECT: data.unstable_redirect.destination,
+ ;(data as any).props = {
+ __N_REDIRECT: data.redirect.destination,
}
} else {
- handleRedirect(res, data.unstable_redirect)
+ handleRedirect(res, data.redirect)
return null
}
}
if (
(dev || isBuildTimeSSG) &&
- !isSerializableProps(pathname, 'getServerSideProps', data.props)
+ !isSerializableProps(
+ pathname,
+ 'getServerSideProps',
+ (data as any).props
+ )
) {
// this fn should throw an error instead of ever returning `false`
throw new Error(
@@ -788,7 +817,7 @@ export async function renderToHTML(
)
}
- props.pageProps = Object.assign({}, props.pageProps, data.props)
+ props.pageProps = Object.assign({}, props.pageProps, (data as any).props)
;(renderOpts as any).pageData = props
}
} catch (dataFetchError) {
diff --git a/packages/next/package.json b/packages/next/package.json
index 95734c554c7d..88f4ab1a906b 100644
--- a/packages/next/package.json
+++ b/packages/next/package.json
@@ -1,6 +1,6 @@
{
"name": "next",
- "version": "9.5.6-canary.14",
+ "version": "9.5.6-canary.16",
"description": "The React Framework",
"main": "./dist/server/next.js",
"license": "MIT",
@@ -79,10 +79,10 @@
"@babel/runtime": "7.11.2",
"@babel/types": "7.11.5",
"@hapi/accept": "5.0.1",
- "@next/env": "9.5.6-canary.14",
- "@next/polyfill-module": "9.5.6-canary.14",
- "@next/react-dev-overlay": "9.5.6-canary.14",
- "@next/react-refresh-utils": "9.5.6-canary.14",
+ "@next/env": "9.5.6-canary.16",
+ "@next/polyfill-module": "9.5.6-canary.16",
+ "@next/react-dev-overlay": "9.5.6-canary.16",
+ "@next/react-refresh-utils": "9.5.6-canary.16",
"ast-types": "0.13.2",
"babel-plugin-transform-define": "2.0.0",
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
@@ -129,7 +129,7 @@
"sharp": "0.26.2"
},
"devDependencies": {
- "@next/polyfill-nomodule": "9.5.6-canary.14",
+ "@next/polyfill-nomodule": "9.5.6-canary.16",
"@taskr/clear": "1.1.0",
"@taskr/esnext": "1.1.0",
"@taskr/watch": "1.1.0",
diff --git a/packages/next/server/next-dev-server.ts b/packages/next/server/next-dev-server.ts
index d2b70721c3c0..17944484b967 100644
--- a/packages/next/server/next-dev-server.ts
+++ b/packages/next/server/next-dev-server.ts
@@ -556,7 +556,7 @@ export default class DevServer extends Server {
return {
staticPaths,
fallbackMode:
- fallback === 'unstable_blocking'
+ fallback === 'blocking'
? 'blocking'
: fallback === true
? 'static'
diff --git a/packages/next/types/index.d.ts b/packages/next/types/index.d.ts
index d55380171625..29d8e361d488 100644
--- a/packages/next/types/index.d.ts
+++ b/packages/next/types/index.d.ts
@@ -86,12 +86,10 @@ export type GetStaticPropsContext = {
defaultLocale?: string
}
-export type GetStaticPropsResult = {
- props?: P
- revalidate?: number | boolean
- unstable_redirect?: Redirect
- unstable_notFound?: true
-}
+export type GetStaticPropsResult
=
+ | { props: P; revalidate?: number | boolean }
+ | { redirect: Redirect; revalidate?: number | boolean }
+ | { notFound: true }
export type GetStaticProps<
P extends { [key: string]: any } = { [key: string]: any },
@@ -113,7 +111,7 @@ export type GetStaticPathsContext = {
export type GetStaticPathsResult
= {
paths: Array
- fallback: boolean | 'unstable_blocking'
+ fallback: boolean | 'blocking'
}
export type GetStaticPaths = (
@@ -135,10 +133,10 @@ export type GetServerSidePropsContext<
defaultLocale?: string
}
-export type GetServerSidePropsResult
= {
- props?: P
- unstable_redirect?: Redirect
-}
+export type GetServerSidePropsResult
=
+ | { props: P }
+ | { redirect: Redirect }
+ | { notFound: true }
export type GetServerSideProps<
P extends { [key: string]: any } = { [key: string]: any },
diff --git a/packages/react-dev-overlay/package.json b/packages/react-dev-overlay/package.json
index a4cbec7fb1b8..ffe0c500fdd5 100644
--- a/packages/react-dev-overlay/package.json
+++ b/packages/react-dev-overlay/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/react-dev-overlay",
- "version": "9.5.6-canary.14",
+ "version": "9.5.6-canary.16",
"description": "A development-only overlay for developing React applications.",
"repository": {
"url": "vercel/next.js",
diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json
index 937856d2c70c..be6e5b038347 100644
--- a/packages/react-refresh-utils/package.json
+++ b/packages/react-refresh-utils/package.json
@@ -1,6 +1,6 @@
{
"name": "@next/react-refresh-utils",
- "version": "9.5.6-canary.14",
+ "version": "9.5.6-canary.16",
"description": "An experimental package providing utilities for React Refresh.",
"repository": {
"url": "vercel/next.js",
diff --git a/test/integration/getserversideprops/pages/not-found/[slug].js b/test/integration/getserversideprops/pages/not-found/[slug].js
new file mode 100644
index 000000000000..a2b94c1582bc
--- /dev/null
+++ b/test/integration/getserversideprops/pages/not-found/[slug].js
@@ -0,0 +1,34 @@
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+
+export default function Page(props) {
+ const router = useRouter()
+
+ return (
+ <>
+
gssp page
+ {JSON.stringify(props)}
+ {JSON.stringify(router.query)}
+ {router.pathname}
+ {router.asPath}
+
+ to /
+
+
+ >
+ )
+}
+
+export const getServerSideProps = ({ query }) => {
+ if (query.hiding) {
+ return {
+ notFound: true,
+ }
+ }
+
+ return {
+ props: {
+ hello: 'world',
+ },
+ }
+}
diff --git a/test/integration/getserversideprops/pages/not-found/index.js b/test/integration/getserversideprops/pages/not-found/index.js
new file mode 100644
index 000000000000..a2b94c1582bc
--- /dev/null
+++ b/test/integration/getserversideprops/pages/not-found/index.js
@@ -0,0 +1,34 @@
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+
+export default function Page(props) {
+ const router = useRouter()
+
+ return (
+ <>
+ gssp page
+ {JSON.stringify(props)}
+ {JSON.stringify(router.query)}
+ {router.pathname}
+ {router.asPath}
+
+ to /
+
+
+ >
+ )
+}
+
+export const getServerSideProps = ({ query }) => {
+ if (query.hiding) {
+ return {
+ notFound: true,
+ }
+ }
+
+ return {
+ props: {
+ hello: 'world',
+ },
+ }
+}
diff --git a/test/integration/getserversideprops/test/index.test.js b/test/integration/getserversideprops/test/index.test.js
index 015ec3d7b10b..b3e910d30a9f 100644
--- a/test/integration/getserversideprops/test/index.test.js
+++ b/test/integration/getserversideprops/test/index.test.js
@@ -118,6 +118,24 @@ const expectedManifestRoutes = () => [
),
page: '/non-json',
},
+ {
+ dataRouteRegex: `^\\/_next\\/data\\/${escapeRegex(
+ buildId
+ )}\\/not-found.json$`,
+ page: '/not-found',
+ },
+ {
+ dataRouteRegex: `^\\/_next\\/data\\/${escapeRegex(
+ buildId
+ )}\\/not\\-found\\/([^\\/]+?)\\.json$`,
+ namedDataRouteRegex: `^/_next/data/${escapeRegex(
+ buildId
+ )}/not\\-found/(?[^/]+?)\\.json$`,
+ page: '/not-found/[slug]',
+ routeKeys: {
+ slug: 'slug',
+ },
+ },
{
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapeRegex(buildId)}\\/refresh.json$`
@@ -235,6 +253,50 @@ const navigateTest = (dev = false) => {
const runTests = (dev = false) => {
navigateTest(dev)
+ it('should render 404 correctly when notFound is returned (non-dynamic)', async () => {
+ const res = await fetchViaHTTP(appPort, '/not-found', { hiding: true })
+
+ expect(res.status).toBe(404)
+ expect(await res.text()).toContain('This page could not be found')
+ })
+
+ it('should render 404 correctly when notFound is returned client-transition (non-dynamic)', async () => {
+ const browser = await webdriver(appPort, '/')
+ await browser.eval(`(function() {
+ window.beforeNav = 1
+ window.next.router.push('/not-found?hiding=true')
+ })()`)
+
+ await browser.waitForElementByCss('h1')
+ expect(await browser.elementByCss('html').text()).toContain(
+ 'This page could not be found'
+ )
+ expect(await browser.eval('window.beforeNav')).toBe(null)
+ })
+
+ it('should render 404 correctly when notFound is returned (dynamic)', async () => {
+ const res = await fetchViaHTTP(appPort, '/not-found/first', {
+ hiding: true,
+ })
+
+ expect(res.status).toBe(404)
+ expect(await res.text()).toContain('This page could not be found')
+ })
+
+ it('should render 404 correctly when notFound is returned client-transition (dynamic)', async () => {
+ const browser = await webdriver(appPort, '/')
+ await browser.eval(`(function() {
+ window.beforeNav = 1
+ window.next.router.push('/not-found/first?hiding=true')
+ })()`)
+
+ await browser.waitForElementByCss('h1')
+ expect(await browser.elementByCss('html').text()).toContain(
+ 'This page could not be found'
+ )
+ expect(await browser.eval('window.beforeNav')).toBe(null)
+ })
+
it('should SSR normal page correctly', async () => {
const html = await renderViaHTTP(appPort, '/')
expect(html).toMatch(/hello.*?world/)
diff --git a/test/integration/gssp-redirect/pages/gsp-blog/[post].js b/test/integration/gssp-redirect/pages/gsp-blog/[post].js
index 18166abd0b48..cd3fae7419a7 100644
--- a/test/integration/gssp-redirect/pages/gsp-blog/[post].js
+++ b/test/integration/gssp-redirect/pages/gsp-blog/[post].js
@@ -26,7 +26,7 @@ export const getStaticProps = ({ params }) => {
}
return {
- unstable_redirect: {
+ redirect: {
destination,
permanent: params.post.includes('permanent'),
},
diff --git a/test/integration/gssp-redirect/pages/gssp-blog/[post].js b/test/integration/gssp-redirect/pages/gssp-blog/[post].js
index 4c6af348a556..d01d19f31ae4 100644
--- a/test/integration/gssp-redirect/pages/gssp-blog/[post].js
+++ b/test/integration/gssp-redirect/pages/gssp-blog/[post].js
@@ -16,7 +16,7 @@ export const getServerSideProps = ({ params }) => {
}
return {
- unstable_redirect: {
+ redirect: {
destination,
permanent: params.post.includes('permanent'),
},
diff --git a/test/integration/gssp-redirect/test/index.test.js b/test/integration/gssp-redirect/test/index.test.js
index bdbb842bd51c..4852aac6ad91 100644
--- a/test/integration/gssp-redirect/test/index.test.js
+++ b/test/integration/gssp-redirect/test/index.test.js
@@ -123,6 +123,10 @@ const runTests = () => {
window.next.router.push('/gssp-blog/redirect-dest-_another')
})()`)
await browser.waitForElementByCss('#another')
+
+ const text = await browser.elementByCss('#another').text()
+
+ expect(text).toEqual('another Page')
})
it('should apply redirect when GSSP page is navigated to client-side (external)', async () => {
@@ -149,6 +153,10 @@ const runTests = () => {
window.next.router.push('/gsp-blog/redirect-dest-_another')
})()`)
await browser.waitForElementByCss('#another')
+
+ const text = await browser.elementByCss('#another').text()
+
+ expect(text).toEqual('another Page')
})
it('should apply redirect when GSP page is navigated to client-side (external)', async () => {
@@ -167,6 +175,94 @@ const runTests = () => {
},
})
})
+
+ it('should not replace history of the origin page when GSSP page is navigated to client-side (internal normal)', async () => {
+ const browser = await webdriver(appPort, '/another?mark_as=root')
+
+ await browser.eval(`(function () {
+ window.location.href = '/'
+ })()`)
+ await browser.waitForElementByCss('#index')
+
+ await browser.eval(`(function () {
+ window.next.router.push('/gssp-blog/redirect-dest-_another')
+ })()`)
+ await browser.waitForElementByCss('#another')
+
+ await browser.eval(`(function () {
+ window.history.back()
+ })()`)
+
+ const curUrl = await browser.url()
+ const { path } = url.parse(curUrl)
+ expect(path).toEqual('/')
+ })
+
+ it('should not replace history of the origin page when GSSP page is navigated to client-side (external)', async () => {
+ const browser = await webdriver(appPort, '/another?mark_as=root')
+
+ await browser.eval(`(function () {
+ window.location.href = '/'
+ })()`)
+ await browser.waitForElementByCss('#index')
+
+ await browser.eval(`(function () {
+ window.next.router.push('/gssp-blog/redirect-dest-_gssp-blog_first')
+ })()`)
+ await browser.waitForElementByCss('#gssp')
+
+ await browser.eval(`(function () {
+ window.history.back()
+ })()`)
+
+ const curUrl = await browser.url()
+ const { path } = url.parse(curUrl)
+ expect(path).toEqual('/')
+ })
+
+ it('should not replace history of the origin page when GSP page is navigated to client-side (internal)', async () => {
+ const browser = await webdriver(appPort, '/another?mark_as=root')
+
+ await browser.eval(`(function () {
+ window.location.href = '/'
+ })()`)
+ await browser.waitForElementByCss('#index')
+
+ await browser.eval(`(function () {
+ window.next.router.push('/gsp-blog/redirect-dest-_another')
+ })()`)
+ await browser.waitForElementByCss('#another')
+
+ await browser.eval(`(function () {
+ window.history.back()
+ })()`)
+
+ const curUrl = await browser.url()
+ const { path } = url.parse(curUrl)
+ expect(path).toEqual('/')
+ })
+
+ it('should not replace history of the origin page when GSP page is navigated to client-side (external)', async () => {
+ const browser = await webdriver(appPort, '/another?mark_as=root')
+
+ await browser.eval(`(function () {
+ window.location.href = '/'
+ })()`)
+ await browser.waitForElementByCss('#index')
+
+ await browser.eval(`(function () {
+ window.next.router.push('/gsp-blog/redirect-dest-_gsp-blog_first')
+ })()`)
+ await browser.waitForElementByCss('#gsp')
+
+ await browser.eval(`(function () {
+ window.history.back()
+ })()`)
+
+ const curUrl = await browser.url()
+ const { path } = url.parse(curUrl)
+ expect(path).toEqual('/')
+ })
}
describe('GS(S)P Redirect Support', () => {
@@ -224,7 +320,7 @@ describe('GS(S)P Redirect Support', () => {
export const getStaticProps = ({ params }) => {
return {
- unstable_redirect: {
+ redirect: {
permanent: true,
destination: '/another'
}
diff --git a/test/integration/i18n-support/next.config.js b/test/integration/i18n-support/next.config.js
index b49138d81668..3e1f4d2507de 100644
--- a/test/integration/i18n-support/next.config.js
+++ b/test/integration/i18n-support/next.config.js
@@ -11,11 +11,13 @@ module.exports = {
http: true,
domain: 'example.be',
defaultLocale: 'nl-BE',
+ locales: ['nl', 'nl-NL', 'nl-BE'],
},
{
http: true,
domain: 'example.fr',
defaultLocale: 'fr',
+ locales: ['fr-BE'],
},
],
},
diff --git a/test/integration/i18n-support/pages/not-found/fallback/[slug].js b/test/integration/i18n-support/pages/not-found/fallback/[slug].js
index e4e809bc4f32..0fde7256d1aa 100644
--- a/test/integration/i18n-support/pages/not-found/fallback/[slug].js
+++ b/test/integration/i18n-support/pages/not-found/fallback/[slug].js
@@ -26,7 +26,7 @@ export default function Page(props) {
export const getStaticProps = ({ params, locale, locales }) => {
if (locale === 'en' || locale === 'nl') {
return {
- unstable_notFound: true,
+ notFound: true,
}
}
diff --git a/test/integration/i18n-support/pages/not-found/index.js b/test/integration/i18n-support/pages/not-found/index.js
index 18a9bd7996f8..b6652953a9af 100644
--- a/test/integration/i18n-support/pages/not-found/index.js
+++ b/test/integration/i18n-support/pages/not-found/index.js
@@ -24,7 +24,7 @@ export default function Page(props) {
export const getStaticProps = ({ locale, locales }) => {
if (locale === 'en' || locale === 'nl') {
return {
- unstable_notFound: true,
+ notFound: true,
}
}
diff --git a/test/integration/i18n-support/test/index.test.js b/test/integration/i18n-support/test/index.test.js
index b756e275b180..4fe4f7b1633b 100644
--- a/test/integration/i18n-support/test/index.test.js
+++ b/test/integration/i18n-support/test/index.test.js
@@ -52,11 +52,13 @@ function runTests(isDev) {
http: true,
domain: 'example.be',
defaultLocale: 'nl-BE',
+ locales: ['nl', 'nl-NL', 'nl-BE'],
},
{
http: true,
domain: 'example.fr',
defaultLocale: 'fr',
+ locales: ['fr-BE'],
},
],
})
@@ -661,27 +663,56 @@ function runTests(isDev) {
})
it('should handle locales with domain', async () => {
- const checkDomainLocales = async (domainDefault = '', domain = '') => {
- for (const locale of locales) {
- // skip other domains' default locale since we redirect these
- if (['fr', 'nl-BE'].includes(locale) && locale !== domainDefault) {
- continue
- }
-
- const res = await fetchViaHTTP(
- appPort,
- `/${locale === domainDefault ? '' : locale}`,
- undefined,
- {
- headers: {
- host: domain,
- },
- redirect: 'manual',
- }
- )
+ const domainItems = [
+ {
+ // used for testing, this should not be needed in most cases
+ // as production domains should always use https
+ http: true,
+ domain: 'example.be',
+ defaultLocale: 'nl-BE',
+ locales: ['nl', 'nl-NL', 'nl-BE'],
+ },
+ {
+ http: true,
+ domain: 'example.fr',
+ defaultLocale: 'fr',
+ locales: ['fr-BE'],
+ },
+ ]
+ const domainLocales = domainItems.reduce((prev, cur) => {
+ return [...prev, ...cur.locales]
+ }, [])
+
+ const checkDomainLocales = async (
+ domainDefault = '',
+ domain = '',
+ locale = ''
+ ) => {
+ const res = await fetchViaHTTP(appPort, `/`, undefined, {
+ headers: {
+ host: domain,
+ 'accept-language': locale,
+ },
+ redirect: 'manual',
+ })
+ const expectedDomainItem = domainItems.find(
+ (item) => item.defaultLocale === locale || item.locales.includes(locale)
+ )
+ const shouldRedirect =
+ expectedDomainItem.domain !== domain ||
+ locale !== expectedDomainItem.defaultLocale
- expect(res.status).toBe(200)
+ expect(res.status).toBe(shouldRedirect ? 307 : 200)
+ if (shouldRedirect) {
+ const parsedUrl = url.parse(res.headers.get('location'), true)
+
+ expect(parsedUrl.pathname).toBe(
+ `/${expectedDomainItem.defaultLocale === locale ? '' : locale}`
+ )
+ expect(parsedUrl.query).toEqual({})
+ expect(parsedUrl.hostname).toBe(expectedDomainItem.domain)
+ } else {
const html = await res.text()
const $ = cheerio.load(html)
@@ -691,8 +722,11 @@ function runTests(isDev) {
}
}
- await checkDomainLocales('nl-BE', 'example.be')
- await checkDomainLocales('fr', 'example.fr')
+ for (const item of domainItems) {
+ for (const locale of domainLocales) {
+ await checkDomainLocales(item.defaultLocale, item.domain, locale)
+ }
+ }
})
it('should generate AMP pages with all locales', async () => {
diff --git a/test/integration/image-component/basic/next.config.js b/test/integration/image-component/basic/next.config.js
index 8f260dea043f..617de0a68d69 100644
--- a/test/integration/image-component/basic/next.config.js
+++ b/test/integration/image-component/basic/next.config.js
@@ -1,6 +1,7 @@
module.exports = {
images: {
- sizes: [480, 1024, 1600],
+ deviceSizes: [480, 1024, 1600, 2000],
+ iconSizes: [16, 64],
path: 'https://example.com/myaccount/',
loader: 'imgix',
},
diff --git a/test/integration/image-component/basic/pages/client-side.js b/test/integration/image-component/basic/pages/client-side.js
index 99f97951f2bd..ddd00fd2fe3e 100644
--- a/test/integration/image-component/basic/pages/client-side.js
+++ b/test/integration/image-component/basic/pages/client-side.js
@@ -53,6 +53,20 @@ const ClientSide = () => {
width={300}
height={400}
/>
+
+
Errors
diff --git a/test/integration/image-component/basic/pages/index.js b/test/integration/image-component/basic/pages/index.js
index 0ad8d8e1ac44..c08992abfb47 100644
--- a/test/integration/image-component/basic/pages/index.js
+++ b/test/integration/image-component/basic/pages/index.js
@@ -19,7 +19,7 @@ const Page = () => {
data-demo="demo-value"
src="bar.jpg"
loading="eager"
- width={300}
+ width={1024}
height={400}
/>
{
width={300}
height={400}
/>
-
{
width={300}
height={400}
/>
+
+
Client Side
diff --git a/test/integration/image-component/basic/pages/lazy.js b/test/integration/image-component/basic/pages/lazy.js
index 8a5fe4a01c8f..161b530bc3c3 100644
--- a/test/integration/image-component/basic/pages/lazy.js
+++ b/test/integration/image-component/basic/pages/lazy.js
@@ -9,7 +9,7 @@ const Lazy = () => {
id="lazy-top"
src="foo1.jpg"
height={400}
- width={300}
+ width={1024}
loading="lazy"
>
@@ -35,7 +35,7 @@ const Lazy = () => {
id="lazy-without-attribute"
src="foo4.jpg"
height={400}
- width={300}
+ width={800}
>
{
src="foo5.jpg"
loading="eager"
height={400}
- width={300}
+ width={1900}
>
)
diff --git a/test/integration/image-component/basic/test/index.test.js b/test/integration/image-component/basic/test/index.test.js
index bda3f67eea79..ff369bda69b8 100644
--- a/test/integration/image-component/basic/test/index.test.js
+++ b/test/integration/image-component/basic/test/index.test.js
@@ -33,27 +33,37 @@ function runTests() {
})
it('should modify src with the loader', async () => {
expect(await browser.elementById('basic-image').getAttribute('src')).toBe(
- 'https://example.com/myaccount/foo.jpg?auto=format&w=1600&q=60'
+ 'https://example.com/myaccount/foo.jpg?auto=format&w=480&q=60'
)
})
it('should correctly generate src even if preceding slash is included in prop', async () => {
expect(
await browser.elementById('preceding-slash-image').getAttribute('src')
- ).toBe('https://example.com/myaccount/fooslash.jpg?auto=format&w=1600')
+ ).toBe('https://example.com/myaccount/fooslash.jpg?auto=format&w=480')
})
it('should add a srcset based on the loader', async () => {
expect(
await browser.elementById('basic-image').getAttribute('srcset')
- ).toBe(
- 'https://example.com/myaccount/foo.jpg?auto=format&w=480&q=60 480w, https://example.com/myaccount/foo.jpg?auto=format&w=1024&q=60 1024w, https://example.com/myaccount/foo.jpg?auto=format&w=1600&q=60 1600w'
- )
+ ).toBe('https://example.com/myaccount/foo.jpg?auto=format&w=480&q=60 480w')
})
it('should add a srcset even with preceding slash in prop', async () => {
expect(
await browser.elementById('preceding-slash-image').getAttribute('srcset')
- ).toBe(
- 'https://example.com/myaccount/fooslash.jpg?auto=format&w=480 480w, https://example.com/myaccount/fooslash.jpg?auto=format&w=1024 1024w, https://example.com/myaccount/fooslash.jpg?auto=format&w=1600 1600w'
+ ).toBe('https://example.com/myaccount/fooslash.jpg?auto=format&w=480 480w')
+ })
+ it('should use iconSizes when width matches, not deviceSizes from next.config.js', async () => {
+ expect(await browser.elementById('icon-image-16').getAttribute('src')).toBe(
+ 'https://example.com/myaccount/icon.png?auto=format&w=16'
+ )
+ expect(
+ await browser.elementById('icon-image-16').getAttribute('srcset')
+ ).toBe('https://example.com/myaccount/icon.png?auto=format&w=16 16w')
+ expect(await browser.elementById('icon-image-64').getAttribute('src')).toBe(
+ 'https://example.com/myaccount/icon.png?auto=format&w=64'
)
+ expect(
+ await browser.elementById('icon-image-64').getAttribute('srcset')
+ ).toBe('https://example.com/myaccount/icon.png?auto=format&w=64 64w')
})
it('should support the unoptimized attribute', async () => {
expect(
@@ -70,10 +80,10 @@ function runTests() {
function lazyLoadingTests() {
it('should have loaded the first image immediately', async () => {
expect(await browser.elementById('lazy-top').getAttribute('src')).toBe(
- 'https://example.com/myaccount/foo1.jpg?auto=format&w=1600'
+ 'https://example.com/myaccount/foo1.jpg?auto=format&w=1024'
)
expect(await browser.elementById('lazy-top').getAttribute('srcset')).toBe(
- 'https://example.com/myaccount/foo1.jpg?auto=format&w=480 480w, https://example.com/myaccount/foo1.jpg?auto=format&w=1024 1024w, https://example.com/myaccount/foo1.jpg?auto=format&w=1600 1600w'
+ 'https://example.com/myaccount/foo1.jpg?auto=format&w=480 480w, https://example.com/myaccount/foo1.jpg?auto=format&w=1024 1024w'
)
})
it('should not have loaded the second image immediately', async () => {
@@ -101,11 +111,11 @@ function lazyLoadingTests() {
await check(() => {
return browser.elementById('lazy-mid').getAttribute('src')
- }, 'https://example.com/myaccount/foo2.jpg?auto=format&w=1600')
+ }, 'https://example.com/myaccount/foo2.jpg?auto=format&w=480')
await check(() => {
return browser.elementById('lazy-mid').getAttribute('srcset')
- }, 'https://example.com/myaccount/foo2.jpg?auto=format&w=480 480w, https://example.com/myaccount/foo2.jpg?auto=format&w=1024 1024w, https://example.com/myaccount/foo2.jpg?auto=format&w=1600 1600w')
+ }, 'https://example.com/myaccount/foo2.jpg?auto=format&w=480 480w')
})
it('should not have loaded the third image after scrolling down', async () => {
expect(
@@ -150,15 +160,17 @@ function lazyLoadingTests() {
await waitFor(200)
expect(
await browser.elementById('lazy-without-attribute').getAttribute('src')
- ).toBe('https://example.com/myaccount/foo4.jpg?auto=format&w=1600')
+ ).toBe('https://example.com/myaccount/foo4.jpg?auto=format&w=1024')
expect(
await browser.elementById('lazy-without-attribute').getAttribute('srcset')
- ).toBeTruthy()
+ ).toBe(
+ 'https://example.com/myaccount/foo4.jpg?auto=format&w=480 480w, https://example.com/myaccount/foo4.jpg?auto=format&w=1024 1024w'
+ )
})
it('should load the fifth image eagerly, without scrolling', async () => {
expect(await browser.elementById('eager-loading').getAttribute('src')).toBe(
- 'https://example.com/myaccount/foo5.jpg?auto=format&w=1600'
+ 'https://example.com/myaccount/foo5.jpg?auto=format&w=2000'
)
expect(
await browser.elementById('eager-loading').getAttribute('srcset')
@@ -198,14 +210,14 @@ describe('Image Component Tests', () => {
it('should add a preload tag for a priority image', async () => {
expect(
await hasPreloadLinkMatchingUrl(
- 'https://example.com/myaccount/withpriority.png?auto=format&w=1600'
+ 'https://example.com/myaccount/withpriority.png?auto=format&w=480&q=60'
)
).toBe(true)
})
it('should add a preload tag for a priority image with preceding slash', async () => {
expect(
await hasPreloadLinkMatchingUrl(
- 'https://example.com/myaccount/fooslash.jpg?auto=format&w=1600'
+ 'https://example.com/myaccount/fooslash.jpg?auto=format&w=480'
)
).toBe(true)
})
@@ -219,7 +231,7 @@ describe('Image Component Tests', () => {
it('should add a preload tag for a priority image, with quality', async () => {
expect(
await hasPreloadLinkMatchingUrl(
- 'https://example.com/myaccount/withpriority.png?auto=format&w=1600&q=60'
+ 'https://example.com/myaccount/withpriority.png?auto=format&w=480&q=60'
)
).toBe(true)
})
diff --git a/test/integration/image-component/default/pages/invalid-src.js b/test/integration/image-component/default/pages/invalid-src.js
new file mode 100644
index 000000000000..d9920d016b95
--- /dev/null
+++ b/test/integration/image-component/default/pages/invalid-src.js
@@ -0,0 +1,13 @@
+import React from 'react'
+import Image from 'next/image'
+
+const Page = () => {
+ return (
+