├── .env.example ├── .eslintrc.js ├── .github ├── dependabot.yml └── workflows │ └── test.yml ├── .gitignore ├── .graphqlrc.ts ├── .nvmrc ├── .prettierignore ├── .vscode ├── launch.json └── settings.json ├── README.md ├── app ├── [page] │ ├── layout.tsx │ ├── opengraph-image.tsx │ └── page.tsx ├── api │ └── revalidate │ │ └── route.ts ├── error.tsx ├── favicon.ico ├── globals.css ├── layout.tsx ├── opengraph-image.tsx ├── page.tsx ├── product │ └── [handle] │ │ └── page.tsx ├── robots.ts ├── search │ ├── [collection] │ │ ├── opengraph-image.tsx │ │ └── page.tsx │ ├── layout.tsx │ ├── loading.tsx │ └── page.tsx └── sitemap.ts ├── components ├── carousel.tsx ├── cart │ ├── actions.ts │ ├── add-to-cart.tsx │ ├── close-cart.tsx │ ├── delete-item-button.tsx │ ├── edit-item-quantity-button.tsx │ ├── index.tsx │ ├── modal.tsx │ └── open-cart.tsx ├── grid │ ├── index.tsx │ ├── three-items.tsx │ └── tile.tsx ├── icons │ └── logo.tsx ├── label.tsx ├── layout │ ├── footer-menu.tsx │ ├── footer.tsx │ ├── navbar │ │ ├── index.tsx │ │ ├── mobile-menu.tsx │ │ └── search.tsx │ ├── product-grid-items.tsx │ └── search │ │ ├── collections.tsx │ │ └── filter │ │ ├── dropdown.tsx │ │ ├── index.tsx │ │ └── item.tsx ├── loading-dots.tsx ├── logo-square.tsx ├── opengraph-image.tsx ├── price.tsx ├── product │ ├── gallery.tsx │ ├── product-description.tsx │ └── variant-selector.tsx └── prose.tsx ├── fonts └── Inter-Bold.ttf ├── lib ├── constants.ts ├── saleor │ ├── editorjs.ts │ ├── fragments │ │ ├── Checkout.graphql │ │ ├── FeaturedProductFragment.graphql │ │ ├── ProductDetailsFragment.graphql │ │ └── VariantFragment.graphql │ ├── generated │ │ ├── fragment-masking.ts │ │ ├── gql.ts │ │ ├── graphql.ts │ │ └── index.ts │ ├── index.ts │ ├── jwks.ts │ ├── mappers.ts │ ├── mutations │ │ ├── CheckoutAddLine.graphql │ │ ├── CheckoutDeleteLine.graphql │ │ ├── CheckoutUpdateLine.graphql │ │ └── CreateCheckout.graphql │ ├── queries │ │ ├── GetCategoryBySlug.graphql │ │ ├── GetCategoryProductsBySlug.graphql │ │ ├── GetCheckoutById.graphql │ │ ├── GetCollectionBySlug.graphql │ │ ├── GetCollectionProductsBySlug.graphql │ │ ├── GetCollections.graphql │ │ ├── GetFeaturedProducts.graphql │ │ ├── GetMenuBySlug.graphql │ │ ├── GetPageBySlug.graphql │ │ ├── GetPages.graphql │ │ ├── GetProductBySlug.graphql │ │ ├── SearchProducts.graphql │ │ └── test.graphql │ ├── utils.ts │ └── webhookSubscription.graphql ├── type-guards.ts ├── types.ts └── utils.ts ├── license.md ├── next.config.js ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── prettier.config.js ├── public └── screenshot.png ├── tailwind.config.js └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | COMPANY_NAME="Saleor Commerce" 2 | TWITTER_CREATOR="@getsaleor" 3 | TWITTER_SITE="https://saleor.io/" 4 | SITE_NAME="Next.js Commerce by Saleor" 5 | SALEOR_INSTANCE_URL=https://demo.saleor.io/graphql/ 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('eslint').Linter.Config} 3 | */ 4 | module.exports = { 5 | extends: ['next', 'prettier'], 6 | plugins: ['unicorn'], 7 | rules: { 8 | 'no-unused-vars': [ 9 | 'error', 10 | { 11 | args: 'after-used', 12 | caughtErrors: 'none', 13 | ignoreRestSiblings: true, 14 | vars: 'all', 15 | }, 16 | ], 17 | 'prefer-const': 'error', 18 | 'react-hooks/exhaustive-deps': 'error', 19 | 'unicorn/filename-case': [ 20 | 'error', 21 | { 22 | case: 'kebabCase', 23 | }, 24 | ], 25 | }, 26 | ignorePatterns: ['lib/saleor/generated'], 27 | }; 28 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: pull_request 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Cancel running workflows 8 | uses: styfle/cancel-workflow-action@0.11.0 9 | with: 10 | access_token: ${{ github.token }} 11 | - name: Checkout repo 12 | uses: actions/checkout@v3 13 | - name: Set node version 14 | uses: actions/setup-node@v3 15 | with: 16 | node-version-file: '.nvmrc' 17 | - name: Set pnpm version 18 | uses: pnpm/action-setup@v2 19 | with: 20 | run_install: false 21 | version: 7 22 | - name: Cache node_modules 23 | id: node-modules-cache 24 | uses: actions/cache@v3 25 | with: 26 | path: '**/node_modules' 27 | key: node-modules-cache-${{ hashFiles('**/pnpm-lock.yaml') }} 28 | - name: Install dependencies 29 | if: steps.node-modules-cache.outputs.cache-hit != 'true' 30 | run: pnpm install 31 | - name: Run tests 32 | run: pnpm test 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | .playwright 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | .pnpm-debug.log* 28 | 29 | # local env files 30 | .env* 31 | !.env.example 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | next-env.d.ts 39 | -------------------------------------------------------------------------------- /.graphqlrc.ts: -------------------------------------------------------------------------------- 1 | import type { CodegenConfig } from '@graphql-codegen/cli'; 2 | import { loadEnvConfig } from '@next/env'; 3 | 4 | loadEnvConfig(process.cwd()); 5 | 6 | const config: CodegenConfig = { 7 | overwrite: true, 8 | schema: process.env.SALEOR_INSTANCE_URL, 9 | documents: 'lib/**/*.graphql', 10 | ignoreNoDocuments: true, 11 | generates: { 12 | 'lib/saleor/generated/': { 13 | preset: 'client', 14 | presetConfig: { 15 | fragmentMasking: false, 16 | dedupeFragments: true, 17 | }, 18 | config: { 19 | useTypeImports: true, 20 | defaultScalarType: 'unknown', 21 | skipTypename: true, 22 | documentMode: 'string', 23 | exportFragmentSpreadSubTypes: true, 24 | dedupeFragments: true, 25 | scalars: { 26 | Date: 'string', 27 | DateTime: 'string', 28 | Decimal: 'number', 29 | JSONString: 'string', 30 | Metadata: 'Record', 31 | PositiveDecimal: 'number', 32 | UUID: 'string', 33 | WeightScalar: 'number', 34 | }, 35 | }, 36 | }, 37 | }, 38 | }; 39 | 40 | export default config; 41 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .vercel 2 | .next 3 | pnpm-lock.yaml 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Next.js: debug server-side", 6 | "type": "node-terminal", 7 | "request": "launch", 8 | "command": "pnpm dev" 9 | }, 10 | { 11 | "name": "Next.js: debug client-side", 12 | "type": "chrome", 13 | "request": "launch", 14 | "url": "http://localhost:3000" 15 | }, 16 | { 17 | "name": "Next.js: debug full stack", 18 | "type": "node-terminal", 19 | "request": "launch", 20 | "command": "pnpm dev", 21 | "serverReadyAction": { 22 | "pattern": "started server on .+, url: (https?://.+)", 23 | "uriFormat": "%s", 24 | "action": "debugWithChrome" 25 | } 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true, 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll": true, 6 | "source.organizeImports": true, 7 | "source.sortMembers": true 8 | }, 9 | "editor.formatOnSave": true 10 | } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fsaleor%2Fnextjs-commerce&env=COMPANY_NAME,TWITTER_CREATOR,TWITTER_SITE,SITE_NAME,SALEOR_INSTANCE_URL&project-name=saleor-nextjs-commerce&repository-name=saleor-nextjs-commerce&demo-title=Saleor%20Next.js%20Commerce&demo-description=Saleor%20%2B%20Next.js%2013%20%2B%20App%20Router-ready%20e-commerce%20template&demo-url=https%3A%2F%2Fsaleor-commerce.vercel.app%2F&demo-image=https%3A%2F%2Fsaleor-commerce.vercel.app%2Fscreenshot.png) 2 | 3 | > [!IMPORTANT] 4 | > For actual production work, we recommend using our official [Next.js Starter](https://github.com/saleor/storefront). This repository can be useful for comparing different commerce platforms that also use the [Next.js Commerce](https://nextjs.org/commerce) 5 | 6 | # Next.js Commerce 7 | 8 | A Next.js 13 and App Router-ready e-commerce template featuring: 9 | 10 | - Next.js App Router 11 | - Optimized for SEO using Next.js's Metadata 12 | - React Server Components (RSCs) and Suspense 13 | - Server Actions for mutations 14 | - Edge Runtime 15 | - New fetching and caching paradigms 16 | - Dynamic OG images 17 | - Styling with Tailwind CSS 18 | - Checkout and payments with Saleor 19 | - Automatic light/dark mode based on system settings 20 | 21 |

