diff --git a/.github/ISSUE_TEMPLATE/1.Bug_report.md b/.github/ISSUE_TEMPLATE/1.Bug_report.md deleted file mode 100644 index 9488cca282ec9cf..000000000000000 --- a/.github/ISSUE_TEMPLATE/1.Bug_report.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -name: Bug report -about: Create a bug report for the Next.js core / examples -labels: 'template: bug' ---- - - - -# Bug report - -## Describe the bug - -A clear and concise description of what the bug is. - -## To Reproduce - -Steps to reproduce the behavior, please provide code snippets or a repository: - -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -## Expected behavior - -A clear and concise description of what you expected to happen. - -## Screenshots - -If applicable, add screenshots to help explain your problem. - -## System information - -- OS: [e.g. macOS, Windows] -- Browser (if applies) [e.g. chrome, safari] -- Version of Next.js: [e.g. 10.0.1] -- Version of Node.js: [e.g. 12.0.0] -- Deployment: [e.g. next start, next export, Vercel, other platform] - -## Additional context - -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/1.bug_report.yml b/.github/ISSUE_TEMPLATE/1.bug_report.yml new file mode 100644 index 000000000000000..b9b94618e9a3486 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1.bug_report.yml @@ -0,0 +1,61 @@ +name: Bug Report +about: Create a bug report for the Next.js core +title: '' +labels: 'template: bug' +issue_body: true +inputs: + - type: description + attributes: + value: Thanks for taking the time to file a bug report! Please fill out this form as completely as possible. + - type: description + attributes: + value: If you leave out sections there is a high likelihood it will be moved to the GitHub Discussions "Help" section. + - type: input + attributes: + label: What version of Next.js are you using? + description: 'For example: 10.0.1' + required: true + - type: input + attributes: + label: What version of Node.js are you using? + description: 'For example: 12.0.0' + required: true + - type: input + attributes: + label: What browser are you using? + description: 'For example: Chrome, Safari' + required: true + - type: input + attributes: + label: What operating system are you using? + description: 'For example: macOS, Windows' + required: true + - type: input + attributes: + label: How are you deploying your application? + description: 'For example: next start, next export, Vercel, Other platform' + required: true + - type: textarea + attributes: + label: Describe the Bug + description: A clear and concise description of what the bug is. + required: true + - type: textarea + attributes: + label: Expected Behavior + description: A clear and concise description of what you expected to happen. + required: true + - type: textarea + attributes: + label: To Reproduce + description: Steps to reproduce the behavior, please provide a clear code snippets that always reproduces the issue or a GitHub repository. Screenshots can be provided in the issue body below. + required: true + - type: description + attributes: + value: Before posting the issue go through the steps you've written down to make sure the steps provided are detailed and clear. + - type: description + attributes: + value: Contributors should be able to follow the steps provided in order to reproduce the bug. + - type: description + attributes: + value: These steps are used to add integration tests to ensure the same issue does not happen again. Thanks in advance! diff --git a/.github/ISSUE_TEMPLATE/2.Feature_request.md b/.github/ISSUE_TEMPLATE/2.Feature_request.md deleted file mode 100644 index 67ac9a7fb2f1b1f..000000000000000 --- a/.github/ISSUE_TEMPLATE/2.Feature_request.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -name: Feature request -about: Create a feature request for the Next.js core -labels: 'template: story' ---- - - - -# Feature request - -## Is your feature request related to a problem? Please describe. - -A clear and concise description of what you want and what your use case is. - -## Describe the solution you'd like - -A clear and concise description of what you want to happen. - -## Describe alternatives you've considered - -A clear and concise description of any alternative solutions or features you've considered. - -## Additional context - -Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/2.example_bug_report.yml b/.github/ISSUE_TEMPLATE/2.example_bug_report.yml new file mode 100644 index 000000000000000..25da453dfd7a5a9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2.example_bug_report.yml @@ -0,0 +1,66 @@ +name: Example Bug Report +about: Create a bug report for the examples +title: '' +labels: 'type: example,template: bug' +issue_body: true +inputs: + - type: description + attributes: + value: Thanks for taking the time to file a examples bug report! Please fill out this form as completely as possible. + - type: description + attributes: + value: If you leave out sections there is a high likelihood it will be moved to the GitHub Discussions "Help" section. + - type: input + attributes: + label: What example does this report relate to? + description: 'For example: with-styled-components' + required: true + - type: input + attributes: + label: What version of Next.js are you using? + description: 'For example: 10.0.1' + required: true + - type: input + attributes: + label: What version of Node.js are you using? + description: 'For example: 12.0.0' + required: true + - type: input + attributes: + label: What browser are you using? + description: 'For example: Chrome, Safari' + required: true + - type: input + attributes: + label: What operating system are you using? + description: 'For example: macOS, Windows' + required: true + - type: input + attributes: + label: How are you deploying your application? + description: 'For example: next start, next export, Vercel, Other platform' + required: true + - type: textarea + attributes: + label: Describe the Bug + description: A clear and concise description of what the bug is. + required: true + - type: textarea + attributes: + label: Expected Behavior + description: A clear and concise description of what you expected to happen. + required: true + - type: textarea + attributes: + label: To Reproduce + description: Steps to reproduce the behavior, please provide a clear code snippets that always reproduces the issue or a GitHub repository. Screenshots can be provided in the issue body below. + required: true + - type: description + attributes: + value: Before posting the issue go through the steps you've written down to make sure the steps provided are detailed and clear. + - type: description + attributes: + value: Contributors should be able to follow the steps provided in order to reproduce the bug. + - type: description + attributes: + value: Thanks in advance! diff --git a/.github/ISSUE_TEMPLATE/3.feature_request.yml b/.github/ISSUE_TEMPLATE/3.feature_request.yml new file mode 100644 index 000000000000000..c4fbcccf7078fe8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/3.feature_request.yml @@ -0,0 +1,27 @@ +name: Feature Request +about: Create a feature request for the Next.js core +title: '' +labels: 'template: story' +issue_body: true +inputs: + - type: description + attributes: + value: Thanks for taking the time to file a feature request! Please fill out this form as completely as possible. + - type: description + attributes: + value: 'Feature requests will be converted to the GitHub Discussions "Ideas" section.' + - type: textarea + attributes: + label: Describe the feature you'd like to request + description: A clear and concise description of what you want and what your use case is. + required: true + - type: textarea + attributes: + label: Describe the solution you'd like + description: A clear and concise description of what you want to happen. + required: true + - type: textarea + attributes: + label: Describe alternatives you've considered + description: A clear and concise description of any alternative solutions or features you've considered. + required: true diff --git a/.github/workflows/build_test_deploy.yml b/.github/workflows/build_test_deploy.yml index 2877acecb5e50ad..dc5d8784f63bd80 100644 --- a/.github/workflows/build_test_deploy.yml +++ b/.github/workflows/build_test_deploy.yml @@ -115,20 +115,13 @@ jobs: runs-on: ubuntu-latest env: NODE_OPTIONS: '--unhandled-rejections=strict' + YARN_COMPRESSION_LEVEL: '0' steps: - uses: actions/checkout@v2 - run: yarn install --frozen-lockfile --check-files - - run: | - mkdir -p ./e2e-tests/next-pnp - cp -r ./examples/with-typescript/. ./e2e-tests/next-pnp - cd ./e2e-tests/next-pnp - touch yarn.lock - yarn set version berry - yarn config set pnpFallbackMode none - yarn link --all --private ../.. - yarn build + - run: bash ./test-pnp.sh testsPass: name: thank you, next @@ -153,7 +146,7 @@ jobs: - run: cat package.json | jq '.resolutions."react-dom" = "^17.0.1"' > package.json.tmp && mv package.json.tmp package.json - run: yarn install --check-files - run: yarn list webpack react react-dom - - run: xvfb-run node run-tests.js test/integration/link-ref/test/index.test.js test/integration/production/test/index.test.js test/integration/basic/test/index.test.js test/integration/async-modules/test/index.test.js test/integration/font-optimization/test/index.test.js test/acceptance/*.test.js + - run: xvfb-run node run-tests.js test/integration/{link-ref,production,basic,async-modules,font-optimization,ssr-ctx}/test/index.test.js test/acceptance/*.test.js testFirefox: name: Test Firefox (production) diff --git a/.gitignore b/.gitignore index bc9176f4c40b933..f043ec68d93fb44 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ test/**/next-env.d.ts # Editors **/.idea +**/.#* # examples examples/**/out diff --git a/.vscode/launch.json b/.vscode/launch.json index 512d385b8e4342a..e7cd5522232f3ec 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -22,7 +22,8 @@ "runtimeExecutable": "yarn", "runtimeArgs": ["run", "debug", "build", "test/integration/basic"], "skipFiles": ["/**"], - "port": 9229 + "port": 9229, + "outFiles": ["${workspaceFolder}/packages/next/dist/**/*"] }, { "name": "Launch app production", diff --git a/.vscode/settings.json b/.vscode/settings.json index e521849f2cdba27..9b9d414d6ec5345 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,6 @@ "javascriptreact", { "language": "typescript", "autoFix": true }, { "language": "typescriptreact", "autoFix": true } - ] + ], + "debug.javascript.unmapMissingSources": true } diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 11de5bce016a147..008c8f15e765071 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -6,10 +6,10 @@ trigger: include: - '*' exclude: - - bench/* - - docs/* - - errors/* - - examples/* + - bench + - docs + - errors + - examples # Do not run Azure on `canary`, `master`, or release tags. This unnecessarily # increases the backlog, and the change was already tested on the PR. branches: @@ -20,6 +20,17 @@ trigger: - master - refs/tags/* +pr: + # Do not run Azure CI for docs-only/example-only changes: + paths: + include: + - '*' + exclude: + - bench + - docs + - errors + - examples + variables: YARN_CACHE_FOLDER: $(Pipeline.Workspace)/.yarn NEXT_TELEMETRY_DISABLED: '1' diff --git a/docs/api-reference/next.config.js/build-target.md b/docs/api-reference/next.config.js/build-target.md deleted file mode 100644 index 3de94984c77b0ea..000000000000000 --- a/docs/api-reference/next.config.js/build-target.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -description: Learn more about the build targets used by Next.js, which decide the way your application is built and run. ---- - -# Build Target - -Next.js supports various build targets, each changing the way your application is built and run. We'll explain each of the targets below. - -## `server` target - -> This is the default target, however, we highly recommend the [`serverless` target](#serverless-target). The `serverless` target enforces [additional constraints](https://rauchg.com/2020/2019-in-review#serverless-upgrades-itself) to keep you in the [Pit of Success](https://blog.codinghorror.com/falling-into-the-pit-of-success/). - -This target is compatible with both `next start` and [custom server](/docs/advanced-features/custom-server.md) setups (it's mandatory for a custom server). - -Your application will be built and deployed as a monolith. This is the default target and no action is required on your part to opt-in. - -## `serverless` target - -> Deployments to [Vercel](https://vercel.com) will automatically enable this target. You should not opt-into it yourself. - -This target will output independent pages that don't require a monolithic server. - -It's only compatible with `next start` or Serverless deployment platforms (like [Vercel](https://vercel.com)) — you cannot use the custom server API. - -To opt-into this target, set the following configuration in your `next.config.js`: - -```js -module.exports = { - target: 'serverless', -} -``` - -## Related - -
- - Introduction to next.config.js: - Learn more about the configuration file used by Next.js. - -
- -
- - Deployment: - Compile and deploy your Next.js app to production. - -
diff --git a/docs/api-reference/next.config.js/compression.md b/docs/api-reference/next.config.js/compression.md index c20ebe798e1c7d1..6947040d2ec0133 100644 --- a/docs/api-reference/next.config.js/compression.md +++ b/docs/api-reference/next.config.js/compression.md @@ -4,7 +4,7 @@ description: Next.js provides gzip compression to compress rendered content and # Compression -Next.js provides [**gzip**](https://tools.ietf.org/html/rfc6713#section-3) compression to compress rendered content and static files. Compression only works with the [`server` target](/docs/api-reference/next.config.js/build-target.md#server-target). In general you will want to enable compression on a HTTP proxy like [nginx](https://www.nginx.com/), to offload load from the `Node.js` process. +Next.js provides [**gzip**](https://tools.ietf.org/html/rfc6713#section-3) compression to compress rendered content and static files. In general you will want to enable compression on a HTTP proxy like [nginx](https://www.nginx.com/), to offload load from the `Node.js` process. To disable **compression**, open `next.config.js` and disable the `compress` config: diff --git a/docs/api-reference/next.config.js/rewrites.md b/docs/api-reference/next.config.js/rewrites.md index 28c040178919dfd..a6f4efcfcf364a2 100644 --- a/docs/api-reference/next.config.js/rewrites.md +++ b/docs/api-reference/next.config.js/rewrites.md @@ -39,6 +39,56 @@ module.exports = { - `source` is the incoming request path pattern. - `destination` is the path you want to route to. +## Rewrite parameters + +When using parameters in a rewrite the parameters will be passed in the query by default when none of the parameters are used in the `destination`. + +```js +module.exports = { + async rewrites() { + return [ + { + source: '/old-about/:path*', + destination: '/about', // The :path parameter isn't used here so will be automatically passed in the query + }, + ] + }, +} +``` + +If a parameter is used in the destination none of the parameters will be automatically passed in the query. + +```js +module.exports = { + async rewrites() { + return [ + { + source: '/docs/:path*', + destination: '/:path*', // The :path parameter is used here so will not be automatically passed in the query + }, + ] + }, +} +``` + +You can still pass the parameters manually in the query if one is already used in the destination by specifying the query in the `destination`. + +```js +module.exports = { + async rewrites() { + return [ + { + source: '/:first/:second', + destination: '/:first?second=:second', + // Since the :first parameter is used in the destination the :second parameter + // will not automatically be added in the query although we can manually add it + // as shown above + }, + ] + }, +} +``` + ## Path Matching Path matches are allowed, for example `/blog/:slug` will match `/blog/hello-world` (no nested paths): @@ -154,9 +204,10 @@ module.exports = { destination: '/another', // automatically becomes /docs/another }, { - // does not add /docs since basePath: false is set + // does not add /docs to /without-basePath since basePath: false is set + // Note: this can not be used for internal rewrites e.g. `destination: '/another'` source: '/without-basePath', - destination: '/another', + destination: 'https://example.com', basePath: false, }, ] diff --git a/docs/api-reference/next.config.js/static-optimization-indicator.md b/docs/api-reference/next.config.js/static-optimization-indicator.md new file mode 100644 index 000000000000000..f8c512d388a4489 --- /dev/null +++ b/docs/api-reference/next.config.js/static-optimization-indicator.md @@ -0,0 +1,21 @@ +--- +description: Optimized pages include an indicator to let you know if it's being statically optimized. You can opt-out of it here. +--- + +# Static Optimization Indicator + +> **Note:** This indicator was removed in Next.js version 10.0.1. We recommend upgrading to the latest version of Next.js. + +When a page qualifies for [Automatic Static Optimization](/docs/advanced-features/automatic-static-optimization.md) we show an indicator to let you know. + +This is helpful since automatic static optimization can be very beneficial and knowing immediately in development if the page qualifies can be useful. + +In some cases this indicator might not be useful, like when working on electron applications. To remove it open `next.config.js` and disable the `autoPrerender` config in `devIndicators`: + +```js +module.exports = { + devIndicators: { + autoPrerender: false, + }, +} +``` diff --git a/docs/api-reference/next/image.md b/docs/api-reference/next/image.md index 0d6cd03a1413a7a..dcca53f4db4ed03 100644 --- a/docs/api-reference/next/image.md +++ b/docs/api-reference/next/image.md @@ -11,9 +11,21 @@ description: Enable Image Optimization with the built-in Image component. -> Before moving forward, we recommend you to read [Image Optimization](/docs/basic-features/image-optimization.md) first. +
+ Version History + +| Version | Changes | +| --------- | ------------------------ | +| `v10.0.1` | `layout` prop added. | +| `v10.0.0` | `next/image` introduced. | + +
+ +> Before moving forward, we recommend you to read +> [Image Optimization](/docs/basic-features/image-optimization.md) first. -Image Optimization can be enabled via the `Image` component exported by `next/image`. +Image Optimization can be enabled via the `` component exported by +`next/image`. ## Usage @@ -47,41 +59,49 @@ export default Home ## Required Props -The `Image` component requires the following properties. +The `` component requires the following properties. ### src The path or URL to the source image. This is required. -When using an external URL, you must add it to [domains](/docs/basic-features/image-optimization.md#domains) in `next.config.js`. +When using an external URL, you must add it to +[domains](/docs/basic-features/image-optimization.md#domains) in +`next.config.js`. ### width The width of the image, in pixels. Must be an integer without a unit. -Required unless [layout="fill"`](#layout). +Required unless [`layout="fill"`](#layout). ### height The height of the image, in pixels. Must be an integer without a unit. -Required unless [layout="fill"`](#layout). +Required unless [`layout="fill"`](#layout). ## Optional Props -The `Image` component optionally accepts the following properties. +The `` component optionally accepts the following properties. ### layout -The layout behavior of the image as the viewport changes size. Defaults to `intrinsic`. +The layout behavior of the image as the viewport changes size. Defaults to +`intrinsic`. -When `fixed`, the image dimensions will not change as the viewport changes (no responsiveness) similar to the native `img` element. +When `fixed`, the image dimensions will not change as the viewport changes (no +responsiveness) similar to the native `img` element. -When `intrinsic`, the image will scale the dimensions down for smaller viewports but maintain the original dimensions for larger viewports. +When `intrinsic`, the image will scale the dimensions down for smaller viewports +but maintain the original dimensions for larger viewports. -When `responsive`, the image will scale the dimensions down for smaller viewports and scale up for larger viewports. +When `responsive`, the image will scale the dimensions down for smaller +viewports and scale up for larger viewports. -When `fill`, the image will stretch both width and height to the dimensions of the parent element, usually paired with [object-fit](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit). +When `fill`, the image will stretch both width and height to the dimensions of +the parent element, usually paired with +[object-fit](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit). Try it out: @@ -95,23 +115,27 @@ Try it out: A string mapping media queries to device sizes. Defaults to `100vw`. -We recommend setting `sizes` when `layout="responsive"` and your image will not be the same width as the viewport. +We recommend setting `sizes` when using `layout="responsive"` or `layout="fill"` and your image will **not** be the same width as the viewport. [Learn more](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-sizes). ### quality -The quality of the optimized image, an integer between 1 and 100 where 100 is the best quality. Defaults to 75. +The quality of the optimized image, an integer between `1` and `100` where `100` +is the best quality. Defaults to `75`. - +Should only be used when the image is visible above the fold. Defaults to +`false`. ## Advanced Props -In some cases, you may need more advanced usage. The `Image` component optionally accepts the following advanced properties. +In some cases, you may need more advanced usage. The `` component +optionally accepts the following advanced properties. ### objectFit @@ -127,9 +151,16 @@ The image position when using `layout="fill"`. ### loading +> **Attention**: This property is only meant for advanced usage. Switching an +> image to load with `eager` will normally **hurt performance**. +> +> We recommend using the [`priority`](#priority) property instead, which +> properly loads the image eagerly for nearly all use cases. + The loading behavior of the image. Defaults to `lazy`. -When `lazy`, defer loading the image until it reaches a calculated distance from the viewport. +When `lazy`, defer loading the image until it reaches a calculated distance from +the viewport. When `eager`, load the image immediately. @@ -137,14 +168,18 @@ When `eager`, load the image immediately. ### unoptimized -When true, the source image will be served as-is instead of changing quality, size, or format. Defaults to false. +When true, the source image will be served as-is instead of changing quality, +size, or format. Defaults to `false`. ## Other Props -Other properties on the `Image` component will be passed to the underlying `img` element with the exception of the following: +Other properties on the `` component will be passed to the underlying +`img` element with the exception of the following: - `style`. Use `className` instead. -- `srcSet`. Use [Device Sizes](/docs/basic-features/image-optimization.md#device-sizes) instead. +- `srcSet`. Use + [Device Sizes](/docs/basic-features/image-optimization.md#device-sizes) + instead. - `decoding`. It is always `"async"`. ## Related diff --git a/docs/api-reference/next/link.md b/docs/api-reference/next/link.md index 22f06ee0799b47c..e7480261de73242 100644 --- a/docs/api-reference/next/link.md +++ b/docs/api-reference/next/link.md @@ -61,6 +61,7 @@ export default Home - [`replace`](#replace-the-url-instead-of-push) - Replace the current `history` state instead of adding a new url into the stack. Defaults to `false` - [`scroll`](#disable-scrolling-to-the-top-of-the-page) - Scroll to the top of the page after a navigation. Defaults to `true` - [`shallow`](/docs/routing/shallow-routing.md) - Update the path of the current page without rerunning [`getStaticProps`](/docs/basic-features/data-fetching.md#getstaticprops-static-generation), [`getServerSideProps`](/docs/basic-features/data-fetching.md#getserversideprops-server-side-rendering) or [`getInitialProps`](/docs/api-reference/data-fetching/getInitialProps.md). Defaults to `false` +- `locale` - The active locale is automatically prepended. `locale` allows for providing a different locale. When `false` `href` has to include the locale as the default behavior is disabled. ## If the route has dynamic segments diff --git a/docs/api-reference/next/router.md b/docs/api-reference/next/router.md index 65d13188845871e..56de35581fb5c3c 100644 --- a/docs/api-reference/next/router.md +++ b/docs/api-reference/next/router.md @@ -49,6 +49,7 @@ The following is the definition of the `router` object returned by both [`useRou - `locale`: `String` - The active locale (if enabled). - `locales`: `String[]` - All supported locales (if enabled). - `defaultLocale`: `String` - The current default locale (if enabled). +- `isReady`: `boolean` - Whether the router fields are updated client-side and ready for use. Should only be used inside of `useEffect` methods and not for conditionally rendering on the server. Additionally, the following methods are also included inside `router`: @@ -100,7 +101,7 @@ export default function Page() { } ``` -Redirecting the user to `pages/login.js`, useful for pages behind authentication: +Redirecting the user to `pages/login.js`, useful for pages behind [authentication](/docs/authentication): ```jsx import { useEffect } from 'react' diff --git a/docs/authentication.md b/docs/authentication.md new file mode 100644 index 000000000000000..591cddb311d9459 --- /dev/null +++ b/docs/authentication.md @@ -0,0 +1,208 @@ +--- +description: Learn about authentication patterns in Next.js apps and explore a few examples. +--- + +# Authentication + +Authentication verifies who a user is, while authorization controls what a user can access. Next.js supports multiple authentication patterns, each designed for different use cases. This page will go through each case so that you can choose based on your constraints. + +## Authentication Patterns + +The first step to identifying which authentication pattern you need is understanding the [data-fetching strategy](/docs/basic-features/data-fetching.md) you want. We can then determine which authentication providers support this strategy. There are two main patterns: + +- Use [static generation](/docs/basic-features/pages.md#static-generation-recommended) to server-render a loading state, followed by fetching user data client-side. +- Fetch user data [server-side](/docs/basic-features/pages.md#server-side-rendering) to eliminate a flash of unauthenticated content. + +### Authenticating Statically Generated Pages + +Next.js automatically determines that a page is static if there are no blocking data requirements. This means the absence of [`getServerSideProps`](/docs/basic-features/data-fetching.md#getserversideprops-server-side-rendering) and `getInitialProps` in the page. Instead, your page can render a loading state from the server, followed by fetching the user client-side. + +One advantage of this pattern is it allows pages to be served from a global CDN and preloaded using [`next/link`](/docs/api-reference/next/link.md). In practice, this results in a faster TTI ([Time to Interactive](https://web.dev/interactive/)). + +Let's look at an example for a profile page. This will initially render a loading skeleton. Once the request for a user has finished, it will show the user's name: + +```jsx +// pages/profile.js + +import useUser from '../lib/useUser' +import Layout from '../components/Layout' + +const Profile = () => { + // Fetch the user client-side + const { user } = useUser({ redirectTo: '/login' }) + + // Server-render loading state + if (!user || user.isLoggedIn === false) { + return Loading... + } + + // Once the user request finishes, show the user + return ( + +

