diff --git a/docs/api-reference/next.config.js/basepath.md b/docs/api-reference/next.config.js/basepath.md
new file mode 100644
index 0000000000000..21bb8b032dbd6
--- /dev/null
+++ b/docs/api-reference/next.config.js/basepath.md
@@ -0,0 +1,43 @@
+---
+description: Learn more about setting a base path in Next.js
+---
+
+# Base Path
+
+To deploy a Next.js application under a sub-path of a domain you can use the `basePath` option.
+
+`basePath` allows you to set a path prefix for the application. For example `/docs` instead of `/` (the default).
+
+For example, to set the base path to `/docs`, set the following configuration in `next.config.js`:
+
+```js
+module.exports = {
+ basePath: '/docs',
+}
+```
+
+## Links
+
+When linking to other pages using `next/link` and `next/router` the `basePath` will be automatically applied.
+
+For example using `/about` will automatically become `/docs/about` when `basePath` is set to `/docs`.
+
+```js
+export default function HomePage() {
+ return (
+ <>
+
+ About Page
+
+ >
+ )
+}
+```
+
+Output html:
+
+```html
+About Page
+```
+
+This makes sure that you don't have to change all links in your application when changing the `basePath` value.
diff --git a/docs/api-reference/next.config.js/headers.md b/docs/api-reference/next.config.js/headers.md
new file mode 100644
index 0000000000000..dad6ace1eb820
--- /dev/null
+++ b/docs/api-reference/next.config.js/headers.md
@@ -0,0 +1,91 @@
+---
+description: Add custom HTTP headers to your Next.js app.
+---
+
+# Headers
+
+Headers allow you to set custom HTTP headers for an incoming request path.
+
+To set custom HTTP headers you can use the `headers` key in `next.config.js`:
+
+```js
+module.exports = {
+ async headers() {
+ return [
+ {
+ source: '/about',
+ headers: [
+ {
+ key: 'x-custom-header',
+ value: 'my custom header value',
+ },
+ {
+ key: 'x-another-custom-header',
+ value: 'my other custom header value',
+ },
+ ],
+ },
+ ,
+ ]
+ },
+}
+```
+
+`headers` is an async function that expects an array to be returned holding objects with `source` and `headers` properties:
+
+- `source` is the incoming request path pattern.
+- `headers` is an array of header objects with the `key` and `value` properties.
+
+## Path Matching
+
+Path matches are allowed, for example `/blog/:slug` will match `/blog/hello-world` (no nested paths):
+
+```js
+module.exports = {
+ async headers() {
+ return [
+ {
+ source: '/blog/:slug',
+ headers: [
+ {
+ key: 'x-slug',
+ value: ':slug', // Matched parameters can be used in the value
+ },
+ {
+ key: 'x-slug-:slug', // Matched parameters can be used in the key
+ value: 'my other custom header value',
+ },
+ ],
+ },
+ ,
+ ]
+ },
+}
+```
+
+### Wildcard Path Matching
+
+To match a wildcard path you can use `*` after a parameter, for example `/blog/:slug*` will match `/blog/a/b/c/d/hello-world`:
+
+```js
+module.exports = {
+ async headers() {
+ return [
+ {
+ source: '/blog/:slug*',
+ headers: [
+ {
+ key: 'x-slug',
+ value: ':slug*', // Matched parameters can be used in the value
+ },
+ {
+ key: 'x-slug-:slug*', // Matched parameters can be used in the key
+ value: 'my other custom header value',
+ },
+ ],
+ },
+ ,
+ ]
+ },
+}
+```
diff --git a/docs/api-reference/next.config.js/redirects.md b/docs/api-reference/next.config.js/redirects.md
new file mode 100644
index 0000000000000..bcc93e24c8a34
--- /dev/null
+++ b/docs/api-reference/next.config.js/redirects.md
@@ -0,0 +1,67 @@
+---
+description: Add redirects to your Next.js app.
+---
+
+# Redirects
+
+Redirects allow you to redirect an incoming request path to a different destination path.
+
+Redirects are only available on the Node.js environment and do not affect client-side routing.
+
+To use Redirects you can use the `redirects` key in `next.config.js`:
+
+```js
+module.exports = {
+ async redirects() {
+ return [
+ {
+ source: '/about',
+ destination: '/',
+ permanent: true,
+ },
+ ]
+ },
+}
+```
+
+`redirects` is an async function that expects an array to be returned holding objects with `source`, `destination`, and `permanent` properties:
+
+- `source` is the incoming request path pattern.
+- `destination` is the path you want to route to.
+- `permanent` if the redirect is permanent or not.
+
+## Path Matching
+
+Path matches are allowed, for example `/old-blog/:slug` will match `/old-blog/hello-world` (no nested paths):
+
+```js
+module.exports = {
+ async redirects() {
+ return [
+ {
+ source: '/old-blog/:slug',
+ destination: '/news/:slug', // Matched parameters can be used in the destination
+ permanent: true,
+ },
+ ]
+ },
+}
+```
+
+### Wildcard Path Matching
+
+To match a wildcard path you can use `*` after a parameter, for example `/blog/:slug*` will match `/blog/a/b/c/d/hello-world`:
+
+```js
+module.exports = {
+ async redirects() {
+ return [
+ {
+ source: '/blog/:slug*',
+ destination: '/news/:slug*', // Matched parameters can be used in the destination
+ permanent: true,
+ },
+ ]
+ },
+}
+```
diff --git a/docs/api-reference/next.config.js/rewrites.md b/docs/api-reference/next.config.js/rewrites.md
new file mode 100644
index 0000000000000..1e4fa48213e2e
--- /dev/null
+++ b/docs/api-reference/next.config.js/rewrites.md
@@ -0,0 +1,112 @@
+---
+description: Add rewrites to your Next.js app.
+---
+
+# Rewrites
+
+Rewrites allow you to map an incoming request path to a different destination path.
+
+Rewrites are only available on the Node.js environment and do not affect client-side routing.
+
+To use rewrites you can use the `rewrites` key in `next.config.js`:
+
+```js
+module.exports = {
+ async rewrites() {
+ return [
+ {
+ source: '/about',
+ destination: '/',
+ },
+ ]
+ },
+}
+```
+
+`rewrites` is an async function that expects an array to be returned holding objects with `source` and `destination` properties:
+
+- `source` is the incoming request path pattern.
+- `destination` is the path you want to route to.
+
+## Path Matching
+
+Path matches are allowed, for example `/blog/:slug` will match `/blog/hello-world` (no nested paths):
+
+```js
+module.exports = {
+ async rewrites() {
+ return [
+ {
+ source: '/blog/:slug',
+ destination: '/news/:slug', // Matched parameters can be used in the destination
+ },
+ ]
+ },
+}
+```
+
+### Wildcard Path Matching
+
+To match a wildcard path you can use `*` after a parameter, for example `/blog/:slug*` will match `/blog/a/b/c/d/hello-world`:
+
+```js
+module.exports = {
+ async rewrites() {
+ return [
+ {
+ source: '/blog/:slug*',
+ destination: '/news/:slug*', // Matched parameters can be used in the destination
+ },
+ ]
+ },
+}
+```
+
+## Rewriting to an external URL
+
+
+ Examples
+
+
+
+Rewrites allow you to rewrite to an external url. This is especially useful for incrementally adopting Next.js.
+
+```js
+module.exports = {
+ async rewrites() {
+ return [
+ {
+ source: '/blog/:slug',
+ destination: 'https://example.com/blog/:slug', // Matched parameters can be used in the destination
+ },
+ ]
+ },
+}
+```
+
+### Incremental adoption of Next.js
+
+You can also make Next.js check the application routes before falling back to proxying to the previous website.
+
+This way you don't have to change the rewrites configuration when migrating more pages to Next.js
+
+```js
+module.exports = {
+ async rewrites() {
+ return [
+ // we need to define a no-op rewrite to trigger checking
+ // all pages/static files before we attempt proxying
+ {
+ source: '/:path*',
+ destination: '/:path*',
+ },
+ {
+ source: '/:path*',
+ destination: `https://custom-routes-proxying-endpoint.vercel.app/:path*`,
+ },
+ ]
+ },
+}
+```
diff --git a/docs/manifest.json b/docs/manifest.json
index 5db0aef99ed25..e4ff3d7a01605 100644
--- a/docs/manifest.json
+++ b/docs/manifest.json
@@ -227,6 +227,22 @@
"title": "Environment Variables",
"path": "/docs/api-reference/next.config.js/environment-variables.md"
},
+ {
+ "title": "Base Path",
+ "path": "/docs/api-reference/next.config.js/basepath.md"
+ },
+ {
+ "title": "Rewrites",
+ "path": "/docs/api-reference/next.config.js/rewrites.md"
+ },
+ {
+ "title": "Redirects",
+ "path": "/docs/api-reference/next.config.js/redirects.md"
+ },
+ {
+ "title": "Custom Headers",
+ "path": "/docs/api-reference/next.config.js/headers.md"
+ },
{
"title": "Custom Page Extensions",
"path": "/docs/api-reference/next.config.js/custom-page-extensions.md"
diff --git a/examples/custom-routes-proxying/package.json b/examples/custom-routes-proxying/package.json
index 748db774f52f1..352eb4ab81731 100644
--- a/examples/custom-routes-proxying/package.json
+++ b/examples/custom-routes-proxying/package.json
@@ -8,7 +8,7 @@
},
"dependencies": {
"next": "latest",
- "react": "16.8.6",
- "react-dom": "16.8.6"
+ "react": "^16.13.1",
+ "react-dom": "^16.13.1"
}
}
diff --git a/examples/dynamic-routing/package.json b/examples/dynamic-routing/package.json
index 0df90de82e529..7b4cb1b6cd7dc 100644
--- a/examples/dynamic-routing/package.json
+++ b/examples/dynamic-routing/package.json
@@ -9,7 +9,7 @@
"license": "ISC",
"dependencies": {
"next": "latest",
- "react": "16.8.6",
- "react-dom": "16.8.6"
+ "react": "^16.13.1",
+ "react-dom": "^16.13.1"
}
}
diff --git a/examples/with-http2/package.json b/examples/with-http2/package.json
index 85ee868e57dfc..1c1e4ae63a6a3 100644
--- a/examples/with-http2/package.json
+++ b/examples/with-http2/package.json
@@ -8,8 +8,8 @@
},
"dependencies": {
"next": "latest",
- "react": "16.8.6",
- "react-dom": "16.8.6"
+ "react": "^16.13.1",
+ "react-dom": "^16.13.1"
},
"license": "ISC"
}
diff --git a/examples/with-linaria/package.json b/examples/with-linaria/package.json
index 8c3ef14f7f388..105fbd56aa31e 100644
--- a/examples/with-linaria/package.json
+++ b/examples/with-linaria/package.json
@@ -10,8 +10,8 @@
"@zeit/next-css": "^1.0.1",
"linaria": "2.0.0-alpha.5",
"next": "latest",
- "react": "16.8.3",
- "react-dom": "16.8.3"
+ "react": "^16.13.1",
+ "react-dom": "^16.13.1"
},
"license": "ISC"
}
diff --git a/examples/with-rbx-bulma-pro/package.json b/examples/with-rbx-bulma-pro/package.json
index a3067ba7b6ddd..0ebc7974352f3 100644
--- a/examples/with-rbx-bulma-pro/package.json
+++ b/examples/with-rbx-bulma-pro/package.json
@@ -9,7 +9,7 @@
"bulma-pro": "^0.1.7",
"next": "^9.1.8-canary.11",
"rbx": "^2.2.0",
- "react": "16.8.6",
- "react-dom": "16.8.6"
+ "react": "^16.13.1",
+ "react-dom": "^16.13.1"
}
}
diff --git a/examples/with-stitches-styled/css/index.js b/examples/with-stitches-styled/css/index.js
index 2b6c855ed5529..82ca04e9d4b7b 100644
--- a/examples/with-stitches-styled/css/index.js
+++ b/examples/with-stitches-styled/css/index.js
@@ -1,17 +1,12 @@
-import { createConfig } from '@stitches/css'
+import { createCss } from '@stitches/css'
import { createStyled } from '@stitches/styled'
-const config = createConfig({
+export const css = createCss({
tokens: {
colors: {
RED: 'tomato',
},
},
})
-/*
- With Typescript:
- const { Provider, styled, useCss } = createStyled()
-*/
-const { Provider, styled, useCss } = createStyled()
-export { config, Provider, styled, useCss }
+export const styled = createStyled(css)
diff --git a/examples/with-stitches-styled/package.json b/examples/with-stitches-styled/package.json
index 9f7688677478b..f02cd6ad3cb8b 100644
--- a/examples/with-stitches-styled/package.json
+++ b/examples/with-stitches-styled/package.json
@@ -8,8 +8,8 @@
"start": "next start"
},
"dependencies": {
- "@stitches/css": "2.0.6",
- "@stitches/styled": "2.0.6",
+ "@stitches/css": "3.0.0",
+ "@stitches/styled": "3.0.0",
"next": "9.3.5",
"react": "16.13.1",
"react-dom": "16.13.1"
diff --git a/examples/with-stitches-styled/pages/_app.js b/examples/with-stitches-styled/pages/_app.js
deleted file mode 100644
index 3a02350e7434f..0000000000000
--- a/examples/with-stitches-styled/pages/_app.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import App from 'next/app'
-import { createCss } from '@stitches/css'
-import { Provider, config } from '../css'
-
-/*
- With Typescript:
- export default class MyApp extends App<{ serverCss: TCss }> {
-*/
-export default class MyApp extends App {
- render() {
- const { Component, pageProps, serverCss } = this.props
- return (
-
-
-
- )
- }
-}
diff --git a/examples/with-stitches-styled/pages/_document.js b/examples/with-stitches-styled/pages/_document.js
index 37446a385eae7..df3b228357357 100644
--- a/examples/with-stitches-styled/pages/_document.js
+++ b/examples/with-stitches-styled/pages/_document.js
@@ -1,25 +1,28 @@
import Document from 'next/document'
-import { createCss } from '@stitches/css'
-import { config } from '../css'
+import { css } from '../css'
export default class MyDocument extends Document {
static async getInitialProps(ctx) {
- const css = createCss(config)
const originalRenderPage = ctx.renderPage
try {
- ctx.renderPage = () =>
- originalRenderPage({
- enhanceApp: (App) => (props) => ,
- })
+ let extractedStyles
+ ctx.renderPage = () => {
+ const { styles, result } = css.getStyles(originalRenderPage)
+ extractedStyles = styles
+ return result
+ }
const initialProps = await Document.getInitialProps(ctx)
+
return {
...initialProps,
styles: (
<>
{initialProps.styles}
-
+ {extractedStyles.map((content) => (
+
+ ))}
>
),
}
diff --git a/examples/with-stitches-styled/pages/index.js b/examples/with-stitches-styled/pages/index.js
index 139f87aa79be4..46a2e51a0daf8 100644
--- a/examples/with-stitches-styled/pages/index.js
+++ b/examples/with-stitches-styled/pages/index.js
@@ -1,6 +1,8 @@
import { styled } from '../css'
-const Header = styled.h1((css) => css.color('RED'))
+const Header = styled.h1({
+ color: 'RED',
+})
export default function Home() {
return
diff --git a/examples/with-stitches/css/index.js b/examples/with-stitches/css/index.js
index 810d0e4bfa19d..bf4ac7ac1acae 100644
--- a/examples/with-stitches/css/index.js
+++ b/examples/with-stitches/css/index.js
@@ -1,30 +1,9 @@
-import { createConfig } from '@stitches/css'
-import * as React from 'react'
+import { createCss } from '@stitches/css'
-const config = createConfig({
+export const css = createCss({
tokens: {
colors: {
RED: 'tomato',
},
},
})
-
-/*
- With Typescript:
- const context = React.createContext>(null)
-*/
-const context = React.createContext(null)
-
-/*
- With Typescript:
- const Provider = ({ css, children }: { css: TCss, children?: React.ReactNode }) => {
- return {children}
- }
-*/
-const Provider = ({ css, children }) => {
- return {children}
-}
-
-const useCss = () => React.useContext(context)
-
-export { config, Provider, useCss }
diff --git a/examples/with-stitches/package.json b/examples/with-stitches/package.json
index 0487dc2e090c9..761c06c9d5fb5 100644
--- a/examples/with-stitches/package.json
+++ b/examples/with-stitches/package.json
@@ -8,7 +8,7 @@
"start": "next start"
},
"dependencies": {
- "@stitches/css": "2.0.6",
+ "@stitches/css": "3.0.0",
"next": "9.3.5",
"react": "16.13.1",
"react-dom": "16.13.1"
diff --git a/examples/with-stitches/pages/_app.js b/examples/with-stitches/pages/_app.js
deleted file mode 100644
index 3b6955437658b..0000000000000
--- a/examples/with-stitches/pages/_app.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import { createCss } from '@stitches/css'
-import App from 'next/app'
-import { config, Provider } from '../css'
-
-/*
- With Typescript:
- export default class MyApp extends App<{ serverCss: TCss }> {
-*/
-export default class MyApp extends App {
- render() {
- const { Component, pageProps, serverCss } = this.props
- return (
-
-
-
- )
- }
-}
diff --git a/examples/with-stitches/pages/_document.js b/examples/with-stitches/pages/_document.js
index 5de659632367e..df3b228357357 100644
--- a/examples/with-stitches/pages/_document.js
+++ b/examples/with-stitches/pages/_document.js
@@ -1,26 +1,27 @@
-import { createCss } from '@stitches/css'
import Document from 'next/document'
-import { config } from '../css'
+import { css } from '../css'
export default class MyDocument extends Document {
static async getInitialProps(ctx) {
- const css = createCss(config)
const originalRenderPage = ctx.renderPage
try {
- ctx.renderPage = () =>
- originalRenderPage({
- enhanceApp: (App) => (props) => ,
- })
+ let extractedStyles
+ ctx.renderPage = () => {
+ const { styles, result } = css.getStyles(originalRenderPage)
+ extractedStyles = styles
+ return result
+ }
const initialProps = await Document.getInitialProps(ctx)
+
return {
...initialProps,
styles: (
<>
{initialProps.styles}
- {css.getStyles().map((css, index) => (
-
+ {extractedStyles.map((content) => (
+
))}
>
),
diff --git a/examples/with-stitches/pages/index.js b/examples/with-stitches/pages/index.js
index aac79783121e0..039130840e4d4 100644
--- a/examples/with-stitches/pages/index.js
+++ b/examples/with-stitches/pages/index.js
@@ -1,6 +1,13 @@
-import { useCss } from '../css'
+import { css } from '../css'
export default function Home() {
- const css = useCss()
- return Hello world
+ return (
+
+ Hello world
+
+ )
}
diff --git a/examples/with-zones/blog/package.json b/examples/with-zones/blog/package.json
index b4f26bca0735e..f26dbba0f0a06 100644
--- a/examples/with-zones/blog/package.json
+++ b/examples/with-zones/blog/package.json
@@ -6,8 +6,8 @@
},
"dependencies": {
"next": "latest",
- "react": "16.8.6",
- "react-dom": "16.8.6"
+ "react": "^16.13.1",
+ "react-dom": "^16.13.1"
},
"license": "ISC"
}
diff --git a/examples/with-zones/home/package.json b/examples/with-zones/home/package.json
index 10e8e1055d365..7ebc19d462400 100644
--- a/examples/with-zones/home/package.json
+++ b/examples/with-zones/home/package.json
@@ -6,8 +6,8 @@
},
"dependencies": {
"next": "latest",
- "react": "16.8.6",
- "react-dom": "16.8.6"
+ "react": "^16.13.1",
+ "react-dom": "^16.13.1"
},
"license": "ISC"
}
diff --git a/packages/next/README.md b/packages/next/README.md
index d4d5cedd1b52e..9a401f10e64b7 100644
--- a/packages/next/README.md
+++ b/packages/next/README.md
@@ -29,9 +29,9 @@ Please see our [contributing.md](/contributing.md).
## Authors
-- Arunoda Susiripala ([@arunoda](https://twitter.com/arunoda)) – [Vercel](https://vercel.com)
-- Tim Neutkens ([@timneutkens](https://twitter.com/timneutkens)) – [Vercel](https://vercel.com)
-- Naoyuki Kanezawa ([@nkzawa](https://twitter.com/nkzawa)) – [Vercel](https://vercel.com)
+- Arunoda Susiripala ([@arunoda](https://twitter.com/arunoda)) – [Vercel](https://vercel.com/about/arunoda-zeit)
+- Tim Neutkens ([@timneutkens](https://twitter.com/timneutkens)) – [Vercel](https://vercel.com/about/timneutkens)
+- Naoyuki Kanezawa ([@nkzawa](https://twitter.com/nkzawa)) – [Vercel](https://vercel.com/about/nkzawa)
- Tony Kovanen ([@tonykovanen](https://twitter.com/tonykovanen)) – [Vercel](https://vercel.com)
-- Guillermo Rauch ([@rauchg](https://twitter.com/rauchg)) – [Vercel](https://vercel.com)
+- Guillermo Rauch ([@rauchg](https://twitter.com/rauchg)) – [Vercel](https://vercel.com/about/rauchg)
- Dan Zajdband ([@impronunciable](https://twitter.com/impronunciable)) – Knight-Mozilla / Coral Project
diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts
index 2c20abbc93034..ada23018f9fa8 100644
--- a/packages/next/build/webpack-config.ts
+++ b/packages/next/build/webpack-config.ts
@@ -863,6 +863,9 @@ export default async function getBaseWebpackConfig(
'process.env.__NEXT_REACT_MODE': JSON.stringify(
config.experimental.reactMode
),
+ 'process.env.__NEXT_SCROLL_RESTORATION': JSON.stringify(
+ config.experimental.scrollRestoration
+ ),
'process.env.__NEXT_ROUTER_BASEPATH': JSON.stringify(config.basePath),
...(isServer
? {
diff --git a/packages/next/next-server/lib/router/router.ts b/packages/next/next-server/lib/router/router.ts
index a9208f0cb70c0..8cdd959d2748f 100644
--- a/packages/next/next-server/lib/router/router.ts
+++ b/packages/next/next-server/lib/router/router.ts
@@ -97,6 +97,11 @@ type ComponentLoadCancel = (() => void) | null
type HistoryMethod = 'replaceState' | 'pushState'
+const manualScrollRestoration =
+ process.env.__NEXT_SCROLL_RESTORATION &&
+ typeof window !== 'undefined' &&
+ 'scrollRestoration' in window.history
+
function fetchNextData(
dataHref: string,
isServerRender: boolean,
@@ -250,6 +255,35 @@ export default class Router implements BaseRouter {
}
window.addEventListener('popstate', this.onPopState)
+
+ // enable custom scroll restoration handling when available
+ // otherwise fallback to browser's default handling
+ if (process.env.__NEXT_SCROLL_RESTORATION) {
+ if (manualScrollRestoration) {
+ window.history.scrollRestoration = 'manual'
+
+ let scrollDebounceTimeout: undefined | NodeJS.Timeout
+
+ const debouncedScrollSave = () => {
+ if (scrollDebounceTimeout) clearTimeout(scrollDebounceTimeout)
+
+ scrollDebounceTimeout = setTimeout(() => {
+ const { url, as: curAs, options } = history.state
+ this.changeState(
+ 'replaceState',
+ url,
+ curAs,
+ Object.assign({}, options, {
+ _N_X: window.scrollX,
+ _N_Y: window.scrollY,
+ })
+ )
+ }, 10)
+ }
+
+ window.addEventListener('scroll', debouncedScrollSave)
+ }
+ }
}
}
@@ -503,7 +537,13 @@ export default class Router implements BaseRouter {
throw error
}
+ if (process.env.__NEXT_SCROLL_RESTORATION) {
+ if (manualScrollRestoration && '_N_X' in options) {
+ window.scrollTo(options._N_X, options._N_Y)
+ }
+ }
Router.events.emit('routeChangeComplete', as)
+
return resolve(true)
})
},
diff --git a/packages/next/next-server/lib/utils.ts b/packages/next/next-server/lib/utils.ts
index a3058b18253c9..6dee4015678c3 100644
--- a/packages/next/next-server/lib/utils.ts
+++ b/packages/next/next-server/lib/utils.ts
@@ -199,6 +199,12 @@ export interface NextApiRequest extends IncomingMessage {
body: any
env: Env
+
+ preview?: boolean
+ /**
+ * Preview data set on the request, if any
+ * */
+ previewData?: any
}
/**
diff --git a/packages/next/next-server/server/api-utils.ts b/packages/next/next-server/server/api-utils.ts
index e2ba57d721737..930412aeb20a6 100644
--- a/packages/next/next-server/server/api-utils.ts
+++ b/packages/next/next-server/server/api-utils.ts
@@ -46,7 +46,16 @@ export async function apiResolver(
setLazyProp({ req: apiReq }, 'cookies', getCookieParser(req))
// Parsing query string
setLazyProp({ req: apiReq, params }, 'query', getQueryParser(req))
- // // Parsing of body
+ // Parsing preview data
+ setLazyProp({ req: apiReq }, 'previewData', () =>
+ tryGetPreviewData(req, res, apiContext)
+ )
+ // Checking if preview mode is enabled
+ setLazyProp({ req: apiReq }, 'preview', () =>
+ apiReq.previewData !== false ? true : undefined
+ )
+
+ // Parsing of body
if (bodyParser) {
apiReq.body = await parseBody(
apiReq,
diff --git a/packages/next/next-server/server/config.ts b/packages/next/next-server/server/config.ts
index deb2e0104fa05..33f87a7ab2b27 100644
--- a/packages/next/next-server/server/config.ts
+++ b/packages/next/next-server/server/config.ts
@@ -53,6 +53,7 @@ const defaultConfig: { [key: string]: any } = {
workerThreads: false,
pageEnv: false,
productionBrowserSourceMaps: false,
+ scrollRestoration: false,
},
future: {
excludeDefaultMomentLocales: false,
diff --git a/packages/react-dev-overlay/package.json b/packages/react-dev-overlay/package.json
index 3f27cbfc1663e..ae2aebe8f7b74 100644
--- a/packages/react-dev-overlay/package.json
+++ b/packages/react-dev-overlay/package.json
@@ -28,6 +28,7 @@
"strip-ansi": "6.0.0"
},
"peerDependencies": {
+ "webpack": "^4|^5",
"react": "^16.9.0",
"react-dom": "^16.9.0"
}
diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json
index 81498ecb60b05..60447961c7758 100644
--- a/packages/react-refresh-utils/package.json
+++ b/packages/react-refresh-utils/package.json
@@ -20,7 +20,7 @@
},
"peerDependencies": {
"react-refresh": "0.8.3",
- "webpack": "^4"
+ "webpack": "^4|^5"
},
"devDependencies": {
"react-refresh": "0.8.3"
diff --git a/test/integration/prerender-preview/pages/api/read.js b/test/integration/prerender-preview/pages/api/read.js
new file mode 100644
index 0000000000000..80a76dba09940
--- /dev/null
+++ b/test/integration/prerender-preview/pages/api/read.js
@@ -0,0 +1,7 @@
+export default (req, res) => {
+ const { preview, previewData } = req
+ res.json({
+ preview,
+ previewData,
+ })
+}
diff --git a/test/integration/prerender-preview/test/index.test.js b/test/integration/prerender-preview/test/index.test.js
index f85c7a1d5d4c7..d0c78a8fa13b6 100644
--- a/test/integration/prerender-preview/test/index.test.js
+++ b/test/integration/prerender-preview/test/index.test.js
@@ -182,6 +182,27 @@ function runTests(startServer = nextStart) {
expect(cookies[1]).not.toHaveProperty('Max-Age')
})
+ it('should pass undefined to API routes when not in preview', async () => {
+ const res = await fetchViaHTTP(appPort, `/api/read`)
+ const json = await res.json()
+
+ expect(json).toMatchObject({})
+ })
+ it('should pass the preview data to API routes', async () => {
+ const res = await fetchViaHTTP(
+ appPort,
+ '/api/read',
+ {},
+ { headers: { Cookie: previewCookieString } }
+ )
+ const json = await res.json()
+
+ expect(json).toMatchObject({
+ preview: true,
+ previewData: { lets: 'goooo' },
+ })
+ })
+
afterAll(async () => {
await killApp(app)
})
diff --git a/test/integration/scroll-back-restoration/next.config.js b/test/integration/scroll-back-restoration/next.config.js
new file mode 100644
index 0000000000000..456258562b6d9
--- /dev/null
+++ b/test/integration/scroll-back-restoration/next.config.js
@@ -0,0 +1,5 @@
+module.exports = {
+ experimental: {
+ scrollRestoration: true,
+ },
+}
diff --git a/test/integration/scroll-back-restoration/pages/another.js b/test/integration/scroll-back-restoration/pages/another.js
new file mode 100644
index 0000000000000..a37308e7d8779
--- /dev/null
+++ b/test/integration/scroll-back-restoration/pages/another.js
@@ -0,0 +1,10 @@
+import Link from 'next/link'
+
+export default () => (
+ <>
+ hi from another
+
+ to index
+
+ >
+)
diff --git a/test/integration/scroll-back-restoration/pages/index.js b/test/integration/scroll-back-restoration/pages/index.js
new file mode 100644
index 0000000000000..edb974d3b7b5f
--- /dev/null
+++ b/test/integration/scroll-back-restoration/pages/index.js
@@ -0,0 +1,50 @@
+import Link from 'next/link'
+
+const Page = ({ loaded }) => {
+ const link = (
+
+
+ to another
+
+
+ )
+
+ if (typeof window !== 'undefined') {
+ window.didHydrate = true
+ }
+
+ if (loaded) {
+ return (
+ <>
+
+ {link}
+ the end
+ >
+ )
+ }
+
+ return link
+}
+
+export default Page
+
+export const getServerSideProps = () => {
+ return {
+ props: {
+ loaded: true,
+ },
+ }
+}
diff --git a/test/integration/scroll-back-restoration/test/index.test.js b/test/integration/scroll-back-restoration/test/index.test.js
new file mode 100644
index 0000000000000..4c3ea725fad81
--- /dev/null
+++ b/test/integration/scroll-back-restoration/test/index.test.js
@@ -0,0 +1,89 @@
+/* eslint-env jest */
+
+import { join } from 'path'
+import webdriver from 'next-webdriver'
+import {
+ killApp,
+ findPort,
+ launchApp,
+ nextStart,
+ nextBuild,
+ check,
+} from 'next-test-utils'
+
+jest.setTimeout(1000 * 60 * 2)
+
+const appDir = join(__dirname, '../')
+let appPort
+let app
+
+const runTests = () => {
+ it('should restore the scroll position on navigating back', async () => {
+ const browser = await webdriver(appPort, '/')
+ await browser.eval(() =>
+ document.querySelector('#to-another').scrollIntoView()
+ )
+ const scrollRestoration = await browser.eval(
+ () => window.history.scrollRestoration
+ )
+
+ expect(scrollRestoration).toBe('manual')
+
+ const scrollX = Math.floor(await browser.eval(() => window.scrollX))
+ const scrollY = Math.floor(await browser.eval(() => window.scrollY))
+
+ expect(scrollX).not.toBe(0)
+ expect(scrollY).not.toBe(0)
+
+ await browser.eval(() => window.next.router.push('/another'))
+
+ await check(
+ () => browser.eval(() => document.documentElement.innerHTML),
+ /hi from another/
+ )
+ await browser.eval(() => (window.didHydrate = false))
+
+ await browser.eval(() => window.history.back())
+ await check(() => browser.eval(() => window.didHydrate), {
+ test(content) {
+ return content
+ },
+ })
+
+ const newScrollX = Math.floor(await browser.eval(() => window.scrollX))
+ const newScrollY = Math.floor(await browser.eval(() => window.scrollY))
+
+ console.log({
+ scrollX,
+ scrollY,
+ newScrollX,
+ newScrollY,
+ })
+
+ expect(scrollX).toBe(newScrollX)
+ expect(scrollY).toBe(newScrollY)
+ })
+}
+
+describe('Scroll Restoration Support', () => {
+ describe('dev mode', () => {
+ beforeAll(async () => {
+ appPort = await findPort()
+ app = await launchApp(appDir, appPort)
+ })
+ afterAll(() => killApp(app))
+
+ runTests()
+ })
+
+ describe('server mode', () => {
+ beforeAll(async () => {
+ await nextBuild(appDir)
+ appPort = await findPort()
+ app = await nextStart(appDir, appPort)
+ })
+ afterAll(() => killApp(app))
+
+ runTests()
+ })
+})
diff --git a/test/integration/scroll-forward-restoration/next.config.js b/test/integration/scroll-forward-restoration/next.config.js
new file mode 100644
index 0000000000000..456258562b6d9
--- /dev/null
+++ b/test/integration/scroll-forward-restoration/next.config.js
@@ -0,0 +1,5 @@
+module.exports = {
+ experimental: {
+ scrollRestoration: true,
+ },
+}
diff --git a/test/integration/scroll-forward-restoration/pages/another.js b/test/integration/scroll-forward-restoration/pages/another.js
new file mode 100644
index 0000000000000..a37308e7d8779
--- /dev/null
+++ b/test/integration/scroll-forward-restoration/pages/another.js
@@ -0,0 +1,10 @@
+import Link from 'next/link'
+
+export default () => (
+ <>
+ hi from another
+
+ to index
+
+ >
+)
diff --git a/test/integration/scroll-forward-restoration/pages/index.js b/test/integration/scroll-forward-restoration/pages/index.js
new file mode 100644
index 0000000000000..edb974d3b7b5f
--- /dev/null
+++ b/test/integration/scroll-forward-restoration/pages/index.js
@@ -0,0 +1,50 @@
+import Link from 'next/link'
+
+const Page = ({ loaded }) => {
+ const link = (
+
+
+ to another
+
+
+ )
+
+ if (typeof window !== 'undefined') {
+ window.didHydrate = true
+ }
+
+ if (loaded) {
+ return (
+ <>
+
+ {link}
+ the end
+ >
+ )
+ }
+
+ return link
+}
+
+export default Page
+
+export const getServerSideProps = () => {
+ return {
+ props: {
+ loaded: true,
+ },
+ }
+}
diff --git a/test/integration/scroll-forward-restoration/test/index.test.js b/test/integration/scroll-forward-restoration/test/index.test.js
new file mode 100644
index 0000000000000..ebb867457f179
--- /dev/null
+++ b/test/integration/scroll-forward-restoration/test/index.test.js
@@ -0,0 +1,97 @@
+/* eslint-env jest */
+
+import { join } from 'path'
+import webdriver from 'next-webdriver'
+import {
+ killApp,
+ findPort,
+ launchApp,
+ nextStart,
+ nextBuild,
+ check,
+} from 'next-test-utils'
+
+jest.setTimeout(1000 * 60 * 2)
+
+const appDir = join(__dirname, '../')
+let appPort
+let app
+
+const runTests = () => {
+ it('should restore the scroll position on navigating forward', async () => {
+ const browser = await webdriver(appPort, '/another')
+ await browser.elementByCss('#to-index').click()
+
+ await check(
+ () => browser.eval(() => document.documentElement.innerHTML),
+ /the end/
+ )
+
+ await browser.eval(() =>
+ document.querySelector('#to-another').scrollIntoView()
+ )
+ const scrollRestoration = await browser.eval(
+ () => window.history.scrollRestoration
+ )
+
+ expect(scrollRestoration).toBe('manual')
+
+ const scrollX = Math.floor(await browser.eval(() => window.scrollX))
+ const scrollY = Math.floor(await browser.eval(() => window.scrollY))
+
+ expect(scrollX).not.toBe(0)
+ expect(scrollY).not.toBe(0)
+
+ await browser.eval(() => window.history.back())
+
+ await check(
+ () => browser.eval(() => document.documentElement.innerHTML),
+ /hi from another/
+ )
+
+ await browser.eval(() => (window.didHydrate = false))
+ await browser.eval(() => window.history.forward())
+
+ await check(() => browser.eval(() => window.didHydrate), {
+ test(content) {
+ return content
+ },
+ })
+
+ const newScrollX = Math.floor(await browser.eval(() => window.scrollX))
+ const newScrollY = Math.floor(await browser.eval(() => window.scrollY))
+
+ console.log({
+ scrollX,
+ scrollY,
+ newScrollX,
+ newScrollY,
+ })
+
+ expect(scrollX).toBe(newScrollX)
+ expect(scrollY).toBe(newScrollY)
+ })
+}
+
+describe('Scroll Restoration Support', () => {
+ describe('dev mode', () => {
+ beforeAll(async () => {
+ appPort = await findPort()
+ app = await launchApp(appDir, appPort)
+ })
+ afterAll(() => killApp(app))
+
+ runTests()
+ })
+
+ describe('server mode', () => {
+ beforeAll(async () => {
+ await nextBuild(appDir)
+ appPort = await findPort()
+ app = await nextStart(appDir, appPort)
+ })
+ afterAll(() => killApp(app))
+
+ runTests()
+ })
+})
diff --git a/yarn.lock b/yarn.lock
index c81f338b6c31b..226b2d14e26a1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -15948,7 +15948,7 @@ webpack-sources@1.4.3, webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack-s
source-list-map "^2.0.0"
source-map "~0.6.1"
-webpack@4.43.0, webpack@^4.42.1:
+webpack@4.43.0:
version "4.43.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.43.0.tgz#c48547b11d563224c561dad1172c8aa0b8a678e6"
integrity sha512-GW1LjnPipFW2Y78OOab8NJlCflB7EFskMih2AHdvjbpKMeDJqEgSx24cXXXiPS65+WSwVyxtDsJH6jGX2czy+g==