22 | 23 | > Note: Looking for Next.js Commerce v1? View the [code](https://github.com/vercel/commerce/tree/v1), [demo](https://commerce-v1.vercel.store), and [release notes](https://github.com/vercel/commerce/releases/tag/v1). 24 | 25 | ## Providers 26 | 27 | Vercel is more than happy to partner and work with any commerce provider to help them get a similar template up and running and listed below. Alternative providers should be able to fork this repository and swap out the `lib/…` files with their own implementation while leaving the rest of the template mostly unchanged. 28 | 29 | ## Running locally 30 | 31 | You will need to use the environment variables [defined in `.env.example`](.env.example) to run Next.js Commerce. It's recommended you use [Vercel Environment Variables](https://vercel.com/docs/concepts/projects/environment-variables) for this, but a `.env` file is all that is necessary. 32 | 33 | > Note: You should not commit your `.env` file or it will expose secrets that will allow others to control your Saleor store. 34 | 35 | 1. Install Vercel CLI: `npm i -g vercel` 36 | 2. Link local instance with Vercel and GitHub accounts (creates `.vercel` directory): `vercel link` 37 | 3. Download your environment variables: `vercel env pull` 38 | 39 | ```bash 40 | pnpm install 41 | pnpm dev 42 | ``` 43 | 44 | Your app should now be running on [localhost:3000](http://localhost:3000/). 45 | 46 | ## How to configure your Saleor store for Next.js Commerce 47 | 48 | Next.js Commerce requires a [Saleor account](https://saleor.io/). 49 | 50 | ### Add the Saleor URL to an environment variable 51 | 52 | Create a `SALEOR_INSTANCE_URL` environment variable and use your Saleor GraphQL API URL as the value (ie. `https://yourinstance.saleor.cloud/graphql/`). 53 | 54 | ### Accessing the Saleor Storefront API 55 | 56 | Next.js Commerce utilizes [Saleor's Storefront API](https://docs.saleor.io/docs/3.x/api-storefront/api-reference) to create unique customer experiences. The API offers a full range of commerce options making it possible for customers to control products, collections, menus, pages, cart, checkout, and more. 57 | 58 | ### Configure webhooks for on-demand incremental static regeneration (ISR) 59 | 60 | Utilizing [Saleor's webhooks](https://docs.saleor.io/docs/3.x/developer/extending/webhooks/overview), and listening for selected [Saleor webhook events](https://docs.saleor.io/docs/3.x/api-reference/webhooks/enums/webhook-event-type-async-enum), we can use [Next'js on-demand revalidation](https://nextjs.org/docs/app/building-your-application/data-fetching/revalidating#using-on-demand-revalidation) to keep data fetches indefinitely cached until certain events in the Saleor store occur. 61 | 62 | Next.js is pre-configured to listen for the following Saleor webhook events and automatically revalidate fetches. 63 | 64 | - `CategoryCreated` 65 | - `CategoryDeleted` 66 | - `CategoryUpdated` 67 | - `CollectionUpdated` 68 | - `CollectionDeleted` 69 | - `CollectionCreated` 70 | - `ProductCreated` 71 | - `ProductDeleted` 72 | - `ProductUpdated` 73 | - `ProductVariantCreated` 74 | - `ProductVariantDeleted` 75 | - `ProductVariantUpdated` 76 | 77 |
78 | Expand to view detailed walkthrough 79 | 80 | #### Configure Saleor webhooks 81 | 82 | 1. Navigate to Configuration -> Webhooks `https://[your_saleor_subdomain].saleor.cloud/dashboard/custom-apps/add``. 83 | 1. Name your app `Next.js Commerce`, add `MANAGE_ORDERS`, `MANAGE_DISCOUNTS`, and `MANAGE_PRODUCTS` permissions and save. 84 | 1. Click on "Create Webhook" button. 85 | 1. Name your webhook and add your Next.js Commerce URL with `/api/revalidate`. 86 | 1. Note: You don't need to add any "secrets" to the URL. Saleor uses public key encryption to verify the webhook is coming from your store. 87 | 1. Copy and paste [the subscription query](https://github.com/saleor/nextjs-commerce/blob/main/lib/saleor/webhookSubscription.graphql) at the bottom of the page and save. 88 | 89 | #### Testing webhooks during local development 90 | 91 | The easiest way to test webhooks while developing locally is to use [ngrok](https://ngrok.com). 92 | 93 | 1. [Install and configure ngrok](https://ngrok.com/download) (you will need to create an account). 94 | 1. Run your app locally, `npm run dev`. 95 | 1. In a separate terminal session, run `ngrok http 3000`. 96 | 1. Use the url generated by ngrok and add or update your webhook urls in Saleor. 97 | 98 |
99 | 100 | ### Using Saleor as a CMS 101 | 102 | Next.js Commerce is fully powered by Saleor in a truly headless and data-driven way. 103 | 104 | #### Products 105 | 106 | `https://yourinstance.saleor.cloud/dashboard/products/` 107 | 108 | Only `Active` products are shown. `Draft` products will not be shown until they are marked as `Active`. 109 | 110 | Product options and option combinations are driven by Saleor options and variants. When selecting options on the product detail page, other options and variant combinations will be visually validated and verified for availability, as Amazon does. 111 | 112 | Products that are active and "out of stock" are still shown on the site, but the ability to add the product to the cart is disabled. 113 | 114 | #### Collections 115 | 116 | `https://yourinstance.saleor.cloud/dashboard/collections/` 117 | 118 | Create whatever collections you want and configure them however you want. All available collections will show on the search page as filters on the left, with one exception... 119 | 120 | Any collection names that start with the word "hidden" will not show up on the headless front end. The Next.js Commerce theme comes pre-configured to look for two hidden collections. Collections were chosen for this over tags so that order of products could be controlled (collections allow for manual ordering). 121 | 122 | Create the following collections: 123 | 124 | - `Featured` – Products in this collection are displayed in the three featured blocks on the homepage. 125 | - `All Products` – Products in this collection are displayed in the auto-scrolling carousel section on the homepage. 126 | 127 | #### Pages 128 | 129 | `https://yourinstance.saleor.cloud/dashboard/pages/` 130 | 131 | Next.js Commerce contains a dynamic `[page]` route. It will use the value to look for a corresponding page in Saleor. If a page is found, it will display its rich content using Tailwind's prose. If a page is not found, a 404 page is displayed. 132 | 133 | #### Navigation menus 134 | 135 | `https://yourinstance.saleor.cloud/dashboard/navigation/` 136 | 137 | Next.js Commerce's header and footer navigation is pre-configured to be controlled by Saleor navigation menus. This means you have full control over what links go here. They can be to collections, pages, external links, and more. 138 | 139 | Create the following navigation menus: 140 | 141 | - `navbar` – Menu items to be shown in the headless frontend header. 142 | - `footer` – Menu items to be shown in the headless frontend footer. 143 | 144 | #### SEO 145 | 146 | Saleor's products, collections, pages, etc. allow you to create custom SEO titles and descriptions. Next.js Commerce is pre-configured to display these custom values but also comes with sensible default fallbacks if they are not provided. 147 | -------------------------------------------------------------------------------- /app/[page]/layout.tsx: -------------------------------------------------------------------------------- 1 | import Footer from 'components/layout/footer'; 2 | import { Suspense } from 'react'; 3 | 4 | export default function Layout({ children }: { children: React.ReactNode }) { 5 | return ( 6 | 7 |
8 |
9 | {children} 10 |
11 |
12 |