Your Profile

+
{JSON.stringify(user, null, 2)}
+
+ ) +} + +export default Profile +``` + +You can view this example in action [here](https://next-with-iron-session.vercel.app/). Check out the [`with-iron-session`](https://github.com/vercel/next.js/tree/canary/examples/with-iron-session) example to see how it works. + +### Authenticating Server-Rendered Pages + +If you export an `async` function called [`getServerSideProps`](/docs/basic-features/data-fetching.md#getserversideprops-server-side-rendering) from a page, Next.js will pre-render this page on each request using the data returned by `getServerSideProps`. + +```jsx +export async function getServerSideProps(context) { + return { + props: {}, // Will be passed to the page component as props + } +} +``` + +Let's transform the profile example to use [server-side rendering](/docs/basic-features/pages#server-side-rendering). If there's a session, return `user` as a prop to the `Profile` component in the page. Notice there is not a loading skeleton in [this example](https://next-with-iron-session.vercel.app/). + +```jsx +// pages/profile.js + +import withSession from '../lib/session' +import useUser from '../lib/useUser' +import Layout from '../components/Layout' + +export const getServerSideProps = withSession(async function ({ req, res }) { + // Get the user's session based on the request + const user = req.session.get('user') + + if (!user) { + return { + redirect: { + destination: '/login', + permanent: false, + }, + } + } + + return { + props: { user }, + } +}) + +const Profile = ({ user }) => { + // Show the user. No loading state is required + return ( + +

Your Profile

+
{JSON.stringify(user, null, 2)}
+
+ ) +} + +export default Profile +``` + +An advantage of this pattern is preventing a flash of unauthenticated content before redirecting. It's important to note fetching user data in `getServerSideProps` will block rendering until the request to your authentication provider resolves. To prevent creating a bottleneck and decreasing your TTFB ([Time to First Byte](https://web.dev/time-to-first-byte/)), you should ensure your authentication lookup is fast. Otherwise, consider [static generation](#authenticating-statically-generated-pages). + +## Authentication Providers + +Now that we've discussed authentication patterns, let's look at specific providers and explore how they're used with Next.js. + +### Bring Your Own Database + +
+ Examples + +
+ +If you have an existing database with user data, you'll likely want to utilize an open-source solution that's provider agnostic. + +- If you need email/password log-in, use [`next-iron-session`](https://github.com/vercel/next.js/tree/canary/examples/with-iron-session). +- If you need to persist session data on the server, use [`next-auth`](https://github.com/vercel/next.js/tree/canary/examples/with-next-auth). +- If you need to support social login (Google, Facebook, etc.), use [`next-auth`](https://github.com/vercel/next.js/tree/canary/examples/with-next-auth). +- If you want to use [JWTs](https://jwt.io/), use [`next-auth`](https://github.com/vercel/next.js/tree/canary/examples/with-next-auth). + +Both of these libraries support either authentication pattern. If you're interested in [Passport](http://www.passportjs.org/), we also have examples for it using secure and encrypted cookies: + +- [with-passport](https://github.com/vercel/next.js/tree/canary/examples/with-passport) +- [with-passport-and-next-connect](https://github.com/vercel/next.js/tree/canary/examples/with-passport-and-next-connect) + +### Firebase + +
+ Examples + +
+ +When using Firebase Authentication, we recommend using the static generation pattern. + +It is possible to use the Firebase Client SDK to generate an ID token and forward it directly to Firebase's REST API on the server to log-in. However, requests to Firebase might take some time to resolve, depending on your user's location. + +You can either use [FirebaseUI](https://github.com/firebase/firebaseui-web-react) for a drop-in UI, or create your own with a [custom React hook](https://usehooks.com/useAuth/). + +### Magic (Passwordless) + +
+ Examples + +
+ +[Magic](https://magic.link/), which uses [passwordless login](https://magic.link/), supports the static generation pattern. Similar to Firebase, a [unique identifier](https://w3c-ccg.github.io/did-primer/) has to be created on the client-side and then forwarded as a header to log-in. Then, Magic's Node SDK can be used to exchange the indentifier for a user's information. + +### Auth0 + +
+ Examples + +
+ +[Auth0](https://auth0.com/) can support both authentication patterns. You can also utilize [API routes](/docs/api-routes/introduction.md) for logging in/out and retrieving user information. After logging in using the [Auth0 SDK](https://github.com/auth0/nextjs-auth0), you can utilize static generation or `getServerSideProps` for server-side rendering. + +### Supabase + +
+ Examples + +
+ +[Supabase](https://supabase.io/) is an open source Firebase alternative that supports many of its features, including authentication. It allows for row level security using JWT tokens and supports third party logins. Either authentication pattern is supported. + +### Userbase + +
+ Examples + +
+ +[Userbase](https://userbase.com/) supports the static generation pattern for authentication. It's open source and allows for a high level of security with end-to-end encryption. You can learn more about it in their [official site](https://userbase.com/). + +## Related + +For more information on what to do next, we recommend the following sections: + +
+ + Pages: + Learn more about pages and the different pre-rendering methods in Next.js. + +
+ +
+ + Data Fetching: + Learn more about data fetching in Next.js. + +
diff --git a/docs/basic-features/data-fetching.md b/docs/basic-features/data-fetching.md index 0a7c05aa96ce2f9..1371dd25e4cc201 100644 --- a/docs/basic-features/data-fetching.md +++ b/docs/basic-features/data-fetching.md @@ -39,6 +39,17 @@ In addition, we’ll talk briefly about how to fetch data on the client side. ## `getStaticProps` (Static Generation) +
+ Version History + +| Version | Changes | +| --------- | ----------------------------------------------------------------------------------------------------------------- | +| `v10.0.0` | `locale`, `locales`, `defaultLocale`, and `notFound` options added. | +| `v9.5.0` | Stable [Incremental Static Regeneration](https://nextjs.org/blog/next-9-5#stable-incremental-static-regeneration) | +| `v9.3.0` | `getStaticProps` introduced. | + +
+ If you export an `async` function called `getStaticProps` from a page, Next.js will pre-render this page at build time using the props returned by `getStaticProps`. ```jsx @@ -364,6 +375,16 @@ This use case is supported by Next.js by the feature called **Preview Mode**. Le ## `getStaticPaths` (Static Generation) +
+ Version History + +| Version | Changes | +| -------- | ----------------------------------------------------------------------------------------------------------------- | +| `v9.5.0` | Stable [Incremental Static Regeneration](https://nextjs.org/blog/next-9-5#stable-incremental-static-regeneration) | +| `v9.3.0` | `getStaticPaths` introduced. | + +
+ If a page has dynamic routes ([documentation](/docs/routing/dynamic-routes.md)) and uses `getStaticProps` it needs to define a list of paths that have to be rendered to HTML at build time. If you export an `async` function called `getStaticPaths` from a page that uses dynamic routes, Next.js will statically pre-render all the paths specified by `getStaticPaths`. @@ -453,7 +474,7 @@ export default Post
Examples
@@ -587,6 +608,16 @@ In development (`next dev`), `getStaticPaths` will be called on every request. ## `getServerSideProps` (Server-side Rendering) +
+ Version History + +| Version | Changes | +| --------- | ------------------------------------------------------------------- | +| `v10.0.0` | `locale`, `locales`, `defaultLocale`, and `notFound` options added. | +| `v9.3.0` | `getServerSideProps` introduced. | + +
+ If you export an `async` function called `getServerSideProps` from a page, Next.js will pre-render this page on each request using the data returned by `getServerSideProps`. ```js @@ -602,7 +633,7 @@ The `context` parameter is an object containing the following keys: - `params`: If this page uses a dynamic route, `params` contains the route parameters. If the page name is `[id].js` , then `params` will look like `{ id: ... }`. To learn more, take a look at the [Dynamic Routing documentation](/docs/routing/dynamic-routes.md). - `req`: [The HTTP IncomingMessage object](https://nodejs.org/api/http.html#http_class_http_incomingmessage). - `res`: [The HTTP response object](https://nodejs.org/api/http.html#http_class_http_serverresponse). -- `query`: The query string. +- `query`: An object representing the query string. - `preview`: `preview` is `true` if the page is in the preview mode and `false` otherwise. See the [Preview Mode documentation](/docs/advanced-features/preview-mode.md). - `previewData`: The preview data set by `setPreviewData`. See the [Preview Mode documentation](/docs/advanced-features/preview-mode.md). - `resolvedUrl`: A normalized version of the request URL that strips the `_next/data` prefix for client transitions and includes original query values. diff --git a/docs/basic-features/environment-variables.md b/docs/basic-features/environment-variables.md index 68d1629a666c7c8..9a155be3f32dfa2 100644 --- a/docs/basic-features/environment-variables.md +++ b/docs/basic-features/environment-variables.md @@ -109,16 +109,18 @@ Next.js allows you to set defaults in `.env` (all environments), `.env.developme ## Environment Variables on Vercel -When deploying on [Vercel](https://vercel.com) you can configure secrets in the [Environment Variables](https://vercel.com/docs/v2/build-step#environment-variables) section of the project in the Vercel dashboard. +When deploying your Next.js application to [Vercel](https://vercel.com), Environment Variables can be configured [in the Project Settings](https://vercel.com/docs/environment-variables). -You can still use `.env`, `.env.development` and `.env.production` to add defaults. +All types of Environment Variables should be configured there. Even Environment Variables used in Development – which can be [downloaded onto your local device](https://vercel.com/docs/environment-variables#development-environment-variables) afterwards. -If you've configured [Development Environment Variables](https://vercel.com/docs/v2/build-step#development-environment-variables) you can pull them into a `.env.local` for usage on your local machine using the following command: +If you've configured [Development Environment Variables](https://vercel.com/docs/environment-variables#development-environment-variables) you can pull them into a `.env.local` for usage on your local machine using the following command: ```bash vercel env pull .env.local ``` +When using the Vercel CLI to deploy make sure you add a [`.vercelignore`](https://vercel.com/guides/prevent-uploading-sourcepaths-with-vercelignore?query=vercelignore#allowlist) that includes files that should not be uploaded, generally these are the same files included in `.gitignore`. + ## Test Environment Variables Apart from `development` and `production` environments, there is a 3rd option available: `test`. In the same way you can set defaults for development or production environments, you can do the same with `.env.test` file for testing environment (though this one is not so common as the previous two). diff --git a/docs/basic-features/image-optimization.md b/docs/basic-features/image-optimization.md index 21094e9b547afef..e3734f53db5cd6c 100644 --- a/docs/basic-features/image-optimization.md +++ b/docs/basic-features/image-optimization.md @@ -59,7 +59,7 @@ In addition to [using properties](/docs/api-reference/next/image.md) available t ### Domains To enable Image Optimization for images hosted on an external website, use an absolute url for the Image `src` and specify which -`domains` are allowed to be optimized. This is needed to ensure that external urls can't be abused. +`domains` are allowed to be optimized. This is needed to ensure that external urls can't be abused. When `loader` is set to an external image service, this option is ignored. ```js module.exports = { diff --git a/docs/manifest.json b/docs/manifest.json index bd32807629d8296..6f248588ba11fb0 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -93,6 +93,10 @@ "title": "Deployment", "path": "/docs/deployment.md" }, + { + "title": "Authentication", + "path": "/docs/authentication.md" + }, { "title": "Advanced Features", "routes": [ @@ -288,10 +292,6 @@ "title": "CDN Support with Asset Prefix", "path": "/docs/api-reference/next.config.js/cdn-support-with-asset-prefix.md" }, - { - "title": "Build Target", - "path": "/docs/api-reference/next.config.js/build-target.md" - }, { "title": "Custom Webpack Config", "path": "/docs/api-reference/next.config.js/custom-webpack-config.md" diff --git a/errors/incompatible-href-as.md b/errors/incompatible-href-as.md index b6c4e68e1d88ddc..f6677e90b6e781a 100644 --- a/errors/incompatible-href-as.md +++ b/errors/incompatible-href-as.md @@ -11,13 +11,15 @@ Note: this error will only show when the `next/link` component is clicked not wh ```jsx import Link from 'next/link' -export default () => ( - <> - - Invalid link - - -) +export default function Page(props) { + return ( + <> + + Invalid link + + + ) +} ``` **Compatible `href` and `as`** @@ -25,13 +27,15 @@ export default () => ( ```jsx import Link from 'next/link' -export default () => ( - <> - - Valid link - - -) +export default function Page(props) { + return ( + <> + + Valid link + + + ) +} ``` #### Possible Ways to Fix It diff --git a/errors/invalid-relative-url-external-as.md b/errors/invalid-relative-url-external-as.md new file mode 100644 index 000000000000000..848b56d422e46e6 --- /dev/null +++ b/errors/invalid-relative-url-external-as.md @@ -0,0 +1,47 @@ +# Invalid relative `href` and external `as` values + +#### Why This Error Occurred + +Somewhere you are utilizing the `next/link` component, `Router#push`, or `Router#replace` with a relative route in your `href` that has an external `as` value. The `as` value must be relative also or only `href` should be used with an external URL. + +Note: this error will only show when the `next/link` component is clicked not when only rendered. + +**Incompatible `href` and `as`** + +```jsx +import Link from 'next/link' + +export default function Page(props) { + return ( + <> + + Invalid link + + + ) +} +``` + +**Compatible `href` and `as`** + +```jsx +import Link from 'next/link' + +export default function Page(props) { + return ( + <> + + Invalid link + + + ) +} +``` + +#### Possible Ways to Fix It + +Look for any usage of the `next/link` component, `Router#push`, or `Router#replace` and make sure that the provided `href` and `as` values are compatible + +### Useful Links + +- [Routing section in Documentation](https://nextjs.org/docs/routing/introduction) diff --git a/examples/api-routes-apollo-server-and-client-auth/README.md b/examples/api-routes-apollo-server-and-client-auth/README.md index c0e2ff213bc2743..509135bbfc3c9d5 100644 --- a/examples/api-routes-apollo-server-and-client-auth/README.md +++ b/examples/api-routes-apollo-server-and-client-auth/README.md @@ -4,6 +4,12 @@ In this simple example, we integrate Apollo seamlessly with [Next.js data fetching methods](https://nextjs.org/docs/basic-features/data-fetching) to fetch queries in the server and hydrate them in the browser. +## 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/api-routes-apollo-server-and-client-auth) + ## How to use Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) or [npx](https://github.com/zkat/npx#readme) to bootstrap the example: diff --git a/examples/api-routes-apollo-server-and-client-auth/lib/auth.js b/examples/api-routes-apollo-server-and-client-auth/lib/auth.js index 6dc386d15e6bc64..fe830e95d5e4535 100644 --- a/examples/api-routes-apollo-server-and-client-auth/lib/auth.js +++ b/examples/api-routes-apollo-server-and-client-auth/lib/auth.js @@ -21,7 +21,9 @@ export async function getLoginSession(req) { const expiresAt = session.createdAt + session.maxAge * 1000 // Validate the expiration date of the session - if (Date.now() < expiresAt) { - return session + if (Date.now() > expiresAt) { + throw new Error('Session expired') } + + return session } diff --git a/examples/api-routes-rate-limit/.gitignore b/examples/api-routes-rate-limit/.gitignore new file mode 100644 index 000000000000000..1437c53f70bc211 --- /dev/null +++ b/examples/api-routes-rate-limit/.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/api-routes-rate-limit/README.md b/examples/api-routes-rate-limit/README.md new file mode 100644 index 000000000000000..381b6e93e3691ae --- /dev/null +++ b/examples/api-routes-rate-limit/README.md @@ -0,0 +1,35 @@ +# API Routes Rate Limiting Example + +This example uses `lru-cache` to implement a simple rate limiter for API routes ([Serverless Functions](https://vercel.com/docs/serverless-functions/introduction)). + +**Demo: https://nextjs-rate-limit.vercel.app/** + +```bash +curl http://localhost:3000/api/user -I +HTTP/1.1 200 OK +X-RateLimit-Limit: 10 +X-RateLimit-Remaining: 9 + +curl http://localhost:3000/api/user -I +HTTP/1.1 429 Too Many Requests +X-RateLimit-Limit: 10 +X-RateLimit-Remaining: 0 +``` + +## 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/api-routes-rate-limit) + +## 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 api-routes api-routes-rate-limit +# or +yarn create next-app --example api-routes api-routes-rate-limit +``` + +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/api-routes-rate-limit/package.json b/examples/api-routes-rate-limit/package.json new file mode 100644 index 000000000000000..b43827f60cb63ce --- /dev/null +++ b/examples/api-routes-rate-limit/package.json @@ -0,0 +1,17 @@ +{ + "name": "nextjs-rate-limit", + "version": "0.0.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "lru-cache": "^6.0.0", + "next": "10.0.3", + "react": "17.0.1", + "react-dom": "17.0.1", + "uuid": "^8.3.1" + } +} diff --git a/examples/api-routes-rate-limit/pages/api/user.js b/examples/api-routes-rate-limit/pages/api/user.js new file mode 100644 index 000000000000000..2380dad3577f3e9 --- /dev/null +++ b/examples/api-routes-rate-limit/pages/api/user.js @@ -0,0 +1,16 @@ +import * as uuid from 'uuid' +import rateLimit from '../../utils/rate-limit' + +const limiter = rateLimit({ + interval: 60 * 1000, // 60 seconds + uniqueTokenPerInterval: 500, // Max 500 users per second +}) + +export default async function handler(req, res) { + try { + await limiter.check(res, 10, 'CACHE_TOKEN') // 10 requests per minute + res.status(200).json({ id: uuid.v4() }) + } catch { + res.status(429).json({ error: 'Rate limit exceeded' }) + } +} diff --git a/examples/api-routes-rate-limit/pages/index.js b/examples/api-routes-rate-limit/pages/index.js new file mode 100644 index 000000000000000..3c5cfc8564998af --- /dev/null +++ b/examples/api-routes-rate-limit/pages/index.js @@ -0,0 +1,52 @@ +import { useState } from 'react' +import styles from '../styles.module.css' + +export default function Index() { + const [response, setResponse] = useState() + + const makeRequest = async () => { + const res = await fetch('/api/user') + + setResponse({ + status: res.status, + body: await res.json(), + limit: res.headers.get('X-RateLimit-Limit'), + remaining: res.headers.get('X-RateLimit-Remaining'), + }) + } + + return ( +
+

Next.js API Routes Rate Limiting

+

+ This example uses lru-cache{' '} + to implement a simple rate limiter for API routes (Serverless + Functions). +

+ + +
+ Status Code: + {response?.status || 'None'} +
+
+ Request Limit: + {response?.limit || 'None'} +
+
+ Remaining Requests: + {response?.remaining || 'None'} +
+
+ Body: + {JSON.stringify(response?.body) || 'None'} +
+
+
+ View Source + {' | '} + Deploy You Own ▲ +
+
+ ) +} diff --git a/examples/api-routes-rate-limit/styles.module.css b/examples/api-routes-rate-limit/styles.module.css new file mode 100644 index 000000000000000..f2cd0c79e7de716 --- /dev/null +++ b/examples/api-routes-rate-limit/styles.module.css @@ -0,0 +1,67 @@ +.container { + padding: 4rem 1rem; + max-width: 50rem; + margin: 0 auto; + font-family: -apple-system, BlinkMacSystemFont, sans-serif; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.container h1 { + font-weight: 800; +} + +.container p { + margin: 1.5rem 0; + line-height: 1.5; +} + +.container button { + border-radius: 4px; + height: 40px; + padding: 0.5rem 1rem; + font-size: 16px; + border: none; + transition: 0.25s all ease; + background-color: #eaeaea; + font-size: 14px; + font-weight: 600; + color: #111; +} + +.container button:hover { + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.12); +} + +.container a { + text-decoration: none; + color: #0070f3; +} + +.inlineCode { + color: #be00ff; + font-size: 16px; + white-space: pre-wrap; +} + +.inlineCode::before, +.inlineCode::after { + content: '`'; +} + +.code { + margin-top: 16px; + display: block; + background: #222222; + border-radius: 8px; + padding: 16px; + color: white; + font-size: 16px; + line-height: 1.4; +} + +.links { + margin-top: 16px; + color: #9c9c9c; +} diff --git a/examples/api-routes-rate-limit/utils/rate-limit.js b/examples/api-routes-rate-limit/utils/rate-limit.js new file mode 100644 index 000000000000000..6727949ada67970 --- /dev/null +++ b/examples/api-routes-rate-limit/utils/rate-limit.js @@ -0,0 +1,31 @@ +const LRU = require('lru-cache') + +const rateLimit = (options) => { + const tokenCache = new LRU({ + max: parseInt(options.uniqueTokenPerInterval || 500, 10), + maxAge: parseInt(options.interval || 60000, 10), + }) + + return { + check: (res, limit, token) => + new Promise((resolve, reject) => { + const tokenCount = tokenCache.get(token) || [0] + if (tokenCount[0] === 0) { + tokenCache.set(token, tokenCount) + } + tokenCount[0] += 1 + + const currentUsage = tokenCount[0] + const isRateLimited = currentUsage >= parseInt(limit, 10) + res.setHeader('X-RateLimit-Limit', limit) + res.setHeader( + 'X-RateLimit-Remaining', + isRateLimited ? 0 : limit - currentUsage + ) + + return isRateLimited ? reject() : resolve() + }), + } +} + +export default rateLimit diff --git a/examples/blog-starter-typescript/README.md b/examples/blog-starter-typescript/README.md index e8db3fc2498ce68..671760cb4b37984 100644 --- a/examples/blog-starter-typescript/README.md +++ b/examples/blog-starter-typescript/README.md @@ -30,4 +30,6 @@ Deploy it to the cloud with [Vercel](https://vercel.com/import?filter=next.js&ut # Notes -This blog-starter-typescript uses [Tailwind CSS](https://tailwindcss.com). To control the generated stylesheet's filesize, this example uses Tailwind CSS' v1.4 [`purge` option](https://tailwindcss.com/docs/controlling-file-size/#removing-unused-css) to remove unused CSS. +This blog-starter-typescript uses [Tailwind CSS](https://tailwindcss.com). To control the generated stylesheet's filesize, this example uses Tailwind CSS' v2.0 [`purge` option](https://tailwindcss.com/docs/controlling-file-size/#removing-unused-css) to remove unused CSS. + +[Tailwind CSS v2.0 no longer supports Node.js 8 or 10](https://tailwindcss.com/docs/upgrading-to-v2#upgrade-to-node-js-12-13-or-higher). To build your CSS you'll need to ensure you are running Node.js 12.13.0 or higher in both your local and CI environments. diff --git a/examples/blog-starter-typescript/components/hero-post.tsx b/examples/blog-starter-typescript/components/hero-post.tsx index 5bd84f9bbe0d390..cd0f121c01faa0e 100644 --- a/examples/blog-starter-typescript/components/hero-post.tsx +++ b/examples/blog-starter-typescript/components/hero-post.tsx @@ -26,7 +26,7 @@ const HeroPost = ({
-
+

diff --git a/examples/blog-starter-typescript/components/more-stories.tsx b/examples/blog-starter-typescript/components/more-stories.tsx index 205c548f665ab07..1c2c038266e2377 100644 --- a/examples/blog-starter-typescript/components/more-stories.tsx +++ b/examples/blog-starter-typescript/components/more-stories.tsx @@ -11,7 +11,7 @@ const MoreStories = ({ posts }: Props) => {

More Stories

-
+
{posts.map((post) => ( ) return ( diff --git a/examples/blog-starter/components/hero-post.js b/examples/blog-starter/components/hero-post.js index cfedfb874fe41ce..ddb182a8b716382 100644 --- a/examples/blog-starter/components/hero-post.js +++ b/examples/blog-starter/components/hero-post.js @@ -14,9 +14,15 @@ export default function HeroPost({ return (
- +
-
+

diff --git a/examples/blog-starter/components/more-stories.js b/examples/blog-starter/components/more-stories.js index dcdd9b4e6ae7b9e..57fdbb6c4659c61 100644 --- a/examples/blog-starter/components/more-stories.js +++ b/examples/blog-starter/components/more-stories.js @@ -6,7 +6,7 @@ export default function MoreStories({ posts }) {

More Stories

-
+
{posts.map((post) => (
- +
diff --git a/examples/blog-starter/components/post-preview.js b/examples/blog-starter/components/post-preview.js index fd067f4f868fe2d..3e3009fa2721fa3 100644 --- a/examples/blog-starter/components/post-preview.js +++ b/examples/blog-starter/components/post-preview.js @@ -14,7 +14,13 @@ export default function PostPreview({ return (
- +

diff --git a/examples/blog-starter/package.json b/examples/blog-starter/package.json index b720e7a34a852b1..134d656d9364f00 100644 --- a/examples/blog-starter/package.json +++ b/examples/blog-starter/package.json @@ -8,17 +8,19 @@ }, "dependencies": { "classnames": "2.2.6", - "date-fns": "2.10.0", + "date-fns": "2.16.1", "gray-matter": "4.0.2", "next": "latest", - "react": "^16.13.0", - "react-dom": "^16.13.0", - "remark": "11.0.2", - "remark-html": "10.0.0" + "react": "^17.0.1", + "react-dom": "^17.0.1", + "remark": "13.0.0", + "remark-html": "13.0.1" }, "devDependencies": { + "autoprefixer": "10.0.4", + "postcss": "8.1.10", "postcss-preset-env": "^6.7.0", - "tailwindcss": "^1.4.0" + "tailwindcss": "2.0.1" }, "license": "MIT" } diff --git a/examples/blog-starter/tailwind.config.js b/examples/blog-starter/tailwind.config.js index dc81b5174ced57c..e32267d853ffea7 100644 --- a/examples/blog-starter/tailwind.config.js +++ b/examples/blog-starter/tailwind.config.js @@ -25,8 +25,8 @@ module.exports = { '8xl': '6.25rem', }, boxShadow: { - small: '0 5px 10px rgba(0, 0, 0, 0.12)', - medium: '0 8px 30px rgba(0, 0, 0, 0.12)', + sm: '0 5px 10px rgba(0, 0, 0, 0.12)', + md: '0 8px 30px rgba(0, 0, 0, 0.12)', }, }, }, diff --git a/examples/with-ant-design/.babelrc b/examples/with-ant-design/.babelrc deleted file mode 100644 index c52d7ff7754428b..000000000000000 --- a/examples/with-ant-design/.babelrc +++ /dev/null @@ -1,22 +0,0 @@ -{ - "presets": ["next/babel"], - "plugins": [ - [ - "import", - { - "libraryName": "antd", - "libraryDirectory": "lib", - "style": "index.css" - } - ], - [ - "import", - { - "libraryName": "@ant-design/icons", - "libraryDirectory": "lib/icons", - "camel2DashComponentName": false - }, - "@ant-design/icons" - ] - ] -} diff --git a/examples/with-ant-design/package.json b/examples/with-ant-design/package.json index ac99c2da75312b1..cae3ecc5bb5114b 100644 --- a/examples/with-ant-design/package.json +++ b/examples/with-ant-design/package.json @@ -10,7 +10,6 @@ "@ant-design/icons": "4.2.1", "@next/bundle-analyzer": "^9.1.4", "antd": "4.3.0", - "babel-plugin-import": "1.13.0", "cross-env": "^7.0.2", "dayjs": "1.8.28", "esm": "^3.2.25", diff --git a/examples/with-firebase-authentication/README.md b/examples/with-firebase-authentication/README.md index b5a1679a1944aec..a121492805aca3b 100644 --- a/examples/with-firebase-authentication/README.md +++ b/examples/with-firebase-authentication/README.md @@ -18,8 +18,9 @@ Set up Firebase: - Create a project at the [Firebase console](https://console.firebase.google.com/). - Copy the contents of `.env.local.example` into a new file called `.env.local` -- Get your account credentials from the Firebase console at _Project settings > Service accounts_, where you can click on _Generate new private key_ and download the credentials as a json file. It will contain keys such as `project_id`, `client_email` and `client_id`. Set them as environment variables in the `.env.local` file at the root of this project. -- Get your authentication credentials from the Firebase console under _Project settings > General> Your apps_ Add a new web app if you don't already have one. Under _Firebase SDK snippet_ choose _Config_ to get the configuration as JSON. It will include keys like `apiKey`, `authDomain` and `databaseUrl`. Set the appropriate environment variables in the `.env.local` file at the root of this project. +- Get your account credentials from the Firebase console at _Project settings > Service accounts_, where you can click on _Generate new private key_ and download the credentials as a json file. It will contain keys such as `project_id`, `client_email` and `private_key`. Set them as environment variables in the `.env.local` file at the root of this project. +- Get your authentication credentials from the Firebase console under _Project settings > General> Your apps_ Add a new web app if you don't already have one. Under _Firebase SDK snippet_ choose _Config_ to get the configuration as JSON. It will include keys like `apiKey` and `authDomain`. Set the appropriate environment variables in the `.env.local` file at the root of this project. +- Go to **Develop**, click on **Realtime Database** and create a database if you don't already have one. Under _data_ get `databaseUrl`(e.g. `https://[dbname].firebaseio.com/`). Set the appropriate environment variables in the `.env.local` file at the root of this project. - Go to **Develop**, click on **Authentication** and in the **Sign-in method** tab enable authentication for the app. Install it and run: diff --git a/examples/with-firebase/firebase/clientApp.js b/examples/with-firebase/firebase/clientApp.js index ed52fa5f790149c..68c829cfd9741b1 100644 --- a/examples/with-firebase/firebase/clientApp.js +++ b/examples/with-firebase/firebase/clientApp.js @@ -15,13 +15,15 @@ const clientCredentials = { appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID, } -// Check that `window` is in scope for the analytics module! -if (typeof window !== 'undefined' && !firebase.apps.length) { +if (!firebase.apps.length) { firebase.initializeApp(clientCredentials) - // To enable analytics. https://firebase.google.com/docs/analytics/get-started - if ('measurementId' in clientCredentials) { - firebase.analytics() - firebase.performance() + // Check that `window` is in scope for the analytics module! + if (typeof window !== 'undefined') { + // Enable analytics. https://firebase.google.com/docs/analytics/get-started + if ('measurementId' in clientCredentials) { + firebase.analytics() + firebase.performance() + } } } diff --git a/examples/with-graphql-faunadb/README.md b/examples/with-graphql-faunadb/README.md index ffe8b9774f63bd9..4c3c3856b928d03 100644 --- a/examples/with-graphql-faunadb/README.md +++ b/examples/with-graphql-faunadb/README.md @@ -6,7 +6,7 @@ This simple Guestbook SPA example shows you how to use [FaunaDB's GraphQL endpoi Deploy the example using [Vercel](https://vercel.com): -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fhello-world&env=NEXT_PUBLIC_FAUNADB_SECRET,NEXT_PUBLIC_FAUNADB_GRAPHQL_ENDPOINT&envDescription=Client%20secret%20and%20GraphQL%20endpoint%20needed%20for%20communicating%20with%20the%20live%20Fauna%20database&project-name=my-nextjs-guestbook&repository-name=my-nextjs-guestbook&demo-title=Next.js%20Fauna%20Guestbook%20App&demo-description=A%20simple%20guestbook%20application%20built%20with%20Next.js%20and%20Fauna&demo-url=https%3A%2F%2Fnextjs-guestbook.vercel.app%2F) +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fwith-graphql-faunadb&env=NEXT_PUBLIC_FAUNADB_SECRET,NEXT_PUBLIC_FAUNADB_GRAPHQL_ENDPOINT&envDescription=Client%20secret%20and%20GraphQL%20endpoint%20needed%20for%20communicating%20with%20the%20live%20Fauna%20database&project-name=my-nextjs-guestbook&repository-name=my-nextjs-guestbook&demo-title=Next.js%20Fauna%20Guestbook%20App&demo-description=A%20simple%20guestbook%20application%20built%20with%20Next.js%20and%20Fauna&demo-url=https%3A%2F%2Fnextjs-guestbook.vercel.app%2F) ## Why FaunaDB @@ -58,4 +58,4 @@ Your app should be up and running on [http://localhost:3000](http://localhost:30 ### Deploy -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fhello-world&env=NEXT_PUBLIC_FAUNADB_SECRET,NEXT_PUBLIC_FAUNADB_GRAPHQL_ENDPOINT&envDescription=Client%20secret%20and%20GraphQL%20endpoint%20needed%20for%20communicating%20with%20the%20live%20Fauna%20database&project-name=my-nextjs-guestbook&repository-name=my-nextjs-guestbook&demo-title=Next.js%20Fauna%20Guestbook%20App&demo-description=A%20simple%20guestbook%20application%20built%20with%20Next.js%20and%20Fauna&demo-url=https%3A%2F%2Fnextjs-guestbook.vercel.app%2F) +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fwith-graphql-faunadb&env=NEXT_PUBLIC_FAUNADB_SECRET,NEXT_PUBLIC_FAUNADB_GRAPHQL_ENDPOINT&envDescription=Client%20secret%20and%20GraphQL%20endpoint%20needed%20for%20communicating%20with%20the%20live%20Fauna%20database&project-name=my-nextjs-guestbook&repository-name=my-nextjs-guestbook&demo-title=Next.js%20Fauna%20Guestbook%20App&demo-description=A%20simple%20guestbook%20application%20built%20with%20Next.js%20and%20Fauna&demo-url=https%3A%2F%2Fnextjs-guestbook.vercel.app%2F) diff --git a/examples/with-iron-session/README.md b/examples/with-iron-session/README.md index 018ac06dc357726..b2f279f74a11bce 100644 --- a/examples/with-iron-session/README.md +++ b/examples/with-iron-session/README.md @@ -8,7 +8,7 @@ It uses current best practices for authentication in the Next.js ecosystem. - [Static Generation](https://nextjs.org/docs/basic-features/pages#static-generation-recommended) (SG), recommended example - [Server-side Rendering](https://nextjs.org/docs/basic-features/pages#server-side-rendering) (SSR) example in case you need it -- Logged in status synchronized between browser windows/tabs using **`withUser`** hook and [`swr`](https://swr.now.sh/) module +- Logged in status synchronized between browser windows/tabs using **`useUser`** hook and [`swr`](https://swr.now.sh/) module - Layout based on the user's logged-in/out status - Session data is signed and encrypted in a cookie diff --git a/examples/with-iron-session/package.json b/examples/with-iron-session/package.json index 6e04d2da95b66b1..bb4961f448eeddc 100644 --- a/examples/with-iron-session/package.json +++ b/examples/with-iron-session/package.json @@ -8,7 +8,7 @@ "start": "next start" }, "dependencies": { - "next": "^9.4.4", + "next": "latest", "next-iron-session": "4.1.7", "prop-types": "15.7.2", "react": "16.13.1", diff --git a/examples/with-iron-session/pages/profile-sg.js b/examples/with-iron-session/pages/profile-sg.js index e2e1446cedf3d99..85cc695412ff0bd 100644 --- a/examples/with-iron-session/pages/profile-sg.js +++ b/examples/with-iron-session/pages/profile-sg.js @@ -25,7 +25,7 @@ const SgProfile = () => { {githubUrl(user.login)}, reduced to `login` and `avatar_url`.

-
{JSON.stringify(user, undefined, 2)}
+
{JSON.stringify(user, null, 2)}
) } diff --git a/examples/with-iron-session/pages/profile-ssr.js b/examples/with-iron-session/pages/profile-ssr.js index d9dc6b410c315af..54ac7974643f773 100644 --- a/examples/with-iron-session/pages/profile-ssr.js +++ b/examples/with-iron-session/pages/profile-ssr.js @@ -24,7 +24,7 @@ const SsrProfile = ({ user }) => { {githubUrl(user.login)}, reduced to `login` and `avatar_url`.

-
{JSON.stringify(user, undefined, 2)}
+
{JSON.stringify(user, null, 2)}
)} @@ -34,11 +34,13 @@ const SsrProfile = ({ user }) => { export const getServerSideProps = withSession(async function ({ req, res }) { const user = req.session.get('user') - if (user === undefined) { - res.setHeader('location', '/login') - res.statusCode = 302 - res.end() - return { props: {} } + if (!user) { + return { + redirect: { + destination: '/login', + permanent: false, + }, + } } return { diff --git a/examples/with-magic/.env.local.example b/examples/with-magic/.env.local.example index 76c61f911348c9a..2d61ef5b1337ec7 100644 --- a/examples/with-magic/.env.local.example +++ b/examples/with-magic/.env.local.example @@ -1,2 +1,3 @@ NEXT_PUBLIC_MAGIC_PUBLISHABLE_KEY= MAGIC_SECRET_KEY= +TOKEN_SECRET="this-is-a-secret-value-with-at-least-32-characters" diff --git a/examples/with-magic/README.md b/examples/with-magic/README.md index d31b6332379e242..bc2336946e2836d 100644 --- a/examples/with-magic/README.md +++ b/examples/with-magic/README.md @@ -1,6 +1,6 @@ # Magic Example -This example show how to use [Magic](https://magic.link) with Next.js. The example features cookie-based, passwordless authentication with email-based magic links. +This example shows how to use [Magic](https://magic.link) with Next.js. The example features cookie-based, passwordless authentication with email-based magic links. The example shows how to do a login and logout; and to get the user info using a hook with [SWR](https://swr.now.sh). @@ -10,9 +10,9 @@ The login cookie is `httpOnly`, meaning it can only be accessed by the API, and ## Deploy your own -Deploy the example using [Vercel Now](https://vercel.com/docs/now-cli#commands/overview/basic-usage): +Once you have access to [the environment variables you'll need](#configuration), deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example): -[![Deploy with Vercel Now](https://vercel.com/button)](https://vercel.com/new/project?template=https://github.com/vercel/next.js/tree/canary/examples/with-magic) +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/import/git?c=1&s=https://github.com/vercel/next.js/tree/canary/examples/with-magic&env=NEXT_PUBLIC_MAGIC_PUBLISHABLE_KEY,MAGIC_SECRET_KEY,TOKEN_SECRET&envDescription=Required%20to%20connect%20the%20app%20with%20Magic&envLink=https://github.com/vercel/next.js/tree/canary/examples/with-magic%23configuration) ## How to use @@ -40,11 +40,30 @@ Then set each variable on `.env.local`: - `NEXT_PUBLIC_MAGIC_PUBLISHABLE_KEY` should look like `pk_test_abc` or `pk_live_ABC` - `MAGIC_SECRET_KEY` should look like `sk_test_ABC` or `sk_live_ABC` +- `TOKEN_SECRET` should be a string with at least 32 characters -To deploy on Vercel, you need to set up the environment variables with the [Environment Variables UI](https://vercel.com/blog/environment-variables-ui) using the [Vercel CLI](https://vercel.com/download) ([Documentation](https://vercel.com/docs/cli#commands/env)). - -Install [Vercel CLI](https://vercel.com/download), log in to your account from the CLI, link your project and run the following command to add the `NEXT_PUBLIC_MAGIC_PUBLISHABLE_KEY` and `MAGIC_SECRET_KEY` environment variables. +Now, run Next.js in development mode ```bash -vercel env add +npm run dev +# or +yarn dev ``` + +Your app should be up and running on [http://localhost:3000](http://localhost:3000)! If it doesn't work, post on [GitHub discussions](https://github.com/vercel/next.js/discussions). + +## Deploy on Vercel + +You can deploy this app to the cloud with [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). + +#### Deploy Your Local Project + +To deploy your local project to Vercel, push it to GitHub/GitLab/Bitbucket and [import to Vercel](https://vercel.com/import/git?utm_source=github&utm_medium=readme&utm_campaign=next-example). + +**Important**: When you import your project on Vercel, make sure to click on **Environment Variables** and set them to match your `.env.local` file. + +#### Deploy from Our Template + +Alternatively, you can deploy using our template by clicking on the Deploy button below. + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/import/git?c=1&s=https://github.com/vercel/next.js/tree/canary/examples/with-magic&env=NEXT_PUBLIC_MAGIC_PUBLISHABLE_KEY,MAGIC_SECRET_KEY,TOKEN_SECRET&envDescription=Required%20to%20connect%20the%20app%20with%20Magic&envLink=https://github.com/vercel/next.js/tree/canary/examples/with-magic%23configuration) diff --git a/examples/with-magic/lib/auth-cookies.js b/examples/with-magic/lib/auth-cookies.js index 0ae93e7523988b9..b0fbf5030b7eca8 100644 --- a/examples/with-magic/lib/auth-cookies.js +++ b/examples/with-magic/lib/auth-cookies.js @@ -1,7 +1,8 @@ import { serialize, parse } from 'cookie' const TOKEN_NAME = 'token' -const MAX_AGE = 60 * 60 * 8 // 8 hours + +export const MAX_AGE = 60 * 60 * 8 // 8 hours export function setTokenCookie(res, token) { const cookie = serialize(TOKEN_NAME, token, { @@ -12,6 +13,7 @@ export function setTokenCookie(res, token) { path: '/', sameSite: 'lax', }) + res.setHeader('Set-Cookie', cookie) } diff --git a/examples/with-magic/lib/auth.js b/examples/with-magic/lib/auth.js new file mode 100644 index 000000000000000..fe830e95d5e4535 --- /dev/null +++ b/examples/with-magic/lib/auth.js @@ -0,0 +1,29 @@ +import Iron from '@hapi/iron' +import { MAX_AGE, setTokenCookie, getTokenCookie } from './auth-cookies' + +const TOKEN_SECRET = process.env.TOKEN_SECRET + +export async function setLoginSession(res, session) { + const createdAt = Date.now() + // Create a session object with a max age that we can validate later + const obj = { ...session, createdAt, maxAge: MAX_AGE } + const token = await Iron.seal(obj, TOKEN_SECRET, Iron.defaults) + + setTokenCookie(res, token) +} + +export async function getLoginSession(req) { + const token = getTokenCookie(req) + + if (!token) return + + const session = await Iron.unseal(token, TOKEN_SECRET, Iron.defaults) + const expiresAt = session.createdAt + session.maxAge * 1000 + + // Validate the expiration date of the session + if (Date.now() > expiresAt) { + throw new Error('Session expired') + } + + return session +} diff --git a/examples/with-magic/lib/iron.js b/examples/with-magic/lib/iron.js deleted file mode 100644 index 977c4b110dd9946..000000000000000 --- a/examples/with-magic/lib/iron.js +++ /dev/null @@ -1,14 +0,0 @@ -import Iron from '@hapi/iron' -import { getTokenCookie } from './auth-cookies' - -// Use an environment variable here instead of a hardcoded value for production -const TOKEN_SECRET = 'this-is-a-secret-value-with-at-least-32-characters' - -export function encryptSession(session) { - return Iron.seal(session, TOKEN_SECRET, Iron.defaults) -} - -export async function getSession(req) { - const token = getTokenCookie(req) - return token && Iron.unseal(token, TOKEN_SECRET, Iron.defaults) -} diff --git a/examples/with-magic/pages/api/login.js b/examples/with-magic/pages/api/login.js index 1e1b11dff9b6bc0..b66975fa1fd3c70 100644 --- a/examples/with-magic/pages/api/login.js +++ b/examples/with-magic/pages/api/login.js @@ -1,15 +1,14 @@ import { magic } from '../../lib/magic' -import { encryptSession } from '../../lib/iron' -import { setTokenCookie } from '../../lib/auth-cookies' +import { setLoginSession } from '../../lib/auth' export default async function login(req, res) { try { const didToken = req.headers.authorization.substr(7) const metadata = await magic.users.getMetadataByToken(didToken) const session = { ...metadata } - // The token is a string with the encrypted session - const token = await encryptSession(session) - setTokenCookie(res, token) + + await setLoginSession(res, session) + res.status(200).send({ done: true }) } catch (error) { res.status(error.status || 500).end(error.message) diff --git a/examples/with-magic/pages/api/logout.js b/examples/with-magic/pages/api/logout.js index d29a38f6c41d054..73444d72b755484 100644 --- a/examples/with-magic/pages/api/logout.js +++ b/examples/with-magic/pages/api/logout.js @@ -1,11 +1,19 @@ import { magic } from '../../lib/magic' import { removeTokenCookie } from '../../lib/auth-cookies' -import { getSession } from '../../lib/iron' +import { getLoginSession } from '../../lib/auth' export default async function logout(req, res) { - const session = await getSession(req) - await magic.users.logoutByIssuer(session.issuer) - removeTokenCookie(res) + try { + const session = await getLoginSession(req) + + if (session) { + await magic.users.logoutByIssuer(session.issuer) + removeTokenCookie(res) + } + } catch (error) { + console.error(error) + } + res.writeHead(302, { Location: '/' }) res.end() } diff --git a/examples/with-magic/pages/api/user.js b/examples/with-magic/pages/api/user.js index dad216c76e9a30c..0a69be28ef398b8 100644 --- a/examples/with-magic/pages/api/user.js +++ b/examples/with-magic/pages/api/user.js @@ -1,7 +1,7 @@ -import { getSession } from '../../lib/iron' +import { getLoginSession } from '../../lib/auth' export default async function user(req, res) { - const session = await getSession(req) + const session = await getLoginSession(req) // After getting the session you may want to fetch for the user instead // of sending the session's payload directly, this example doesn't have a DB // so it won't matter in this case diff --git a/examples/with-magic/pages/index.js b/examples/with-magic/pages/index.js index 9009f76a4e3d7d3..efa11804d8ae70b 100644 --- a/examples/with-magic/pages/index.js +++ b/examples/with-magic/pages/index.js @@ -45,6 +45,10 @@ const Home = () => { li { margin-bottom: 0.5rem; } + pre { + white-space: pre-wrap; + word-wrap: break-word; + } `} ) diff --git a/examples/with-magic/pages/profile.js b/examples/with-magic/pages/profile.js index aec3ae9dcb7ecff..2903c7427850909 100644 --- a/examples/with-magic/pages/profile.js +++ b/examples/with-magic/pages/profile.js @@ -7,12 +7,20 @@ const Profile = () => { return (

Profile

+ {user && ( <>

Your session:

{JSON.stringify(user, null, 2)}
)} + +
) } diff --git a/examples/with-mdbreact/.gitignore b/examples/with-mdbreact/.gitignore new file mode 100644 index 000000000000000..87af23af33e815d --- /dev/null +++ b/examples/with-mdbreact/.gitignore @@ -0,0 +1,35 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +/.idea +# 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/with-mdbreact/README.md b/examples/with-mdbreact/README.md new file mode 100644 index 000000000000000..ac623cc3d214262 --- /dev/null +++ b/examples/with-mdbreact/README.md @@ -0,0 +1,21 @@ +# mdbreact Example + +This example shows how to use [MDBReact](https://mdbootstrap.com/docs/react) with Next.js. + +## Deploy your own + +Deploy the example using [Vercel](https://vercel.com): + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/import/git?s=https://github.com/vercel/next.js/tree/canary/examples/with-mdbreact) + +## 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 with-mdbreact with-mdbreact-app +# or +yarn create next-app --example with-mdbreact with-mdbreact-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/with-mdbreact/package.json b/examples/with-mdbreact/package.json new file mode 100644 index 000000000000000..e76af95a4d3a79a --- /dev/null +++ b/examples/with-mdbreact/package.json @@ -0,0 +1,16 @@ +{ + "name": "with-mdbreact", + "version": "0.1.0", + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "mdbreact": "^5.0.0", + "next": "latest", + "react": "17.0.1", + "react-dom": "17.0.1" + }, + "license": "MIT" +} diff --git a/examples/with-mdbreact/pages/_app.js b/examples/with-mdbreact/pages/_app.js new file mode 100644 index 000000000000000..d19b16ba08f6f09 --- /dev/null +++ b/examples/with-mdbreact/pages/_app.js @@ -0,0 +1,10 @@ +import '@fortawesome/fontawesome-free/css/all.min.css' +import 'bootstrap-css-only/css/bootstrap.min.css' +import 'mdbreact/dist/css/mdb.css' +import '../styles/globals.css' + +function MyApp({ Component, pageProps }) { + return +} + +export default MyApp diff --git a/examples/with-mdbreact/pages/index.js b/examples/with-mdbreact/pages/index.js new file mode 100644 index 000000000000000..63110767b2eff02 --- /dev/null +++ b/examples/with-mdbreact/pages/index.js @@ -0,0 +1,118 @@ +import Head from 'next/head' +import { + MDBBtn, + MDBCard, + MDBCardBody, + MDBCardText, + MDBCardTitle, + MDBCol, + MDBContainer, + MDBFooter, + MDBRow, +} from 'mdbreact' + +export default function Home() { + return ( + <> + + NextJS with Material Design Bootstrap for React + + + +

+ Welcome to Next.js! +

+

+ Get started by editing pages/index.js +

+ + + + + Documentation + + Find in-depth information about Next.js features and API. + + + More → + + + + + + + + Learn + + Learn about Next.js in an interactive course with quizzes! + + + More → + + + + + + + + + + Examples + + Discover and deploy boilerplate example Next.js projects. + + + More → + + + + + + + + Deploy + + Instantly deploy your Next.js site to a public URL with + Vercel. + + + More → + + + + + + + Powered by + + Vercel Logo + + +
+ + ) +} diff --git a/examples/with-mdbreact/public/favicon.ico b/examples/with-mdbreact/public/favicon.ico new file mode 100644 index 000000000000000..4965832f2c9b060 Binary files /dev/null and b/examples/with-mdbreact/public/favicon.ico differ diff --git a/examples/with-mdbreact/public/vercel.svg b/examples/with-mdbreact/public/vercel.svg new file mode 100644 index 000000000000000..fbf0e25a651c289 --- /dev/null +++ b/examples/with-mdbreact/public/vercel.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/examples/with-mdbreact/styles/globals.css b/examples/with-mdbreact/styles/globals.css new file mode 100644 index 000000000000000..e5e2dcc23baf192 --- /dev/null +++ b/examples/with-mdbreact/styles/globals.css @@ -0,0 +1,16 @@ +html, +body { + padding: 0; + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, + Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; +} + +a { + color: inherit; + text-decoration: none; +} + +* { + box-sizing: border-box; +} diff --git a/examples/with-mdx/package.json b/examples/with-mdx/package.json index 96d98b11cde3de7..44c4a6c2b2ed5c3 100644 --- a/examples/with-mdx/package.json +++ b/examples/with-mdx/package.json @@ -8,6 +8,7 @@ }, "dependencies": { "@mdx-js/loader": "^1.5.1", + "@mdx-js/react": "^1.6.18", "@next/mdx": "^9.1.1", "next": "latest", "react": "^16.8.6", diff --git a/examples/with-mongodb-mongoose/package.json b/examples/with-mongodb-mongoose/package.json index 241b1abf98372ff..964cbcfc60943e0 100644 --- a/examples/with-mongodb-mongoose/package.json +++ b/examples/with-mongodb-mongoose/package.json @@ -8,7 +8,7 @@ }, "dependencies": { "mongoose": "^5.9.13", - "next": "^9.4.2", + "next": "latest", "react": "^16.13.1", "react-dom": "^16.13.1", "swr": "0.2.2" diff --git a/examples/with-mongodb-mongoose/utils/dbConnect.js b/examples/with-mongodb-mongoose/utils/dbConnect.js index a28bda7f1096a83..517bc42baa12e4b 100644 --- a/examples/with-mongodb-mongoose/utils/dbConnect.js +++ b/examples/with-mongodb-mongoose/utils/dbConnect.js @@ -1,22 +1,17 @@ -/* This is a database connection function*/ import mongoose from 'mongoose' -const connection = {} /* creating connection object*/ - async function dbConnect() { - /* check if we have connection to our databse*/ - if (connection.isConnected) { + // check if we have a connection to the database or if it's currently + // connecting or disconnecting (readyState 1, 2 and 3) + if (mongoose.connection.readyState >= 1) { return } - /* connecting to our database */ - const db = await mongoose.connect(process.env.MONGODB_URI, { + return mongoose.connect(process.env.MONGODB_URI, { useNewUrlParser: true, useUnifiedTopology: true, useFindAndModify: false, }) - - connection.isConnected = db.connections[0].readyState } export default dbConnect diff --git a/examples/with-passport-and-next-connect/.env b/examples/with-passport-and-next-connect/.env new file mode 100644 index 000000000000000..f7448eb30a7926b --- /dev/null +++ b/examples/with-passport-and-next-connect/.env @@ -0,0 +1,3 @@ +# Secrets like the one below should go into `.env.local` instead to avoid pushing +# them to a repository, this is an exception for the sake of the example +TOKEN_SECRET="this-is-a-secret-value-with-at-least-32-characters" \ No newline at end of file diff --git a/examples/with-passport-and-next-connect/README.md b/examples/with-passport-and-next-connect/README.md index 2377586df2e09c3..18e21ad124a07da 100644 --- a/examples/with-passport-and-next-connect/README.md +++ b/examples/with-passport-and-next-connect/README.md @@ -6,8 +6,6 @@ The example shows how to do a sign up, login, logout, and account deactivation. For demo purpose, the users database is stored in the cookie session. You need to replace it with an actual database to store users in [db.js](lib/db.js). -In production, you must use a password hashing library, such as [argon2](https://github.com/ranisalt/node-argon2) or [bcrypt](https://www.npmjs.com/package/bcrypt). - ## Deploy your own Deploy the example using [Vercel](https://vercel.com): diff --git a/examples/with-passport-and-next-connect/lib/auth.js b/examples/with-passport-and-next-connect/lib/auth.js new file mode 100644 index 000000000000000..c76d31a873d7600 --- /dev/null +++ b/examples/with-passport-and-next-connect/lib/auth.js @@ -0,0 +1,21 @@ +import Iron from '@hapi/iron' + +export async function createLoginSession(session, secret) { + const createdAt = Date.now() + const obj = { ...session, createdAt } + const token = await Iron.seal(obj, secret, Iron.defaults) + + return token +} + +export async function getLoginSession(token, secret) { + const session = await Iron.unseal(token, secret, Iron.defaults) + const expiresAt = session.createdAt + session.maxAge * 1000 + + // Validate the expiration date of the session + if (session.maxAge && Date.now() > expiresAt) { + throw new Error('Session expired') + } + + return session +} diff --git a/examples/with-passport-and-next-connect/lib/db.js b/examples/with-passport-and-next-connect/lib/db.js index 51ecad5973ccf71..03f767ea9524792 100644 --- a/examples/with-passport-and-next-connect/lib/db.js +++ b/examples/with-passport-and-next-connect/lib/db.js @@ -1,9 +1,27 @@ +import crypto from 'crypto' +import { v4 as uuidv4 } from 'uuid' + export function getAllUsers(req) { // For demo purpose only. You are not likely to have to return all users. return req.session.users } -export function createUser(req, user) { +export function createUser(req, { username, password, name }) { + // Here you should create the user and save the salt and hashed password (some dbs may have + // authentication methods that will do it for you so you don't have to worry about it): + const salt = crypto.randomBytes(16).toString('hex') + const hash = crypto + .pbkdf2Sync(password, salt, 1000, 64, 'sha512') + .toString('hex') + const user = { + id: uuidv4(), + createdAt: Date.now(), + username, + name, + hash, + salt, + } + // Here you should insert the user into the database // await db.createUser(user) req.session.users.push(user) @@ -30,3 +48,13 @@ export function deleteUser(req, username) { (user) => user.username !== req.user.username ) } + +// Compare the password of an already fetched user (using `findUserByUsername`) and compare the +// password for a potential match +export function validatePassword(user, inputPassword) { + const inputHash = crypto + .pbkdf2Sync(inputPassword, user.salt, 1000, 64, 'sha512') + .toString('hex') + const passwordsMatch = user.hash === inputHash + return passwordsMatch +} diff --git a/examples/with-passport-and-next-connect/lib/passport.js b/examples/with-passport-and-next-connect/lib/passport.js index eade1a163f588eb..46ad5fbf5fb3e13 100644 --- a/examples/with-passport-and-next-connect/lib/passport.js +++ b/examples/with-passport-and-next-connect/lib/passport.js @@ -1,6 +1,6 @@ import passport from 'passport' import LocalStrategy from 'passport-local' -import { findUserByUsername } from './db' +import { findUserByUsername, validatePassword } from './db' passport.serializeUser(function (user, done) { // serialize the username into session @@ -21,7 +21,7 @@ passport.use( const user = findUserByUsername(req, username) // Security-wise, if you hashed the password earlier, you must verify it // if (!user || await argon2.verify(user.password, password)) - if (!user || user.password !== password) { + if (!user || !validatePassword(user, password)) { done(null, null) } else { done(null, user) diff --git a/examples/with-passport-and-next-connect/lib/session.js b/examples/with-passport-and-next-connect/lib/session.js index c6930a18d61d81c..eb2792d659f6339 100644 --- a/examples/with-passport-and-next-connect/lib/session.js +++ b/examples/with-passport-and-next-connect/lib/session.js @@ -1,29 +1,43 @@ import { parse, serialize } from 'cookie' -import Iron from '@hapi/iron' +import { createLoginSession, getLoginSession } from './auth' + +function parseCookies(req) { + // For API Routes we don't need to parse the cookies. + if (req.cookies) return req.cookies + + // For pages we do need to parse the cookies. + const cookie = req.headers?.cookie + return parse(cookie || '') +} export default function session({ name, secret, cookie: cookieOpts }) { return async (req, res, next) => { - const cookie = req.headers?.cookie ? parse(req.headers.cookie) : null - let unsealed - if (cookie?.[name]) { + const cookies = parseCookies(req) + const token = cookies[name] + let unsealed = {} + + if (token) { try { // the cookie needs to be unsealed using the password `secret` - unsealed = await Iron.unseal(cookie[name], secret, Iron.defaults) + unsealed = await getLoginSession(token, secret) } catch (e) { - // To cookie is invalid, do nothing + // The cookie is invalid } } - // Initialize the session - req.session = unsealed || {} + req.session = unsealed // We are proxying res.end to commit the session cookie const oldEnd = res.end res.end = async function resEndProxy(...args) { if (res.finished || res.writableEnded || res.headersSent) return - // sealing the cookie to be sent to client - const sealed = await Iron.seal(req.session, secret, Iron.defaults) - res.setHeader('Set-Cookie', serialize(name, sealed, cookieOpts)) + if (cookieOpts.maxAge) { + req.session.maxAge = cookieOpts.maxAge + } + + const token = await createLoginSession(req.session, secret) + + res.setHeader('Set-Cookie', serialize(name, token, cookieOpts)) oldEnd.apply(this, args) } diff --git a/examples/with-passport-and-next-connect/middleware/auth.js b/examples/with-passport-and-next-connect/middleware/auth.js index 536ccc16514909f..388e58e571b9e0e 100644 --- a/examples/with-passport-and-next-connect/middleware/auth.js +++ b/examples/with-passport-and-next-connect/middleware/auth.js @@ -6,7 +6,7 @@ const auth = nextConnect() .use( session({ name: 'sess', - secret: 'some_not_random_password_that_is_at_least_32_characters', // This should be kept securely, preferably in env vars + secret: process.env.TOKEN_SECRET, cookie: { maxAge: 60 * 60 * 8, // 8 hours, httpOnly: true, diff --git a/examples/with-passport-and-next-connect/package.json b/examples/with-passport-and-next-connect/package.json index df32791ed6c885e..d449904e40099ce 100644 --- a/examples/with-passport-and-next-connect/package.json +++ b/examples/with-passport-and-next-connect/package.json @@ -14,7 +14,8 @@ "passport-local": "^1.0.0", "react": "^16.13.1", "react-dom": "^16.13.1", - "swr": "^0.1.18" + "swr": "^0.1.18", + "uuidv4": "6.2.5" }, "license": "MIT" } diff --git a/examples/with-passport-and-next-connect/pages/index.js b/examples/with-passport-and-next-connect/pages/index.js index 29fbfd82020793f..35910864088ac72 100644 --- a/examples/with-passport-and-next-connect/pages/index.js +++ b/examples/with-passport-and-next-connect/pages/index.js @@ -9,8 +9,17 @@ function UserList() { {!!users?.length && (
    {users.map((user) => ( -
  • {JSON.stringify(user)}
  • +
  • +
    {JSON.stringify(user, null, 2)}
    +
  • ))} + +
)} @@ -53,12 +62,21 @@ export default function HomePage() { Home - {user &&

Currently logged in as: {JSON.stringify(user)}

} + {user && ( + <> +

Currently logged in as:

+
{JSON.stringify(user, null, 2)}
+ + )} ) diff --git a/examples/with-passport-and-next-connect/pages/profile.js b/examples/with-passport-and-next-connect/pages/profile.js index c461870744087e7..160f18a4fc70401 100644 --- a/examples/with-passport-and-next-connect/pages/profile.js +++ b/examples/with-passport-and-next-connect/pages/profile.js @@ -78,12 +78,21 @@ export default function ProfilePage() { return ( <>

Profile

+ {user && ( <> -

Your profile: {JSON.stringify(user)}

+

Your session:

+
{JSON.stringify(user, null, 2)}
)} + + ) } diff --git a/examples/with-passport/.env b/examples/with-passport/.env new file mode 100644 index 000000000000000..f7448eb30a7926b --- /dev/null +++ b/examples/with-passport/.env @@ -0,0 +1,3 @@ +# Secrets like the one below should go into `.env.local` instead to avoid pushing +# them to a repository, this is an exception for the sake of the example +TOKEN_SECRET="this-is-a-secret-value-with-at-least-32-characters" \ No newline at end of file diff --git a/examples/with-passport/lib/auth-cookies.js b/examples/with-passport/lib/auth-cookies.js index 1d215f3a664239b..b0fbf5030b7eca8 100644 --- a/examples/with-passport/lib/auth-cookies.js +++ b/examples/with-passport/lib/auth-cookies.js @@ -1,7 +1,8 @@ import { serialize, parse } from 'cookie' const TOKEN_NAME = 'token' -const MAX_AGE = 60 * 60 * 8 // 8 hours + +export const MAX_AGE = 60 * 60 * 8 // 8 hours export function setTokenCookie(res, token) { const cookie = serialize(TOKEN_NAME, token, { diff --git a/examples/with-passport/lib/auth.js b/examples/with-passport/lib/auth.js new file mode 100644 index 000000000000000..fe830e95d5e4535 --- /dev/null +++ b/examples/with-passport/lib/auth.js @@ -0,0 +1,29 @@ +import Iron from '@hapi/iron' +import { MAX_AGE, setTokenCookie, getTokenCookie } from './auth-cookies' + +const TOKEN_SECRET = process.env.TOKEN_SECRET + +export async function setLoginSession(res, session) { + const createdAt = Date.now() + // Create a session object with a max age that we can validate later + const obj = { ...session, createdAt, maxAge: MAX_AGE } + const token = await Iron.seal(obj, TOKEN_SECRET, Iron.defaults) + + setTokenCookie(res, token) +} + +export async function getLoginSession(req) { + const token = getTokenCookie(req) + + if (!token) return + + const session = await Iron.unseal(token, TOKEN_SECRET, Iron.defaults) + const expiresAt = session.createdAt + session.maxAge * 1000 + + // Validate the expiration date of the session + if (Date.now() > expiresAt) { + throw new Error('Session expired') + } + + return session +} diff --git a/examples/with-passport/lib/iron.js b/examples/with-passport/lib/iron.js deleted file mode 100644 index 977c4b110dd9946..000000000000000 --- a/examples/with-passport/lib/iron.js +++ /dev/null @@ -1,14 +0,0 @@ -import Iron from '@hapi/iron' -import { getTokenCookie } from './auth-cookies' - -// Use an environment variable here instead of a hardcoded value for production -const TOKEN_SECRET = 'this-is-a-secret-value-with-at-least-32-characters' - -export function encryptSession(session) { - return Iron.seal(session, TOKEN_SECRET, Iron.defaults) -} - -export async function getSession(req) { - const token = getTokenCookie(req) - return token && Iron.unseal(token, TOKEN_SECRET, Iron.defaults) -} diff --git a/examples/with-passport/lib/password-local.js b/examples/with-passport/lib/password-local.js index fe060b034139daa..bdd9dcc6b12f378 100644 --- a/examples/with-passport/lib/password-local.js +++ b/examples/with-passport/lib/password-local.js @@ -1,14 +1,18 @@ import Local from 'passport-local' -import { findUser } from './user' +import { findUser, validatePassword } from './user' export const localStrategy = new Local.Strategy(function ( username, password, done ) { - findUser({ username, password }) + findUser({ username }) .then((user) => { - done(null, user) + if (user && validatePassword(user, password)) { + done(null, user) + } else { + done(new Error('Invalid username and password combination')) + } }) .catch((error) => { done(error) diff --git a/examples/with-passport/lib/user.js b/examples/with-passport/lib/user.js index 80276dabd035e90..1c4dc6297669696 100644 --- a/examples/with-passport/lib/user.js +++ b/examples/with-passport/lib/user.js @@ -1,27 +1,46 @@ -// import crypto from 'crypto' +import crypto from 'crypto' +import { v4 as uuidv4 } from 'uuid' /** * User methods. The example doesn't contain a DB, but for real applications you must use a * db here, such as MongoDB, Fauna, SQL, etc. */ +const users = [] + export async function createUser({ username, password }) { // Here you should create the user and save the salt and hashed password (some dbs may have // authentication methods that will do it for you so you don't have to worry about it): - // - // const salt = crypto.randomBytes(16).toString('hex') - // const hash = crypto.pbkdf2Sync(password, salt, 1000, 64, 'sha512').toString('hex') - // const user = await DB.createUser({ username, salt, hash }) + const salt = crypto.randomBytes(16).toString('hex') + const hash = crypto + .pbkdf2Sync(password, salt, 1000, 64, 'sha512') + .toString('hex') + const user = { + id: uuidv4(), + createdAt: Date.now(), + username, + hash, + salt, + } + + // This is an in memory store for users, there is no data persistence without a proper DB + users.push(user) return { username, createdAt: Date.now() } } -export async function findUser({ username, password }) { - // Here you should lookup for the user in your DB and compare the password: - // - // const user = await DB.findUser(...) - // const hash = crypto.pbkdf2Sync(password, user.salt, 1000, 64, 'sha512').toString('hex') - // const passwordsMatch = user.hash === hash +// Here you should lookup for the user in your DB +export async function findUser({ username }) { + // This is an in memory store for users, there is no data persistence without a proper DB + return users.find((user) => user.username === username) +} - return { username, createdAt: Date.now() } +// Compare the password of an already fetched user (using `findUser`) and compare the +// password for a potential match +export function validatePassword(user, inputPassword) { + const inputHash = crypto + .pbkdf2Sync(inputPassword, user.salt, 1000, 64, 'sha512') + .toString('hex') + const passwordsMatch = user.hash === inputHash + return passwordsMatch } diff --git a/examples/with-passport/package.json b/examples/with-passport/package.json index 3c65a2ac6ca5cb8..45126d8e507202b 100644 --- a/examples/with-passport/package.json +++ b/examples/with-passport/package.json @@ -14,7 +14,8 @@ "passport-local": "1.0.0", "react": "latest", "react-dom": "latest", - "swr": "0.3.0" + "swr": "0.3.0", + "uuid": "8.3.1" }, "license": "MIT" } diff --git a/examples/with-passport/pages/api/login.js b/examples/with-passport/pages/api/login.js index a2c249b619bc6fe..cb72f79292012e2 100644 --- a/examples/with-passport/pages/api/login.js +++ b/examples/with-passport/pages/api/login.js @@ -1,8 +1,7 @@ import passport from 'passport' import nextConnect from 'next-connect' import { localStrategy } from '../../lib/password-local' -import { encryptSession } from '../../lib/iron' -import { setTokenCookie } from '../../lib/auth-cookies' +import { setLoginSession } from '../../lib/auth' const authenticate = (method, req, res) => new Promise((resolve, reject) => { @@ -24,10 +23,9 @@ export default nextConnect() const user = await authenticate('local', req, res) // session is the payload to save in the token, it may contain basic info about the user const session = { ...user } - // The token is a string with the encrypted session - const token = await encryptSession(session) - setTokenCookie(res, token) + await setLoginSession(res, session) + res.status(200).send({ done: true }) } catch (error) { console.error(error) diff --git a/examples/with-passport/pages/api/user.js b/examples/with-passport/pages/api/user.js index dad216c76e9a30c..873a44b4d40d712 100644 --- a/examples/with-passport/pages/api/user.js +++ b/examples/with-passport/pages/api/user.js @@ -1,9 +1,14 @@ -import { getSession } from '../../lib/iron' +import { getLoginSession } from '../../lib/auth' +import { findUser } from '../../lib/user' export default async function user(req, res) { - const session = await getSession(req) - // After getting the session you may want to fetch for the user instead - // of sending the session's payload directly, this example doesn't have a DB - // so it won't matter in this case - res.status(200).json({ user: session || null }) + try { + const session = await getLoginSession(req) + const user = (session && (await findUser(session))) ?? null + + res.status(200).json({ user }) + } catch (error) { + console.error(error) + res.status(500).end('Authentication token is invalid, please log in') + } } diff --git a/examples/with-passport/pages/index.js b/examples/with-passport/pages/index.js index 0870d849e3d8cda..1ca76dee2bd831d 100644 --- a/examples/with-passport/pages/index.js +++ b/examples/with-passport/pages/index.js @@ -11,7 +11,7 @@ const Home = () => {

Steps to test the example:

    -
  1. Click Login and enter an username and password.
  2. +
  3. Click Login and enter a username and password.
  4. You'll be redirected to Home. Click on Profile, notice how your session is being used through a token stored in a cookie. @@ -22,12 +22,21 @@ const Home = () => {
- {user &&

Currently logged in as: {JSON.stringify(user)}

} + {user && ( + <> +

Currently logged in as:

+
{JSON.stringify(user, null, 2)}
+ + )} ) diff --git a/examples/with-passport/pages/profile.js b/examples/with-passport/pages/profile.js index e864f7480f04b0a..6c02924bfe65081 100644 --- a/examples/with-passport/pages/profile.js +++ b/examples/with-passport/pages/profile.js @@ -7,7 +7,19 @@ const Profile = () => { return (

Profile

- {user &&

Your session: {JSON.stringify(user)}

} + {user && ( + <> +

Your session:

+
{JSON.stringify(user, null, 2)}
+ + )} + +
) } diff --git a/examples/with-react-intl/package.json b/examples/with-react-intl/package.json index eeaee257d9a553a..c9e0ff93b7a5ea3 100644 --- a/examples/with-react-intl/package.json +++ b/examples/with-react-intl/package.json @@ -6,7 +6,7 @@ "dev-no-custom-server": "next dev", "build": "npm run extract:i18n && npm run compile:i18n && next build && tsc -p tsconfig.server.json", "extract:i18n": "formatjs extract '{pages,components}/*.{js,ts,tsx}' --format simple --id-interpolation-pattern '[sha512:contenthash:base64:6]' --out-file lang/en.json", - "compile:i18n": "formatjs compile-folder --ast --format simple lang/ compiled-lang/", + "compile:i18n": "formatjs compile-folder --ast --format simple lang compiled-lang", "start": "cross-env NODE_ENV=production NODE_ICU_DATA=node_modules/full-icu node dist/server", "start-no-custom-server": "next start" }, @@ -32,7 +32,7 @@ "@types/accepts": "^1.3.5", "cross-spawn": "7.0.3", "prettier": "2.0.5", - "ts-node": "8.0.0", + "ts-node": "8.0.1", "typescript": "4.0" }, "prettier": { diff --git a/examples/with-sentry/next.config.js b/examples/with-sentry/next.config.js index 1579ed872c5095d..96d8cd909a0a1a4 100644 --- a/examples/with-sentry/next.config.js +++ b/examples/with-sentry/next.config.js @@ -1,7 +1,3 @@ -// Use the hidden-source-map option when you don't want the source maps to be -// publicly available on the servers, only to the error reporting -const withSourceMaps = require('@zeit/next-source-maps')() - // Use the SentryWebpack plugin to upload the source maps during build step const SentryWebpackPlugin = require('@sentry/webpack-plugin') const { @@ -23,7 +19,8 @@ const COMMIT_SHA = process.env.SENTRY_DSN = SENTRY_DSN const basePath = '' -module.exports = withSourceMaps({ +module.exports = { + productionBrowserSourceMaps: true, env: { // Make the COMMIT_SHA available to the client so that Sentry events can be // marked for the release they belong to. It may be undefined if running @@ -85,4 +82,4 @@ module.exports = withSourceMaps({ return config }, basePath, -}) +} diff --git a/examples/with-sentry/package.json b/examples/with-sentry/package.json index fc423c0f95233fc..ceeab004b5515f2 100644 --- a/examples/with-sentry/package.json +++ b/examples/with-sentry/package.json @@ -12,7 +12,6 @@ "@sentry/integrations": "^5.21.3", "@sentry/node": "^5.21.3", "@sentry/webpack-plugin": "^1.12.1", - "@zeit/next-source-maps": "0.0.4-canary.1", "next": "latest", "react": "^16.8.6", "react-dom": "^16.8.6" diff --git a/examples/with-sitemap/README.md b/examples/with-sitemap/README.md index 9fbe6e410b82c11..d37b012317d39bd 100644 --- a/examples/with-sitemap/README.md +++ b/examples/with-sitemap/README.md @@ -6,7 +6,7 @@ This example shows how to generate a `sitemap.xml` file based on the pages in yo 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/hello-world) +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/vercel/next.js/tree/canary/examples/with-sitemap) ## How to use diff --git a/examples/with-storybook-styled-jsx-scss/.babelrc b/examples/with-storybook-styled-jsx-scss/.babelrc new file mode 100644 index 000000000000000..3eaec7a8b855747 --- /dev/null +++ b/examples/with-storybook-styled-jsx-scss/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "next/babel", + { + "styled-jsx": { + "plugins": ["styled-jsx-plugin-sass"] + } + } + ] + ] +} diff --git a/examples/with-storybook-styled-jsx-scss/.gitignore b/examples/with-storybook-styled-jsx-scss/.gitignore new file mode 100644 index 000000000000000..a423604f9778881 --- /dev/null +++ b/examples/with-storybook-styled-jsx-scss/.gitignore @@ -0,0 +1,37 @@ +# 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 + +# Storybook +/storybook-static \ No newline at end of file diff --git a/examples/with-storybook-styled-jsx-scss/.storybook/main.js b/examples/with-storybook-styled-jsx-scss/.storybook/main.js new file mode 100644 index 000000000000000..f041eb8c96a17f4 --- /dev/null +++ b/examples/with-storybook-styled-jsx-scss/.storybook/main.js @@ -0,0 +1,7 @@ +module.exports = { + stories: [ + '../stories/**/*.stories.mdx', + '../stories/**/*.stories.@(js|jsx|ts|tsx)', + ], + addons: ['@storybook/addon-links', '@storybook/addon-essentials'], +} diff --git a/examples/with-storybook-styled-jsx-scss/.storybook/preview.js b/examples/with-storybook-styled-jsx-scss/.storybook/preview.js new file mode 100644 index 000000000000000..6dd87694930ad25 --- /dev/null +++ b/examples/with-storybook-styled-jsx-scss/.storybook/preview.js @@ -0,0 +1,3 @@ +export const parameters = { + actions: { argTypesRegex: '^on[A-Z].*' }, +} diff --git a/examples/with-storybook-styled-jsx-scss/README.md b/examples/with-storybook-styled-jsx-scss/README.md new file mode 100644 index 000000000000000..c35210600ec7e3b --- /dev/null +++ b/examples/with-storybook-styled-jsx-scss/README.md @@ -0,0 +1,45 @@ +# Example app with Storybook setup for SCSS in Styled-jsx + +This example shows Styled-jsx (with SCSS) working for components written in TypeScript rendered both inside and outside of Storybook. + +## 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/with-storybook-styled-jsx-scss) + +## 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 with-storybook-styled-jsx-scss with-storybook-styled-jsx-scss-app +# or +yarn create next-app --example with-storybook-styled-jsx-scss with-storybook-styled-jsx-scss-app +``` + +### Run Storybook + +```bash +npm run storybook +# or +yarn storybook +``` + +### Build Static Storybook + +```bash +npm run build-storybook +# or +yarn build-storybook +``` + +You can use [Vercel](https://vercel.com/import?filter=next.js&utm_source=github&utm_medium=readme&utm_campaign=next-example) to deploy Storybook. Specify `storybook-static` as the output directory. + +## Notes + +This example combines the following examples, with some required extra config added: + +- [with-storybook](https://github.com/vercel/next.js/tree/canary/examples/with-storybook) +- [with-styled-jsx-scss](https://github.com/vercel/next.js/tree/canary/examples/with-styled-jsx-scss) +- [with-typescript](https://github.com/vercel/next.js/tree/canary/examples/with-typescript) diff --git a/examples/with-storybook-styled-jsx-scss/components/Button.tsx b/examples/with-storybook-styled-jsx-scss/components/Button.tsx new file mode 100644 index 000000000000000..87c29a508483464 --- /dev/null +++ b/examples/with-storybook-styled-jsx-scss/components/Button.tsx @@ -0,0 +1,86 @@ +import React from 'react' + +export interface ButtonProps { + /** + * Is this the principal call to action on the page? + */ + primary?: boolean + /** + * What background color to use + */ + backgroundColor?: string + /** + * How large should the button be? + */ + size?: 'small' | 'medium' | 'large' + /** + * Button contents + */ + label: string + /** + * Optional click handler + */ + onClick?: () => void +} + +/** + * Primary UI component for user interaction + */ +export const Button: React.FC = ({ + primary = false, + size = 'medium', + backgroundColor, + label, + ...props +}) => { + const mode = primary + ? 'storybook-button--primary' + : 'storybook-button--secondary' + return ( + <> + + + + ) +} diff --git a/examples/with-storybook-styled-jsx-scss/components/Header.tsx b/examples/with-storybook-styled-jsx-scss/components/Header.tsx new file mode 100644 index 000000000000000..467d84061da7c05 --- /dev/null +++ b/examples/with-storybook-styled-jsx-scss/components/Header.tsx @@ -0,0 +1,89 @@ +import React from 'react' + +import { Button } from './Button' + +export interface HeaderProps { + user?: {} + onLogin: () => void + onLogout: () => void + onCreateAccount: () => void +} + +export const Header: React.FC = ({ + user, + onLogin, + onLogout, + onCreateAccount, +}) => ( + <> +
+
+
+ + + + + + + +

Acme

+
+
+ {user ? ( +
+
+
+ + +) diff --git a/examples/with-storybook-styled-jsx-scss/components/Page.tsx b/examples/with-storybook-styled-jsx-scss/components/Page.tsx new file mode 100644 index 000000000000000..d99229dc66ddbae --- /dev/null +++ b/examples/with-storybook-styled-jsx-scss/components/Page.tsx @@ -0,0 +1,211 @@ +import React from 'react' +import Link from 'next/link' +import { Header } from './Header' + +export interface PageProps { + user?: {} + onLogin: () => void + onLogout: () => void + onCreateAccount: () => void +} + +export const Page: React.FC = ({ + user, + onLogin, + onLogout, + onCreateAccount, +}) => ( + <> +
+
+
+

Example app with Storybook setup for SCSS in Styled-jsx

+

+ This example shows Styled-jsx (with SCSS) working for components + rendered both inside and outside of Storybook. +

+

This example combines the following other examples:

+ +

+ Additionally, the Storybook demo components are moved into the + components directory and their css is refactored into component level + Styled JSX + SCSS. +

+

+ Story files live in their own directory and refer to the components + within /components. +

+

+ The /styles directory contains styles, both global and for the + index.js file. +

+

+ You might also want to check out the{' '} + + Styled JSX documentation + +

+

+ + Improvements welcome! + +

+
+
+

Pages in Storybook

+

+ (From the storybook demo setup) +

+

+ We recommend building UIs with a{' '} + + component-driven + {' '} + process starting with atomic components and ending with pages. +

+

+ Render pages with mock data. This makes it easy to build and review + page states without needing to navigate to them in your app. Here are + some handy patterns for managing page data in Storybook: +

+
    +
  • + Use a higher-level connected component. Storybook helps you compose + such data from the "args" of child component stories +
  • +
  • + Assemble data in the page component from your services. You can mock + these services out using Storybook. +
  • +
+

+ Get a guided tutorial on component-driven development at{' '} + + Learn Storybook + + . Read more in the{' '} + + docs + + . +

+
+ Tip Adjust the width of the canvas with + the{' '} + + + + + + Viewports addon in the toolbar (when viewed in Storybook) +
+
+
+ + +) diff --git a/examples/with-storybook-styled-jsx-scss/next-env.d.ts b/examples/with-storybook-styled-jsx-scss/next-env.d.ts new file mode 100644 index 000000000000000..7b7aa2c7727d88b --- /dev/null +++ b/examples/with-storybook-styled-jsx-scss/next-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/examples/with-storybook-styled-jsx-scss/package.json b/examples/with-storybook-styled-jsx-scss/package.json new file mode 100644 index 000000000000000..0a85565d4b3ce1b --- /dev/null +++ b/examples/with-storybook-styled-jsx-scss/package.json @@ -0,0 +1,33 @@ +{ + "name": "with-storybook-styled-jsx-scss", + "version": "0.1.0", + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "type-check": "tsc", + "storybook": "start-storybook -p 6006 --no-dll", + "build-storybook": "build-storybook --no-dll" + }, + "dependencies": { + "next": "^10.0.0", + "node-sass": "^4.14.1", + "react": "17.0.1", + "react-dom": "17.0.1", + "styled-jsx-plugin-sass": "^1.0.0" + }, + "license": "MIT", + "devDependencies": { + "@babel/core": "^7.12.3", + "@storybook/addon-actions": "^6.1.11", + "@storybook/addon-essentials": "^6.1.11", + "@storybook/addon-links": "^6.1.11", + "@storybook/react": "^6.1.11", + "@types/node": "^14.14.2", + "@types/react": "^16.9.53", + "@types/react-dom": "^16.9.8", + "babel-loader": "^8.1.0", + "react-is": "^17.0.1", + "typescript": "^4.0.3" + } +} diff --git a/examples/with-storybook-styled-jsx-scss/pages/_app.tsx b/examples/with-storybook-styled-jsx-scss/pages/_app.tsx new file mode 100644 index 000000000000000..27cd5bfc2166f7f --- /dev/null +++ b/examples/with-storybook-styled-jsx-scss/pages/_app.tsx @@ -0,0 +1,8 @@ +import { AppProps } from 'next/app' +import '../styles/globals.css' + +function MyApp({ Component, pageProps }: AppProps) { + return +} + +export default MyApp diff --git a/examples/with-storybook-styled-jsx-scss/pages/index.tsx b/examples/with-storybook-styled-jsx-scss/pages/index.tsx new file mode 100644 index 000000000000000..cb3bcd9323e9b66 --- /dev/null +++ b/examples/with-storybook-styled-jsx-scss/pages/index.tsx @@ -0,0 +1,35 @@ +import Head from 'next/head' +import styles from '../styles/Home.module.css' +import { Page } from '../components/Page' + +const demoProps = { + user: {}, + onLogin: () => {}, + onLogout: () => {}, + onCreateAccount: () => {}, +} + +export default function Home() { + return ( +
+ + Create Next App + + + + {/* Including demo props here for example */} + + + +
+ ) +} diff --git a/examples/with-storybook-styled-jsx-scss/public/favicon.ico b/examples/with-storybook-styled-jsx-scss/public/favicon.ico new file mode 100644 index 000000000000000..4965832f2c9b060 Binary files /dev/null and b/examples/with-storybook-styled-jsx-scss/public/favicon.ico differ diff --git a/examples/with-storybook-styled-jsx-scss/public/vercel.svg b/examples/with-storybook-styled-jsx-scss/public/vercel.svg new file mode 100644 index 000000000000000..fbf0e25a651c289 --- /dev/null +++ b/examples/with-storybook-styled-jsx-scss/public/vercel.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/examples/with-storybook-styled-jsx-scss/stories/Button.stories.tsx b/examples/with-storybook-styled-jsx-scss/stories/Button.stories.tsx new file mode 100644 index 000000000000000..3a6504b17d2cb4b --- /dev/null +++ b/examples/with-storybook-styled-jsx-scss/stories/Button.stories.tsx @@ -0,0 +1,38 @@ +import React from 'react' +// also exported from '@storybook/react' if you can deal with breaking changes in 6.1 +import { Story, Meta } from '@storybook/react/types-6-0' + +import { Button, ButtonProps } from '../components/Button' + +export default { + title: 'Example/Button', + component: Button, + argTypes: { + backgroundColor: { control: 'color' }, + }, +} as Meta + +const Template: Story = (args) =>