diff --git a/.eslintignore b/.eslintignore index d168d82d7a56..e540ff4579a1 100644 --- a/.eslintignore +++ b/.eslintignore @@ -25,6 +25,7 @@ packages/next-codemod/**/*.d.ts packages/next-env/**/*.d.ts packages/create-next-app/templates/** test/integration/eslint/** +test/integration/script-loader/**/* test/development/basic/legacy-decorators/**/* test/production/emit-decorator-metadata/**/*.js test-timings.json diff --git a/.eslintrc.json b/.eslintrc.json index 26824c94ba66..224a065e585b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -15,7 +15,7 @@ "jsx": true }, "babelOptions": { - "presets": ["@babel/preset-env", "@babel/preset-react"], + "presets": ["next/babel"], "caller": { // Eslint supports top level await when a parser for it is included. We enable the parser by default for Babel. "supportsTopLevelAwait": true diff --git a/.github/workflows/build_test_deploy.yml b/.github/workflows/build_test_deploy.yml index d30b8207ca2a..bf9b28cf3a04 100644 --- a/.github/workflows/build_test_deploy.yml +++ b/.github/workflows/build_test_deploy.yml @@ -97,7 +97,7 @@ jobs: if: ${{needs.build.outputs.docsChange != 'docs only change'}} with: profile: minimal - toolchain: nightly-2021-11-15 + toolchain: nightly-2022-02-23 components: rustfmt, clippy - name: Cache cargo registry @@ -181,7 +181,7 @@ jobs: steps: - name: Setup node uses: actions/setup-node@v2 - if: ${{ steps.docs-change.outputs.docsChange != 'docs only change' }} + if: ${{needs.build.outputs.docsChange != 'docs only change'}} with: node-version: 14 @@ -211,7 +211,7 @@ jobs: steps: - name: Setup node uses: actions/setup-node@v2 - if: ${{ steps.docs-change.outputs.docsChange != 'docs only change' }} + if: ${{needs.build.outputs.docsChange != 'docs only change'}} with: node-version: 14 @@ -241,6 +241,52 @@ jobs: name: Run test/development if: ${{needs.build.outputs.docsChange != 'docs only change'}} + - name: Upload test trace + if: always() + uses: actions/upload-artifact@v2 + with: + name: test-trace + if-no-files-found: ignore + retention-days: 2 + path: | + test/traces + + testDevE2E: + name: Test Development (E2E) + runs-on: ubuntu-latest + needs: [build, build-native-dev] + env: + NEXT_TELEMETRY_DISABLED: 1 + NEXT_TEST_JOB: 1 + steps: + - name: Setup node + uses: actions/setup-node@v2 + if: ${{needs.build.outputs.docsChange != 'docs only change'}} + with: + node-version: 14 + + - run: echo ${{needs.build.outputs.docsChange}} + + # https://github.com/actions/virtual-environments/issues/1187 + - name: tune linux network + run: sudo ethtool -K eth0 tx off rx off + + - uses: actions/cache@v2 + if: ${{needs.build.outputs.docsChange != 'docs only change'}} + id: restore-build + with: + path: ./* + key: ${{ github.sha }}-${{ github.run_number }}-${{ github.run_attempt }} + + - uses: actions/download-artifact@v2 + if: ${{needs.build.outputs.docsChange != 'docs only change'}} + with: + name: next-swc-dev-binary + path: packages/next-swc/native + + - run: npm i -g playwright-chromium@1.14.1 && npx playwright install-deps + if: ${{needs.build.outputs.docsChange != 'docs only change'}} + - run: NEXT_TEST_MODE=dev node run-tests.js --type e2e name: Run test/e2e (dev) if: ${{needs.build.outputs.docsChange != 'docs only change'}} @@ -265,7 +311,7 @@ jobs: steps: - name: Setup node uses: actions/setup-node@v2 - if: ${{ steps.docs-change.outputs.docsChange != 'docs only change' }} + if: ${{needs.build.outputs.docsChange != 'docs only change'}} with: node-version: 14 @@ -295,6 +341,42 @@ jobs: name: Run test/production if: ${{needs.build.outputs.docsChange != 'docs only change'}} + testProdE2E: + name: Test Production (E2E) + runs-on: ubuntu-latest + needs: [build, build-native-dev] + env: + NEXT_TELEMETRY_DISABLED: 1 + NEXT_TEST_JOB: 1 + steps: + - name: Setup node + uses: actions/setup-node@v2 + if: ${{needs.build.outputs.docsChange != 'docs only change'}} + with: + node-version: 14 + + - run: echo ${{needs.build.outputs.docsChange}} + + # https://github.com/actions/virtual-environments/issues/1187 + - name: tune linux network + run: sudo ethtool -K eth0 tx off rx off + + - uses: actions/cache@v2 + if: ${{needs.build.outputs.docsChange != 'docs only change'}} + id: restore-build + with: + path: ./* + key: ${{ github.sha }}-${{ github.run_number }}-${{ github.run_attempt }} + + - uses: actions/download-artifact@v2 + if: ${{needs.build.outputs.docsChange != 'docs only change'}} + with: + name: next-swc-dev-binary + path: packages/next-swc/native + + - run: npm i -g playwright-chromium@1.14.1 && npx playwright install-deps + if: ${{needs.build.outputs.docsChange != 'docs only change'}} + - run: NEXT_TEST_MODE=start node run-tests.js --type e2e name: Run test/e2e (production) if: ${{needs.build.outputs.docsChange != 'docs only change'}} @@ -310,11 +392,11 @@ jobs: strategy: fail-fast: false matrix: - group: [1, 2, 3, 4, 5, 6] + group: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18] steps: - name: Setup node uses: actions/setup-node@v2 - if: ${{ steps.docs-change.outputs.docsChange != 'docs only change' }} + if: ${{needs.build.outputs.docsChange != 'docs only change'}} with: node-version: 14 @@ -331,6 +413,10 @@ jobs: path: ./* key: ${{ github.sha }}-${{ github.run_number }}-${{ github.run_attempt }} + - uses: pnpm/action-setup@v2.2.1 + with: + version: 6.32.2 + - uses: actions/download-artifact@v2 if: ${{needs.build.outputs.docsChange != 'docs only change'}} with: @@ -340,7 +426,7 @@ jobs: - run: npm i -g playwright-chromium@1.14.1 && npx playwright install-deps if: ${{needs.build.outputs.docsChange != 'docs only change'}} - - run: xvfb-run node run-tests.js --timings -g ${{ matrix.group }}/6 + - run: xvfb-run node run-tests.js --timings -g ${{ matrix.group }}/18 if: ${{needs.build.outputs.docsChange != 'docs only change'}} - name: Upload test trace @@ -364,7 +450,7 @@ jobs: steps: - name: Setup node uses: actions/setup-node@v2 - if: ${{ steps.docs-change.outputs.docsChange != 'docs only change' }} + if: ${{needs.build.outputs.docsChange != 'docs only change'}} with: node-version: 14 @@ -414,7 +500,7 @@ jobs: steps: - name: Setup node uses: actions/setup-node@v2 - if: ${{ steps.docs-change.outputs.docsChange != 'docs only change' }} + if: ${{needs.build.outputs.docsChange != 'docs only change'}} with: node-version: 14 @@ -449,7 +535,7 @@ jobs: steps: - name: Setup node uses: actions/setup-node@v2 - if: ${{ steps.docs-change.outputs.docsChange != 'docs only change' }} + if: ${{needs.build.outputs.docsChange != 'docs only change'}} with: node-version: 14 @@ -493,7 +579,7 @@ jobs: steps: - name: Setup node uses: actions/setup-node@v2 - if: ${{ steps.docs-change.outputs.docsChange != 'docs only change' }} + if: ${{needs.build.outputs.docsChange != 'docs only change'}} with: node-version: 14 @@ -530,7 +616,7 @@ jobs: steps: - name: Setup node uses: actions/setup-node@v2 - if: ${{ steps.docs-change.outputs.docsChange != 'docs only change' }} + if: ${{needs.build.outputs.docsChange != 'docs only change'}} with: node-version: 17 check-latest: true @@ -563,7 +649,7 @@ jobs: steps: - name: Setup node uses: actions/setup-node@v2 - if: ${{ steps.docs-change.outputs.docsChange != 'docs only change' }} + if: ${{needs.build.outputs.docsChange != 'docs only change'}} with: node-version: 14 @@ -598,7 +684,6 @@ jobs: steps: - name: Setup node uses: actions/setup-node@v2 - if: ${{ steps.docs-change.outputs.docsChange != 'docs only change' }} with: node-version: 14 @@ -647,7 +732,7 @@ jobs: if: ${{ steps.docs-change.outputs.DOCS_CHANGE != 'docs only change' }} with: profile: minimal - toolchain: nightly-2021-11-15 + toolchain: nightly-2022-02-23 - name: Cache cargo registry uses: actions/cache@v2 @@ -726,7 +811,7 @@ jobs: if: ${{ steps.docs-change.outputs.DOCS_CHANGE != 'docs only change' }} uses: actions-rs/toolchain@v1 with: - toolchain: nightly-2021-11-15 + toolchain: nightly-2022-02-23 profile: minimal - run: cd packages/next-swc && cargo test if: ${{ steps.docs-change.outputs.DOCS_CHANGE != 'docs only change' }} @@ -804,8 +889,8 @@ jobs: # Node.js in Baidu need to compatible with `GLIBC_2.12` build: >- set -e && - rustup toolchain install nightly-2021-11-15 && - rustup default nightly-2021-11-15 && + rustup toolchain install nightly-2022-02-23 && + rustup default nightly-2022-02-23 && rustup target add x86_64-unknown-linux-gnu && npm i -g @napi-rs/cli@2.4.4 turbo@1.0.28 && turbo run build-native --cache-dir=".turbo" -- --release --target x86_64-unknown-linux-gnu --zig --zig-abi-suffix 2.12 && @@ -815,8 +900,8 @@ jobs: docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine build: >- set -e && - rustup toolchain install nightly-2021-11-15 && - rustup default nightly-2021-11-15 && + rustup toolchain install nightly-2022-02-23 && + rustup default nightly-2022-02-23 && rustup target add x86_64-unknown-linux-musl && npm i -g @napi-rs/cli@2.4.4 turbo@1.0.28 && turbo run build-native --cache-dir=".turbo" -- --release --target x86_64-unknown-linux-musl && @@ -837,8 +922,8 @@ jobs: docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine-zig build: >- set -e && - rustup toolchain install nightly-2021-11-15 && - rustup default nightly-2021-11-15 && + rustup toolchain install nightly-2022-02-23 && + rustup default nightly-2022-02-23 && rustup target add aarch64-unknown-linux-gnu && npm i -g @napi-rs/cli@2.4.4 turbo@1.0.28 && turbo run build-native --cache-dir=".turbo" -- --release --target aarch64-unknown-linux-gnu --zig --zig-abi-suffix 2.12 && @@ -878,8 +963,8 @@ jobs: build: >- set -e && npm i -g @napi-rs/cli@2.4.4 turbo@1.0.28 && - rustup toolchain install nightly-2021-11-15 && - rustup default nightly-2021-11-15 && + rustup toolchain install nightly-2022-02-23 && + rustup default nightly-2022-02-23 && rustup target add aarch64-unknown-linux-musl && turbo run build-native --cache-dir=".turbo" -- --release --target aarch64-unknown-linux-musl && llvm-strip -x packages/next-swc/native/next-swc.*.node @@ -945,7 +1030,7 @@ jobs: with: profile: minimal override: true - toolchain: nightly-2021-11-15 + toolchain: nightly-2022-02-23 target: ${{ matrix.settings.target }} - name: Cache cargo registry @@ -1007,7 +1092,7 @@ jobs: uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: nightly-2021-11-15 + toolchain: nightly-2022-02-23 override: true target: wasm32-unknown-unknown @@ -1063,7 +1148,7 @@ jobs: uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: nightly-2021-11-15 + toolchain: nightly-2022-02-23 override: true target: wasm32-unknown-unknown diff --git a/docs/advanced-features/compiler.md b/docs/advanced-features/compiler.md index 439474fa11f3..16d415a0a11d 100644 --- a/docs/advanced-features/compiler.md +++ b/docs/advanced-features/compiler.md @@ -189,6 +189,36 @@ First, update to the latest version of Next.js: `npm install next@latest`. Then, ## Experimental Features +### Emotion + +We're working to port `@emotion/babel-plugin` to the Next.js Compiler. + +First, update to the latest version of Next.js: `npm install next@latest`. Then, update your `next.config.js` file: + +```js +// next.config.js + +module.exports = { + experimental: { + emotion: boolean | { + // default is true. It will be disabled when build type is production. + sourceMap?: boolean, + // default is 'dev-only'. + autoLabel?: 'never' | 'dev-only' | 'always', + // default is '[local]'. + // Allowed values: `[local]` `[filename]` and `[dirname]` + // This option only works when autoLabel is set to 'dev-only' or 'always'. + // It allows you to define the format of the resulting label. + // The format is defined via string where variable parts are enclosed in square brackets []. + // For example labelFormat: "my-classname--[local]", where [local] will be replaced with the name of the variable the result is assigned to. + labelFormat?: string, + }, + }, +} +``` + +Only `importMap` in `@emotion/babel-plugin` is not supported for now. + ### Minification You can opt-in to using the Next.js compiler for minification. This is 7x faster than Terser. diff --git a/docs/advanced-features/custom-app.md b/docs/advanced-features/custom-app.md index cf8a8fd1fee3..2a7d541cfd72 100644 --- a/docs/advanced-features/custom-app.md +++ b/docs/advanced-features/custom-app.md @@ -40,6 +40,8 @@ The `Component` prop is the active `page`, so whenever you navigate between rout `pageProps` is an object with the initial props that were preloaded for your page by one of our [data fetching methods](/docs/basic-features/data-fetching/overview.md), otherwise it's an empty object. +The `App.getInitialProps` receives a single argument called `context.ctx`. It's an object with the same set of properties as the [`context` object](/docs/api-reference/data-fetching/get-initial-props#context-object) in `getInitialProps`. + ### Caveats - If your app is running and you added a custom `App`, you'll need to restart the development server. Only required if `pages/_app.js` didn't exist before. diff --git a/docs/advanced-features/custom-document.md b/docs/advanced-features/custom-document.md index 2ef135fa783a..97d427fbec1b 100644 --- a/docs/advanced-features/custom-document.md +++ b/docs/advanced-features/custom-document.md @@ -41,7 +41,7 @@ Or add a `className` to the `body` tag: ## Caveats - The `` component used in `_document` is not the same as [`next/head`](/docs/api-reference/next/head.md). The `` component used here should only be used for any `` code that is common for all pages. For all other cases, such as `` tags, we recommend using [`next/head`](/docs/api-reference/next/head.md) in your pages or components. -- React components outside of `<Main />` will not be initialized by the browser. Do _not_ add application logic here or custom CSS (like `styled-jsx`). If you need shared components in all your pages (like a menu or a toolbar), read [Layouts](/docs/basic-features/layouts.md) intead. +- React components outside of `<Main />` will not be initialized by the browser. Do _not_ add application logic here or custom CSS (like `styled-jsx`). If you need shared components in all your pages (like a menu or a toolbar), read [Layouts](/docs/basic-features/layouts.md) instead. - `Document` currently does not support Next.js [Data Fetching methods](/docs/basic-features/data-fetching/overview.md) like [`getStaticProps`](/docs/basic-features/data-fetching/get-static-props.md) or [`getServerSideProps`](/docs/basic-features/data-fetching/get-server-side-props.md). ## Customizing `renderPage` diff --git a/docs/advanced-features/error-handling.md b/docs/advanced-features/error-handling.md new file mode 100644 index 000000000000..a164c58e4839 --- /dev/null +++ b/docs/advanced-features/error-handling.md @@ -0,0 +1,104 @@ +--- +description: Handle errors in your Next.js app. +--- + +# Error Handling + +This documentation explains how you can handle development, server-side, and client-side errors. + +## Handling Errors in Development + +When there is a runtime error during the development phase of your Next.js application, you will encounter an **overlay**. It is a modal that covers the webpage. It is only visible when the development server runs using `next dev`, `npm run dev`, or `yarn dev` and not in production. Fixing the error will automatically dismiss the overlay. + +Here is an example of an overlay: + +![Example of an overlay when in development mode](https://assets.vercel.com/image/upload/v1645118290/docs-assets/static/docs/error-handling/overlay.png) + +## Handling Server Errors + +Next.js provides a static 500 page by default to handle server-side errors that occur in your application. You can also [customize this page](/docs/advanced-features/custom-error-page#customizing-the-500-page) by creating a `pages/500.js` file. + +Having a 500 page in your application does not show specific errors to the app user. + +You can also use [404 page](/docs/advanced-features/custom-error-page#404-page) to handle specific runtime error like `file not found`. + +## Handling Client Errors + +React [Error Boundaries](https://reactjs.org/docs/error-boundaries.html) is a graceful way to handle a JavaScript error on the client so that the other parts of the application continue working. In addition to preventing the page from crashing, it allows you to provide a custom fallback component and even log error information. + +To use Error Boundaries for your Next.js application, you must create a class component `ErrorBoundary` and wrap the `Component` prop in the `pages/_app.js` file. This component will be responsible to: + +- Render a fallback UI after an error is thrown +- Provide a way to reset the Application's state +- Log error information + +You can create an `ErrorBoundary` class component by extending `React.Component`. For example: + +```jsx +class ErrorBoundary extends React.Component { + constructor(props) { + super(props) + + // Define a state variable to track whether is an error or not + this.state = { hasError: false } + } + static getDerivedStateFromError(error) { + // Update state so the next render will show the fallback UI + + return { hasError: true } + } + componentDidCatch(error, errorInfo) { + // You can use your own error logging service here + console.log({ error, errorInfo }) + } + render() { + // Check if the error is thrown + if (this.state.hasError) { + // You can render any custom fallback UI + return ( + <div> + <h2>Oops, there is an error!</h2> + <button + type="button" + onClick={() => this.setState({ hasError: false })} + > + Try again? + </button> + </div> + ) + } + + // Return children components in case of no error + + return this.props.children + } +} + +export default ErrorBoundary +``` + +The `ErrorBoundary` component keeps track of an `hasError` state. The value of this state variable is a boolean. When the value of `hasError` is `true`, then the `ErrorBoundary` component will render a fallback UI. Otherwise, it will render the children components. + +After creating an `ErrorBoundary` component, import it in the `pages/_app.js` file to wrap the `Component` prop in your Next.js application. + +```jsx +// Import the ErrorBoundary component +import ErrorBoundary from '../components/ErrorBoundary' + +function MyApp({ Component, pageProps }) { + return ( + // Wrap the Component prop with ErrorBoundary component + <ErrorBoundary FallbackComponent={ErrorFallback}> + <Component {...pageProps} /> + </ErrorBoundary> + ) +} + +export default MyApp +``` + +You can learn more about [Error Boundaries](https://reactjs.org/docs/error-boundaries.html) in React's documentation. + +### Reporting Errors + +To monitor client errors, use a service like [Sentry](https://github.com/vercel/next.js/tree/canary/examples/with-sentry), Bugsnag or Datadog. diff --git a/docs/advanced-features/react-18/streaming.md b/docs/advanced-features/react-18/streaming.md index 544759b3daa6..984264190dc5 100644 --- a/docs/advanced-features/react-18/streaming.md +++ b/docs/advanced-features/react-18/streaming.md @@ -1,7 +1,7 @@ # Streaming SSR (Alpha) React 18 will include architectural improvements to React server-side rendering (SSR) performance. This means you can use `Suspense` in your React components in streaming SSR mode and React will render them on the server and send them through HTTP streams. -It's worth noting that another experimental feature, React Server Components, is based on streaming. You can read more about server components related streaming APIs in [`next/streaming`](docs/api-reference/next/streaming.md). However, this guide focuses on basic React 18 streaming. +It's worth noting that another experimental feature, React Server Components, is based on streaming. You can read more about server components related streaming APIs in [`next/streaming`](/docs/api-reference/next/streaming.md). However, this guide focuses on basic React 18 streaming. ## Enable Streaming SSR diff --git a/docs/advanced-features/static-html-export.md b/docs/advanced-features/static-html-export.md index 6408adf0b7c6..a2a7a7f2b80a 100644 --- a/docs/advanced-features/static-html-export.md +++ b/docs/advanced-features/static-html-export.md @@ -27,7 +27,7 @@ Update your build script in `package.json` to use `next export`: Running `npm run build` will generate an `out` directory. -`next export` builds an HTML version of your app. During `next build`, [`getStaticProps`](/docs/basic-features/data-fetching/get-static-props.md) and [`getStaticPaths`](/docs/basic-features/data-fetching/get-static-paths.md) will generate an HTML file for each page in your `pages` directory (or more for [dynamic routes](/docs/routing/dynamic-routes.md). Then, `next export` will copy the already exported files into the correct directory. `getInitialProps` will generate the HTML files during `next export` instead of `next build`. +`next export` builds an HTML version of your app. During `next build`, [`getStaticProps`](/docs/basic-features/data-fetching/get-static-props.md) and [`getStaticPaths`](/docs/basic-features/data-fetching/get-static-paths.md) will generate an HTML file for each page in your `pages` directory (or more for [dynamic routes](/docs/routing/dynamic-routes.md)). Then, `next export` will copy the already exported files into the correct directory. `getInitialProps` will generate the HTML files during `next export` instead of `next build`. For more advanced scenarios, you can define a parameter called [`exportPathMap`](/docs/api-reference/next.config.js/exportPathMap.md) in your [`next.config.js`](/docs/api-reference/next.config.js/introduction.md) file to configure exactly which pages will be generated. diff --git a/docs/advanced-features/using-mdx.md b/docs/advanced-features/using-mdx.md index 8087473ca22d..6ed9b3f5756f 100644 --- a/docs/advanced-features/using-mdx.md +++ b/docs/advanced-features/using-mdx.md @@ -178,7 +178,7 @@ Then setup the provider in your page import { MDXProvider } from '@mdx-js/react' import Image from 'next/image' -import { Heading, Text, Pre, Code, Table } from 'my-components' +import { Heading, InlineCode, Pre, Table, Text } from 'my-components' const ResponsiveImage = (props) => ( <Image alt={props.alt} layout="responsive" {...props} /> @@ -189,8 +189,8 @@ const components = { h1: Heading.H1, h2: Heading.H2, p: Text, - code: Pre, - inlineCode: Code, + pre: Pre, + code: InlineCode, } export default function Post(props) { diff --git a/docs/api-reference/create-next-app.md b/docs/api-reference/create-next-app.md index f463840b88b9..e291dd5e94f5 100644 --- a/docs/api-reference/create-next-app.md +++ b/docs/api-reference/create-next-app.md @@ -28,6 +28,7 @@ yarn create next-app --typescript - **-e, --example [name]|[github-url]** - An example to bootstrap the app with. You can use an example name from the [Next.js repo](https://github.com/vercel/next.js/tree/canary/examples) or a GitHub URL. The URL can use any branch and/or subdirectory. - **--example-path [path-to-example]** - In a rare case, your GitHub URL might contain a branch name with a slash (e.g. bug/fix-1) and the path to the example (e.g. foo/bar). In this case, you must specify the path to the example separately: `--example-path foo/bar` - **--use-npm** - Explicitly tell the CLI to bootstrap the app using npm. To bootstrap using yarn we recommend running `yarn create next-app` +- **--use-pnpm** - Explicitly tell the CLI to bootstrap the app using pnpm. To bootstrap using yarn we recommend running `yarn create next-app` ### Why use Create Next App? diff --git a/docs/api-reference/next.config.js/ignoring-typescript-errors.md b/docs/api-reference/next.config.js/ignoring-typescript-errors.md index 57335b30cf8c..a26cbc26cbc8 100644 --- a/docs/api-reference/next.config.js/ignoring-typescript-errors.md +++ b/docs/api-reference/next.config.js/ignoring-typescript-errors.md @@ -8,7 +8,7 @@ Next.js fails your **production build** (`next build`) when TypeScript errors ar If you'd like Next.js to dangerously produce production code even when your application has errors, you can disable the built-in type checking step. -> Be sure you are running type checks as part of your build or deploy process, otherwise this can be very dangerous. +If disabled, be sure you are running type checks as part of your build or deploy process, otherwise this can be very dangerous. Open `next.config.js` and enable the `ignoreBuildErrors` option in the `typescript` config: diff --git a/docs/api-reference/next.config.js/rewrites.md b/docs/api-reference/next.config.js/rewrites.md index 16ae2b533df8..47571668dfb2 100644 --- a/docs/api-reference/next.config.js/rewrites.md +++ b/docs/api-reference/next.config.js/rewrites.md @@ -300,15 +300,20 @@ module.exports = { <summary><b>Examples</b></summary> <ul> <li><a href="https://github.com/vercel/next.js/tree/canary/examples/custom-routes-proxying">Incremental adoption of Next.js</a></li> + <li><a href="https://github.com/vercel/next.js/tree/canary/examples/with-zones">Using Multiple Zones</a></li> </ul> </details> -Rewrites allow you to rewrite to an external url. This is especially useful for incrementally adopting Next.js. +Rewrites allow you to rewrite to an external url. This is especially useful for incrementally adopting Next.js. The following is an example rewrite for redirecting the `/blog` route of your main app to an external site. ```js module.exports = { async rewrites() { return [ + { + source: '/blog', + destination: 'https://example.com/blog', + }, { source: '/blog/:slug', destination: 'https://example.com/blog/:slug', // Matched parameters can be used in the destination @@ -318,6 +323,26 @@ module.exports = { } ``` +If you're using `trailingSlash: true`, you also need to insert a trailing slash in the `source` paramater. If the destination server is also expecting a trailing slash it should be included in the `destination` parameter as well. + +```js +module.exports = { + trailingSlash: 'true', + async rewrites() { + return [ + { + source: '/blog/', + destination: 'https://example.com/blog/', + }, + { + source: '/blog/:path*/', + destination: 'https://example.com/blog/:path*/', + }, + ] + }, +} +``` + ### Incremental adoption of Next.js You can also have Next.js fall back to proxying to an existing website after checking all Next.js routes. diff --git a/docs/api-reference/next/image.md b/docs/api-reference/next/image.md index 4905a0f1125a..4035fa8c65ec 100644 --- a/docs/api-reference/next/image.md +++ b/docs/api-reference/next/image.md @@ -14,16 +14,17 @@ description: Enable Image Optimization with the built-in Image component. <details> <summary><b>Version History</b></summary> -| Version | Changes | -| --------- | ------------------------------------------------------------------------------------------------- | -| `v12.1.0` | `dangerouslyAllowSVG` and `contentSecurityPolicy` configuration added. | -| `v12.0.9` | `lazyRoot` prop added. | -| `v12.0.0` | `formats` configuration added.<br/>AVIF support added.<br/>Wrapper `<div>` changed to `<span>`. | -| `v11.1.0` | `onLoadingComplete` and `lazyBoundary` props added. | -| `v11.0.0` | `src` prop support for static import.<br/>`placeholder` prop added.<br/>`blurDataURL` prop added. | -| `v10.0.5` | `loader` prop added. | -| `v10.0.1` | `layout` prop added. | -| `v10.0.0` | `next/image` introduced. | +| Version | Changes | +| --------- | ----------------------------------------------------------------------------------------------------- | +| `v12.1.1` | `style` prop added. Experimental[\*](#experimental-raw-layout-mode) support for `layout="raw"` added. | +| `v12.1.0` | `dangerouslyAllowSVG` and `contentSecurityPolicy` configuration added. | +| `v12.0.9` | `lazyRoot` prop added. | +| `v12.0.0` | `formats` configuration added.<br/>AVIF support added.<br/>Wrapper `<div>` changed to `<span>`. | +| `v11.1.0` | `onLoadingComplete` and `lazyBoundary` props added. | +| `v11.0.0` | `src` prop support for static import.<br/>`placeholder` prop added.<br/>`blurDataURL` prop added. | +| `v10.0.5` | `loader` prop added. | +| `v10.0.1` | `layout` prop added. | +| `v10.0.0` | `next/image` introduced. | </details> @@ -65,12 +66,13 @@ The `<Image />` component accepts a number of additional properties beyond those The layout behavior of the image as the viewport changes size. -| `layout` | Behavior | `srcSet` | `sizes` | -| --------------------- | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ------- | -| `intrinsic` (default) | Scale *down* to fit width of container, up to image size | `1x`, `2x` (based on [imageSizes](#image-sizes)) | N/A | -| `fixed` | Sized to `width` and `height` exactly | `1x`, `2x` (based on [imageSizes](#image-sizes)) | N/A | -| `responsive` | Scale to fit width of container | `640w`, `750w`, ... `2048w`, `3840w` (based on [imageSizes](#image-sizes) and [deviceSizes](#device-sizes)) | `100vw` | -| `fill` | Grow in both X and Y axes to fill container | `640w`, `750w`, ... `2048w`, `3840w` (based on [imageSizes](#image-sizes) and [deviceSizes](#device-sizes)) | `100vw` | +| `layout` | Behavior | `srcSet` | `sizes` | Has wrapper and sizer | +| ---------------------------------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | -------- | --------------------- | +| `intrinsic` (default) | Scale *down* to fit width of container, up to image size | `1x`, `2x` (based on [imageSizes](#image-sizes)) | N/A | yes | +| `fixed` | Sized to `width` and `height` exactly | `1x`, `2x` (based on [imageSizes](#image-sizes)) | N/A | yes | +| `responsive` | Scale to fit width of container | `640w`, `750w`, ... `2048w`, `3840w` (based on [imageSizes](#image-sizes) and [deviceSizes](#device-sizes)) | `100vw` | yes | +| `fill` | Grow in both X and Y axes to fill container | `640w`, `750w`, ... `2048w`, `3840w` (based on [imageSizes](#image-sizes) and [deviceSizes](#device-sizes)) | `100vw` | yes | +| `raw`[\*](#experimental-raw-layout-mode) | Insert the image element with no automatic layout behavior | Behaves like `responsive` if the image has the `sizes` prop, and like `fixed` if it does not | optional | no | - [Demo the `intrinsic` layout (default)](https://image-component.nextjs.gallery/layout-intrinsic) - When `intrinsic`, the image will scale the dimensions down for smaller viewports, but maintain the original dimensions for larger viewports. @@ -83,6 +85,9 @@ The layout behavior of the image as the viewport changes size. - When `fill`, the image will stretch both width and height to the dimensions of the parent element, provided the parent element is relative. - This is usually paired with the [`objectFit`](#objectFit) property. - Ensure the parent element has `position: relative` in their stylesheet. +- When `raw`[\*](#experimental-raw-layout-mode), the image will be rendered as a single image element with no wrappers, sizers or other responsive behavior. + - If your image styling will change the size of a `raw` image, you should include the `sizes` property for proper image serving. Otherwise your image will receive a fixed height and width. + - The other layout modes are optimized for performance and should cover nearly all use cases. It is recommended to try to use those modes before using `raw`. - [Demo background image](https://image-component.nextjs.gallery/background) ### loader @@ -121,7 +126,7 @@ const MyImage = (props) => { A string that provides information about how wide the image will be at different breakpoints. Defaults to `100vw` (the full width of the screen) when using `layout="responsive"` or `layout="fill"`. -If you are using `layout="fill"` or `layout="responsive"`, it's important to assign `sizes` for any image that takes up less than the full viewport width. +If you are using `layout="fill"`, `layout="responsive"`, or `layout="raw"`[\*](#experimental-raw-layout-mode) it's important to assign `sizes` for any image that takes up less than the full viewport width. For example, when the parent element will constrain the image to always be less than half the viewport width, use `sizes="50vw"`. Without `sizes`, the image will be sent at twice the necessary resolution, decreasing performance. @@ -162,6 +167,12 @@ Try it out: In some cases, you may need more advanced usage. The `<Image />` component optionally accepts the following advanced properties. +### style + +Allows [passing CSS styles](https://reactjs.org/docs/dom-elements.html#style) to the underlying image element. + +Note that all `layout` modes other than `"raw"`[\*](#experimental-raw-layout-mode) apply their own styles to the image element, and these automatic styles take precedence over the `style` prop. + ### objectFit Defines how the image will fit into its parent container when using `layout="fill"`. @@ -285,7 +296,6 @@ size, or format. Defaults to `false`. Other properties on the `<Image />` component will be passed to the underlying `img` element with the exception of the following: -- `style`. Use `className` instead. - `srcSet`. Use [Device Sizes](#device-sizes) instead. @@ -374,7 +384,7 @@ module.exports = { The default [Image Optimization API](#loader-configuration) will automatically detect the browser's supported image formats via the request's `Accept` header. -If the `Accept` head matches more than one of the configured formats, the first match in the array is used. Therefore, the array order matters. If there is no match, the Image Optimization API will fallback to the original image's format. +If the `Accept` head matches more than one of the configured formats, the first match in the array is used. Therefore, the array order matters. If there is no match (or the source image is [animated](#animated-images)), the Image Optimization API will fallback to the original image's format. If no configuration is provided, the default below is used. @@ -408,7 +418,7 @@ The expiration (or rather Max Age) is defined by either the [`minimumCacheTTL`]( - You can configure [`minimumCacheTTL`](#minimum-cache-ttl) to increase the cache duration when the upstream image does not include `Cache-Control` header or the value is very low. - You can configure [`deviceSizes`](#device-sizes) and [`imageSizes`](#device-sizes) to reduce the total number of possible generated images. -- You can configure [formats](/docs/basic-features/image-optimization.md#acceptable-formats) to disable multiple formats in favor of a single image format. +- You can configure [formats](#acceptable-formats) to disable multiple formats in favor of a single image format. ### Minimum Cache TTL @@ -455,6 +465,30 @@ module.exports = { } ``` +### Experimental "raw" layout mode + +The image component currently supports an additional `layout="raw"` mode, which renders the image without wrappers or styling. This layout mode is currently an experimental feature, while user feedback is gathered. As there is the possibility of breaking changes to the `layout="raw"` interface, the feature is locked behind an experimental feature flag. If you would like to use the `raw` layout mode, you must add the following to your `next.config.js`: + +```js +module.exports = { + experimental: { + images: { + layoutRaw: true, + }, + }, +} +``` + +> Note on CLS with `layout="raw"`: +> It is possible to cause [layout shift](https://web.dev/cls/) with the image component in `raw` mode. If you include a `sizes` property, the image component will not pass `height` and `width` attributes to the image, to allow you to apply your own responsive sizing. +> An [aspect-ratio](https://developer.mozilla.org/en-US/docs/Web/CSS/aspect-ratio) style property is automatically applied to prevent layout shift, but this won't apply on [older browsers](https://caniuse.com/mdn-css_properties_aspect-ratio). + +### Animated Images + +The default [loader](#loader) will automatically bypass Image Optimization for animated images and serve the image as-is. + +Auto-detection for animated files is best-effort and supports GIF, APNG, and WebP. If you want to explicitly bypass Image Optimization for a given animated image, use the [unoptimized](#unoptimized) prop. + ## Related For an overview of the Image component features and usage guidelines, see: diff --git a/docs/api-reference/next/script.md b/docs/api-reference/next/script.md index 79c83341f887..7ef25eca3c3e 100644 --- a/docs/api-reference/next/script.md +++ b/docs/api-reference/next/script.md @@ -41,6 +41,9 @@ The loading strategy of the script. | `beforeInteractive` | Load script before the page becomes interactive | | `afterInteractive` | Load script immediately after the page becomes interactive | | `lazyOnload` | Load script during browser idle time | +| `worker` | Load script in a web worker | + +> **Note: `worker` is an experimental strategy that can only be used when enabled in `next.config.js`. See [Off-loading Scripts To A Web Worker](/docs/basic-features/script#off-loading-scripts-to-a-web-worker-experimental).** ### onLoad diff --git a/docs/api-routes/api-middlewares.md b/docs/api-routes/api-middlewares.md index 40b15ac2f718..1815a1192dea 100644 --- a/docs/api-routes/api-middlewares.md +++ b/docs/api-routes/api-middlewares.md @@ -68,6 +68,29 @@ export const config = { } ``` +`responseLimit` is automatically enabled, warning when an API routes' response body is over 4MB. + +If you are not using Next.js in a serverless environment, and understand the performance implications of not using a CDN or dedicated media host, you can set this limit to `false`. + +```js +export const config = { + api: { + responseLimit: false, + }, +} +``` + +`responseLimit` can also take the number of bytes or any string format supported by `bytes`, for example `1000`, `'500kb'` or `'3mb'`. +This value will be the maximum response size before a warning is displayed. Default is 4MB. (see above) + +```js +export const config = { + api: { + responseLimit: '8mb', + }, +} +``` + ## Connect/Express middleware support You can also use [Connect](https://github.com/senchalabs/connect) compatible middleware. diff --git a/docs/basic-features/built-in-css-support.md b/docs/basic-features/built-in-css-support.md index 822e64599c58..df4d254c93ef 100644 --- a/docs/basic-features/built-in-css-support.md +++ b/docs/basic-features/built-in-css-support.md @@ -153,7 +153,7 @@ You can use component-level Sass via CSS Modules and the `.module.scss` or `.mod Before you can use Next.js' built-in Sass support, be sure to install [`sass`](https://github.com/sass/sass): ```bash -npm install sass +npm install --save-dev sass ``` Sass support has the same benefits and restrictions as the built-in CSS support detailed above. diff --git a/docs/basic-features/data-fetching/get-static-paths.md b/docs/basic-features/data-fetching/get-static-paths.md index ecb2e7f57c8c..a5fa8d9c7453 100644 --- a/docs/basic-features/data-fetching/get-static-paths.md +++ b/docs/basic-features/data-fetching/get-static-paths.md @@ -35,15 +35,15 @@ You should use `getStaticPaths` if you’re statically pre-rendering pages that ## When does getStaticPaths run -`getStaticPaths` always runs on the server and never on the client. You can validate code written inside `getStaticPaths` is removed from the client-side bundle [with this tool](https://next-code-elimination.vercel.app/). +`getStaticPaths` will only run during build in production, it will not be called during runtime. You can validate code written inside `getStaticPaths` is removed from the client-side bundle [with this tool](https://next-code-elimination.vercel.app/). -- `getStaticPaths` runs during `next build` for Pages included in `paths` -- `getStaticPaths` runs on-demand in the background when using `fallback: true` -- `getStaticPaths` runs on-demand blocking rendering when using `fallback: blocking` +- `getStaticProps` runs during `next build` for any `paths` returned during build +- `getStaticProps` runs in the background when using `fallback: true` +- `getStaticProps` is called before initial render when using `fallback: blocking` ## Where can I use getStaticPaths -`getStaticPaths` can only be exported from a **page**. You **cannot** export it from non-page files. +`getStaticPaths` can only be exported from a [dynamic route](/docs/routing/dynamic-routes.md) that also uses `getStaticProps`. You **cannot** export it from non-page files e.g. from your `components` folder. Note that you must use export `getStaticPaths` as a standalone function — it will **not** work if you add `getStaticPaths` as a property of the page component. diff --git a/docs/basic-features/image-optimization.md b/docs/basic-features/image-optimization.md index 0c7cb5a6a292..9de528f1910b 100644 --- a/docs/basic-features/image-optimization.md +++ b/docs/basic-features/image-optimization.md @@ -181,7 +181,7 @@ The image component has several different [layout modes](/docs/api-reference/nex **Target the image with className, not based on DOM structure** -Regardless of the layout mode used, the Image component will have a consistent DOM structure of one `<img>` tag wrapped by exactly one `<span>`. For some modes, it may also have a sibling `<span>` for spacing. These additional `<span>` elements are critical to allow the component to prevent layout shifts. +For most layout modes, the Image component will have a DOM structure of one `<img>` tag wrapped by exactly one `<span>`. For some modes, it may also have a sibling `<span>` for spacing. These additional `<span>` elements are critical to allow the component to prevent layout shifts. The recommended way to style the inner `<img>` is to set the `className` prop on the Image component to the value of an imported [CSS Module](/docs/basic-features/built-in-css-support.md#adding-component-level-css). The value of `className` will be automatically applied to the underlying `<img>` element. @@ -189,8 +189,6 @@ Alternatively, you can import a [global stylesheet](/docs/basic-features/built-i You cannot use [styled-jsx](/docs/basic-features/built-in-css-support.md#css-in-js) because it's scoped to the current component. -You cannot use the `style` prop because the `<Image>` component does not pass it through to the underlying `<img>`. - **When using `layout='fill'`, the parent element must have `position: relative`** This is necessary for the proper rendering of the image element in that layout mode. diff --git a/docs/basic-features/script.md b/docs/basic-features/script.md index e8642346389e..70839f6a457b 100644 --- a/docs/basic-features/script.md +++ b/docs/basic-features/script.md @@ -67,8 +67,9 @@ With `next/script`, you decide when to load your third-party script by using the There are three different loading strategies that can be used: - `beforeInteractive`: Load before the page is interactive -- `afterInteractive`: (**default**): Load immediately after the page becomes interactive +- `afterInteractive`: (**default**) Load immediately after the page becomes interactive - `lazyOnload`: Load during idle time +- `worker`: (experimental) Load in a web worker #### beforeInteractive @@ -123,6 +124,87 @@ Examples of scripts that do not need to load immediately and can be lazy-loaded - Chat support plugins - Social media widgets +### Off-loading Scripts To A Web Worker (experimental) + +> **Note: The `worker` strategy is not yet stable and can cause unexpected issues in your application. Use with caution.** + +Scripts that use the `worker` strategy are relocated and executed in a web worker with [Partytown](https://partytown.builder.io/). This can improve the performance of your site by dedicating the main thread to the rest of your application code. + +This strategy is still experimental and can only be used if the `nextScriptWorkers` flag is enabled in `next.config.js`: + +```js +module.exports = { + experimental: { + nextScriptWorkers: true, + }, +} +``` + +Then, run `next` (normally `npm run dev` or `yarn dev`) and Next.js will guide you through the installation of the required packages to finish the setup: + +```bash +npm run dev + +# You'll see instructions like these: +# +# Please install Partytown by running: +# +# npm install @builder.io/partytown +# +# ... +``` + +Once setup is complete, defining `strategy="worker` will automatically instantiate Partytown in your application and off-load the script to a web worker. + +```jsx +<Script src="https://example.com/analytics.js" strategy="worker" /> +``` + +There are a number of trade-offs that need to be considered when loading a third-party script in a web worker. Please see Partytown's [Trade-Offs](https://partytown.builder.io/trade-offs) documentation for more information. + +#### Configuration + +Although the `worker` strategy does not require any additional configuration to work, Partytown supports the use of a config object to modify some of its settings, including enabling `debug` mode and forwarding events and triggers. + +If you would like to add additonal configuration options, you can include it within the `<Head />` component used in a [custom `_document.js`](/docs/advanced-features/custom-document.md): + +```jsx +import { Html, Head, Main, NextScript } from 'next/document' + +export default function Document() { + return ( + <Html> + <Head> + <script + data-partytown-config + dangerouslySetInnerHTML={{ + __html: ` + partytown = { + lib: "/_next/static/~partytown/", + debug: true + }; + `, + }} + /> + </Head> + <body> + <Main /> + <NextScript /> + </body> + </Html> + ) +} +``` + +In order to modify Partytown's configuration, the following conditions must be met: + +1. The `data-partytown-config` attribute must be used in order to overwrite the default configuration used by Next.js +2. Unless you decide to save Partytown's library files in a separate directory, the `lib: "/_next/static/~partytown/"` property and value must be included in the configuration object in order to let Partytown know where Next.js stores the necessary static files. + +> **Note**: If you are using an [asset prefix](/docs/api-reference/next.config.js/cdn-support-with-asset-prefix.md) and would like to modify Partytown's default configuration, you must include it as part of the `lib` path. + +Take a look at Partytown's [configuration options](https://partytown.builder.io/configuration) to see the full list of other properties that can be added. + ### Inline Scripts Inline scripts, or scripts not loaded from an external file, are also supported by the Script component. They can be written by placing the JavaScript within curly braces: diff --git a/docs/basic-features/typescript.md b/docs/basic-features/typescript.md index db23d29dad07..9869dc850e43 100644 --- a/docs/basic-features/typescript.md +++ b/docs/basic-features/typescript.md @@ -5,14 +5,19 @@ description: Next.js supports TypeScript by default and has built-in types for p # TypeScript <details> - <summary><b>Examples</b></summary> - <ul> - <li><a href="https://github.com/vercel/next.js/tree/canary/examples/with-typescript">TypeScript</a></li> - </ul> + <summary><b>Version History</b></summary> + +| Version | Changes | +| --------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| `v12.0.0` | [SWC](https://nextjs.org/docs/advanced-features/compiler) is now used by default to compile TypeScript and TSX for faster builds. | +| `v10.2.1` | [Incremental type checking](https://www.typescriptlang.org/tsconfig#incremental) support added when enabled in your `tsconfig.json`. | + </details> -Next.js provides an integrated [TypeScript](https://www.typescriptlang.org/) -experience out of the box, similar to an IDE. +Next.js provides an integrated [TypeScript](https://www.typescriptlang.org/) experience, including zero-configuration set up and built-in types for Pages, APIs, and more. + +- [Clone and deploy the TypeScript starter](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fwith-typescript&project-name=with-typescript&repository-name=with-typescript) +- [View an example application](https://github.com/vercel/next.js/tree/canary/examples/with-typescript) ## `create-next-app` support @@ -120,26 +125,11 @@ export default (req: NextApiRequest, res: NextApiResponse<Data>) => { If you have a [custom `App`](/docs/advanced-features/custom-app.md), you can use the built-in type `AppProps` and change file name to `./pages/_app.tsx` like so: ```ts -// import App from "next/app"; -import type { AppProps /*, AppContext */ } from 'next/app' +import type { AppProps } from 'next/app' -function MyApp({ Component, pageProps }: AppProps) { +export default function MyApp({ Component, pageProps }: AppProps) { return <Component {...pageProps} /> } - -// Only uncomment this method if you have blocking data requirements for -// every single page in your application. This disables the ability to -// perform automatic static optimization, causing every page in your app to -// be server-side rendered. -// -// MyApp.getInitialProps = async (appContext: AppContext) => { -// // calls page's `getInitialProps` and fills `appProps.pageProps` -// const appProps = await App.getInitialProps(appContext); - -// return { ...appProps } -// } - -export default MyApp ``` ## Path aliases and baseUrl @@ -170,3 +160,25 @@ module.exports = nextConfig Since `v10.2.1` Next.js supports [incremental type checking](https://www.typescriptlang.org/tsconfig#incremental) when enabled in your `tsconfig.json`, this can help speed up type checking in larger applications. It is highly recommended to be on at least `v4.3.2` of TypeScript to experience the [best performance](https://devblogs.microsoft.com/typescript/announcing-typescript-4-3/#lazier-incremental) when leveraging this feature. + +## Ignoring TypeScript Errors + +Next.js fails your **production build** (`next build`) when TypeScript errors are present in your project. + +If you'd like Next.js to dangerously produce production code even when your application has errors, you can disable the built-in type checking step. + +If disabled, be sure you are running type checks as part of your build or deploy process, otherwise this can be very dangerous. + +Open `next.config.js` and enable the `ignoreBuildErrors` option in the `typescript` config: + +```js +module.exports = { + typescript: { + // !! WARN !! + // Dangerously allow production builds to successfully complete even if + // your project has type errors. + // !! WARN !! + ignoreBuildErrors: true, + }, +} +``` diff --git a/docs/going-to-production.md b/docs/going-to-production.md index f65f055d0677..c0527b966a54 100644 --- a/docs/going-to-production.md +++ b/docs/going-to-production.md @@ -69,6 +69,12 @@ export async function getServerSideProps({ req, res }) { } ``` +By default, `Cache-Control` headers will be set differently depending on how your page fetches data. + +If the page is using `getServerSideProps` or `getInitialProps`, then it will use the default `Cache-Control` header configured by `next start` in order to prevent accidental caching of responses that cannot be cached. If you want a different cache behavior while using SSR you can use `res.setHeader('Cache-Control', 'value_you_prefer')`. + +If the page is using `getStaticProps` or automatic static optimization, then it will have s-maxage=REVALIDATE_SECONDS, stale-while-revalidate or if revalidate is not used s-maxage=31536000, stale-while-revalidate. + > **Note:** Your deployment provider must support edge caching for dynamic responses. If you are self-hosting, you will need to add this logic to the edge yourself using a key/value store. If you are using Vercel, [edge caching works without configuration](https://vercel.com/docs/edge-network/caching). ## Reducing JavaScript Size diff --git a/docs/manifest.json b/docs/manifest.json index 208cbe9f906f..d2f8c5cca592 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -267,6 +267,10 @@ "title": "Debugging", "path": "/docs/advanced-features/debugging.md" }, + { + "title": "Error Handling", + "path": "/docs/advanced-features/error-handling.md" + }, { "title": "Source Maps", "path": "/docs/advanced-features/source-maps.md" diff --git a/docs/routing/shallow-routing.md b/docs/routing/shallow-routing.md index d1703af6b6e1..534804bb8369 100644 --- a/docs/routing/shallow-routing.md +++ b/docs/routing/shallow-routing.md @@ -54,7 +54,7 @@ componentDidUpdate(prevProps) { ## Caveats -Shallow routing **only** works for same page URL changes. For example, let's assume we have another page called `pages/about.js`, and you run this: +Shallow routing **only** works for URL changes in the current page. For example, let's assume we have another page called `pages/about.js`, and you run this: ```jsx router.push('/?counter=10', '/about?counter=10', { shallow: true }) diff --git a/docs/testing.md b/docs/testing.md index 2b3c6f692bca..500016c6bff8 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -232,7 +232,7 @@ Run `npm run build` and `npm run start`, then run `npm run test:e2e` in another ### Running Playwright on Continuous Integration (CI) -Playwright will by default run your tests in the [headless mode]https://playwright.dev/docs/ci#running-headed). To install all the Playwright dependencies, run `npx playwright install-deps`. +Playwright will by default run your tests in the [headless mode](https://playwright.dev/docs/ci#running-headed). To install all the Playwright dependencies, run `npx playwright install-deps`. You can learn more about Playwright and Continuous Integration from these resources: diff --git a/errors/api-routes-body-size-limit.md b/errors/api-routes-response-size-limit.md similarity index 93% rename from errors/api-routes-body-size-limit.md rename to errors/api-routes-response-size-limit.md index 5697768c4a08..2b557cc852ac 100644 --- a/errors/api-routes-body-size-limit.md +++ b/errors/api-routes-response-size-limit.md @@ -1,4 +1,4 @@ -# API Routes Body Size Limited to 4MB +# API Routes Response Size Limited to 4MB #### Why This Error Occurred diff --git a/errors/manifest.json b/errors/manifest.json index d8afb228b260..d80ef5787008 100644 --- a/errors/manifest.json +++ b/errors/manifest.json @@ -41,8 +41,14 @@ "path": "/errors/amp-export-validation.md" }, { - "title": "api-routes-body-size-limit", - "path": "/errors/api-routes-body-size-limit.md" + "path": "/errors/api-routes-body-size-limit.md", + "redirect": { + "destination": "/docs/messages/api-routes-response-size-limit" + } + }, + { + "title": "api-routes-response-size-limit", + "path": "/errors/api-routes-response-size-limit.md" }, { "title": "api-routes-static-export", diff --git a/errors/react-hydration-error.md b/errors/react-hydration-error.md index 38ea8e05540b..5b0ae7b6a500 100644 --- a/errors/react-hydration-error.md +++ b/errors/react-hydration-error.md @@ -41,8 +41,9 @@ Common causes with css-in-js libraries: - When using Styled Components / Emotion - When css-in-js libraries are not set up for pre-rendering (SSR/SSG) it will often lead to a hydration mismatch. In general this means the application has to follow the Next.js example for the library. For example if `pages/_document` is missing and the Babel plugin is not added. - - Possible fix for Styled Components: https://github.com/vercel/next.js/tree/canary/examples/with-styled-components - - If you want to leverage Styled Components with the new Next.js Compiler in Next.js 12 there is an [experimental flag available](https://github.com/vercel/next.js/discussions/30174#discussion-3643870) + - Possible fix for Styled Components: + - If you want to leverage Styled Components with SWC in Next.js 12.1+ you need to [add it to your Next.js config under compiler options](https://nextjs.org/docs/advanced-features/compiler#styled-components): https://github.com/vercel/next.js/tree/canary/examples/with-styled-components + - If you want to use Styled Components with Babel, you need `pages/_document` and the Babel plugin: https://github.com/vercel/next.js/tree/canary/examples/with-styled-components-babel - Possible fix for Emotion: https://github.com/vercel/next.js/tree/canary/examples/with-emotion - When using other css-in-js libraries - Similar to Styled Components / Emotion css-in-js libraries generally need configuration specified in their examples in the [examples directory](https://github.com/vercel/next.js/tree/canary/examples) diff --git a/examples/blog-starter/components/avatar.js b/examples/blog-starter/components/avatar.js index 2dcc7eee1069..106d1e0bba9f 100644 --- a/examples/blog-starter/components/avatar.js +++ b/examples/blog-starter/components/avatar.js @@ -1,7 +1,14 @@ +import Image from 'next/image' export default function Avatar({ name, picture }) { return ( <div className="flex items-center"> - <img src={picture} className="w-12 h-12 rounded-full mr-4" alt={name} /> + <Image + src={picture} + width={48} + height={48} + className="w-12 h-12 rounded-full mr-4" + alt={name} + /> <div className="text-xl font-bold">{name}</div> </div> ) diff --git a/examples/blog-starter/components/cover-image.js b/examples/blog-starter/components/cover-image.js index 04832ba5107b..d3e21d21f161 100644 --- a/examples/blog-starter/components/cover-image.js +++ b/examples/blog-starter/components/cover-image.js @@ -18,7 +18,7 @@ export default function CoverImage({ title, src, slug, height, width }) { return ( <div className="sm:mx-0"> {slug ? ( - <Link as={`/posts/${slug}`} href="/posts/[slug]"> + <Link href={`/posts/${slug}`}> <a aria-label={title}>{image}</a> </Link> ) : ( diff --git a/examples/blog-starter/components/hero-post.js b/examples/blog-starter/components/hero-post.js index ddb182a8b716..6dc49f20a8bb 100644 --- a/examples/blog-starter/components/hero-post.js +++ b/examples/blog-starter/components/hero-post.js @@ -25,7 +25,7 @@ export default function HeroPost({ <div className="md:grid md:grid-cols-2 md:gap-x-16 lg:gap-x-8 mb-20 md:mb-28"> <div> <h3 className="mb-4 text-4xl lg:text-6xl leading-tight"> - <Link as={`/posts/${slug}`} href="/posts/[slug]"> + <Link href={`/posts/${slug}`}> <a className="hover:underline">{title}</a> </Link> </h3> diff --git a/examples/blog-starter/components/post-preview.js b/examples/blog-starter/components/post-preview.js index 3e3009fa2721..712eea6e92ec 100644 --- a/examples/blog-starter/components/post-preview.js +++ b/examples/blog-starter/components/post-preview.js @@ -23,7 +23,7 @@ export default function PostPreview({ /> </div> <h3 className="text-3xl mb-3 leading-snug"> - <Link as={`/posts/${slug}`} href="/posts/[slug]"> + <Link href={`/posts/${slug}`}> <a className="hover:underline">{title}</a> </Link> </h3> diff --git a/examples/blog-starter/package.json b/examples/blog-starter/package.json index ee9d9ba4dc95..29d3f61c590c 100644 --- a/examples/blog-starter/package.json +++ b/examples/blog-starter/package.json @@ -16,8 +16,8 @@ "remark-html": "15.0.1" }, "devDependencies": { - "autoprefixer": "^10.4.0", - "postcss": "^8.4.4", - "tailwindcss": "^3.0.1" + "autoprefixer": "^10.4.2", + "postcss": "^8.4.7", + "tailwindcss": "^3.0.23" } } diff --git a/examples/blog/theme.config.js b/examples/blog/theme.config.js index fb56fe005f6d..993a7b2d7f42 100644 --- a/examples/blog/theme.config.js +++ b/examples/blog/theme.config.js @@ -2,20 +2,19 @@ const YEAR = new Date().getFullYear() export default { footer: ( - <small style={{ display: 'block', marginTop: '8rem' }}> - <time>{YEAR}</time> © Your Name. - <a href="/feed.xml">RSS</a> + <footer> + <small> + <time>{YEAR}</time> © Your Name. + <a href="/feed.xml">RSS</a> + </small> <style jsx>{` + footer { + margin-top: 8rem; + } a { float: right; } - @media screen and (max-width: 480px) { - article { - padding-top: 2rem; - padding-bottom: 4rem; - } - } `}</style> - </small> + </footer> ) } diff --git a/examples/cms-agilitycms/components/hero-post.js b/examples/cms-agilitycms/components/hero-post.js index 141b7e641751..d6e8869ddc2b 100644 --- a/examples/cms-agilitycms/components/hero-post.js +++ b/examples/cms-agilitycms/components/hero-post.js @@ -20,7 +20,7 @@ export default function HeroPost({ slug={slug} /> </div> - <div className="md:grid md:grid-cols-2 md:col-gap-16 lg:col-gap-8 mb-20 md:mb-28"> + <div className="md:grid md:grid-cols-2 md:gap-x-16 lg:gap-x-8 mb-20 md:mb-28"> <div> <h3 className="mb-4 text-4xl lg:text-6xl leading-tight"> <Link href={`/posts/${slug}`}> diff --git a/examples/cms-agilitycms/components/more-stories.js b/examples/cms-agilitycms/components/more-stories.js index 12c3cbc50f93..a4e0fdf761af 100644 --- a/examples/cms-agilitycms/components/more-stories.js +++ b/examples/cms-agilitycms/components/more-stories.js @@ -6,7 +6,7 @@ export default function MoreStories({ title, posts }) { <h2 className="mb-8 text-6xl md:text-7xl font-bold tracking-tighter leading-tight"> {title} </h2> - <div className="grid grid-cols-1 md:grid-cols-2 md:col-gap-16 lg:col-gap-32 row-gap-20 md:row-gap-32 mb-32"> + <div className="grid grid-cols-1 md:grid-cols-2 md:gap-x-16 lg:gap-x-32 gap-y-20 md:gap-y-32 mb-32"> {posts.map((post) => ( <PostPreview key={post.slug} diff --git a/examples/cms-builder-io/components/hero-post.js b/examples/cms-builder-io/components/hero-post.js index 680afd982901..0a7b8f68f7fb 100644 --- a/examples/cms-builder-io/components/hero-post.js +++ b/examples/cms-builder-io/components/hero-post.js @@ -16,7 +16,7 @@ export default function HeroPost({ <div className="mb-8 md:mb-16"> <CoverImage title={title} slug={slug} url={coverImage} /> </div> - <div className="md:grid md:grid-cols-2 md:col-gap-16 lg:col-gap-8 mb-20 md:mb-28"> + <div className="md:grid md:grid-cols-2 md:gap-x-16 lg:gap-x-8 mb-20 md:mb-28"> <div> <h3 className="mb-4 text-4xl lg:text-6xl leading-tight"> <Link href={`/posts/${slug}`}> diff --git a/examples/cms-builder-io/components/more-stories.js b/examples/cms-builder-io/components/more-stories.js index bcf78e019cd2..45714977feea 100644 --- a/examples/cms-builder-io/components/more-stories.js +++ b/examples/cms-builder-io/components/more-stories.js @@ -6,7 +6,7 @@ export default function MoreStories({ posts }) { <h2 className="mb-8 text-6xl md:text-7xl font-bold tracking-tighter leading-tight"> More Stories </h2> - <div className="grid grid-cols-1 md:grid-cols-2 md:col-gap-16 lg:col-gap-32 row-gap-20 md:row-gap-32 mb-32"> + <div className="grid grid-cols-1 md:grid-cols-2 md:gap-x-16 lg:gap-x-32 gap-y-20 md:gap-y-32 mb-32"> {posts.map((post) => ( <PostPreview key={post.data.slug} diff --git a/examples/cms-buttercms/components/hero-post.js b/examples/cms-buttercms/components/hero-post.js index a7a43667a016..f45ed8853b12 100644 --- a/examples/cms-buttercms/components/hero-post.js +++ b/examples/cms-buttercms/components/hero-post.js @@ -16,7 +16,7 @@ export default function HeroPost({ <div className="mb-8 md:mb-16"> <CoverImage title={title} url={coverImage} slug={slug} /> </div> - <div className="md:grid md:grid-cols-2 md:col-gap-16 lg:col-gap-8 mb-20 md:mb-28"> + <div className="md:grid md:grid-cols-2 md:gap-x-16 lg:gap-x-8 mb-20 md:mb-28"> <div> <h3 className="mb-4 text-4xl lg:text-6xl leading-tight"> <Link href={`/posts/${slug}`}> diff --git a/examples/cms-buttercms/components/more-stories.js b/examples/cms-buttercms/components/more-stories.js index 6f90f9b01c7a..b12d7771a65a 100644 --- a/examples/cms-buttercms/components/more-stories.js +++ b/examples/cms-buttercms/components/more-stories.js @@ -6,7 +6,7 @@ export default function MoreStories({ posts }) { <h2 className="mb-8 text-6xl md:text-7xl font-bold tracking-tighter leading-tight"> More Stories </h2> - <div className="grid grid-cols-1 md:grid-cols-2 md:col-gap-16 lg:col-gap-32 row-gap-20 md:row-gap-32 mb-32"> + <div className="grid grid-cols-1 md:grid-cols-2 md:gap-x-16 lg:gap-x-32 gap-y-20 md:gap-y-32 mb-32"> {posts.map((post) => ( <PostPreview key={post.slug} diff --git a/examples/cms-contentful/components/hero-post.js b/examples/cms-contentful/components/hero-post.js index 5b71cfaf43b2..5a1551333ae4 100644 --- a/examples/cms-contentful/components/hero-post.js +++ b/examples/cms-contentful/components/hero-post.js @@ -16,7 +16,7 @@ export default function HeroPost({ <div className="mb-8 md:mb-16"> <CoverImage title={title} slug={slug} url={coverImage.url} /> </div> - <div className="md:grid md:grid-cols-2 md:col-gap-16 lg:col-gap-8 mb-20 md:mb-28"> + <div className="md:grid md:grid-cols-2 md:gap-x-16 lg:gap-x-8 mb-20 md:mb-28"> <div> <h3 className="mb-4 text-4xl lg:text-6xl leading-tight"> <Link href={`/posts/${slug}`}> diff --git a/examples/cms-contentful/components/more-stories.js b/examples/cms-contentful/components/more-stories.js index dcdd9b4e6ae7..57fdbb6c4659 100644 --- a/examples/cms-contentful/components/more-stories.js +++ b/examples/cms-contentful/components/more-stories.js @@ -6,7 +6,7 @@ export default function MoreStories({ posts }) { <h2 className="mb-8 text-6xl md:text-7xl font-bold tracking-tighter leading-tight"> More Stories </h2> - <div className="grid grid-cols-1 md:grid-cols-2 md:col-gap-16 lg:col-gap-32 row-gap-20 md:row-gap-32 mb-32"> + <div className="grid grid-cols-1 md:grid-cols-2 md:gap-x-16 lg:gap-x-32 gap-y-20 md:gap-y-32 mb-32"> {posts.map((post) => ( <PostPreview key={post.slug} diff --git a/examples/cms-cosmic/components/hero-post.js b/examples/cms-cosmic/components/hero-post.js index 453b1816ee4e..e8f7823c2b03 100644 --- a/examples/cms-cosmic/components/hero-post.js +++ b/examples/cms-cosmic/components/hero-post.js @@ -16,7 +16,7 @@ export default function HeroPost({ <div className="mb-8 md:mb-16"> <CoverImage title={title} url={coverImage.imgix_url} slug={slug} /> </div> - <div className="md:grid md:grid-cols-2 md:col-gap-16 lg:col-gap-8 mb-20 md:mb-28"> + <div className="md:grid md:grid-cols-2 md:gap-x-16 lg:gap-x-8 mb-20 md:mb-28"> <div> <h3 className="mb-4 text-4xl lg:text-6xl leading-tight"> <Link href={`/posts/${slug}`}> diff --git a/examples/cms-cosmic/components/more-stories.js b/examples/cms-cosmic/components/more-stories.js index 2e25326c352d..35bb95e0ab66 100644 --- a/examples/cms-cosmic/components/more-stories.js +++ b/examples/cms-cosmic/components/more-stories.js @@ -6,7 +6,7 @@ export default function MoreStories({ posts }) { <h2 className="mb-8 text-6xl md:text-7xl font-bold tracking-tighter leading-tight"> More Stories </h2> - <div className="grid grid-cols-1 md:grid-cols-2 md:col-gap-16 lg:col-gap-32 row-gap-20 md:row-gap-32 mb-32"> + <div className="grid grid-cols-1 md:grid-cols-2 md:gap-x-16 lg:gap-x-32 gap-y-20 md:gap-y-32 mb-32"> {posts.map((post) => ( <PostPreview key={post.slug} diff --git a/examples/cms-datocms/components/hero-post.js b/examples/cms-datocms/components/hero-post.js index 133dc33e5869..0ca3e0cc4978 100644 --- a/examples/cms-datocms/components/hero-post.js +++ b/examples/cms-datocms/components/hero-post.js @@ -20,7 +20,7 @@ export default function HeroPost({ slug={slug} /> </div> - <div className="md:grid md:grid-cols-2 md:col-gap-16 lg:col-gap-8 mb-20 md:mb-28"> + <div className="md:grid md:grid-cols-2 md:gap-x-16 lg:gap-x-8 mb-20 md:mb-28"> <div> <h3 className="mb-4 text-4xl lg:text-6xl leading-tight"> <Link href={`/posts/${slug}`}> diff --git a/examples/cms-datocms/components/more-stories.js b/examples/cms-datocms/components/more-stories.js index 13a09bd8f729..4bab320f6537 100644 --- a/examples/cms-datocms/components/more-stories.js +++ b/examples/cms-datocms/components/more-stories.js @@ -6,7 +6,7 @@ export default function MoreStories({ posts }) { <h2 className="mb-8 text-6xl md:text-7xl font-bold tracking-tighter leading-tight"> More Stories </h2> - <div className="grid grid-cols-1 md:grid-cols-2 md:col-gap-16 lg:col-gap-32 row-gap-20 md:row-gap-32 mb-32"> + <div className="grid grid-cols-1 md:grid-cols-2 md:gap-x-16 lg:gap-x-32 gap-y-20 md:gap-y-32 mb-32"> {posts.map((post) => ( <PostPreview key={post.slug} diff --git a/examples/cms-drupal/components/hero-post.js b/examples/cms-drupal/components/hero-post.js index c5aa6d0a7660..cf3e657a9def 100644 --- a/examples/cms-drupal/components/hero-post.js +++ b/examples/cms-drupal/components/hero-post.js @@ -18,7 +18,7 @@ export default function HeroPost({ <CoverImage title={title} coverImage={coverImage} slug={slug} /> )} </div> - <div className="md:grid md:grid-cols-2 md:col-gap-16 lg:col-gap-8 mb-20 md:mb-28"> + <div className="md:grid md:grid-cols-2 md:gap-x-16 lg:gap-x-8 mb-20 md:mb-28"> <div> <h3 className="mb-4 text-4xl lg:text-6xl leading-tight"> <Link href={slug}> diff --git a/examples/cms-ghost/components/hero-post.js b/examples/cms-ghost/components/hero-post.js index 1d786d96d2be..6a91725afd64 100644 --- a/examples/cms-ghost/components/hero-post.js +++ b/examples/cms-ghost/components/hero-post.js @@ -22,7 +22,7 @@ export default function HeroPost({ height={1216} /> </div> - <div className="md:grid md:grid-cols-2 md:col-gap-16 lg:col-gap-8 mb-20 md:mb-28"> + <div className="md:grid md:grid-cols-2 md:gap-x-16 lg:gap-x-8 mb-20 md:mb-28"> <div> <h3 className="mb-4 text-4xl lg:text-6xl leading-tight"> <Link href={`/posts/${slug}`}> diff --git a/examples/cms-ghost/components/more-stories.js b/examples/cms-ghost/components/more-stories.js index 92eefcf51c36..07c12287d7cf 100644 --- a/examples/cms-ghost/components/more-stories.js +++ b/examples/cms-ghost/components/more-stories.js @@ -6,7 +6,7 @@ export default function MoreStories({ posts }) { <h2 className="mb-8 text-6xl md:text-7xl font-bold tracking-tighter leading-tight"> More Stories </h2> - <div className="grid grid-cols-1 md:grid-cols-2 md:col-gap-16 lg:col-gap-32 row-gap-20 md:row-gap-32 mb-32"> + <div className="grid grid-cols-1 md:grid-cols-2 md:gap-x-16 lg:gap-x-32 gap-y-20 md:gap-y-32 mb-32"> {posts.map((post) => ( <PostPreview key={post.slug} diff --git a/examples/cms-graphcms/components/hero-post.js b/examples/cms-graphcms/components/hero-post.js index 9f39ead6849e..776866d8dc76 100644 --- a/examples/cms-graphcms/components/hero-post.js +++ b/examples/cms-graphcms/components/hero-post.js @@ -16,7 +16,7 @@ export default function HeroPost({ <div className="mb-8 md:mb-16"> <CoverImage slug={slug} title={title} url={coverImage.url} /> </div> - <div className="mb-20 md:grid md:grid-cols-2 md:col-gap-16 lg:col-gap-8 md:mb-28"> + <div className="mb-20 md:grid md:grid-cols-2 md:gap-x-16 lg:gap-x-8 md:mb-28"> <div> <h3 className="mb-4 text-4xl leading-tight lg:text-6xl"> <Link href={`/posts/${slug}`}> diff --git a/examples/cms-kontent/components/hero-post.js b/examples/cms-kontent/components/hero-post.js index 8536e825c5d5..76495802b579 100644 --- a/examples/cms-kontent/components/hero-post.js +++ b/examples/cms-kontent/components/hero-post.js @@ -16,7 +16,7 @@ export default function HeroPost({ <div className="mb-8 md:mb-16"> <CoverImage title={title} src={coverImage} slug={slug} /> </div> - <div className="md:grid md:grid-cols-2 md:col-gap-16 lg:col-gap-8 mb-20 md:mb-28"> + <div className="md:grid md:grid-cols-2 md:gap-x-16 lg:gap-x-8 mb-20 md:mb-28"> <div> <h3 className="mb-4 text-4xl lg:text-6xl leading-tight"> <Link href={`/posts/${slug}`}> diff --git a/examples/cms-kontent/components/more-stories.js b/examples/cms-kontent/components/more-stories.js index dcdd9b4e6ae7..57fdbb6c4659 100644 --- a/examples/cms-kontent/components/more-stories.js +++ b/examples/cms-kontent/components/more-stories.js @@ -6,7 +6,7 @@ export default function MoreStories({ posts }) { <h2 className="mb-8 text-6xl md:text-7xl font-bold tracking-tighter leading-tight"> More Stories </h2> - <div className="grid grid-cols-1 md:grid-cols-2 md:col-gap-16 lg:col-gap-32 row-gap-20 md:row-gap-32 mb-32"> + <div className="grid grid-cols-1 md:grid-cols-2 md:gap-x-16 lg:gap-x-32 gap-y-20 md:gap-y-32 mb-32"> {posts.map((post) => ( <PostPreview key={post.slug} diff --git a/examples/cms-prepr/components/hero-post.js b/examples/cms-prepr/components/hero-post.js index c2ff575ee26a..5fb5c6909be7 100644 --- a/examples/cms-prepr/components/hero-post.js +++ b/examples/cms-prepr/components/hero-post.js @@ -16,7 +16,7 @@ export default function HeroPost({ <div className="mb-8 md:mb-16"> <CoverImage slug={slug} title={title} url={coverImage} /> </div> - <div className="mb-20 md:grid md:grid-cols-2 md:col-gap-16 lg:col-gap-8 md:mb-28"> + <div className="mb-20 md:grid md:grid-cols-2 md:gap-x-16 lg:gap-x-8 md:mb-28"> <div> <h3 className="mb-4 text-4xl leading-tight lg:text-6xl"> <Link href={`/posts/${slug}`}> diff --git a/examples/cms-prismic/components/hero-post.js b/examples/cms-prismic/components/hero-post.js index 8d181d29dfa8..b594f5420020 100644 --- a/examples/cms-prismic/components/hero-post.js +++ b/examples/cms-prismic/components/hero-post.js @@ -21,7 +21,7 @@ export default function HeroPost({ url={coverImage.url} /> </div> - <div className="md:grid md:grid-cols-2 md:col-gap-16 lg:col-gap-8 mb-20 md:mb-28"> + <div className="md:grid md:grid-cols-2 md:gap-x-16 lg:gap-x-8 mb-20 md:mb-28"> <div> <h3 className="mb-4 text-4xl lg:text-6xl leading-tight"> <Link href={`/posts/${slug}`}> diff --git a/examples/cms-prismic/components/more-stories.js b/examples/cms-prismic/components/more-stories.js index cfe7b62e4150..2a29128c8117 100644 --- a/examples/cms-prismic/components/more-stories.js +++ b/examples/cms-prismic/components/more-stories.js @@ -6,7 +6,7 @@ export default function MoreStories({ posts }) { <h2 className="mb-8 text-6xl md:text-7xl font-bold tracking-tighter leading-tight"> More Stories </h2> - <div className="grid grid-cols-1 md:grid-cols-2 md:col-gap-16 lg:col-gap-32 row-gap-20 md:row-gap-32 mb-32"> + <div className="grid grid-cols-1 md:grid-cols-2 md:gap-x-16 lg:gap-x-32 gap-y-20 md:gap-y-32 mb-32"> {posts.map(({ node }) => ( <PostPreview key={node._meta.uid} diff --git a/examples/cms-sanity/.env.local.example b/examples/cms-sanity/.env.local.example index 6bc5dc43020b..a246ea991e7c 100644 --- a/examples/cms-sanity/.env.local.example +++ b/examples/cms-sanity/.env.local.example @@ -2,3 +2,4 @@ NEXT_PUBLIC_SANITY_PROJECT_ID= NEXT_PUBLIC_SANITY_DATASET= SANITY_API_TOKEN= SANITY_PREVIEW_SECRET= +SANITY_STUDIO_REVALIDATE_SECRET= diff --git a/examples/cms-sanity/README.md b/examples/cms-sanity/README.md index 7dfcb0a530ea..baa88574e837 100644 --- a/examples/cms-sanity/README.md +++ b/examples/cms-sanity/README.md @@ -2,6 +2,12 @@ This example showcases Next.js's [Static Generation](https://nextjs.org/docs/basic-features/pages) feature using [Sanity](https://www.sanity.io/) as the data source. +You'll get: + +- Sanity Studio running on localhost +- Sub-second as-you-type previews in Next.js +- [On-demand revalidation of pages](https://nextjs.org/blog/next-12-1#on-demand-incremental-static-regeneration-beta) with [GROQ powered webhooks](https://www.sanity.io/docs/webhooks) + ## Demo ### [https://next-blog-sanity.vercel.app/](https://next-blog-sanity.vercel.app/) @@ -10,7 +16,7 @@ This example showcases Next.js's [Static Generation](https://nextjs.org/docs/bas Once you have access to [the environment variables you'll need](#step-4-set-up-environment-variables), deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example): -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/cms-sanity&project-name=cms-sanity&repository-name=cms-sanity&env=NEXT_PUBLIC_SANITY_PROJECT_ID,SANITY_API_TOKEN,SANITY_PREVIEW_SECRET&envDescription=Required%20to%20connect%20the%20app%20with%20Sanity&envLink=https://vercel.link/cms-sanity-env) +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/cms-sanity&project-name=cms-sanity&repository-name=cms-sanity&env=NEXT_PUBLIC_SANITY_PROJECT_ID,SANITY_API_TOKEN,SANITY_PREVIEW_SECRET,SANITY_STUDIO_REVALIDATE_SECRET&envDescription=Required%20to%20connect%20the%20app%20with%20Sanity&envLink=https://vercel.link/cms-sanity-env) ### Related examples @@ -75,6 +81,7 @@ Then set each variable on `.env.local`: - `NEXT_PUBLIC_SANITY_DATASET` should be the `dataset` value from the `sanity.json` file created in step 2 - defaults to `production` if not set. - `SANITY_API_TOKEN` should be the API token generated in the previous step. - `SANITY_PREVIEW_SECRET` can be any random string (but avoid spaces), like `MY_SECRET` - this is used for [Preview Mode](https://nextjs.org/docs/advanced-features/preview-mode). +- `SANITY_STUDIO_REVALIDATE_SECRET` should be setup the same way as `SANITY_PREVIEW_SECRET` - this is used for [on-demand revalidation](https://nextjs.org/blog/next-12-1#on-demand-incremental-static-regeneration-beta) with [webhooks](https://www.sanity.io/docs/webhooks). Your `.env.local` file should look like this: @@ -83,13 +90,33 @@ NEXT_PUBLIC_SANITY_PROJECT_ID=... NEXT_PUBLIC_SANITY_DATASET=... SANITY_API_TOKEN=... SANITY_PREVIEW_SECRET=... +SANITY_STUDIO_REVALIDATE_SECRET=... ``` -### Step 5. Prepare project for previewing +### Step 5. Prepare the project for previewing + +5.1. Install the `@sanity/production-preview` plugin with `sanity install @sanity/production-preview`. + +5.2. Create a file called `resolveProductionUrl.js` (we'll get back to that file in a bit). -Go to https://www.sanity.io/docs/preview-content-on-site and follow the three steps on that page. It should be done inside the studio project generated in Step 2. +5.3. Open your studio's sanity.json, and add the following entry to the parts-array: + +```diff +{ + "plugins": [ + "@sanity/production-preview" + ], + "parts": [ + //... ++ { ++ "implements": "part:@sanity/production-preview/resolve-production-url", ++ "path": "./resolveProductionUrl.js" ++ } + ] +} +``` -When you get to the second step about creating a file called `resolveProductionUrl.js`, copy the following instead: +Now, go back to `resolveProductionUrl.js` and add a function that will receive the full document that was selected for previewing: ```js const previewSecret = 'MY_SECRET' // Copy the string you used for SANITY_PREVIEW_SECRET @@ -100,6 +127,8 @@ export default function resolveProductionUrl(document) { } ``` +For more information on live previewing check the [full guide.](https://www.sanity.io/guides/nextjs-live-preview) + ### Step 6. Copy the schema file After initializing your Sanity studio project there should be a `schemas` folder. @@ -167,4 +196,25 @@ To deploy your local project to Vercel, push it to GitHub/GitLab/Bitbucket and [ Alternatively, you can deploy using our template by clicking on the Deploy button below. -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/cms-sanity&project-name=cms-sanity&repository-name=cms-sanity&env=NEXT_PUBLIC_SANITY_PROJECT_ID,SANITY_API_TOKEN,SANITY_PREVIEW_SECRET&envDescription=Required%20to%20connect%20the%20app%20with%20Sanity&envLink=https://vercel.link/cms-sanity-env) +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/cms-sanity&project-name=cms-sanity&repository-name=cms-sanity&env=NEXT_PUBLIC_SANITY_PROJECT_ID,SANITY_API_TOKEN,SANITY_PREVIEW_SECRET,SANITY_STUDIO_REVALIDATE_SECRET&envDescription=Required%20to%20connect%20the%20app%20with%20Sanity&envLink=https://vercel.link/cms-sanity-env) + +### Step 11. Setup Revalidation Webhook + +- Open your Sanity manager, go to **API**, and **Create new webhook**. +- Set the **URL** to use the Vercel app url from [Step 10](#step-10-deploy-on-vercel) and append `/api/revalidate`, for example: `https://cms-sanity.vercel.app/api/revalidate` +- Set the **Trigger on** field to <label><input type=checkbox checked> Create</label> <label><input type=checkbox checked> Update</label> <label><input type=checkbox checked> Delete</label> +- Set the **Filter** to `_type == "post" || _type == "author"` +- Set the **Secret** to the same value you gave `SANITY_STUDIO_REVALIDATE_SECRET` earlier. +- Hit **Save**! + +#### Testing the Webhook + +- Open the Deployment function log. (**Vercel Dashboard > Deployment > Functions** and filter by `api/revalidate`) +- Edit a Post in your Sanity Studio and publish. +- The log should start showing calls. +- And the published changes show up on the site after you reload. + +### Next steps + +- Mount your preview inside the Sanity Studio for comfortable side-by-side editing +- [Join the Sanity community](https://slack.sanity.io/) diff --git a/examples/cms-sanity/components/cover-image.js b/examples/cms-sanity/components/cover-image.js index 60bf2e8847a3..818e8eef5e47 100644 --- a/examples/cms-sanity/components/cover-image.js +++ b/examples/cms-sanity/components/cover-image.js @@ -5,15 +5,19 @@ import { urlForImage } from '../lib/sanity' export default function CoverImage({ title, slug, image: source }) { const image = source ? ( - <Image - width={2000} - height={1000} - alt={`Cover Image for ${title}`} - src={urlForImage(source).height(1000).width(2000).url()} + <div className={cn('shadow-small', { 'hover:shadow-medium transition-shadow duration-200': slug, })} - /> + > + <Image + layout="responsive" + width={2000} + height={1000} + alt={`Cover Image for ${title}`} + src={urlForImage(source).height(1000).width(2000).url()} + /> + </div> ) : ( <div style={{ paddingTop: '50%', backgroundColor: '#ddd' }} /> ) diff --git a/examples/cms-sanity/components/hero-post.js b/examples/cms-sanity/components/hero-post.js index f198fb3f54d7..4d81ed0850ac 100644 --- a/examples/cms-sanity/components/hero-post.js +++ b/examples/cms-sanity/components/hero-post.js @@ -16,7 +16,7 @@ export default function HeroPost({ <div className="mb-8 md:mb-16"> <CoverImage slug={slug} title={title} image={coverImage} /> </div> - <div className="md:grid md:grid-cols-2 md:col-gap-16 lg:col-gap-8 mb-20 md:mb-28"> + <div className="md:grid md:grid-cols-2 md:gap-x-16 lg:gap-x-8 mb-20 md:mb-28"> <div> <h3 className="mb-4 text-4xl lg:text-6xl leading-tight"> <Link href={`/posts/${slug}`}> diff --git a/examples/cms-sanity/components/more-stories.js b/examples/cms-sanity/components/more-stories.js index dcdd9b4e6ae7..57fdbb6c4659 100644 --- a/examples/cms-sanity/components/more-stories.js +++ b/examples/cms-sanity/components/more-stories.js @@ -6,7 +6,7 @@ export default function MoreStories({ posts }) { <h2 className="mb-8 text-6xl md:text-7xl font-bold tracking-tighter leading-tight"> More Stories </h2> - <div className="grid grid-cols-1 md:grid-cols-2 md:col-gap-16 lg:col-gap-32 row-gap-20 md:row-gap-32 mb-32"> + <div className="grid grid-cols-1 md:grid-cols-2 md:gap-x-16 lg:gap-x-32 gap-y-20 md:gap-y-32 mb-32"> {posts.map((post) => ( <PostPreview key={post.slug} diff --git a/examples/cms-sanity/components/post-body.js b/examples/cms-sanity/components/post-body.js index 98a958318f08..2e0cb68a48dc 100644 --- a/examples/cms-sanity/components/post-body.js +++ b/examples/cms-sanity/components/post-body.js @@ -1,10 +1,10 @@ import markdownStyles from './markdown-styles.module.css' -import BlockContent from '@sanity/block-content-to-react' +import { PortableText } from '@portabletext/react' export default function PostBody({ content }) { return ( - <div className="max-w-2xl mx-auto"> - <BlockContent blocks={content} className={markdownStyles.markdown} /> + <div className={`max-w-2xl mx-auto ${markdownStyles.markdown}`}> + <PortableText value={content} /> </div> ) } diff --git a/examples/cms-sanity/lib/config.js b/examples/cms-sanity/lib/config.js index b4b0101e5494..c28d1f9c1b86 100644 --- a/examples/cms-sanity/lib/config.js +++ b/examples/cms-sanity/lib/config.js @@ -2,10 +2,13 @@ export const sanityConfig = { // Find your project ID and dataset in `sanity.json` in your studio project dataset: process.env.NEXT_PUBLIC_SANITY_DATASET || 'production', projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID, - useCdn: process.env.NODE_ENV === 'production', + useCdn: process.env.NODE_ENV !== 'production', // useCdn == true gives fast, cheap responses using a globally distributed cache. - // Set this to false if your application require the freshest possible - // data always (potentially slightly slower and a bit more expensive). + // When in production the Sanity API is only queried on build-time, and on-demand when responding to webhooks. + // Thus the data need to be fresh and API response time is less important. + // When in development/working locally, it's more important to keep costs down as hot reloading can incurr a lot of API calls + // And every page load calls getStaticProps. + // To get the lowest latency, lowest cost, and latest data, use the Instant Preview mode apiVersion: '2021-03-25', // see https://www.sanity.io/docs/api-versioning for how versioning works } diff --git a/examples/cms-sanity/lib/sanity.js b/examples/cms-sanity/lib/sanity.js index 76cedf4cb57d..1936305c132c 100644 --- a/examples/cms-sanity/lib/sanity.js +++ b/examples/cms-sanity/lib/sanity.js @@ -1,7 +1,5 @@ -import { - createImageUrlBuilder, - createPreviewSubscriptionHook, -} from 'next-sanity' +import createImageUrlBuilder from '@sanity/image-url' +import { createPreviewSubscriptionHook } from 'next-sanity' import { sanityConfig } from './config' export const imageBuilder = createImageUrlBuilder(sanityConfig) diff --git a/examples/cms-sanity/package.json b/examples/cms-sanity/package.json index 494c94ac75ff..529122e867f4 100644 --- a/examples/cms-sanity/package.json +++ b/examples/cms-sanity/package.json @@ -6,17 +6,19 @@ "start": "next start" }, "dependencies": { - "@sanity/block-content-to-react": "2.0.7", + "@portabletext/react": "^1.0.3", + "@sanity/image-url": "^1.0.1", + "@sanity/webhook": "^1.0.2", "classnames": "2.3.1", "date-fns": "2.28.0", "next": "latest", - "next-sanity": "0.3.0", + "next-sanity": "0.5.0", "react": "^17.0.2", "react-dom": "^17.0.2" }, "devDependencies": { "autoprefixer": "10.4.2", - "postcss": "8.4.5", - "tailwindcss": "^3.0.15" + "postcss": "8.4.7", + "tailwindcss": "^3.0.23" } } diff --git a/examples/cms-sanity/pages/api/revalidate.js b/examples/cms-sanity/pages/api/revalidate.js new file mode 100644 index 000000000000..9e2af5b3ebbd --- /dev/null +++ b/examples/cms-sanity/pages/api/revalidate.js @@ -0,0 +1,56 @@ +import { isValidRequest } from '@sanity/webhook' +import { sanityClient } from '../../lib/sanity.server' + +const AUTHOR_UPDATED_QUERY = ` + *[_type == "author" && _id == $id] { + "slug": *[_type == "post" && references(^._id)].slug.current + }["slug"][]` +const POST_UPDATED_QUERY = `*[_type == "post" && _id == $id].slug.current` + +const getQueryForType = (type) => { + switch (type) { + case 'author': + return AUTHOR_UPDATED_QUERY + case 'post': + return POST_UPDATED_QUERY + default: + throw new TypeError(`Unknown type: ${type}`) + } +} + +const log = (msg, error) => + console[error ? 'error' : 'log'](`[revalidate] ${msg}`) + +export default async function revalidate(req, res) { + if (!isValidRequest(req, process.env.SANITY_STUDIO_REVALIDATE_SECRET)) { + const invalidRequest = 'Invalid request' + log(invalidRequest, true) + return res.status(401).json({ message: invalidRequest }) + } + + const { _id: id, _type } = req.body + if (typeof id !== 'string' || !id) { + const invalidId = 'Invalid _id' + log(invalidId, true) + return res.status(400).json({ message: invalidId }) + } + + log(`Querying post slug for _id '${id}', type '${_type}' ..`) + const slug = await sanityClient.fetch(getQueryForType(_type), { id }) + const slugs = (Array.isArray(slug) ? slug : [slug]).map( + (_slug) => `/posts/${_slug}` + ) + const staleRoutes = ['/', ...slugs] + + try { + await Promise.all( + staleRoutes.map((route) => res.unstable_revalidate(route)) + ) + const updatedRoutes = `Updated routes: ${staleRoutes.join(', ')}` + log(updatedRoutes) + return res.status(200).json({ message: updatedRoutes }) + } catch (err) { + log(err.message, true) + return res.status(500).json({ message: err.message }) + } +} diff --git a/examples/cms-storyblok/components/hero-post.js b/examples/cms-storyblok/components/hero-post.js index 2cab6b993289..eadefeaac135 100644 --- a/examples/cms-storyblok/components/hero-post.js +++ b/examples/cms-storyblok/components/hero-post.js @@ -16,7 +16,7 @@ export default function HeroPost({ <div className="mb-8 md:mb-16"> <CoverImage title={title} url={coverImage} slug={slug} /> </div> - <div className="md:grid md:grid-cols-2 md:col-gap-16 lg:col-gap-8 mb-20 md:mb-28"> + <div className="md:grid md:grid-cols-2 md:gap-x-16 lg:gap-x-8 mb-20 md:mb-28"> <div> <h3 className="mb-4 text-4xl lg:text-6xl leading-tight"> <Link href={`/posts/${slug}`}> diff --git a/examples/cms-storyblok/components/more-stories.js b/examples/cms-storyblok/components/more-stories.js index 2ea20c3f6b25..583c01eda265 100644 --- a/examples/cms-storyblok/components/more-stories.js +++ b/examples/cms-storyblok/components/more-stories.js @@ -6,7 +6,7 @@ export default function MoreStories({ posts }) { <h2 className="mb-8 text-6xl md:text-7xl font-bold tracking-tighter leading-tight"> More Stories </h2> - <div className="grid grid-cols-1 md:grid-cols-2 md:col-gap-16 lg:col-gap-32 row-gap-20 md:row-gap-32 mb-32"> + <div className="grid grid-cols-1 md:grid-cols-2 md:gap-x-16 lg:gap-x-32 gap-y-20 md:gap-y-32 mb-32"> {posts.map((post) => ( <PostPreview key={post.slug} diff --git a/examples/cms-strapi/README.md b/examples/cms-strapi/README.md index aa9e755f9d1f..6ef231d35274 100644 --- a/examples/cms-strapi/README.md +++ b/examples/cms-strapi/README.md @@ -48,8 +48,8 @@ yarn create next-app --example cms-strapi cms-strapi-app Use the provided [Strapi template Next example](https://github.com/strapi/strapi-template-next-example) to run a pre-configured Strapi project locally. See the [Strapi template docs](https://strapi.io/documentation/developer-docs/latest/setup-deployment-guides/installation/templates.html#templates) for more information ```bash -npx create-strapi-app my-project --template next-example --quickstart -# or: yarn create strapi-app my-project --template next-example --quickstart +npx create-strapi-app@3 my-project --template next-example --quickstart +# or: yarn create strapi-app@3 my-project --template next-example --quickstart npm run develop # or: yarn develop ``` diff --git a/examples/cms-strapi/components/hero-post.js b/examples/cms-strapi/components/hero-post.js index a246a0847317..910933b96169 100644 --- a/examples/cms-strapi/components/hero-post.js +++ b/examples/cms-strapi/components/hero-post.js @@ -16,7 +16,7 @@ export default function HeroPost({ <div className="mb-8 md:mb-16"> <CoverImage title={title} url={coverImage.url} slug={slug} /> </div> - <div className="md:grid md:grid-cols-2 md:col-gap-16 lg:col-gap-8 mb-20 md:mb-28"> + <div className="md:grid md:grid-cols-2 md:gap-x-16 lg:gap-x-8 mb-20 md:mb-28"> <div> <h3 className="mb-4 text-4xl lg:text-6xl leading-tight"> <Link href={`/posts/${slug}`}> diff --git a/examples/cms-strapi/components/more-stories.js b/examples/cms-strapi/components/more-stories.js index 13a09bd8f729..4bab320f6537 100644 --- a/examples/cms-strapi/components/more-stories.js +++ b/examples/cms-strapi/components/more-stories.js @@ -6,7 +6,7 @@ export default function MoreStories({ posts }) { <h2 className="mb-8 text-6xl md:text-7xl font-bold tracking-tighter leading-tight"> More Stories </h2> - <div className="grid grid-cols-1 md:grid-cols-2 md:col-gap-16 lg:col-gap-32 row-gap-20 md:row-gap-32 mb-32"> + <div className="grid grid-cols-1 md:grid-cols-2 md:gap-x-16 lg:gap-x-32 gap-y-20 md:gap-y-32 mb-32"> {posts.map((post) => ( <PostPreview key={post.slug} diff --git a/examples/cms-takeshape/components/hero-post.js b/examples/cms-takeshape/components/hero-post.js index 209d62c118fc..70d417ef5792 100644 --- a/examples/cms-takeshape/components/hero-post.js +++ b/examples/cms-takeshape/components/hero-post.js @@ -16,7 +16,7 @@ export default function HeroPost({ <div className="mb-8 md:mb-16"> <CoverImage title={title} coverImage={coverImage} slug={slug} /> </div> - <div className="md:grid md:grid-cols-2 md:col-gap-16 lg:col-gap-8 mb-20 md:mb-28"> + <div className="md:grid md:grid-cols-2 md:gap-x-16 lg:gap-x-8 mb-20 md:mb-28"> <div> <h3 className="mb-4 text-4xl lg:text-6xl leading-tight"> <Link href={`/posts/${slug}`}> diff --git a/examples/cms-takeshape/components/more-stories.js b/examples/cms-takeshape/components/more-stories.js index dcdd9b4e6ae7..57fdbb6c4659 100644 --- a/examples/cms-takeshape/components/more-stories.js +++ b/examples/cms-takeshape/components/more-stories.js @@ -6,7 +6,7 @@ export default function MoreStories({ posts }) { <h2 className="mb-8 text-6xl md:text-7xl font-bold tracking-tighter leading-tight"> More Stories </h2> - <div className="grid grid-cols-1 md:grid-cols-2 md:col-gap-16 lg:col-gap-32 row-gap-20 md:row-gap-32 mb-32"> + <div className="grid grid-cols-1 md:grid-cols-2 md:gap-x-16 lg:gap-x-32 gap-y-20 md:gap-y-32 mb-32"> {posts.map((post) => ( <PostPreview key={post.slug} diff --git a/examples/with-custom-reverse-proxy/.gitignore b/examples/cms-tina/.gitignore similarity index 100% rename from examples/with-custom-reverse-proxy/.gitignore rename to examples/cms-tina/.gitignore diff --git a/examples/cms-tina/.tina/__generated__/.gitignore b/examples/cms-tina/.tina/__generated__/.gitignore new file mode 100644 index 000000000000..5baa59d64639 --- /dev/null +++ b/examples/cms-tina/.tina/__generated__/.gitignore @@ -0,0 +1 @@ +db \ No newline at end of file diff --git a/examples/cms-tina/.tina/__generated__/_graphql.json b/examples/cms-tina/.tina/__generated__/_graphql.json new file mode 100644 index 000000000000..4f7ac4cfe7ef --- /dev/null +++ b/examples/cms-tina/.tina/__generated__/_graphql.json @@ -0,0 +1,2072 @@ +{ + "kind": "Document", + "definitions": [ + { + "kind": "ScalarTypeDefinition", + "name": { + "kind": "Name", + "value": "Reference" + }, + "description": { + "kind": "StringValue", + "value": "References another document, used as a foreign key" + }, + "directives": [] + }, + { + "kind": "ScalarTypeDefinition", + "name": { + "kind": "Name", + "value": "JSON" + }, + "description": { + "kind": "StringValue", + "value": "" + }, + "directives": [] + }, + { + "kind": "ObjectTypeDefinition", + "interfaces": [], + "directives": [], + "name": { + "kind": "Name", + "value": "SystemInfo" + }, + "fields": [ + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "filename" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "basename" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "breadcrumbs" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "excludeExtension" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Boolean" + } + } + } + ], + "type": { + "kind": "NonNullType", + "type": { + "kind": "ListType", + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "path" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "relativePath" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "extension" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "template" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "collection" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Collection" + } + } + } + } + ] + }, + { + "kind": "ObjectTypeDefinition", + "interfaces": [], + "directives": [], + "name": { + "kind": "Name", + "value": "PageInfo" + }, + "fields": [ + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "hasPreviousPage" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Boolean" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "hasNextPage" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Boolean" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "startCursor" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "endCursor" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + } + ] + }, + { + "kind": "InterfaceTypeDefinition", + "description": { + "kind": "StringValue", + "value": "" + }, + "name": { + "kind": "Name", + "value": "Node" + }, + "interfaces": [], + "directives": [], + "fields": [ + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "id" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ID" + } + } + } + } + ] + }, + { + "kind": "InterfaceTypeDefinition", + "description": { + "kind": "StringValue", + "value": "" + }, + "name": { + "kind": "Name", + "value": "Document" + }, + "interfaces": [], + "directives": [], + "fields": [ + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "sys" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "SystemInfo" + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "id" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ID" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "form" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "JSON" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "values" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "JSON" + } + } + } + } + ] + }, + { + "kind": "InterfaceTypeDefinition", + "description": { + "kind": "StringValue", + "value": "A relay-compliant pagination connection" + }, + "name": { + "kind": "Name", + "value": "Connection" + }, + "interfaces": [], + "directives": [], + "fields": [ + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "totalCount" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Float" + } + } + } + } + ] + }, + { + "kind": "ObjectTypeDefinition", + "interfaces": [], + "directives": [], + "name": { + "kind": "Name", + "value": "Query" + }, + "fields": [ + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "getCollection" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "collection" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + ], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Collection" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "getCollections" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "ListType", + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Collection" + } + } + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "node" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "id" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + ], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Node" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "getDocument" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "collection" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "relativePath" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + ], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "DocumentNode" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "getDocumentList" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "before" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "after" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "first" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Float" + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "last" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Float" + } + } + } + ], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "DocumentConnection" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "getDocumentFields" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "JSON" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "getPostsDocument" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "relativePath" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + ], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "PostsDocument" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "getPostsList" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "before" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "after" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "first" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Float" + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "last" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Float" + } + } + } + ], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "PostsConnection" + } + } + } + } + ] + }, + { + "kind": "ObjectTypeDefinition", + "interfaces": [], + "directives": [], + "name": { + "kind": "Name", + "value": "DocumentConnectionEdges" + }, + "fields": [ + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "cursor" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "node" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "DocumentNode" + } + } + } + ] + }, + { + "kind": "ObjectTypeDefinition", + "interfaces": [ + { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Connection" + } + } + ], + "directives": [], + "name": { + "kind": "Name", + "value": "DocumentConnection" + }, + "fields": [ + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "pageInfo" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "PageInfo" + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "totalCount" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Float" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "edges" + }, + "arguments": [], + "type": { + "kind": "ListType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "DocumentConnectionEdges" + } + } + } + } + ] + }, + { + "kind": "ObjectTypeDefinition", + "interfaces": [], + "directives": [], + "name": { + "kind": "Name", + "value": "Collection" + }, + "fields": [ + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "name" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "slug" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "label" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "path" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "format" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "matches" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "templates" + }, + "arguments": [], + "type": { + "kind": "ListType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "JSON" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "fields" + }, + "arguments": [], + "type": { + "kind": "ListType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "JSON" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "documents" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "before" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "after" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "first" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Float" + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "last" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Float" + } + } + } + ], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "DocumentConnection" + } + } + } + } + ] + }, + { + "kind": "UnionTypeDefinition", + "name": { + "kind": "Name", + "value": "DocumentNode" + }, + "directives": [], + "types": [ + { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "PostsDocument" + } + } + ] + }, + { + "kind": "ObjectTypeDefinition", + "interfaces": [], + "directives": [], + "name": { + "kind": "Name", + "value": "PostsAuthor" + }, + "fields": [ + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "name" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "picture" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + ] + }, + { + "kind": "ObjectTypeDefinition", + "interfaces": [], + "directives": [], + "name": { + "kind": "Name", + "value": "PostsOgImage" + }, + "fields": [ + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "url" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + ] + }, + { + "kind": "ObjectTypeDefinition", + "interfaces": [], + "directives": [], + "name": { + "kind": "Name", + "value": "Posts" + }, + "fields": [ + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "title" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "excerpt" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "coverImage" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "date" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "author" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "PostsAuthor" + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "ogImage" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "PostsOgImage" + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "body" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + ] + }, + { + "kind": "ObjectTypeDefinition", + "interfaces": [ + { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Node" + } + }, + { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Document" + } + } + ], + "directives": [], + "name": { + "kind": "Name", + "value": "PostsDocument" + }, + "fields": [ + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "id" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ID" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "sys" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "SystemInfo" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "data" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Posts" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "form" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "JSON" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "values" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "JSON" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "dataJSON" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "JSON" + } + } + } + } + ] + }, + { + "kind": "ObjectTypeDefinition", + "interfaces": [], + "directives": [], + "name": { + "kind": "Name", + "value": "PostsConnectionEdges" + }, + "fields": [ + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "cursor" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "node" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "PostsDocument" + } + } + } + ] + }, + { + "kind": "ObjectTypeDefinition", + "interfaces": [ + { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Connection" + } + } + ], + "directives": [], + "name": { + "kind": "Name", + "value": "PostsConnection" + }, + "fields": [ + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "pageInfo" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "PageInfo" + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "totalCount" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Float" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "edges" + }, + "arguments": [], + "type": { + "kind": "ListType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "PostsConnectionEdges" + } + } + } + } + ] + }, + { + "kind": "ObjectTypeDefinition", + "interfaces": [], + "directives": [], + "name": { + "kind": "Name", + "value": "Mutation" + }, + "fields": [ + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "addPendingDocument" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "collection" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "relativePath" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "template" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + ], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "DocumentNode" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "updateDocument" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "collection" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "relativePath" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "params" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "DocumentMutation" + } + } + } + } + ], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "DocumentNode" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "createDocument" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "collection" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "relativePath" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "params" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "DocumentMutation" + } + } + } + } + ], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "DocumentNode" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "updatePostsDocument" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "relativePath" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "params" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "PostsMutation" + } + } + } + } + ], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "PostsDocument" + } + } + } + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "createPostsDocument" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "relativePath" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "params" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "PostsMutation" + } + } + } + } + ], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "PostsDocument" + } + } + } + } + ] + }, + { + "kind": "InputObjectTypeDefinition", + "name": { + "kind": "Name", + "value": "DocumentMutation" + }, + "fields": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "posts" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "PostsMutation" + } + } + } + ] + }, + { + "kind": "InputObjectTypeDefinition", + "name": { + "kind": "Name", + "value": "PostsAuthorMutation" + }, + "fields": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "name" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "picture" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + ] + }, + { + "kind": "InputObjectTypeDefinition", + "name": { + "kind": "Name", + "value": "PostsOgImageMutation" + }, + "fields": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "url" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + ] + }, + { + "kind": "InputObjectTypeDefinition", + "name": { + "kind": "Name", + "value": "PostsMutation" + }, + "fields": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "title" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "excerpt" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "coverImage" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "date" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "author" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "PostsAuthorMutation" + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "ogImage" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "PostsOgImageMutation" + } + } + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "body" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + } + ] + } + ] +} diff --git a/examples/cms-tina/.tina/__generated__/_lookup.json b/examples/cms-tina/.tina/__generated__/_lookup.json new file mode 100644 index 000000000000..6e651b843207 --- /dev/null +++ b/examples/cms-tina/.tina/__generated__/_lookup.json @@ -0,0 +1,29 @@ +{ + "DocumentConnection": { + "type": "DocumentConnection", + "resolveType": "multiCollectionDocumentList", + "collections": ["posts"] + }, + "Node": { + "type": "Node", + "resolveType": "nodeDocument" + }, + "DocumentNode": { + "type": "DocumentNode", + "resolveType": "multiCollectionDocument", + "createDocument": "create", + "updateDocument": "update" + }, + "PostsDocument": { + "type": "PostsDocument", + "resolveType": "collectionDocument", + "collection": "posts", + "createPostsDocument": "create", + "updatePostsDocument": "update" + }, + "PostsConnection": { + "type": "PostsConnection", + "resolveType": "collectionDocumentList", + "collection": "posts" + } +} diff --git a/examples/cms-tina/.tina/__generated__/_schema.json b/examples/cms-tina/.tina/__generated__/_schema.json new file mode 100644 index 000000000000..46e9b5f5c4c1 --- /dev/null +++ b/examples/cms-tina/.tina/__generated__/_schema.json @@ -0,0 +1,87 @@ +{ + "version": { + "fullVersion": "0.59.7", + "major": "0", + "minor": "59", + "patch": "7" + }, + "meta": {}, + "collections": [ + { + "label": "Blog Posts", + "name": "posts", + "path": "_posts", + "fields": [ + { + "type": "string", + "label": "Title", + "name": "title", + "namespace": ["posts", "title"] + }, + { + "type": "string", + "label": "Excerpt", + "name": "excerpt", + "namespace": ["posts", "excerpt"] + }, + { + "type": "string", + "label": "Cover Image", + "name": "coverImage", + "namespace": ["posts", "coverImage"] + }, + { + "type": "string", + "label": "Date", + "name": "date", + "namespace": ["posts", "date"] + }, + { + "type": "object", + "label": "author", + "name": "author", + "fields": [ + { + "type": "string", + "label": "Name", + "name": "name", + "namespace": ["posts", "author", "name"] + }, + { + "type": "string", + "label": "Picture", + "name": "picture", + "namespace": ["posts", "author", "picture"] + } + ], + "namespace": ["posts", "author"] + }, + { + "type": "object", + "label": "OG Image", + "name": "ogImage", + "fields": [ + { + "type": "string", + "label": "Url", + "name": "url", + "namespace": ["posts", "ogImage", "url"] + } + ], + "namespace": ["posts", "ogImage"] + }, + { + "type": "string", + "label": "Blog Post Body", + "name": "body", + "isBody": true, + "ui": { + "component": "textarea" + }, + "namespace": ["posts", "body"] + } + ], + "namespace": ["posts"] + } + ] +} diff --git a/examples/cms-tina/.tina/__generated__/frags.gql b/examples/cms-tina/.tina/__generated__/frags.gql new file mode 100644 index 000000000000..4c1472d2fce0 --- /dev/null +++ b/examples/cms-tina/.tina/__generated__/frags.gql @@ -0,0 +1,16 @@ +fragment PostsParts on Posts { + title + excerpt + coverImage + date + author { + __typename + name + picture + } + ogImage { + __typename + url + } + body +} diff --git a/examples/cms-tina/.tina/__generated__/queries.gql b/examples/cms-tina/.tina/__generated__/queries.gql new file mode 100644 index 000000000000..949e4040d5e9 --- /dev/null +++ b/examples/cms-tina/.tina/__generated__/queries.gql @@ -0,0 +1,38 @@ +query getPostsDocument($relativePath: String!) { + getPostsDocument(relativePath: $relativePath) { + sys { + filename + basename + breadcrumbs + path + relativePath + extension + } + id + data { + ...PostsParts + } + } +} + +query getPostsList { + getPostsList { + totalCount + edges { + node { + id + sys { + filename + basename + breadcrumbs + path + relativePath + extension + } + data { + ...PostsParts + } + } + } + } +} diff --git a/examples/cms-tina/.tina/__generated__/schema.gql b/examples/cms-tina/.tina/__generated__/schema.gql new file mode 100644 index 000000000000..42acbd89517d --- /dev/null +++ b/examples/cms-tina/.tina/__generated__/schema.gql @@ -0,0 +1,196 @@ +# DO NOT MODIFY THIS FILE. This file is automatically generated by Tina +""" +References another document, used as a foreign key +""" +scalar Reference + +""" + +""" +scalar JSON + +type SystemInfo { + filename: String! + basename: String! + breadcrumbs(excludeExtension: Boolean): [String!]! + path: String! + relativePath: String! + extension: String! + template: String! + collection: Collection! +} + +type PageInfo { + hasPreviousPage: Boolean! + hasNextPage: Boolean! + startCursor: String! + endCursor: String! +} + +""" + +""" +interface Node { + id: ID! +} + +""" + +""" +interface Document { + sys: SystemInfo + id: ID! + form: JSON! + values: JSON! +} + +""" +A relay-compliant pagination connection +""" +interface Connection { + totalCount: Float! +} + +type Query { + getCollection(collection: String): Collection! + getCollections: [Collection!]! + node(id: String): Node! + getDocument(collection: String, relativePath: String): DocumentNode! + getDocumentList( + before: String + after: String + first: Float + last: Float + ): DocumentConnection! + getDocumentFields: JSON! + getPostsDocument(relativePath: String): PostsDocument! + getPostsList( + before: String + after: String + first: Float + last: Float + ): PostsConnection! +} + +type DocumentConnectionEdges { + cursor: String + node: DocumentNode +} + +type DocumentConnection implements Connection { + pageInfo: PageInfo + totalCount: Float! + edges: [DocumentConnectionEdges] +} + +type Collection { + name: String! + slug: String! + label: String + path: String! + format: String + matches: String + templates: [JSON] + fields: [JSON] + documents( + before: String + after: String + first: Float + last: Float + ): DocumentConnection! +} + +union DocumentNode = PostsDocument + +type PostsAuthor { + name: String + picture: String +} + +type PostsOgImage { + url: String +} + +type Posts { + title: String + excerpt: String + coverImage: String + date: String + author: PostsAuthor + ogImage: PostsOgImage + body: String +} + +type PostsDocument implements Node & Document { + id: ID! + sys: SystemInfo! + data: Posts! + form: JSON! + values: JSON! + dataJSON: JSON! +} + +type PostsConnectionEdges { + cursor: String + node: PostsDocument +} + +type PostsConnection implements Connection { + pageInfo: PageInfo + totalCount: Float! + edges: [PostsConnectionEdges] +} + +type Mutation { + addPendingDocument( + collection: String! + relativePath: String! + template: String + ): DocumentNode! + updateDocument( + collection: String + relativePath: String! + params: DocumentMutation! + ): DocumentNode! + createDocument( + collection: String + relativePath: String! + params: DocumentMutation! + ): DocumentNode! + updatePostsDocument( + relativePath: String! + params: PostsMutation! + ): PostsDocument! + createPostsDocument( + relativePath: String! + params: PostsMutation! + ): PostsDocument! +} + +input DocumentMutation { + posts: PostsMutation +} + +input PostsAuthorMutation { + name: String + picture: String +} + +input PostsOgImageMutation { + url: String +} + +input PostsMutation { + title: String + excerpt: String + coverImage: String + date: String + author: PostsAuthorMutation + ogImage: PostsOgImageMutation + body: String +} + +schema { + query: Query + mutation: Mutation +} diff --git a/examples/cms-tina/.tina/__generated__/types.ts b/examples/cms-tina/.tina/__generated__/types.ts new file mode 100644 index 000000000000..bf78adacfbc9 --- /dev/null +++ b/examples/cms-tina/.tina/__generated__/types.ts @@ -0,0 +1,472 @@ +//@ts-nocheck +// DO NOT MODIFY THIS FILE. This file is automatically generated by Tina +import { gql } from 'tinacms' +export type Maybe<T> = T | null +export type InputMaybe<T> = Maybe<T> +export type Exact<T extends { [key: string]: unknown }> = { + [K in keyof T]: T[K] +} +export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { + [SubKey in K]?: Maybe<T[SubKey]> +} +export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { + [SubKey in K]: Maybe<T[SubKey]> +} +/** All built-in and custom scalars, mapped to their actual values */ +export type Scalars = { + ID: string + String: string + Boolean: boolean + Int: number + Float: number + /** References another document, used as a foreign key */ + Reference: any + JSON: any +} + +export type SystemInfo = { + __typename?: 'SystemInfo' + filename: Scalars['String'] + basename: Scalars['String'] + breadcrumbs: Array<Scalars['String']> + path: Scalars['String'] + relativePath: Scalars['String'] + extension: Scalars['String'] + template: Scalars['String'] + collection: Collection +} + +export type SystemInfoBreadcrumbsArgs = { + excludeExtension?: InputMaybe<Scalars['Boolean']> +} + +export type PageInfo = { + __typename?: 'PageInfo' + hasPreviousPage: Scalars['Boolean'] + hasNextPage: Scalars['Boolean'] + startCursor: Scalars['String'] + endCursor: Scalars['String'] +} + +export type Node = { + id: Scalars['ID'] +} + +export type Document = { + sys?: Maybe<SystemInfo> + id: Scalars['ID'] + form: Scalars['JSON'] + values: Scalars['JSON'] +} + +/** A relay-compliant pagination connection */ +export type Connection = { + totalCount: Scalars['Float'] +} + +export type Query = { + __typename?: 'Query' + getCollection: Collection + getCollections: Array<Collection> + node: Node + getDocument: DocumentNode + getDocumentList: DocumentConnection + getDocumentFields: Scalars['JSON'] + getPostsDocument: PostsDocument + getPostsList: PostsConnection +} + +export type QueryGetCollectionArgs = { + collection?: InputMaybe<Scalars['String']> +} + +export type QueryNodeArgs = { + id?: InputMaybe<Scalars['String']> +} + +export type QueryGetDocumentArgs = { + collection?: InputMaybe<Scalars['String']> + relativePath?: InputMaybe<Scalars['String']> +} + +export type QueryGetDocumentListArgs = { + before?: InputMaybe<Scalars['String']> + after?: InputMaybe<Scalars['String']> + first?: InputMaybe<Scalars['Float']> + last?: InputMaybe<Scalars['Float']> +} + +export type QueryGetPostsDocumentArgs = { + relativePath?: InputMaybe<Scalars['String']> +} + +export type QueryGetPostsListArgs = { + before?: InputMaybe<Scalars['String']> + after?: InputMaybe<Scalars['String']> + first?: InputMaybe<Scalars['Float']> + last?: InputMaybe<Scalars['Float']> +} + +export type DocumentConnectionEdges = { + __typename?: 'DocumentConnectionEdges' + cursor?: Maybe<Scalars['String']> + node?: Maybe<DocumentNode> +} + +export type DocumentConnection = Connection & { + __typename?: 'DocumentConnection' + pageInfo?: Maybe<PageInfo> + totalCount: Scalars['Float'] + edges?: Maybe<Array<Maybe<DocumentConnectionEdges>>> +} + +export type Collection = { + __typename?: 'Collection' + name: Scalars['String'] + slug: Scalars['String'] + label?: Maybe<Scalars['String']> + path: Scalars['String'] + format?: Maybe<Scalars['String']> + matches?: Maybe<Scalars['String']> + templates?: Maybe<Array<Maybe<Scalars['JSON']>>> + fields?: Maybe<Array<Maybe<Scalars['JSON']>>> + documents: DocumentConnection +} + +export type CollectionDocumentsArgs = { + before?: InputMaybe<Scalars['String']> + after?: InputMaybe<Scalars['String']> + first?: InputMaybe<Scalars['Float']> + last?: InputMaybe<Scalars['Float']> +} + +export type DocumentNode = PostsDocument + +export type PostsAuthor = { + __typename?: 'PostsAuthor' + name?: Maybe<Scalars['String']> + picture?: Maybe<Scalars['String']> +} + +export type PostsOgImage = { + __typename?: 'PostsOgImage' + url?: Maybe<Scalars['String']> +} + +export type Posts = { + __typename?: 'Posts' + title?: Maybe<Scalars['String']> + excerpt?: Maybe<Scalars['String']> + coverImage?: Maybe<Scalars['String']> + date?: Maybe<Scalars['String']> + author?: Maybe<PostsAuthor> + ogImage?: Maybe<PostsOgImage> + body?: Maybe<Scalars['String']> +} + +export type PostsDocument = Node & + Document & { + __typename?: 'PostsDocument' + id: Scalars['ID'] + sys: SystemInfo + data: Posts + form: Scalars['JSON'] + values: Scalars['JSON'] + dataJSON: Scalars['JSON'] + } + +export type PostsConnectionEdges = { + __typename?: 'PostsConnectionEdges' + cursor?: Maybe<Scalars['String']> + node?: Maybe<PostsDocument> +} + +export type PostsConnection = Connection & { + __typename?: 'PostsConnection' + pageInfo?: Maybe<PageInfo> + totalCount: Scalars['Float'] + edges?: Maybe<Array<Maybe<PostsConnectionEdges>>> +} + +export type Mutation = { + __typename?: 'Mutation' + addPendingDocument: DocumentNode + updateDocument: DocumentNode + createDocument: DocumentNode + updatePostsDocument: PostsDocument + createPostsDocument: PostsDocument +} + +export type MutationAddPendingDocumentArgs = { + collection: Scalars['String'] + relativePath: Scalars['String'] + template?: InputMaybe<Scalars['String']> +} + +export type MutationUpdateDocumentArgs = { + collection?: InputMaybe<Scalars['String']> + relativePath: Scalars['String'] + params: DocumentMutation +} + +export type MutationCreateDocumentArgs = { + collection?: InputMaybe<Scalars['String']> + relativePath: Scalars['String'] + params: DocumentMutation +} + +export type MutationUpdatePostsDocumentArgs = { + relativePath: Scalars['String'] + params: PostsMutation +} + +export type MutationCreatePostsDocumentArgs = { + relativePath: Scalars['String'] + params: PostsMutation +} + +export type DocumentMutation = { + posts?: InputMaybe<PostsMutation> +} + +export type PostsAuthorMutation = { + name?: InputMaybe<Scalars['String']> + picture?: InputMaybe<Scalars['String']> +} + +export type PostsOgImageMutation = { + url?: InputMaybe<Scalars['String']> +} + +export type PostsMutation = { + title?: InputMaybe<Scalars['String']> + excerpt?: InputMaybe<Scalars['String']> + coverImage?: InputMaybe<Scalars['String']> + date?: InputMaybe<Scalars['String']> + author?: InputMaybe<PostsAuthorMutation> + ogImage?: InputMaybe<PostsOgImageMutation> + body?: InputMaybe<Scalars['String']> +} + +export type PostsPartsFragment = { + __typename?: 'Posts' + title?: string | null + excerpt?: string | null + coverImage?: string | null + date?: string | null + body?: string | null + author?: { + __typename: 'PostsAuthor' + name?: string | null + picture?: string | null + } | null + ogImage?: { __typename: 'PostsOgImage'; url?: string | null } | null +} + +export type GetPostsDocumentQueryVariables = Exact<{ + relativePath: Scalars['String'] +}> + +export type GetPostsDocumentQuery = { + __typename?: 'Query' + getPostsDocument: { + __typename?: 'PostsDocument' + id: string + sys: { + __typename?: 'SystemInfo' + filename: string + basename: string + breadcrumbs: Array<string> + path: string + relativePath: string + extension: string + } + data: { + __typename?: 'Posts' + title?: string | null + excerpt?: string | null + coverImage?: string | null + date?: string | null + body?: string | null + author?: { + __typename: 'PostsAuthor' + name?: string | null + picture?: string | null + } | null + ogImage?: { __typename: 'PostsOgImage'; url?: string | null } | null + } + } +} + +export type GetPostsListQueryVariables = Exact<{ [key: string]: never }> + +export type GetPostsListQuery = { + __typename?: 'Query' + getPostsList: { + __typename?: 'PostsConnection' + totalCount: number + edges?: Array<{ + __typename?: 'PostsConnectionEdges' + node?: { + __typename?: 'PostsDocument' + id: string + sys: { + __typename?: 'SystemInfo' + filename: string + basename: string + breadcrumbs: Array<string> + path: string + relativePath: string + extension: string + } + data: { + __typename?: 'Posts' + title?: string | null + excerpt?: string | null + coverImage?: string | null + date?: string | null + body?: string | null + author?: { + __typename: 'PostsAuthor' + name?: string | null + picture?: string | null + } | null + ogImage?: { __typename: 'PostsOgImage'; url?: string | null } | null + } + } | null + } | null> | null + } +} + +export const PostsPartsFragmentDoc = gql` + fragment PostsParts on Posts { + title + excerpt + coverImage + date + author { + __typename + name + picture + } + ogImage { + __typename + url + } + body + } +` +export const GetPostsDocumentDocument = gql` + query getPostsDocument($relativePath: String!) { + getPostsDocument(relativePath: $relativePath) { + sys { + filename + basename + breadcrumbs + path + relativePath + extension + } + id + data { + ...PostsParts + } + } + } + ${PostsPartsFragmentDoc} +` +export const GetPostsListDocument = gql` + query getPostsList { + getPostsList { + totalCount + edges { + node { + id + sys { + filename + basename + breadcrumbs + path + relativePath + extension + } + data { + ...PostsParts + } + } + } + } + } + ${PostsPartsFragmentDoc} +` +export type Requester<C = {}> = <R, V>( + doc: DocumentNode, + vars?: V, + options?: C +) => Promise<R> +export function getSdk<C>(requester: Requester<C>) { + return { + getPostsDocument( + variables: GetPostsDocumentQueryVariables, + options?: C + ): Promise<{ + data: GetPostsDocumentQuery + variables: GetPostsDocumentQueryVariables + query: string + }> { + return requester< + { + data: GetPostsDocumentQuery + variables: GetPostsDocumentQueryVariables + query: string + }, + GetPostsDocumentQueryVariables + >(GetPostsDocumentDocument, variables, options) + }, + getPostsList( + variables?: GetPostsListQueryVariables, + options?: C + ): Promise<{ + data: GetPostsListQuery + variables: GetPostsListQueryVariables + query: string + }> { + return requester< + { + data: GetPostsListQuery + variables: GetPostsListQueryVariables + query: string + }, + GetPostsListQueryVariables + >(GetPostsListDocument, variables, options) + }, + } +} +export type Sdk = ReturnType<typeof getSdk> + +// TinaSDK generated code +import { staticRequest } from 'tinacms' +const requester: (doc: any, vars?: any, options?: any) => Promise<any> = async ( + doc, + vars, + _options +) => { + let data = {} + try { + data = await staticRequest({ + query: doc, + variables: vars, + }) + } catch (e) { + // swallow errors related to document creation + console.warn('Warning: There was an error when fetching data') + console.warn(e) + } + + return { data, query: doc, variables: vars || {} } +} + +/** + * @experimental this class can be used but may change in the future + **/ +export const ExperimentalGetTinaClient = () => getSdk(requester) diff --git a/examples/cms-tina/.tina/components/TinaDynamicProvider.js b/examples/cms-tina/.tina/components/TinaDynamicProvider.js new file mode 100644 index 000000000000..5f6bd24bc870 --- /dev/null +++ b/examples/cms-tina/.tina/components/TinaDynamicProvider.js @@ -0,0 +1,15 @@ +import dynamic from 'next/dynamic' +const TinaProvider = dynamic(() => import('./TinaProvider'), { ssr: false }) +import { TinaEditProvider } from 'tinacms/dist/edit-state' + +const DynamicTina = ({ children }) => { + return ( + <> + <TinaEditProvider editMode={<TinaProvider>{children}</TinaProvider>}> + {children} + </TinaEditProvider> + </> + ) +} + +export default DynamicTina diff --git a/examples/cms-tina/.tina/components/TinaProvider.js b/examples/cms-tina/.tina/components/TinaProvider.js new file mode 100644 index 000000000000..96d00b56aa8b --- /dev/null +++ b/examples/cms-tina/.tina/components/TinaProvider.js @@ -0,0 +1,14 @@ +import TinaCMS from 'tinacms' +import { tinaConfig } from '../schema.ts' + +// Importing the TinaProvider directly into your page will cause Tina to be added to the production bundle. +// Instead, import the tina/provider/index default export to have it dynamially imported in edit-moode +/** + * + * @private Do not import this directly, please import the dynamic provider instead + */ +const TinaProvider = ({ children }) => { + return <TinaCMS {...tinaConfig}>{children}</TinaCMS> +} + +export default TinaProvider diff --git a/examples/cms-tina/.tina/schema.ts b/examples/cms-tina/.tina/schema.ts new file mode 100644 index 000000000000..12f738c64a26 --- /dev/null +++ b/examples/cms-tina/.tina/schema.ts @@ -0,0 +1,105 @@ +import { defineSchema, defineConfig } from 'tinacms' + +export default defineSchema({ + collections: [ + { + label: 'Blog Posts', + name: 'posts', + path: '_posts', + fields: [ + { + type: 'string', + label: 'Title', + name: 'title', + }, + { + type: 'string', + label: 'Excerpt', + name: 'excerpt', + }, + { + type: 'string', + label: 'Cover Image', + name: 'coverImage', + }, + { + type: 'string', + label: 'Date', + name: 'date', + }, + { + type: 'object', + label: 'author', + name: 'author', + fields: [ + { + type: 'string', + label: 'Name', + name: 'name', + }, + { + type: 'string', + label: 'Picture', + name: 'picture', + }, + ], + }, + { + type: 'object', + label: 'OG Image', + name: 'ogImage', + fields: [ + { + type: 'string', + label: 'Url', + name: 'url', + }, + ], + }, + { + type: 'string', + label: 'Blog Post Body', + name: 'body', + isBody: true, + ui: { + component: 'textarea', + }, + }, + ], + }, + ], +}) + +// Your tina config +// ============== +const branch = 'main' +// When working locally, hit our local filesystem. +// On a Vercel deployment, hit the Tina Cloud API +const apiURL = + process.env.NODE_ENV == 'development' + ? 'http://localhost:4001/graphql' + : `https://content.tinajs.io/content/${process.env.NEXT_PUBLIC_TINA_CLIENT_ID}/github/${branch}` + +export const tinaConfig = defineConfig({ + apiURL, + cmsCallback: (cms) => { + // add your CMS callback code here (if you want) + + // The Route Mapper + /** + * 1. Import `tinacms` and `RouteMappingPlugin` + **/ + import('tinacms').then(({ RouteMappingPlugin }) => { + /** + * 2. Define the `RouteMappingPlugin` see https://tina.io/docs/tinacms-context/#the-routemappingplugin for more details + **/ + const RouteMapping = new RouteMappingPlugin((collection, document) => { + return undefined + }) + /** + * 3. Add the `RouteMappingPlugin` to the `cms`. + **/ + cms.plugins.add(RouteMapping) + }) + }, +}) diff --git a/examples/cms-tina/README.md b/examples/cms-tina/README.md new file mode 100644 index 000000000000..6f0b3efb5d94 --- /dev/null +++ b/examples/cms-tina/README.md @@ -0,0 +1,77 @@ +# A statically generated blog example using Next.js and TinaCMS + +This example showcases Next.js's [Static Generation](https://nextjs.org/docs/basic-features/pages) feature using [TinaCMS](https://tina.io/) as the CMS and editor. + +> This boilerplate demonstrates a basic usage and best practices. If you are looking for a more feature rich Tina experience with contextual editing. +> check out [tina-cloud-starter](https://github.com/tinacms/tina-cloud-start/git). + +### Related examples + +- [WordPress](/examples/cms-wordpress) +- [DatoCMS](/examples/cms-datocms) +- [Sanity](/examples/cms-sanity) +- [TakeShape](/examples/cms-takeshape) +- [Prismic](/examples/cms-prismic) +- [Contentful](/examples/cms-contentful) +- [Strapi](/examples/cms-strapi) +- [Agility CMS](/examples/cms-agilitycms) +- [ButterCMS](/examples/cms-buttercms) +- [Storyblok](/examples/cms-storyblok) +- [GraphCMS](/examples/cms-graphcms) +- [Kontent](/examples/cms-kontent) +- [Umbraco Heartcore](/examples/cms-umbraco-heartcore) +- [Blog Starter](/examples/blog-starter) +- [Builder.io](/examples/cms-builder-io) + +## 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 cms-tina cms-tina-app +# or +yarn create next-app --example cms-ghost cms-tina-app +``` + +### Setp 1. Run Next.js in development mode + +To get started, no configuration is needed for local development and editing. + +```bash +npm install +npm run tina-dev + +# or + +yarn install +yarn tina-dev +``` + +Your blog 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). + +### Step 2. Editing blog posts. + +Tina is git backed and uses markdown, JSON or MDX to power websites. To enter edit mode locally you just need to visit [http://localhost:3000/admin](http://localhost:3000/admin) + +You can then select the collection "Blog Posts" and then the content you would like to edit. + +Once you hit save, Tina will use our graphQL modify the content on your filesystem. + +### Step 4. 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. Once you have pushed to GitHub, sign up for your Tina Cloud account at [https://app.tina.io/register](https://app.tina.io/register). Then follow the steps below: + +1. Select new project +2. Select Import your site +3. Follow steps to connect your GitHub repo. +4. Copy your Client ID + +Then [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 NEXT_PUBLIC_TINA_CLIENT_ID to the client ID above. + +Once you have successfully deployed to Vercel, go back to your Tina dashboard and under the project configuration enter the url in the Site URL(s) for example: https://tina-cms.vercel.app. diff --git a/examples/cms-tina/_posts/dynamic-routing.md b/examples/cms-tina/_posts/dynamic-routing.md new file mode 100644 index 000000000000..b6ef7a26e6ae --- /dev/null +++ b/examples/cms-tina/_posts/dynamic-routing.md @@ -0,0 +1,19 @@ +--- +title: 'Dynamic Routing and Static Generation' +excerpt: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Praesent elementum facilisis leo vel fringilla est ullamcorper eget. At imperdiet dui accumsan sit amet nulla facilities morbi tempus.' +coverImage: '/assets/blog/dynamic-routing/cover.jpg' +date: '2020-03-16T05:35:07.322Z' +author: + name: JJ Kasper + picture: '/assets/blog/authors/jj.jpeg' +ogImage: + url: '/assets/blog/dynamic-routing/cover.jpg' +--- + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Praesent elementum facilisis leo vel fringilla est ullamcorper eget. At imperdiet dui accumsan sit amet nulla facilities morbi tempus. Praesent elementum facilisis leo vel fringilla. Congue mauris rhoncus aenean vel. Egestas sed tempus urna et pharetra pharetra massa massa ultricies. + +Venenatis cras sed felis eget velit. Consectetur libero id faucibus nisl tincidunt. Gravida in fermentum et sollicitudin ac orci phasellus egestas tellus. Volutpat consequat mauris nunc congue nisi vitae. Id aliquet risus feugiat in ante metus dictum at tempor. Sed blandit libero volutpat sed cras. Sed odio morbi quis commodo odio aenean sed adipiscing. Velit euismod in pellentesque massa placerat. Mi bibendum neque egestas congue quisque egestas diam in arcu. Nisi lacus sed viverra tellus in. Nibh cras pulvinar mattis nunc sed. Luctus accumsan tortor posuere ac ut consequat semper viverra. Fringilla ut morbi tincidunt augue interdum velit euismod. + +## Lorem Ipsum + +Tristique senectus et netus et malesuada fames ac turpis. Ridiculous mus mauris vitae ultricies leo integer malesuada nunc vel. In mollis nunc sed id semper. Egestas tellus rutrum tellus pellentesque. Phasellus vestibulum lorem sed risus ultricies tristique nulla. Quis blandit turpis cursus in hac habitasse platea dictumst quisque. Eros donec ac odio tempor orci dapibus ultrices. Aliquam sem et tortor consequat id porta nibh. Adipiscing elit duis tristique sollicitudin nibh sit amet commodo nulla. Diam vulputate ut pharetra sit amet. Ut tellus elementum sagittis vitae et leo. Arcu non odio euismod lacinia at quis risus sed vulputate. diff --git a/examples/cms-tina/_posts/hello-world.md b/examples/cms-tina/_posts/hello-world.md new file mode 100644 index 000000000000..8d85a1df8f17 --- /dev/null +++ b/examples/cms-tina/_posts/hello-world.md @@ -0,0 +1,19 @@ +--- +title: 'Learn How to Pre-render Pages Using Static Generation with Next.js' +excerpt: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Praesent elementum facilisis leo vel fringilla est ullamcorper eget. At imperdiet dui accumsan sit amet nulla facilities morbi tempus.' +coverImage: '/assets/blog/hello-world/cover.jpg' +date: '2020-03-16T05:35:07.322Z' +author: + name: Tim Neutkens + picture: '/assets/blog/authors/tim.jpeg' +ogImage: + url: '/assets/blog/hello-world/cover.jpg' +--- + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Praesent elementum facilisis leo vel fringilla est ullamcorper eget. At imperdiet dui accumsan sit amet nulla facilities morbi tempus. Praesent elementum facilisis leo vel fringilla. Congue mauris rhoncus aenean vel. Egestas sed tempus urna et pharetra pharetra massa massa ultricies. + +Venenatis cras sed felis eget velit. Consectetur libero id faucibus nisl tincidunt. Gravida in fermentum et sollicitudin ac orci phasellus egestas tellus. Volutpat consequat mauris nunc congue nisi vitae. Id aliquet risus feugiat in ante metus dictum at tempor. Sed blandit libero volutpat sed cras. Sed odio morbi quis commodo odio aenean sed adipiscing. Velit euismod in pellentesque massa placerat. Mi bibendum neque egestas congue quisque egestas diam in arcu. Nisi lacus sed viverra tellus in. Nibh cras pulvinar mattis nunc sed. Luctus accumsan tortor posuere ac ut consequat semper viverra. Fringilla ut morbi tincidunt augue interdum velit euismod. + +## Lorem Ipsum + +Tristique senectus et netus et malesuada fames ac turpis. Ridiculous mus mauris vitae ultricies leo integer malesuada nunc vel. In mollis nunc sed id semper. Egestas tellus rutrum tellus pellentesque. Phasellus vestibulum lorem sed risus ultricies tristique nulla. Quis blandit turpis cursus in hac habitasse platea dictumst quisque. Eros donec ac odio tempor orci dapibus ultrices. Aliquam sem et tortor consequat id porta nibh. Adipiscing elit duis tristique sollicitudin nibh sit amet commodo nulla. Diam vulputate ut pharetra sit amet. Ut tellus elementum sagittis vitae et leo. Arcu non odio euismod lacinia at quis risus sed vulputate. diff --git a/examples/cms-tina/_posts/preview.md b/examples/cms-tina/_posts/preview.md new file mode 100644 index 000000000000..3d70ba7f99d1 --- /dev/null +++ b/examples/cms-tina/_posts/preview.md @@ -0,0 +1,19 @@ +--- +title: 'Preview Mode for Static Generation' +excerpt: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Praesent elementum facilisis leo vel fringilla est ullamcorper eget. At imperdiet dui accumsan sit amet nulla facilities morbi tempus.' +coverImage: '/assets/blog/preview/cover.jpg' +date: '2020-03-16T05:35:07.322Z' +author: + name: Joe Haddad + picture: '/assets/blog/authors/joe.jpeg' +ogImage: + url: '/assets/blog/preview/cover.jpg' +--- + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Praesent elementum facilisis leo vel fringilla est ullamcorper eget. At imperdiet dui accumsan sit amet nulla facilities morbi tempus. Praesent elementum facilisis leo vel fringilla. Congue mauris rhoncus aenean vel. Egestas sed tempus urna et pharetra pharetra massa massa ultricies. + +Venenatis cras sed felis eget velit. Consectetur libero id faucibus nisl tincidunt. Gravida in fermentum et sollicitudin ac orci phasellus egestas tellus. Volutpat consequat mauris nunc congue nisi vitae. Id aliquet risus feugiat in ante metus dictum at tempor. Sed blandit libero volutpat sed cras. Sed odio morbi quis commodo odio aenean sed adipiscing. Velit euismod in pellentesque massa placerat. Mi bibendum neque egestas congue quisque egestas diam in arcu. Nisi lacus sed viverra tellus in. Nibh cras pulvinar mattis nunc sed. Luctus accumsan tortor posuere ac ut consequat semper viverra. Fringilla ut morbi tincidunt augue interdum velit euismod. + +## Lorem Ipsum + +Tristique senectus et netus et malesuada fames ac turpis. Ridiculous mus mauris vitae ultricies leo integer malesuada nunc vel. In mollis nunc sed id semper. Egestas tellus rutrum tellus pellentesque. Phasellus vestibulum lorem sed risus ultricies tristique nulla. Quis blandit turpis cursus in hac habitasse platea dictumst quisque. Eros donec ac odio tempor orci dapibus ultrices. Aliquam sem et tortor consequat id porta nibh. Adipiscing elit duis tristique sollicitudin nibh sit amet commodo nulla. Diam vulputate ut pharetra sit amet. Ut tellus elementum sagittis vitae et leo. Arcu non odio euismod lacinia at quis risus sed vulputate. diff --git a/examples/cms-tina/components/alert.js b/examples/cms-tina/components/alert.js new file mode 100644 index 000000000000..b924cb097f16 --- /dev/null +++ b/examples/cms-tina/components/alert.js @@ -0,0 +1,42 @@ +import Container from './container' +import cn from 'classnames' +import { EXAMPLE_PATH } from '../lib/constants' + +export default function Alert({ preview }) { + return ( + <div + className={cn('border-b', { + 'bg-accent-7 border-accent-7 text-white': preview, + 'bg-accent-1 border-accent-2': !preview, + })} + > + <Container> + <div className="py-2 text-center text-sm"> + {preview ? ( + <> + This page is a preview.{' '} + <a + href="/api/exit-preview" + className="underline hover:text-cyan duration-200 transition-colors" + > + Click here + </a>{' '} + to exit preview mode. + </> + ) : ( + <> + The source code for this blog is{' '} + <a + href={`https://github.com/vercel/next.js/tree/canary/examples/${EXAMPLE_PATH}`} + className="underline hover:text-success duration-200 transition-colors" + > + available on GitHub + </a> + . + </> + )} + </div> + </Container> + </div> + ) +} diff --git a/examples/cms-tina/components/avatar.js b/examples/cms-tina/components/avatar.js new file mode 100644 index 000000000000..0ae7f66413cc --- /dev/null +++ b/examples/cms-tina/components/avatar.js @@ -0,0 +1,16 @@ +import Image from 'next/image' + +export default function Avatar({ name, picture }) { + return ( + <div className="flex items-center"> + <Image + src={picture} + width={48} + height={48} + className="w-12 h-12 rounded-full mr-4" + alt={name} + /> + <div className="text-xl font-bold">{name}</div> + </div> + ) +} diff --git a/examples/cms-tina/components/container.js b/examples/cms-tina/components/container.js new file mode 100644 index 000000000000..fc1c29dfb074 --- /dev/null +++ b/examples/cms-tina/components/container.js @@ -0,0 +1,3 @@ +export default function Container({ children }) { + return <div className="container mx-auto px-5">{children}</div> +} diff --git a/examples/cms-tina/components/cover-image.js b/examples/cms-tina/components/cover-image.js new file mode 100644 index 000000000000..d3e21d21f161 --- /dev/null +++ b/examples/cms-tina/components/cover-image.js @@ -0,0 +1,29 @@ +import cn from 'classnames' +import Link from 'next/link' +import Image from 'next/image' + +export default function CoverImage({ title, src, slug, height, width }) { + const image = ( + <Image + src={src} + alt={`Cover Image for ${title}`} + className={cn('shadow-sm', { + 'hover:shadow-md transition-shadow duration-200': slug, + })} + layout="responsive" + width={width} + height={height} + /> + ) + return ( + <div className="sm:mx-0"> + {slug ? ( + <Link href={`/posts/${slug}`}> + <a aria-label={title}>{image}</a> + </Link> + ) : ( + image + )} + </div> + ) +} diff --git a/examples/cms-tina/components/date-formatter.js b/examples/cms-tina/components/date-formatter.js new file mode 100644 index 000000000000..9de4f4880011 --- /dev/null +++ b/examples/cms-tina/components/date-formatter.js @@ -0,0 +1,6 @@ +import { parseISO, format } from 'date-fns' + +export default function DateFormatter({ dateString }) { + const date = parseISO(dateString) + return <time dateTime={dateString}>{format(date, 'LLLL d, yyyy')}</time> +} diff --git a/examples/cms-tina/components/footer.js b/examples/cms-tina/components/footer.js new file mode 100644 index 000000000000..da9eed88ec26 --- /dev/null +++ b/examples/cms-tina/components/footer.js @@ -0,0 +1,30 @@ +import Container from './container' +import { EXAMPLE_PATH } from '../lib/constants' + +export default function Footer() { + return ( + <footer className="bg-accent-1 border-t border-accent-2"> + <Container> + <div className="py-28 flex flex-col lg:flex-row items-center"> + <h3 className="text-4xl lg:text-5xl font-bold tracking-tighter leading-tight text-center lg:text-left mb-10 lg:mb-0 lg:pr-4 lg:w-1/2"> + Statically Generated with Next.js. + </h3> + <div className="flex flex-col lg:flex-row justify-center items-center lg:pl-4 lg:w-1/2"> + <a + href="https://nextjs.org/docs/basic-features/pages" + className="mx-3 bg-black hover:bg-white hover:text-black border border-black text-white font-bold py-3 px-12 lg:px-8 duration-200 transition-colors mb-6 lg:mb-0" + > + Read Documentation + </a> + <a + href={`https://github.com/vercel/next.js/tree/canary/examples/${EXAMPLE_PATH}`} + className="mx-3 font-bold hover:underline" + > + View on GitHub + </a> + </div> + </div> + </Container> + </footer> + ) +} diff --git a/examples/cms-tina/components/header.js b/examples/cms-tina/components/header.js new file mode 100644 index 000000000000..562e7e3eebb6 --- /dev/null +++ b/examples/cms-tina/components/header.js @@ -0,0 +1,12 @@ +import Link from 'next/link' + +export default function Header() { + return ( + <h2 className="text-2xl md:text-4xl font-bold tracking-tight md:tracking-tighter leading-tight mb-20 mt-8"> + <Link href="/"> + <a className="hover:underline">Blog</a> + </Link> + . + </h2> + ) +} diff --git a/examples/cms-tina/components/hero-post.js b/examples/cms-tina/components/hero-post.js new file mode 100644 index 000000000000..6dc49f20a8bb --- /dev/null +++ b/examples/cms-tina/components/hero-post.js @@ -0,0 +1,43 @@ +import Avatar from '../components/avatar' +import DateFormatter from '../components/date-formatter' +import CoverImage from '../components/cover-image' +import Link from 'next/link' + +export default function HeroPost({ + title, + coverImage, + date, + excerpt, + author, + slug, +}) { + return ( + <section> + <div className="mb-8 md:mb-16"> + <CoverImage + title={title} + src={coverImage} + slug={slug} + height={620} + width={1240} + /> + </div> + <div className="md:grid md:grid-cols-2 md:gap-x-16 lg:gap-x-8 mb-20 md:mb-28"> + <div> + <h3 className="mb-4 text-4xl lg:text-6xl leading-tight"> + <Link href={`/posts/${slug}`}> + <a className="hover:underline">{title}</a> + </Link> + </h3> + <div className="mb-4 md:mb-0 text-lg"> + <DateFormatter dateString={date} /> + </div> + </div> + <div> + <p className="text-lg leading-relaxed mb-4">{excerpt}</p> + <Avatar name={author.name} picture={author.picture} /> + </div> + </div> + </section> + ) +} diff --git a/examples/cms-tina/components/intro.js b/examples/cms-tina/components/intro.js new file mode 100644 index 000000000000..048fc170a13e --- /dev/null +++ b/examples/cms-tina/components/intro.js @@ -0,0 +1,21 @@ +import { CMS_NAME } from '../lib/constants' + +export default function Intro() { + return ( + <section className="flex-col md:flex-row flex items-center md:justify-between mt-16 mb-16 md:mb-12"> + <h1 className="text-6xl md:text-8xl font-bold tracking-tighter leading-tight md:pr-8"> + Blog. + </h1> + <h4 className="text-center md:text-left text-lg mt-5 md:pl-8"> + A statically generated blog example using{' '} + <a + href="https://nextjs.org/" + className="underline hover:text-success duration-200 transition-colors" + > + Next.js + </a>{' '} + and {CMS_NAME}. + </h4> + </section> + ) +} diff --git a/examples/cms-tina/components/layout.js b/examples/cms-tina/components/layout.js new file mode 100644 index 000000000000..99d95353131e --- /dev/null +++ b/examples/cms-tina/components/layout.js @@ -0,0 +1,16 @@ +import Alert from '../components/alert' +import Footer from '../components/footer' +import Meta from '../components/meta' + +export default function Layout({ preview, children }) { + return ( + <> + <Meta /> + <div className="min-h-screen"> + <Alert preview={preview} /> + <main>{children}</main> + </div> + <Footer /> + </> + ) +} diff --git a/examples/cms-tina/components/markdown-styles.module.css b/examples/cms-tina/components/markdown-styles.module.css new file mode 100644 index 000000000000..95d4f8b04172 --- /dev/null +++ b/examples/cms-tina/components/markdown-styles.module.css @@ -0,0 +1,18 @@ +.markdown { + @apply text-lg leading-relaxed; +} + +.markdown p, +.markdown ul, +.markdown ol, +.markdown blockquote { + @apply my-6; +} + +.markdown h2 { + @apply text-3xl mt-12 mb-4 leading-snug; +} + +.markdown h3 { + @apply text-2xl mt-8 mb-4 leading-snug; +} diff --git a/examples/cms-tina/components/meta.js b/examples/cms-tina/components/meta.js new file mode 100644 index 000000000000..349fc3d73a7a --- /dev/null +++ b/examples/cms-tina/components/meta.js @@ -0,0 +1,42 @@ +import Head from 'next/head' +import { CMS_NAME, HOME_OG_IMAGE_URL } from '../lib/constants' + +export default function Meta() { + return ( + <Head> + <link + rel="apple-touch-icon" + sizes="180x180" + href="/favicons/apple-touch-icon.png" + /> + <link + rel="icon" + type="image/png" + sizes="32x32" + href="/favicons/favicon-32x32.png" + /> + <link + rel="icon" + type="image/png" + sizes="16x16" + href="/favicons/favicon-16x16.png" + /> + <link rel="manifest" href="/favicons/site.webmanifest" /> + <link + rel="mask-icon" + href="/favicons/safari-pinned-tab.svg" + color="#000000" + /> + <link rel="shortcut icon" href="/favicons/favicon.ico" /> + <meta name="msapplication-TileColor" content="#000000" /> + <meta name="msapplication-config" content="/favicons/browserconfig.xml" /> + <meta name="theme-color" content="#000" /> + <link rel="alternate" type="application/rss+xml" href="/feed.xml" /> + <meta + name="description" + content={`A statically generated blog example using Next.js and ${CMS_NAME}.`} + /> + <meta property="og:image" content={HOME_OG_IMAGE_URL} /> + </Head> + ) +} diff --git a/examples/cms-tina/components/more-stories.js b/examples/cms-tina/components/more-stories.js new file mode 100644 index 000000000000..57fdbb6c4659 --- /dev/null +++ b/examples/cms-tina/components/more-stories.js @@ -0,0 +1,24 @@ +import PostPreview from '../components/post-preview' + +export default function MoreStories({ posts }) { + return ( + <section> + <h2 className="mb-8 text-6xl md:text-7xl font-bold tracking-tighter leading-tight"> + More Stories + </h2> + <div className="grid grid-cols-1 md:grid-cols-2 md:gap-x-16 lg:gap-x-32 gap-y-20 md:gap-y-32 mb-32"> + {posts.map((post) => ( + <PostPreview + key={post.slug} + title={post.title} + coverImage={post.coverImage} + date={post.date} + author={post.author} + slug={post.slug} + excerpt={post.excerpt} + /> + ))} + </div> + </section> + ) +} diff --git a/examples/cms-tina/components/post-body.js b/examples/cms-tina/components/post-body.js new file mode 100644 index 000000000000..4cc2bb52bcce --- /dev/null +++ b/examples/cms-tina/components/post-body.js @@ -0,0 +1,12 @@ +import markdownStyles from './markdown-styles.module.css' + +export default function PostBody({ content }) { + return ( + <div className="max-w-2xl mx-auto"> + <div + className={markdownStyles['markdown']} + dangerouslySetInnerHTML={{ __html: content }} + /> + </div> + ) +} diff --git a/examples/cms-tina/components/post-header.js b/examples/cms-tina/components/post-header.js new file mode 100644 index 000000000000..76789f7a98c1 --- /dev/null +++ b/examples/cms-tina/components/post-header.js @@ -0,0 +1,26 @@ +import Avatar from '../components/avatar' +import DateFormatter from '../components/date-formatter' +import CoverImage from '../components/cover-image' +import PostTitle from '../components/post-title' + +export default function PostHeader({ title, coverImage, date, author }) { + return ( + <> + <PostTitle>{title}</PostTitle> + <div className="hidden md:block md:mb-12"> + <Avatar name={author.name} picture={author.picture} /> + </div> + <div className="mb-8 md:mb-16 sm:mx-0"> + <CoverImage title={title} src={coverImage} height={620} width={1240} /> + </div> + <div className="max-w-2xl mx-auto"> + <div className="block md:hidden mb-6"> + <Avatar name={author.name} picture={author.picture} /> + </div> + <div className="mb-6 text-lg"> + <DateFormatter dateString={date} /> + </div> + </div> + </> + ) +} diff --git a/examples/cms-tina/components/post-preview.js b/examples/cms-tina/components/post-preview.js new file mode 100644 index 000000000000..712eea6e92ec --- /dev/null +++ b/examples/cms-tina/components/post-preview.js @@ -0,0 +1,37 @@ +import Avatar from '../components/avatar' +import DateFormatter from '../components/date-formatter' +import CoverImage from './cover-image' +import Link from 'next/link' + +export default function PostPreview({ + title, + coverImage, + date, + excerpt, + author, + slug, +}) { + return ( + <div> + <div className="mb-5"> + <CoverImage + slug={slug} + title={title} + src={coverImage} + height={278} + width={556} + /> + </div> + <h3 className="text-3xl mb-3 leading-snug"> + <Link href={`/posts/${slug}`}> + <a className="hover:underline">{title}</a> + </Link> + </h3> + <div className="text-lg mb-4"> + <DateFormatter dateString={date} /> + </div> + <p className="text-lg leading-relaxed mb-4">{excerpt}</p> + <Avatar name={author.name} picture={author.picture} /> + </div> + ) +} diff --git a/examples/cms-tina/components/post-title.js b/examples/cms-tina/components/post-title.js new file mode 100644 index 000000000000..edd8cba65c25 --- /dev/null +++ b/examples/cms-tina/components/post-title.js @@ -0,0 +1,7 @@ +export default function PostTitle({ children }) { + return ( + <h1 className="text-6xl md:text-7xl lg:text-8xl font-bold tracking-tighter leading-tight md:leading-none mb-12 text-center md:text-left"> + {children} + </h1> + ) +} diff --git a/examples/cms-tina/components/section-separator.js b/examples/cms-tina/components/section-separator.js new file mode 100644 index 000000000000..4ca5c65fdc6e --- /dev/null +++ b/examples/cms-tina/components/section-separator.js @@ -0,0 +1,3 @@ +export default function SectionSeparator() { + return <hr className="border-accent-2 mt-28 mb-24" /> +} diff --git a/examples/cms-tina/lib/api.js b/examples/cms-tina/lib/api.js new file mode 100644 index 000000000000..b9612a8ac536 --- /dev/null +++ b/examples/cms-tina/lib/api.js @@ -0,0 +1,43 @@ +import fs from 'fs' +import { join } from 'path' +import matter from 'gray-matter' + +const postsDirectory = join(process.cwd(), '_posts') + +export function getPostSlugs() { + return fs.readdirSync(postsDirectory) +} + +export function getPostBySlug(slug, fields = []) { + const realSlug = slug.replace(/\.md$/, '') + const fullPath = join(postsDirectory, `${realSlug}.md`) + const fileContents = fs.readFileSync(fullPath, 'utf8') + const { data, content } = matter(fileContents) + + const items = {} + + // Ensure only the minimal needed data is exposed + fields.forEach((field) => { + if (field === 'slug') { + items[field] = realSlug + } + if (field === 'content') { + items[field] = content + } + + if (typeof data[field] !== 'undefined') { + items[field] = data[field] + } + }) + + return items +} + +export function getAllPosts(fields = []) { + const slugs = getPostSlugs() + const posts = slugs + .map((slug) => getPostBySlug(slug, fields)) + // sort posts by date in descending order + .sort((post1, post2) => (post1.date > post2.date ? -1 : 1)) + return posts +} diff --git a/examples/cms-tina/lib/constants.js b/examples/cms-tina/lib/constants.js new file mode 100644 index 000000000000..671a9c22b49a --- /dev/null +++ b/examples/cms-tina/lib/constants.js @@ -0,0 +1,4 @@ +export const EXAMPLE_PATH = 'cms-tina' +export const CMS_NAME = 'Tina' +export const HOME_OG_IMAGE_URL = + 'https://og-image.vercel.app/Next.js%20Blog%20Starter%20Example.png?theme=light&md=1&fontSize=100px&images=https%3A%2F%2Fassets.vercel.com%2Fimage%2Fupload%2Ffront%2Fassets%2Fdesign%2Fnextjs-black-logo.svg' diff --git a/examples/cms-tina/lib/markdownToHtml.js b/examples/cms-tina/lib/markdownToHtml.js new file mode 100644 index 000000000000..735fed2df383 --- /dev/null +++ b/examples/cms-tina/lib/markdownToHtml.js @@ -0,0 +1,7 @@ +import { remark } from 'remark' +import html from 'remark-html' + +export default async function markdownToHtml(markdown) { + const result = await remark().use(html).process(markdown) + return result.toString() +} diff --git a/examples/cms-tina/package.json b/examples/cms-tina/package.json new file mode 100644 index 000000000000..4ab85e58294a --- /dev/null +++ b/examples/cms-tina/package.json @@ -0,0 +1,29 @@ +{ + "private": true, + "scripts": { + "dev": "next", + "build": "next build", + "start": "next start", + "tina-dev": "yarn tinacms server:start -c \"next dev\"", + "tina-build": "yarn tinacms server:start -c \"next build\"", + "tina-start": "yarn tinacms server:start -c \"next start\"" + }, + "dependencies": { + "@tinacms/cli": "^0.60.8", + "classnames": "2.3.1", + "date-fns": "2.28.0", + "gray-matter": "4.0.3", + "next": "latest", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "remark": "14.0.2", + "remark-html": "15.0.1", + "styled-components": "^5.3.3", + "tinacms": "^0.66.7" + }, + "devDependencies": { + "autoprefixer": "^10.4.2", + "postcss": "^8.4.7", + "tailwindcss": "^3.0.23" + } +} diff --git a/examples/cms-tina/pages/_app.js b/examples/cms-tina/pages/_app.js new file mode 100644 index 000000000000..8ad946c7ebb1 --- /dev/null +++ b/examples/cms-tina/pages/_app.js @@ -0,0 +1,12 @@ +import Tina from '../.tina/components/TinaDynamicProvider.js' + +import '../styles/index.css' +const App = ({ Component, pageProps }) => { + return ( + <Tina> + <Component {...pageProps} /> + </Tina> + ) +} + +export default App diff --git a/examples/cms-tina/pages/_document.js b/examples/cms-tina/pages/_document.js new file mode 100644 index 000000000000..c55951c0d5da --- /dev/null +++ b/examples/cms-tina/pages/_document.js @@ -0,0 +1,15 @@ +import Document, { Html, Head, Main, NextScript } from 'next/document' + +export default class MyDocument extends Document { + render() { + return ( + <Html lang="en"> + <Head /> + <body> + <Main /> + <NextScript /> + </body> + </Html> + ) + } +} diff --git a/examples/cms-tina/pages/admin.js b/examples/cms-tina/pages/admin.js new file mode 100644 index 000000000000..edebe7a815a7 --- /dev/null +++ b/examples/cms-tina/pages/admin.js @@ -0,0 +1,2 @@ +import { TinaAdmin } from 'tinacms' +export default TinaAdmin diff --git a/examples/cms-tina/pages/index.js b/examples/cms-tina/pages/index.js new file mode 100644 index 000000000000..c46462c0f423 --- /dev/null +++ b/examples/cms-tina/pages/index.js @@ -0,0 +1,51 @@ +import Container from '../components/container' +import MoreStories from '../components/more-stories' +import HeroPost from '../components/hero-post' +import Intro from '../components/intro' +import Layout from '../components/layout' +import { getAllPosts } from '../lib/api' +import Head from 'next/head' +import { CMS_NAME } from '../lib/constants' + +export default function Index({ allPosts }) { + const heroPost = allPosts[0] + const morePosts = allPosts.slice(1) + return ( + <> + <Layout> + <Head> + <title>Next.js Blog Example with {CMS_NAME} + + + + {heroPost && ( + + )} + {morePosts.length > 0 && } + + + + ) +} + +export async function getStaticProps() { + const allPosts = getAllPosts([ + 'title', + 'date', + 'slug', + 'author', + 'coverImage', + 'excerpt', + ]) + + return { + props: { allPosts }, + } +} diff --git a/examples/cms-tina/pages/posts/[slug].js b/examples/cms-tina/pages/posts/[slug].js new file mode 100644 index 000000000000..83a1fb464af4 --- /dev/null +++ b/examples/cms-tina/pages/posts/[slug].js @@ -0,0 +1,84 @@ +import { useRouter } from 'next/router' +import ErrorPage from 'next/error' +import Container from '../../components/container' +import PostBody from '../../components/post-body' +import Header from '../../components/header' +import PostHeader from '../../components/post-header' +import Layout from '../../components/layout' +import { getPostBySlug, getAllPosts } from '../../lib/api' +import PostTitle from '../../components/post-title' +import Head from 'next/head' +import { CMS_NAME } from '../../lib/constants' +import markdownToHtml from '../../lib/markdownToHtml' + +export default function Post({ post, morePosts, preview }) { + const router = useRouter() + if (!router.isFallback && !post?.slug) { + return + } + return ( + + +
+ {router.isFallback ? ( + Loading… + ) : ( + <> +
+ + + {post.title} | Next.js Blog Example with {CMS_NAME} + + + + + +
+ + )} + + + ) +} + +export async function getStaticProps({ params }) { + const post = getPostBySlug(params.slug, [ + 'title', + 'date', + 'slug', + 'author', + 'content', + 'ogImage', + 'coverImage', + ]) + const content = await markdownToHtml(post.content || '') + + return { + props: { + post: { + ...post, + content, + }, + }, + } +} + +export async function getStaticPaths() { + const posts = getAllPosts(['slug']) + + return { + paths: posts.map((post) => { + return { + params: { + slug: post.slug, + }, + } + }), + fallback: false, + } +} diff --git a/examples/cms-tina/postcss.config.js b/examples/cms-tina/postcss.config.js new file mode 100644 index 000000000000..3fa0a9514dc9 --- /dev/null +++ b/examples/cms-tina/postcss.config.js @@ -0,0 +1,8 @@ +// If you want to use other PostCSS plugins, see the following: +// https://tailwindcss.com/docs/using-with-preprocessors +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/examples/cms-tina/public/assets/blog/authors/jj.jpeg b/examples/cms-tina/public/assets/blog/authors/jj.jpeg new file mode 100644 index 000000000000..e3d521436a6c Binary files /dev/null and b/examples/cms-tina/public/assets/blog/authors/jj.jpeg differ diff --git a/examples/cms-tina/public/assets/blog/authors/joe.jpeg b/examples/cms-tina/public/assets/blog/authors/joe.jpeg new file mode 100644 index 000000000000..d9677ad61c00 Binary files /dev/null and b/examples/cms-tina/public/assets/blog/authors/joe.jpeg differ diff --git a/examples/cms-tina/public/assets/blog/authors/tim.jpeg b/examples/cms-tina/public/assets/blog/authors/tim.jpeg new file mode 100644 index 000000000000..cc49257b8230 Binary files /dev/null and b/examples/cms-tina/public/assets/blog/authors/tim.jpeg differ diff --git a/examples/cms-tina/public/assets/blog/dynamic-routing/cover.jpg b/examples/cms-tina/public/assets/blog/dynamic-routing/cover.jpg new file mode 100644 index 000000000000..c660c92679ee Binary files /dev/null and b/examples/cms-tina/public/assets/blog/dynamic-routing/cover.jpg differ diff --git a/examples/cms-tina/public/assets/blog/hello-world/cover.jpg b/examples/cms-tina/public/assets/blog/hello-world/cover.jpg new file mode 100644 index 000000000000..33b7dc4b73ce Binary files /dev/null and b/examples/cms-tina/public/assets/blog/hello-world/cover.jpg differ diff --git a/examples/cms-tina/public/assets/blog/preview/cover.jpg b/examples/cms-tina/public/assets/blog/preview/cover.jpg new file mode 100644 index 000000000000..6a975fb36d05 Binary files /dev/null and b/examples/cms-tina/public/assets/blog/preview/cover.jpg differ diff --git a/examples/cms-tina/public/favicons/android-chrome-192x192.png b/examples/cms-tina/public/favicons/android-chrome-192x192.png new file mode 100644 index 000000000000..2f07282a59cd Binary files /dev/null and b/examples/cms-tina/public/favicons/android-chrome-192x192.png differ diff --git a/examples/cms-tina/public/favicons/android-chrome-512x512.png b/examples/cms-tina/public/favicons/android-chrome-512x512.png new file mode 100644 index 000000000000..dbb0faea8404 Binary files /dev/null and b/examples/cms-tina/public/favicons/android-chrome-512x512.png differ diff --git a/examples/cms-tina/public/favicons/apple-touch-icon.png b/examples/cms-tina/public/favicons/apple-touch-icon.png new file mode 100644 index 000000000000..8f4033b2a8b3 Binary files /dev/null and b/examples/cms-tina/public/favicons/apple-touch-icon.png differ diff --git a/examples/cms-tina/public/favicons/browserconfig.xml b/examples/cms-tina/public/favicons/browserconfig.xml new file mode 100644 index 000000000000..9824d87b1151 --- /dev/null +++ b/examples/cms-tina/public/favicons/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #000000 + + + diff --git a/examples/cms-tina/public/favicons/favicon-16x16.png b/examples/cms-tina/public/favicons/favicon-16x16.png new file mode 100644 index 000000000000..29deaf6716e7 Binary files /dev/null and b/examples/cms-tina/public/favicons/favicon-16x16.png differ diff --git a/examples/cms-tina/public/favicons/favicon-32x32.png b/examples/cms-tina/public/favicons/favicon-32x32.png new file mode 100644 index 000000000000..e3b4277bf093 Binary files /dev/null and b/examples/cms-tina/public/favicons/favicon-32x32.png differ diff --git a/examples/cms-tina/public/favicons/favicon.ico b/examples/cms-tina/public/favicons/favicon.ico new file mode 100644 index 000000000000..ea2f437d9db6 Binary files /dev/null and b/examples/cms-tina/public/favicons/favicon.ico differ diff --git a/examples/cms-tina/public/favicons/mstile-150x150.png b/examples/cms-tina/public/favicons/mstile-150x150.png new file mode 100644 index 000000000000..f2dfd904bf1b Binary files /dev/null and b/examples/cms-tina/public/favicons/mstile-150x150.png differ diff --git a/examples/cms-tina/public/favicons/safari-pinned-tab.svg b/examples/cms-tina/public/favicons/safari-pinned-tab.svg new file mode 100644 index 000000000000..72ab6e050cb1 --- /dev/null +++ b/examples/cms-tina/public/favicons/safari-pinned-tab.svg @@ -0,0 +1,33 @@ + + + + +Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + + + + + + + + diff --git a/examples/cms-tina/public/favicons/site.webmanifest b/examples/cms-tina/public/favicons/site.webmanifest new file mode 100644 index 000000000000..a672d9a233c5 --- /dev/null +++ b/examples/cms-tina/public/favicons/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "Next.js", + "short_name": "Next.js", + "icons": [ + { + "src": "/favicons/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/favicons/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#000000", + "background_color": "#000000", + "display": "standalone" +} diff --git a/examples/cms-tina/styles/index.css b/examples/cms-tina/styles/index.css new file mode 100644 index 000000000000..b5c61c956711 --- /dev/null +++ b/examples/cms-tina/styles/index.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/examples/cms-tina/tailwind.config.js b/examples/cms-tina/tailwind.config.js new file mode 100644 index 000000000000..b176069a2c6b --- /dev/null +++ b/examples/cms-tina/tailwind.config.js @@ -0,0 +1,33 @@ +module.exports = { + content: ['./components/**/*.js', './pages/**/*.js'], + theme: { + extend: { + colors: { + 'accent-1': '#FAFAFA', + 'accent-2': '#EAEAEA', + 'accent-7': '#333', + success: '#0070f3', + cyan: '#79FFE1', + }, + spacing: { + 28: '7rem', + }, + letterSpacing: { + tighter: '-.04em', + }, + lineHeight: { + tight: 1.2, + }, + fontSize: { + '5xl': '2.5rem', + '6xl': '2.75rem', + '7xl': '4.5rem', + '8xl': '6.25rem', + }, + boxShadow: { + sm: '0 5px 10px rgba(0, 0, 0, 0.12)', + md: '0 8px 30px rgba(0, 0, 0, 0.12)', + }, + }, + }, +} diff --git a/examples/cms-wordpress/components/hero-post.js b/examples/cms-wordpress/components/hero-post.js index 302ff1e74e1b..46d376e81ad6 100644 --- a/examples/cms-wordpress/components/hero-post.js +++ b/examples/cms-wordpress/components/hero-post.js @@ -18,7 +18,7 @@ export default function HeroPost({ )} -
+

diff --git a/examples/cms-wordpress/components/more-stories.js b/examples/cms-wordpress/components/more-stories.js index d77cd124e75c..951fe76a1500 100644 --- a/examples/cms-wordpress/components/more-stories.js +++ b/examples/cms-wordpress/components/more-stories.js @@ -6,14 +6,14 @@ export default function MoreStories({ posts }) {

More Stories

-
+
{posts.map(({ node }) => ( diff --git a/examples/cms-wordpress/lib/api.js b/examples/cms-wordpress/lib/api.js index cabad8f8113f..9ec551c9d7a6 100644 --- a/examples/cms-wordpress/lib/api.js +++ b/examples/cms-wordpress/lib/api.js @@ -70,18 +70,14 @@ export async function getAllPostsForHome(preview) { slug date featuredImage { - node { - sourceUrl - } + sourceUrl } author { - node { - name - firstName - lastName - avatar { - url - } + name + firstName + lastName + avatar { + url } } } @@ -125,14 +121,10 @@ export async function getPostAndMorePosts(slug, preview, previewData) { slug date featuredImage { - node { - sourceUrl - } + sourceUrl } author { - node { - ...AuthorFields - } + ...AuthorFields } categories { edges { @@ -164,9 +156,7 @@ export async function getPostAndMorePosts(slug, preview, previewData) { excerpt content author { - node { - ...AuthorFields - } + ...AuthorFields } } } diff --git a/examples/cms-wordpress/next.config.js b/examples/cms-wordpress/next.config.js new file mode 100644 index 000000000000..de6202d75136 --- /dev/null +++ b/examples/cms-wordpress/next.config.js @@ -0,0 +1,8 @@ +module.exports = { + images: { + domains: [ + // "[yourapp].wpengine.com" (Update this to be your Wordpress application name in order to load images connected to your posts) + 'secure.gravatar.com', + ], + }, +} diff --git a/examples/cms-wordpress/pages/index.js b/examples/cms-wordpress/pages/index.js index e6c987fb8870..2d01e152485e 100644 --- a/examples/cms-wordpress/pages/index.js +++ b/examples/cms-wordpress/pages/index.js @@ -22,9 +22,9 @@ export default function Index({ allPosts: { edges }, preview }) { {heroPost && ( @@ -38,6 +38,7 @@ export default function Index({ allPosts: { edges }, preview }) { export async function getStaticProps({ preview = false }) { const allPosts = await getAllPostsForHome(preview) + return { props: { allPosts, preview }, } diff --git a/examples/cms-wordpress/pages/posts/[slug].js b/examples/cms-wordpress/pages/posts/[slug].js index b57d69536ff9..dc06089f5831 100644 --- a/examples/cms-wordpress/pages/posts/[slug].js +++ b/examples/cms-wordpress/pages/posts/[slug].js @@ -36,14 +36,14 @@ export default function Post({ post, posts, preview }) { diff --git a/examples/with-custom-reverse-proxy/.babelrc b/examples/with-custom-reverse-proxy/.babelrc deleted file mode 100644 index 1ff94f7ed28e..000000000000 --- a/examples/with-custom-reverse-proxy/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["next/babel"] -} diff --git a/examples/with-custom-reverse-proxy/README.md b/examples/with-custom-reverse-proxy/README.md deleted file mode 100644 index edd8710d323d..000000000000 --- a/examples/with-custom-reverse-proxy/README.md +++ /dev/null @@ -1,55 +0,0 @@ -# Reverse Proxy example - -This example applies this gist https://gist.github.com/jamsesso/67fd937b74989dc52e33 to Nextjs and provides: - -- Reverse proxy in development mode by add `http-proxy-middleware` to custom server -- NOT a recommended approach to production scale (hence explicit dev flag) as we should scope proxy as outside UI applications and have separate web server taking care of that. - -Sorry for the extra packages. I belong to the minority camp of writing ES6 code on Windows developers. Essentially you only need `http-proxy-middleware` on top of bare-bone Nextjs setup to run this example. - -## 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-custom-reverse-proxy with-custom-reverse-proxy-app -# or -yarn create next-app --example with-custom-reverse-proxy with-custom-reverse-proxy-app -``` - -## What it does - -Take any random query string to the index page and does a GET to `/api/` which gets routed internally to `https://swapi.co/api/`, or any API endpoint you wish to configure through the proxy. - -## Expectation - -/api/people/2 routed to https://swapi.co/api/people/2 -Try Reset - -```json -{ - "name": "C-3PO", - "height": "167", - "mass": "75", - "hair_color": "n/a", - "skin_color": "gold", - "eye_color": "yellow", - "birth_year": "112BBY", - "gender": "n/a", - "homeworld": "https://swapi.co/api/planets/1/", - "films": [ - "https://swapi.co/api/films/2/", - "https://swapi.co/api/films/5/", - "https://swapi.co/api/films/4/", - "https://swapi.co/api/films/6/", - "https://swapi.co/api/films/3/", - "https://swapi.co/api/films/1/" - ], - "species": ["https://swapi.co/api/species/2/"], - "vehicles": [], - "starships": [], - "created": "2014-12-10T15:10:51.357000Z", - "edited": "2014-12-20T21:17:50.309000Z", - "url": "https://swapi.co/api/people/2/" -} -``` diff --git a/examples/with-custom-reverse-proxy/package.json b/examples/with-custom-reverse-proxy/package.json deleted file mode 100644 index 962d3e9ecbe0..000000000000 --- a/examples/with-custom-reverse-proxy/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "private": true, - "dependencies": { - "express": "^4.15.3", - "next": "latest", - "react": "^17.0.2", - "react-dom": "^17.0.2", - "swr": "0.2.0" - }, - "devDependencies": { - "cross-env": "^5.0.1", - "http-proxy-middleware": "^1.0.4" - }, - "scripts": { - "dev": "cross-env NODE_ENV=development PORT=3000 node server.js", - "build": "next build", - "prod": "cross-env NODE_ENV=production PORT=3000 node server.js" - } -} diff --git a/examples/with-custom-reverse-proxy/pages/index.js b/examples/with-custom-reverse-proxy/pages/index.js deleted file mode 100644 index 9c01f77fea83..000000000000 --- a/examples/with-custom-reverse-proxy/pages/index.js +++ /dev/null @@ -1,35 +0,0 @@ -import { useState, useEffect } from 'react' -import { useRouter } from 'next/router' -import useSWR from 'swr' - -const fetcher = (url) => fetch(url).then((res) => res.json()) - -const useMounted = () => { - const [mounted, setMounted] = useState(false) - useEffect(() => setMounted(true), []) - return mounted -} - -export default function Index() { - const mounted = useMounted() - const router = useRouter() - const queryString = `/api/${Object.keys(router.query).join('')}` - const { data, error } = useSWR(() => (mounted ? queryString : null), fetcher) - - if (error) return
Failed to load
- if (!data) return
Loading...
- - return ( - -

- {queryString} routed to https://swapi.co{queryString} -

-

- Try -   - Reset -

-
{data ? JSON.stringify(data, null, 2) : 'Loading...'}
-
- ) -} diff --git a/examples/with-custom-reverse-proxy/server.js b/examples/with-custom-reverse-proxy/server.js deleted file mode 100644 index 8371a0f28b44..000000000000 --- a/examples/with-custom-reverse-proxy/server.js +++ /dev/null @@ -1,50 +0,0 @@ -/* eslint-disable no-console */ -const express = require('express') -const next = require('next') - -const devProxy = { - '/api': { - target: 'https://swapi.co/api/', - pathRewrite: { '^/api': '/' }, - changeOrigin: true, - }, -} - -const port = parseInt(process.env.PORT, 10) || 3000 -const env = process.env.NODE_ENV -const dev = env !== 'production' -const app = next({ - dir: '.', // base directory where everything is, could move to src later - dev, -}) - -const handle = app.getRequestHandler() - -let server -app - .prepare() - .then(() => { - server = express() - - // Set up the proxy. - if (dev && devProxy) { - const { createProxyMiddleware } = require('http-proxy-middleware') - Object.keys(devProxy).forEach(function (context) { - server.use(context, createProxyMiddleware(devProxy[context])) - }) - } - - // Default catch-all handler to allow Next.js to handle all other routes - server.all('*', (req, res) => handle(req, res)) - - server.listen(port, (err) => { - if (err) { - throw err - } - console.log(`> Ready on port ${port} [${env}]`) - }) - }) - .catch((err) => { - console.log('An error occurred, unable to start the server') - console.log(err) - }) diff --git a/examples/with-docker/Dockerfile b/examples/with-docker/Dockerfile index 9a478dd0b6c7..67bec4d37092 100644 --- a/examples/with-docker/Dockerfile +++ b/examples/with-docker/Dockerfile @@ -23,6 +23,9 @@ COPY . . RUN yarn build +# If using npm comment out above and use below instead +# RUN npm run build + # Production image, copy all the files and run next FROM node:16-alpine AS runner WORKDIR /app diff --git a/examples/with-emotion-swc/.gitignore b/examples/with-emotion-swc/.gitignore new file mode 100644 index 000000000000..1437c53f70bc --- /dev/null +++ b/examples/with-emotion-swc/.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/with-emotion-swc/README.md b/examples/with-emotion-swc/README.md new file mode 100644 index 000000000000..12b4f677a8d0 --- /dev/null +++ b/examples/with-emotion-swc/README.md @@ -0,0 +1,31 @@ +# Emotion Example + +Extract and inline critical css with +[@emotion/css](https://github.com/emotion-js/emotion/tree/master/packages/css), +[@emotion/server](https://github.com/emotion-js/emotion/tree/master/packages/server), +[@emotion/react](https://github.com/emotion-js/emotion/tree/master/packages/react), +and [@emotion/styled](https://github.com/emotion-js/emotion/tree/master/packages/styled). + +## Preview + +Preview the example live on [StackBlitz](http://stackblitz.com/): + +[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/vercel/next.js/tree/canary/examples/with-emotion) + +## Deploy your own + +Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example): + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-emotion&project-name=with-emotion&repository-name=with-emotion) + +## 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-emotion with-emotion-app +# or +yarn create next-app --example with-emotion with-emotion-app +``` + +Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). diff --git a/examples/with-emotion-swc/next.config.js b/examples/with-emotion-swc/next.config.js new file mode 100644 index 000000000000..0928286e81ee --- /dev/null +++ b/examples/with-emotion-swc/next.config.js @@ -0,0 +1,10 @@ +/** @type {import('next').NextConfig} */ + +const nextConfig = { + reactStrictMode: true, + experimental: { + emotion: true, + }, +} + +module.exports = nextConfig diff --git a/examples/with-emotion-swc/package.json b/examples/with-emotion-swc/package.json new file mode 100644 index 000000000000..411e3fcb8efb --- /dev/null +++ b/examples/with-emotion-swc/package.json @@ -0,0 +1,15 @@ +{ + "private": true, + "scripts": { + "dev": "next", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "@emotion/react": "11.8.1", + "@emotion/styled": "11.8.1", + "next": "latest", + "react": "17.0.2", + "react-dom": "17.0.2" + } +} diff --git a/examples/with-emotion-swc/pages/_app.js b/examples/with-emotion-swc/pages/_app.js new file mode 100644 index 000000000000..0fe238a5d232 --- /dev/null +++ b/examples/with-emotion-swc/pages/_app.js @@ -0,0 +1,15 @@ +import createCache from '@emotion/cache' +import { CacheProvider } from '@emotion/react' + +import { globalStyles } from '../shared/styles' + +const cache = createCache({ key: 'next' }) + +const App = ({ Component, pageProps }) => ( + + {globalStyles} + + +) + +export default App diff --git a/examples/with-emotion-swc/pages/index.js b/examples/with-emotion-swc/pages/index.js new file mode 100644 index 000000000000..481abe91b200 --- /dev/null +++ b/examples/with-emotion-swc/pages/index.js @@ -0,0 +1,20 @@ +import { css } from '@emotion/react' +import { Animated, Basic, bounce, Combined, Pink } from '../shared/styles' + +const Home = () => ( +
+ Cool Styles + Pink text + + With :hover. + + Let's bounce. +
+) + +export default Home diff --git a/examples/with-emotion-swc/shared/styles.js b/examples/with-emotion-swc/shared/styles.js new file mode 100644 index 000000000000..cb9720bbfeba --- /dev/null +++ b/examples/with-emotion-swc/shared/styles.js @@ -0,0 +1,72 @@ +import { css, Global, keyframes } from '@emotion/react' +import styled from '@emotion/styled' + +export const globalStyles = ( + +) + +export const basicStyles = css({ + backgroundColor: 'white', + color: 'cornflowerblue', + border: '1px solid lightgreen', + borderRight: 'none', + borderBottom: 'none', + boxShadow: '5px 5px 0 0 lightgreen, 10px 10px 0 0 lightyellow', + transition: 'all 0.1s linear', + margin: '3rem 0', + padding: '1rem 0.5rem', +}) + +export const hoverStyles = css` + &:hover { + color: white; + background-color: lightgray; + border-color: aqua; + box-shadow: -15px -15px 0 0 aqua, -30px -30px 0 0 cornflowerblue; + } +` +export const bounce = keyframes` + from { + transform: scale(1.01); + } + to { + transform: scale(0.99); + } +` + +export const Basic = styled.div` + ${basicStyles}; +` + +export const Combined = styled.div` + ${basicStyles}; + ${hoverStyles}; + & code { + background-color: linen; + } +` + +export const Pink = styled.div(basicStyles, { + color: 'hotpink', +}) + +export const Animated = styled.div` + ${basicStyles}; + ${hoverStyles}; + & code { + background-color: linen; + } + animation: ${({ animation }) => animation} 0.2s infinite ease-in-out alternate; +` diff --git a/examples/with-mysql/.env.example b/examples/with-mysql/.env.example new file mode 100644 index 000000000000..9205e1aeee0b --- /dev/null +++ b/examples/with-mysql/.env.example @@ -0,0 +1 @@ +DATABASE_URL=mysql://:@/?sslaccept=strict diff --git a/examples/with-mysql/.env.local.example b/examples/with-mysql/.env.local.example deleted file mode 100644 index 2062d45cde42..000000000000 --- a/examples/with-mysql/.env.local.example +++ /dev/null @@ -1,7 +0,0 @@ -# Example .env.local file for MySQL Database credentials - -MYSQL_HOST= -MYSQL_DATABASE= -MYSQL_USERNAME= -MYSQL_PASSWORD= -MYSQL_PORT= diff --git a/examples/with-mysql/README.md b/examples/with-mysql/README.md index d8d75d5e4578..a853b6584dea 100644 --- a/examples/with-mysql/README.md +++ b/examples/with-mysql/README.md @@ -1,82 +1,102 @@ -# MySQL Example +# Next.js + MySQL -This is an example of using [MySQL](https://www.mysql.com/) in a Next.js project. +This is a [Next.js](https://nextjs.org/) project that uses [Prisma](https://www.prisma.io/) to connect to a [PlanetScale](https://planetscale.com/) MySQL database and [Tailwind CSS](https://tailwindcss.com/) for styling. ## Demo -### [https://next-mysql.vercel.app](https://next-mysql.vercel.app/) +https://next-mysql.vercel.app -## Deploy your own +## Prerequisites + +- [Node.js](https://nodejs.org/en/download/) +- [PlanetScale CLI](https://github.com/planetscale/cli) +- Authenticate the CLI with the following command: + +```sh +pscale auth login +``` + +## Set up the database + +Create a new database with the following command: -Once you have access to [the environment variables you'll need](#step-5-set-up-environment-variables), deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example): +```sh +pscale database create +``` -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-mysql&project-name=nextjs-mysql&repository-name=nextjs-mysql&env=MYSQL_HOST,MYSQL_DATABASE,MYSQL_USERNAME,MYSQL_PASSWORD&envDescription=Required%20to%20connect%20the%20app%20with%20MySQL&envLink=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fwith-mysql%23step-2-set-up-environment-variables&demo-title=Next.js%20%2B%20MySQL%20Demo&demo-description=A%20simple%20app%20demonstrating%20Next.js%20and%20MySQL%20&demo-url=https%3A%2F%2Fnext-mysql.vercel.app%2F) +> A branch, `main`, was automatically created when you created your database, so you can use that for `BRANCH_NAME` in the steps below. -## How to use +## Set up the starter Next.js app 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-mysql next-mysql-app +npx create-next-app --example with-mysql nextjs-mysql # or -yarn create next-app --example with-mysql next-mysql-app +yarn create next-app --example with-mysql nextjs-mysql ``` -## Configuration - -### Step 1. Set up a MySQL database +Next, you'll need to create a database username and password through the CLI to connect to your application. If you'd prefer to use the dashboard for this step, you can find those instructions in the [Connection Strings documentation](https://docs.planetscale.com/concepts/connection-strings#creating-a-password) and then come back here to finish setup. -Set up a MySQL server either locally or any cloud provider. +First, create your `.env` file by renaming the `.env.example` file to `.env`: -### Step 2. Set up environment variables +```sh +mv .env.example .env +``` -Copy the `env.local.example` file in this directory to `.env.local` (which will be ignored by Git): +Next, using the PlanetScale CLI, create a new username and password for the branch of your database: -```bash -cp .env.local.example .env.local +```sh +pscale password create ``` -Set each variable on `.env.local`: - -- `MYSQL_HOST` - Your MySQL host URL. -- `MYSQL_DATABASE` - The name of the MySQL database you want to use. -- `MYSQL_USERNAME` - The name of the MySQL user with access to database. -- `MYSQL_PASSWORD` - The passowrd of the MySQL user. +> The `PASSWORD_NAME` value represents the name of the username and password being generated. You can have multiple credentials for a branch, so this gives you a way to categorize them. To manage your passwords in the dashboard, go to your database overview page, click "Settings", and then click "Passwords". -### Step 3. Run migration script +Take note of the values returned to you, as you won't be able to see this password again. -You'll need to run a migration to create the necessary table for the example. +```text +Password production-password was successfully created. +Please save the values below as they will not be shown again -```bash -npm run migrate -# or -yarn migrate + NAME USERNAME ACCESS HOST URL ROLE PLAIN TEXT + --------------------- -------------- ----------------------------------- ------------------ ------------------------------------------------------- + production-password xxxxxxxxxxxxx xxxxxx.us-east-2.psdb.cloud Can Read & Write pscale_pw_xxxxxxx ``` -### Step 4. Run Next.js in development mode +You'll use these properties to construct your connection string, which will be the value for `DATABASE_URL` in your `.env` file. Update the `DATABASE_URL` property with your connection string in the following format: -```bash -npm install -npm run dev -# or -yarn install -yarn dev +```text +mysql://:@/?sslaccept=strict ``` -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). +Push the database schema to your PlanetScale database using Prisma. + +`npx prisma db push` + +Run the seed script to populate your database with `Product` and `Category` data. + +`npm run seed` -## Deploy on Vercel +## Run the App -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)). +Run the app with following command: -#### Deploy Your Local Project +`npm run dev` -To deploy your local project to Vercel, push it to GitHub/GitLab/Bitbucket and [import to Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example). +Open your browser at [localhost:3000](localhost:3000) to see the running application. + +## Deploy your own + +After you've got your application running locally, it's time to deploy it. To do so, you'll need to promote your database branch (`main` by default) to be the production branch ([read the branching documentation for more information](https://docs.planetscale.com/concepts/branching)). + +```sh +pscale branch promote +``` -**Important**: When you import your project on Vercel, make sure to click on **Environment Variables** and set them to match your `.env.local` file. +Now that your branch has been promoted to production, you can either use the existing password you generated earlier for running locally or create a new password. Regardless, you'll need a password in the deployment steps below. -#### Deploy from Our Template +Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example): -Alternatively, you can deploy using our template by clicking on the Deploy button below. +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-mysql&project-name=with-mysql&repository-name=with-mysql&env=DATABASE_URL) -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-mysql&project-name=nextjs-mysql&repository-name=nextjs-mysql&env=MYSQL_HOST,MYSQL_DATABASE,MYSQL_USERNAME,MYSQL_PASSWORD&envDescription=Required%20to%20connect%20the%20app%20with%20MySQL&envLink=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fwith-mysql%23step-2-set-up-environment-variables&demo-title=Next.js%20%2B%20MySQL%20Demo&demo-description=A%20simple%20app%20demonstrating%20Next.js%20and%20MySQL%20&demo-url=https%3A%2F%2Fnext-mysql.vercel.app%2F) +> Make sure to update the `DATABASE_URL` variable during this setup process. diff --git a/examples/with-mysql/components/Product.js b/examples/with-mysql/components/Product.js new file mode 100644 index 000000000000..16f32b38399b --- /dev/null +++ b/examples/with-mysql/components/Product.js @@ -0,0 +1,31 @@ +import Image from 'next/image' + +export default function Product({ product }) { + const { name, description, price, image, category } = product + + return ( +
+ {name} +
+
{name}
+

{description}

+

${price}

+
+
+ + {category.name} + +
+
+ ) +} diff --git a/examples/with-mysql/components/button-link/index.tsx b/examples/with-mysql/components/button-link/index.tsx deleted file mode 100644 index 88e46586e476..000000000000 --- a/examples/with-mysql/components/button-link/index.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import Link from 'next/link' -import cn from 'clsx' - -function ButtonLink({ href = '/', className = '', children }) { - return ( - - - {children} - - - ) -} - -export default ButtonLink diff --git a/examples/with-mysql/components/button/index.tsx b/examples/with-mysql/components/button/index.tsx deleted file mode 100644 index ca8714a1b961..000000000000 --- a/examples/with-mysql/components/button/index.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import cn from 'clsx' - -function Button({ - onClick = console.log, - className = '', - children = null, - type = null, - disabled = false, -}) { - return ( - - ) -} - -export default Button diff --git a/examples/with-mysql/components/container/index.tsx b/examples/with-mysql/components/container/index.tsx deleted file mode 100644 index 4c5b421a04c0..000000000000 --- a/examples/with-mysql/components/container/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -function Container({ className = '', children }) { - return
{children}
-} - -export default Container diff --git a/examples/with-mysql/components/edit-entry-form/index.tsx b/examples/with-mysql/components/edit-entry-form/index.tsx deleted file mode 100644 index f84307d00fea..000000000000 --- a/examples/with-mysql/components/edit-entry-form/index.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { useState, useEffect } from 'react' -import Router, { useRouter } from 'next/router' - -import Button from '../button' - -export default function EntryForm() { - const [_title, setTitle] = useState('') - const [_content, setContent] = useState('') - const [submitting, setSubmitting] = useState(false) - const router = useRouter() - const { id, title, content } = router.query - - useEffect(() => { - if (typeof title === 'string') { - setTitle(title) - } - if (typeof content === 'string') { - setContent(content) - } - }, [title, content]) - - async function submitHandler(e) { - e.preventDefault() - setSubmitting(true) - try { - const res = await fetch('/api/edit-entry', { - method: 'PATCH', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - id, - title: _title, - content: _content, - }), - }) - const json = await res.json() - setSubmitting(false) - if (!res.ok) throw Error(json.message) - Router.push('/') - } catch (e) { - throw Error(e.message) - } - } - - return ( -
-
- - setTitle(e.target.value)} - /> -
-
- -