├── .coderabbit.yaml ├── .env ├── .env.production ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── CI.yml │ ├── checkly.yml │ ├── crowdin.yml │ └── release.yml ├── .gitignore ├── .storybook ├── main.ts └── preview.ts ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── checkly.config.ts ├── codecov.yml ├── commitlint.config.ts ├── crowdin.yml ├── drizzle.config.ts ├── eslint.config.mjs ├── lefthook.yml ├── migrations ├── 0000_init-db.sql └── meta │ ├── 0000_snapshot.json │ └── _journal.json ├── next.config.ts ├── package-lock.json ├── package.json ├── playwright.config.ts ├── postcss.config.mjs ├── public ├── apple-touch-icon.png ├── assets │ └── images │ │ ├── arcjet-dark.svg │ │ ├── arcjet-light.svg │ │ ├── better-stack-dark.png │ │ ├── better-stack-white.png │ │ ├── checkly-logo-dark.png │ │ ├── checkly-logo-light.png │ │ ├── clerk-logo-dark.png │ │ ├── codecov-dark.svg │ │ ├── codecov-white.svg │ │ ├── coderabbit-logo-dark.svg │ │ ├── coderabbit-logo-light.svg │ │ ├── crowdin-dark.png │ │ ├── crowdin-white.png │ │ ├── nextjs-boilerplate-saas.png │ │ ├── nextjs-boilerplate-sign-in.png │ │ ├── nextjs-boilerplate-sign-up.png │ │ ├── nextjs-starter-banner.png │ │ ├── sentry-dark.png │ │ ├── sentry-white.png │ │ ├── sevalla-dark.png │ │ └── sevalla-light.png ├── favicon-16x16.png ├── favicon-32x32.png └── favicon.ico ├── src ├── app │ ├── [locale] │ │ ├── (auth) │ │ │ ├── (center) │ │ │ │ ├── layout.tsx │ │ │ │ ├── sign-in │ │ │ │ │ └── [[...sign-in]] │ │ │ │ │ │ └── page.tsx │ │ │ │ └── sign-up │ │ │ │ │ └── [[...sign-up]] │ │ │ │ │ └── page.tsx │ │ │ ├── dashboard │ │ │ │ ├── layout.tsx │ │ │ │ ├── page.tsx │ │ │ │ └── user-profile │ │ │ │ │ └── [[...user-profile]] │ │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── (marketing) │ │ │ ├── about │ │ │ │ └── page.tsx │ │ │ ├── api │ │ │ │ └── counter │ │ │ │ │ └── route.ts │ │ │ ├── counter │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── portfolio │ │ │ │ ├── [slug] │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── global-error.tsx │ ├── robots.ts │ └── sitemap.ts ├── components │ ├── CounterForm.tsx │ ├── CurrentCount.tsx │ ├── DemoBadge.tsx │ ├── DemoBanner.tsx │ ├── Hello.tsx │ ├── LocaleSwitcher.tsx │ ├── Sponsors.tsx │ └── analytics │ │ ├── PostHogPageView.tsx │ │ └── PostHogProvider.tsx ├── instrumentation-client.ts ├── instrumentation.ts ├── libs │ ├── Arcjet.ts │ ├── DB.ts │ ├── Env.ts │ ├── Logger.ts │ ├── i18n.ts │ ├── i18nNavigation.ts │ └── i18nRouting.ts ├── locales │ ├── en.json │ └── fr.json ├── middleware.ts ├── models │ └── Schema.ts ├── styles │ └── global.css ├── templates │ ├── BaseTemplate.stories.tsx │ ├── BaseTemplate.test.tsx │ └── BaseTemplate.tsx ├── types │ └── i18n.ts ├── utils │ ├── AppConfig.ts │ ├── Helpers.test.ts │ └── Helpers.ts └── validations │ └── CounterValidation.ts ├── tests ├── e2e │ ├── Counter.e2e.ts │ ├── I18n.e2e.ts │ ├── Sanity.check.e2e.ts │ └── Visual.e2e.ts └── integration │ └── Counter.spec.ts ├── tsconfig.json └── vitest.config.mts /.coderabbit.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json 2 | 3 | # CodeRabbit is an AI-powered code reviewer that cuts review time and bugs in half 4 | 5 | language: en-US 6 | early_access: false 7 | reviews: 8 | profile: chill 9 | request_changes_workflow: false 10 | high_level_summary: true 11 | poem: true 12 | review_status: true 13 | collapse_walkthrough: false 14 | path_instructions: 15 | - path: '**/*.{ts,tsx}' 16 | instructions: 17 | 'Review the Typescript and React code for conformity with best practices in React, and Typescript. Highlight any deviations.' 18 | auto_review: 19 | enabled: true 20 | ignore_title_keywords: 21 | - WIP 22 | - DO NOT MERGE 23 | - DRAFT 24 | drafts: false 25 | chat: 26 | auto_reply: true 27 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # FIXME: Configure environment variables for your project 2 | 3 | # If you need to build a SaaS application with Stripe subscription payment with checkout page, customer portal, webhook, etc. 4 | # You can check out the Next.js Boilerplate SaaS: https://nextjs-boilerplate.com/pro-saas-starter-kit 5 | 6 | # Clerk authentication 7 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_cmVsYXhlZC10dXJrZXktNjcuY2xlcmsuYWNjb3VudHMuZGV2JA 8 | 9 | # PostHog 10 | NEXT_PUBLIC_POSTHOG_KEY= 11 | NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com 12 | 13 | # Next.js 14 | NEXT_TELEMETRY_DISABLED=1 15 | 16 | ######## [BEGIN] SENSITIVE DATA ######## For security reason, don't update the following variables (secret key) directly in this file. 17 | ######## Please create a new file named `.env.local`, all environment files ending with `.local` won't be tracked by Git. 18 | ######## After creating the file, you can add the following variables. 19 | # Arcjet security 20 | # Get your key from https://launch.arcjet.com/Q6eLbRE 21 | # ARCJET_KEY= 22 | 23 | # Clerk authentication 24 | CLERK_SECRET_KEY=your_clerk_secret_key 25 | ######## [END] SENSITIVE DATA 26 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # FIXME: Configure environment variables for production 2 | 3 | # If you need to build a SaaS application with Stripe subscription payment with checkout page, customer portal, webhook, etc. 4 | # You can check out the Next.js Boilerplate SaaS: https://nextjs-boilerplate.com/pro-saas-starter-kit 5 | 6 | # Hosting 7 | # Replace by your domain name 8 | # NEXT_PUBLIC_APP_URL=https://example.com 9 | 10 | # Sentry DSN 11 | NEXT_PUBLIC_SENTRY_DSN= 12 | 13 | ######## [BEGIN] SENSITIVE DATA ######## For security reason, don't update the following variables (secret key) directly in this file. 14 | ######## Please create a new file named `.env.production.local`, all environment files ending with `.local` won't be tracked by Git. 15 | ######## After creating the file, you can add the following variables. 16 | # Arcjet security 17 | # Get your key from https://launch.arcjet.com/Q6eLbRE 18 | # ARCJET_KEY= 19 | 20 | # Database 21 | # Using an incorrect DATABASE_URL value, Next.js build will timeout and you will get the following error: "because it took more than 60 seconds" 22 | # DATABASE_URL=postgresql://postgres@localhost:5432/postgres 23 | 24 | # Error monitoring 25 | # SENTRY_AUTH_TOKEN= 26 | 27 | # Logging ingestion 28 | # LOGTAIL_SOURCE_TOKEN= 29 | ######## [END] SENSITIVE DATA 30 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ixartz 2 | custom: 3 | - 'https://nextjs-boilerplate.com/pro-saas-starter-kit' 4 | - 'https://nextlessjs.com' 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Enable version updates for npm 4 | - package-ecosystem: npm 5 | # Look for `package.json` and `lock` files in the root directory 6 | directory: / 7 | # Check the npm registry for updates every day 8 | schedule: 9 | interval: monthly 10 | time: '06:00' 11 | # Limit the number of open pull requests for version updates to 1 12 | open-pull-requests-limit: 1 13 | # Commit message format 14 | commit-message: 15 | # Prefix all commit messages and pull request titles with "chore" 16 | prefix: chore 17 | # Group updates into a single pull request 18 | groups: 19 | # The name of the group (identifier) 20 | npm-deps: 21 | update-types: [minor, patch] 22 | # Only allow minor and patch updates 23 | ignore: 24 | - dependency-name: '*' 25 | update-types: ['version-update:semver-major'] 26 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | node-version: [20.x, 22.x] 14 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 15 | 16 | name: Build with ${{ matrix.node-version }} 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | cache: npm 26 | - run: npm ci 27 | - run: npm run build 28 | 29 | test: 30 | strategy: 31 | matrix: 32 | node-version: [20.x] 33 | 34 | name: Run all tests 35 | runs-on: ubuntu-latest 36 | 37 | steps: 38 | - uses: actions/checkout@v4 39 | with: 40 | fetch-depth: 0 # Retrieve Git history, needed to verify commits 41 | - name: Use Node.js ${{ matrix.node-version }} 42 | uses: actions/setup-node@v4 43 | with: 44 | node-version: ${{ matrix.node-version }} 45 | cache: npm 46 | - run: npm ci 47 | 48 | - name: Build Next.js for E2E tests 49 | run: npm run build 50 | env: 51 | SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} 52 | 53 | - if: github.event_name == 'pull_request' 54 | name: Validate all commits from PR 55 | run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose 56 | 57 | - name: Linter 58 | run: npm run lint 59 | 60 | - name: Type checking 61 | run: npm run check-types 62 | 63 | - name: Install Playwright (used for Storybook and E2E tests) 64 | run: npx playwright install --with-deps 65 | 66 | - name: Run unit tests 67 | run: npm run test -- --coverage 68 | 69 | - name: Upload coverage reports to Codecov 70 | uses: codecov/codecov-action@v5 71 | env: 72 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 73 | 74 | - name: Run storybook tests 75 | run: npm run test-storybook:ci 76 | 77 | - name: Run E2E tests 78 | run: npx percy exec -- npm run test:e2e 79 | env: 80 | PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }} 81 | CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }} 82 | 83 | - uses: actions/upload-artifact@v4 84 | if: always() 85 | with: 86 | name: test-results 87 | path: test-results/ 88 | retention-days: 7 89 | 90 | synchronize-with-crowdin: 91 | name: GitHub PR synchronize with Crowdin 92 | runs-on: ubuntu-latest 93 | 94 | needs: [build, test] 95 | if: github.event_name == 'pull_request' 96 | 97 | steps: 98 | - uses: actions/checkout@v4 99 | with: 100 | ref: ${{ github.event.pull_request.head.sha }} # Crowdin Actions needs to push commits to the PR branch, checkout HEAD commit instead of merge commit 101 | fetch-depth: 0 102 | 103 | - name: crowdin action 104 | uses: crowdin/github-action@v2 105 | with: 106 | upload_sources: true 107 | upload_translations: true 108 | download_translations: true 109 | create_pull_request: false 110 | localization_branch_name: ${{ github.head_ref || github.ref_name }} # explanation here: https://stackoverflow.com/a/71158878 111 | commit_message: 'chore: new Crowdin translations by GitHub Action' 112 | env: 113 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 114 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} 115 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} 116 | -------------------------------------------------------------------------------- /.github/workflows/checkly.yml: -------------------------------------------------------------------------------- 1 | name: Checkly 2 | 3 | on: [deployment_status] 4 | 5 | env: 6 | CHECKLY_API_KEY: ${{ secrets.CHECKLY_API_KEY }} 7 | CHECKLY_ACCOUNT_ID: ${{ secrets.CHECKLY_ACCOUNT_ID }} 8 | CHECKLY_TEST_ENVIRONMENT: ${{ github.event.deployment_status.environment }} 9 | 10 | jobs: 11 | test-e2e: 12 | strategy: 13 | matrix: 14 | node-version: [20.x] 15 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 16 | 17 | # Only run when the deployment was successful 18 | if: github.event.deployment_status.state == 'success' 19 | 20 | name: Test E2E on Checkly 21 | runs-on: ubuntu-latest 22 | timeout-minutes: 10 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | with: 27 | ref: '${{ github.event.deployment_status.deployment.ref }}' 28 | fetch-depth: 0 29 | 30 | - name: Set branch name # workaround to detect branch name in "deployment_status" actions 31 | run: echo "CHECKLY_TEST_REPO_BRANCH=$(git show -s --pretty=%D HEAD | tr -s ',' '\n' | sed 's/^ //' | grep -e 'origin/' | head -1 | sed 's/\origin\///g')" >> $GITHUB_ENV 32 | 33 | - uses: actions/setup-node@v4 34 | with: 35 | node-version: ${{ matrix.node-version }} 36 | cache: npm 37 | 38 | - name: Restore or cache node_modules 39 | id: cache-node-modules 40 | uses: actions/cache@v4 41 | with: 42 | path: node_modules 43 | key: node-modules-${{ hashFiles('package-lock.json') }} 44 | 45 | - name: Install dependencies 46 | if: steps.cache-node-modules.outputs.cache-hit != 'true' 47 | run: npm ci 48 | 49 | - name: Run checks # run the checks passing in the ENVIRONMENT_URL and recording a test session. 50 | id: run-checks 51 | run: npx checkly test --reporter=github --record 52 | env: 53 | VERCEL_BYPASS_TOKEN: ${{ secrets.VERCEL_BYPASS_TOKEN }} 54 | ENVIRONMENT_URL: ${{ github.event.deployment_status.environment_url }} 55 | 56 | - name: Create summary # export the markdown report to the job summary. 57 | id: create-summary 58 | run: cat checkly-github-report.md > $GITHUB_STEP_SUMMARY 59 | 60 | - name: Deploy checks # if the test run was successful and we are on Production, deploy the checks 61 | id: deploy-checks 62 | if: steps.run-checks.outcome == 'success' && github.event.deployment_status.environment == 'Production' 63 | run: npx checkly deploy --force 64 | -------------------------------------------------------------------------------- /.github/workflows/crowdin.yml: -------------------------------------------------------------------------------- 1 | name: Crowdin Action 2 | 3 | on: 4 | push: 5 | branches: [main] # Run on push to the main branch 6 | schedule: 7 | - cron: '0 5 * * *' # Run every day at 5am 8 | workflow_dispatch: # Run manually 9 | 10 | jobs: 11 | synchronize-with-crowdin: 12 | name: Synchronize with Crowdin 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: crowdin action 19 | uses: crowdin/github-action@v2 20 | with: 21 | upload_sources: true 22 | upload_translations: true 23 | download_translations: true 24 | localization_branch_name: l10n_crowdin_translations 25 | create_pull_request: true 26 | pull_request_title: New Crowdin Translations 27 | pull_request_body: 'New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)' 28 | pull_request_base_branch_name: main 29 | commit_message: 'chore: new Crowdin translations by GitHub Action' 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} 33 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} 34 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_run: 5 | workflows: [CI] 6 | types: 7 | - completed 8 | branches: 9 | - main 10 | 11 | jobs: 12 | release: 13 | strategy: 14 | matrix: 15 | node-version: [20.x] 16 | 17 | name: Create a new release 18 | runs-on: ubuntu-latest 19 | 20 | permissions: 21 | contents: write # to be able to publish a GitHub release 22 | issues: write # to be able to comment on released issues 23 | pull-requests: write # to be able to comment on released pull requests 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | with: 28 | fetch-depth: 0 29 | - name: Use Node.js ${{ matrix.node-version }} 30 | uses: actions/setup-node@v4 31 | with: 32 | node-version: ${{ matrix.node-version }} 33 | cache: npm 34 | - run: npm ci 35 | 36 | - name: Release 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | run: npx semantic-release 40 | -------------------------------------------------------------------------------- /.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.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | /vitest-test-results 16 | 17 | # next.js 18 | /.next/ 19 | /out/ 20 | 21 | # production 22 | /build 23 | 24 | # misc 25 | .DS_Store 26 | *.pem 27 | Thumbs.db 28 | 29 | # debug 30 | npm-debug.log* 31 | yarn-debug.log* 32 | yarn-error.log* 33 | .pnpm-debug.log* 34 | 35 | # local env files 36 | .env*.local 37 | 38 | # Sentry Config File 39 | .env.sentry-build-plugin 40 | 41 | # local folder 42 | local 43 | 44 | # vercel 45 | .vercel 46 | 47 | # typescript 48 | *.tsbuildinfo 49 | next-env.d.ts 50 | 51 | # Database 52 | *.db 53 | 54 | # storybook 55 | storybook-static 56 | *storybook.log 57 | 58 | # playwright 59 | /test-results/ 60 | /playwright-report/ 61 | /playwright/.cache/ 62 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/nextjs'; 2 | 3 | const config: StorybookConfig = { 4 | stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], 5 | addons: [ 6 | '@storybook/addon-onboarding', 7 | '@storybook/addon-links', 8 | '@storybook/addon-essentials', 9 | '@storybook/addon-interactions', 10 | ], 11 | framework: { 12 | name: '@storybook/nextjs', 13 | options: {}, 14 | }, 15 | staticDirs: ['../public'], 16 | core: { 17 | disableTelemetry: true, 18 | }, 19 | }; 20 | 21 | export default config; 22 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from '@storybook/react'; 2 | import '../src/styles/global.css'; 3 | 4 | const preview: Preview = { 5 | parameters: { 6 | controls: { 7 | matchers: { 8 | color: /(background|color)$/i, 9 | date: /Date$/i, 10 | }, 11 | }, 12 | nextjs: { 13 | appDirectory: true, 14 | }, 15 | }, 16 | }; 17 | 18 | export default preview; 19 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "mikestead.dotenv", 5 | "bradlc.vscode-tailwindcss", 6 | "vitest.explorer", 7 | "humao.rest-client", 8 | "yoavbls.pretty-ts-errors", 9 | "ms-playwright.playwright", 10 | "github.vscode-github-actions", 11 | "lokalise.i18n-ally" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Next.js: debug full stack", 9 | "type": "node", 10 | "request": "launch", 11 | "program": "${workspaceFolder}/node_modules/next/dist/bin/next", 12 | "runtimeArgs": ["--inspect"], 13 | "skipFiles": ["/**"], 14 | "env": { 15 | "NEXT_PUBLIC_SENTRY_DISABLED": "true" 16 | }, 17 | "serverReadyAction": { 18 | "action": "debugWithChrome", 19 | "killOnServerStop": true, 20 | "pattern": "- Local:.+(https?://.+)", 21 | "uriFormat": "%s", 22 | "webRoot": "${workspaceFolder}" 23 | } 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "editor.detectIndentation": false, 4 | "search.exclude": { 5 | "package-lock.json": true 6 | }, 7 | 8 | // TypeScript 9 | "typescript.tsdk": "node_modules/typescript/lib", // Use the workspace version of TypeScript 10 | "typescript.enablePromptUseWorkspaceTsdk": true, // For security reasons it's require that users opt into using the workspace version of typescript 11 | "typescript.preferences.autoImportSpecifierExcludeRegexes": [ 12 | // useRouter should be imported from `next/navigation` instead of `next/router` 13 | "next/router", 14 | // give priority for Link to next/link instead of lucide-react 15 | "lucide-react", 16 | // Not used in the project and conflicts with `use()` from React 17 | "chai" 18 | ], 19 | "typescript.preferences.preferTypeOnlyAutoImports": true, // Prefer type-only imports 20 | 21 | // Vitest 22 | "testing.automaticallyOpenTestResults": "neverOpen", // Don't open the test results automatically 23 | 24 | // I18n 25 | "i18n-ally.localesPaths": ["src/locales"], 26 | "i18n-ally.keystyle": "nested", 27 | 28 | // Disable the default formatter, use ESLint instead 29 | "prettier.enable": false, 30 | "editor.formatOnSave": false, 31 | 32 | // Auto fix with ESLint on save 33 | "editor.codeActionsOnSave": { 34 | "source.addMissingImports": "explicit", 35 | "source.fixAll.eslint": "explicit" 36 | }, 37 | 38 | // Enable eslint for all supported languages 39 | "eslint.validate": [ 40 | "javascript", 41 | "javascriptreact", 42 | "typescript", 43 | "typescriptreact", 44 | "vue", 45 | "html", 46 | "markdown", 47 | "json", 48 | "jsonc", 49 | "yaml", 50 | "toml", 51 | "xml", 52 | "gql", 53 | "graphql", 54 | "astro", 55 | "svelte", 56 | "css", 57 | "less", 58 | "scss", 59 | "pcss", 60 | "postcss", 61 | "github-actions-workflow" 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Project wide type checking with TypeScript", 8 | "type": "npm", 9 | "script": "check-types", 10 | "problemMatcher": ["$tsc"], 11 | "group": { 12 | "kind": "build", 13 | "isDefault": true 14 | }, 15 | "presentation": { 16 | "clear": true, 17 | "reveal": "never" 18 | } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Remi W. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Boilerplate and Starter for Next.js 15+, Tailwind CSS 4, and TypeScript. 2 | 3 |

4 | 5 | Next js starter banner 10 | 11 |

12 | 13 | 🚀 Boilerplate and Starter for Next.js with App Router, Tailwind CSS, and TypeScript ⚡️ Prioritizing developer experience first: Next.js, TypeScript, ESLint, Prettier, Husky, Lint-Staged, Vitest (replacing Jest), Testing Library, Playwright, Commitlint, VSCode, Tailwind CSS, Authentication with [Clerk](https://clerk.com?utm_source=github&utm_medium=sponsorship&utm_campaign=nextjs-boilerplate), Database with DrizzleORM (PostgreSQL, SQLite, and MySQL), Error Monitoring with [Sentry](https://sentry.io/for/nextjs/?utm_source=github&utm_medium=paid-community&utm_campaign=general-fy25q1-nextjs&utm_content=github-banner-nextjsboilerplate-logo), Logging with Pino.js and Log Management, Monitoring as Code, Storybook, Multi-language (i18n), AI-powered code reviews with [CodeRabbit](https://www.coderabbit.ai?utm_source=next_js_starter&utm_medium=github&utm_campaign=next_js_starter_oss_2025), Secure with [Arcjet](https://launch.arcjet.com/Q6eLbRE) (Bot detection, Rate limiting, Attack protection, etc.) and more. 14 | 15 | Clone this project and use it to create your own Next.js project. You can check out the live demo at [Next.js Boilerplate](https://demo.nextjs-boilerplate.com), which includes a working authentication system. 16 | 17 | ## Sponsors 18 | 19 | 20 | 21 | 30 | 39 | 55 | 56 | 57 | 66 | 75 | 84 | 85 | 86 | 95 | 104 | 113 | 114 | 115 | 120 | 125 | 126 |
22 | 23 | 24 | 25 | 26 | Clerk – Authentication & User Management for Next.js 27 | 28 | 29 | 31 | 32 | 33 | 34 | 35 | CodeRabbit 36 | 37 | 38 | 40 | 41 | 42 | 43 | 44 | Sentry 45 | 46 | 47 | 48 | 49 | 50 | 51 | Codecov 52 | 53 | 54 |
58 | 59 | 60 | 61 | 62 | Arcjet 63 | 64 | 65 | 67 | 68 | 69 | 70 | 71 | Sevalla 72 | 73 | 74 | 76 | 77 | 78 | 79 | 80 | Crowdin 81 | 82 | 83 |
87 | 88 | 89 | 90 | 91 | PostHog 92 | 93 | 94 | 96 | 97 | 98 | 99 | 100 | Better Stack 101 | 102 | 103 | 105 | 106 | 107 | 108 | 109 | Checkly 110 | 111 | 112 |
116 | 117 | Next.js SaaS Boilerplate with React 118 | 119 | 121 | 122 | Add your logo here 123 | 124 |
127 | 128 | ### Demo 129 | 130 | **Live demo: [Next.js Boilerplate](https://demo.nextjs-boilerplate.com)** 131 | 132 | | Sign Up | Sign In | 133 | | --- | --- | 134 | | [![Next.js Boilerplate SaaS Sign Up](public/assets/images/nextjs-boilerplate-sign-in.png)](https://demo.nextjs-boilerplate.com/sign-up) | [![Next.js Boilerplate SaaS Sign In](public/assets/images/nextjs-boilerplate-sign-in.png)](https://demo.nextjs-boilerplate.com/sign-in) | 135 | 136 | ### Features 137 | 138 | Developer experience first, extremely flexible code structure and only keep what you need: 139 | 140 | - ⚡ [Next.js](https://nextjs.org) with App Router support 141 | - 🔥 Type checking [TypeScript](https://www.typescriptlang.org) 142 | - 💎 Integrate with [Tailwind CSS](https://tailwindcss.com) 143 | - ✅ Strict Mode for TypeScript and React 19 144 | - 🔒 Authentication with [Clerk](https://clerk.com?utm_source=github&utm_medium=sponsorship&utm_campaign=nextjs-boilerplate): Sign up, Sign in, Sign out, Forgot password, Reset password, and more. 145 | - 👤 Passwordless Authentication with Magic Links, Multi-Factor Auth (MFA), Social Auth (Google, Facebook, Twitter, GitHub, Apple, and more), Passwordless login with Passkeys, User Impersonation 146 | - 📦 Type-safe ORM with DrizzleORM, compatible with PostgreSQL, SQLite, and MySQL 147 | - 💽 Offline and local development database with PGlite 148 | - 🌐 Multi-language (i18n) with [next-intl](https://next-intl-docs.vercel.app/) and [Crowdin](https://l.crowdin.com/next-js) 149 | - ♻️ Type-safe environment variables with T3 Env 150 | - ⌨️ Form handling with React Hook Form 151 | - 🔴 Validation library with Zod 152 | - 📏 Linter with [ESLint](https://eslint.org) (default Next.js, Next.js Core Web Vitals, Tailwind CSS and Antfu configuration) 153 | - 💖 Code Formatter with [Prettier](https://prettier.io) 154 | - 🦊 Husky for Git Hooks 155 | - 🚫 Lint-staged for running linters on Git staged files 156 | - 🚓 Lint git commit with Commitlint 157 | - 📓 Write standard compliant commit messages with Commitizen 158 | - 🦺 Unit Testing with Vitest and React Testing Library 159 | - 🧪 Integration and E2E Testing with Playwright 160 | - 👷 Run tests on pull request with GitHub Actions 161 | - 🎉 Storybook for UI development 162 | - 🐰 AI-powered code reviews with [CodeRabbit](https://www.coderabbit.ai?utm_source=next_js_starter&utm_medium=github&utm_campaign=next_js_starter_oss_2025) 163 | - 🚨 Error Monitoring with [Sentry](https://sentry.io/for/nextjs/?utm_source=github&utm_medium=paid-community&utm_campaign=general-fy25q1-nextjs&utm_content=github-banner-nextjsboilerplate-logo) 164 | - ☂️ Code coverage with [Codecov](https://about.codecov.io/codecov-free-trial/?utm_source=github&utm_medium=paid-community&utm_campaign=general-fy25q1-nextjs&utm_content=github-banner-nextjsboilerplate-logo) 165 | - 📝 Logging with Pino.js and Log Management with [Better Stack](https://betterstack.com/?utm_source=github&utm_medium=sponsorship&utm_campaign=next-js-boilerplate) 166 | - 🖥️ Monitoring as Code with [Checkly](https://www.checklyhq.com/?utm_source=github&utm_medium=sponsorship&utm_campaign=next-js-boilerplate) 167 | - 🔐 Security and bot protection ([Arcjet](https://launch.arcjet.com/Q6eLbRE)) 168 | - 📊 Analytics with PostHog 169 | - 🎁 Automatic changelog generation with Semantic Release 170 | - 🔍 Visual testing with Percy (Optional) 171 | - 💡 Absolute Imports using `@` prefix 172 | - 🗂 VSCode configuration: Debug, Settings, Tasks and Extensions 173 | - 🤖 SEO metadata, JSON-LD and Open Graph tags 174 | - 🗺️ Sitemap.xml and robots.txt 175 | - 👷 Automatic dependency updates with Dependabot 176 | - ⌘ Database exploration with Drizzle Studio and CLI migration tool with Drizzle Kit 177 | - ⚙️ [Bundler Analyzer](https://www.npmjs.com/package/@next/bundle-analyzer) 178 | - 🌈 Include a FREE minimalist theme 179 | - 💯 Maximize lighthouse score 180 | 181 | Built-in feature from Next.js: 182 | 183 | - ☕ Minify HTML & CSS 184 | - 💨 Live reload 185 | - ✅ Cache busting 186 | 187 | ### Philosophy 188 | 189 | - Nothing is hidden from you, allowing you to make any necessary adjustments to suit your requirements and preferences. 190 | - Dependencies are regularly updated on a monthly basis 191 | - Start for free without upfront costs 192 | - Easy to customize 193 | - Minimal code 194 | - Unstyled template 195 | - SEO-friendly 196 | - 🚀 Production-ready 197 | 198 | ### Requirements 199 | 200 | - Node.js 20+ and npm 201 | 202 | ### Getting started 203 | 204 | Run the following command on your local environment: 205 | 206 | ```shell 207 | git clone --depth=1 https://github.com/ixartz/Next-js-Boilerplate.git my-project-name 208 | cd my-project-name 209 | npm install 210 | ``` 211 | 212 | For your information, all dependencies are updated every month. 213 | 214 | Then, you can run the project locally in development mode with live reload by executing: 215 | 216 | ```shell 217 | npm run dev 218 | ``` 219 | 220 | Open http://localhost:3000 with your favorite browser to see your project. 221 | 222 | ### Set up authentication 223 | 224 | To get started, you will need to create a Clerk account at [Clerk.com](https://clerk.com?utm_source=github&utm_medium=sponsorship&utm_campaign=nextjs-boilerplate) and create a new application in the Clerk Dashboard. Once you have done that, copy the `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY` values and add them to the `.env.local` file (not tracked by Git): 225 | 226 | ```shell 227 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_clerk_pub_key 228 | CLERK_SECRET_KEY=your_clerk_secret_key 229 | ``` 230 | 231 | Now you have a fully functional authentication system with Next.js, including features such as sign up, sign in, sign out, forgot password, reset password, update profile, update password, update email, delete account, and more. 232 | 233 | ### Set up remote database 234 | 235 | The project uses DrizzleORM, a type-safe ORM that is compatible with PostgreSQL, SQLite, and MySQL databases. By default, the project is configured to seamlessly work with PostgreSQL, and you have the flexibility to choose any PostgreSQL database provider of your choice. 236 | 237 | ### Translation (i18n) setup 238 | 239 | For translation, the project uses `next-intl` combined with [Crowdin](https://l.crowdin.com/next-js). As a developer, you only need to take care of the English (or another default language) version. Translations for other languages are automatically generated and handled by Crowdin. You can use Crowdin to collaborate with your translation team or translate the messages yourself with the help of machine translation. 240 | 241 | To set up translation (i18n), create an account at [Crowdin.com](https://l.crowdin.com/next-js) and create a new project. In the newly created project, you will be able to find the project ID. You will also need to create a new Personal Access Token by going to Account Settings > API. Then, in your GitHub Actions, you need to define the following environment variables: `CROWDIN_PROJECT_ID` and `CROWDIN_PERSONAL_TOKEN`. 242 | 243 | After defining the environment variables in your GitHub Actions, your localization files will be synchronized with Crowdin every time you push a new commit to the `main` branch. 244 | 245 | ### Project structure 246 | 247 | ```shell 248 | . 249 | ├── README.md # README file 250 | ├── .github # GitHub folder 251 | ├── .husky # Husky configuration 252 | ├── .storybook # Storybook folder 253 | ├── .vscode # VSCode configuration 254 | ├── migrations # Database migrations 255 | ├── public # Public assets folder 256 | ├── src 257 | │ ├── app # Next JS App (App Router) 258 | │ ├── components # React components 259 | │ ├── libs # 3rd party libraries configuration 260 | │ ├── locales # Locales folder (i18n messages) 261 | │ ├── models # Database models 262 | │ ├── styles # Styles folder 263 | │ ├── templates # Templates folder 264 | │ ├── types # Type definitions 265 | │ ├── utils # Utilities folder 266 | │ └── validations # Validation schemas 267 | ├── tests 268 | │ ├── e2e # E2E tests, also includes Monitoring as Code 269 | │ └── integration # Integration tests 270 | ├── tailwind.config.js # Tailwind CSS configuration 271 | └── tsconfig.json # TypeScript configuration 272 | ``` 273 | 274 | ### Customization 275 | 276 | You can easily configure Next js Boilerplate by searching the entire project for `FIXME:` to make quick customizations. Here are some of the most important files to customize: 277 | 278 | - `public/apple-touch-icon.png`, `public/favicon.ico`, `public/favicon-16x16.png` and `public/favicon-32x32.png`: your website favicon 279 | - `src/utils/AppConfig.ts`: configuration file 280 | - `src/templates/BaseTemplate.tsx`: default theme 281 | - `next.config.ts`: Next.js configuration 282 | - `.env`: default environment variables 283 | 284 | You have full access to the source code for further customization. The provided code is just an example to help you start your project. The sky's the limit 🚀. 285 | 286 | ### Change database schema 287 | 288 | To modify the database schema in the project, you can update the schema file located at `./src/models/Schema.ts`. This file defines the structure of your database tables using the Drizzle ORM library. 289 | 290 | After making changes to the schema, generate a migration by running the following command: 291 | 292 | ```shell 293 | npm run db:generate 294 | ``` 295 | 296 | This will create a migration file that reflects your schema changes. The migration is automatically applied during the next database interaction, so there is no need to run it manually or restart the Next.js server. 297 | 298 | ### Commit Message Format 299 | 300 | The project follows the [Conventional Commits](https://www.conventionalcommits.org/) specification, meaning all commit messages must be formatted accordingly. To help you write commit messages, the project uses [Commitizen](https://github.com/commitizen/cz-cli), an interactive CLI that guides you through the commit process. To use it, run the following command: 301 | 302 | ```shell 303 | npm run commit 304 | ``` 305 | 306 | One of the benefits of using Conventional Commits is the ability to automatically generate a `CHANGELOG` file. It also allows us to automatically determine the next version number based on the types of commits that are included in a release. 307 | 308 | ### CodeRabbit AI Code Reviews 309 | 310 | The project uses [CodeRabbit](https://www.coderabbit.ai?utm_source=next_js_starter&utm_medium=github&utm_campaign=next_js_starter_oss_2025), an AI-powered code reviewer. CodeRabbit monitors your repository and automatically provides intelligent code reviews on all new pull requests using its powerful AI engine. 311 | 312 | Setting up CodeRabbit is simple, visit [coderabbit.ai](https://www.coderabbit.ai?utm_source=next_js_starter&utm_medium=github&utm_campaign=next_js_starter_oss_2025), sign in with your GitHub account, and add your repository from the dashboard. That's it! 313 | 314 | ### Testing 315 | 316 | All unit tests are located alongside the source code in the same directory, making them easier to find. The project uses Vitest and React Testing Library for unit testing. You can run the tests with the following command: 317 | 318 | ```shell 319 | npm run test 320 | ``` 321 | 322 | ### Integration & E2E Testing 323 | 324 | The project uses Playwright for integration and end-to-end (E2E) testing. You can run the tests with the following commands: 325 | 326 | ```shell 327 | npx playwright install # Only for the first time in a new environment 328 | npm run test:e2e 329 | ``` 330 | 331 | In the local environment, visual testing is disabled, and the terminal will display the message `[percy] Percy is not running, disabling snapshots.`. By default, visual testing only runs in GitHub Actions. 332 | 333 | ### Deploy to production 334 | 335 | During the build process, database migrations are automatically executed, so there's no need to run them manually. However, you must define `DATABASE_URL` in your environment variables. 336 | 337 | Then, you can generate a production build with: 338 | 339 | ```shell 340 | $ npm run build 341 | ``` 342 | 343 | It generates an optimized production build of the boilerplate. To test the generated build, run: 344 | 345 | ```shell 346 | $ npm run start 347 | ``` 348 | 349 | You also need to defined the environment variables `CLERK_SECRET_KEY` using your own key. 350 | 351 | This command starts a local server using the production build. You can now open http://localhost:3000 in your preferred browser to see the result. 352 | 353 | ### Deploy to Sevalla 354 | 355 | You can deploy a Next.js application along with its database on a single platform. First, create an account on [Sevalla](https://sevalla.com). 356 | 357 | After registration, you will be redirected to the dashboard. From there, navigate to `Database > Create a database`. Select PostgreSQL and and use the default settings for a quick setup. For advanced users, you can customize the database location and resource size. Finally, click on `Create` to complete the process. 358 | 359 | Once the database is created and ready, return to the dashboard and click `Application > Create an App`. After connecting your GitHub account, select the repository you want to deploy. Keep the default settings for the remaining options, then click `Create`. 360 | 361 | Next, connect your database to your application by going to `Networking > Connected services > Add connection` and select the database you just created. You also need to enable the `Add environment variables to the application` option, and rename `DB_URL` to `DATABASE_URL`. Then, click `Add connection`. 362 | 363 | Go to `Environment variables > Add environment variable`, and define the environment variables `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY` from your Clerk account. Click `Save`. 364 | 365 | Finally, initiate a new deployment by clicking `Overview > Latest deployments > Deploy now`. If everything is set up correctly, your application will be deployed successfully with a working database. 366 | 367 | ### Error Monitoring 368 | 369 | The project uses [Sentry](https://sentry.io/for/nextjs/?utm_source=github&utm_medium=paid-community&utm_campaign=general-fy25q1-nextjs&utm_content=github-banner-nextjsboilerplate-logo) to monitor errors. In the development environment, no additional setup is needed: Next.js Boilerplate is pre-configured to use Sentry and Spotlight (Sentry for Development). All errors will automatically be sent to your local Spotlight instance, allowing you to experience Sentry locally. 370 | 371 | For production environment, you'll need to create a Sentry account and a new project. Then, in `next.config.mjs`, you need to update the `org` and `project` attributes in `withSentryConfig` function. Additionally, add your Sentry DSN to `sentry.client.config.ts`, `sentry.edge.config.ts` and `sentry.server.config.ts`. 372 | 373 | ### Code coverage 374 | 375 | Next.js Boilerplate relies on [Codecov](https://about.codecov.io/codecov-free-trial/?utm_source=github&utm_medium=paid-community&utm_campaign=general-fy25q1-nextjs&utm_content=github-banner-nextjsboilerplate-logo) for code coverage reporting solution. To enable Codecov, create a Codecov account and connect it to your GitHub account. Your repositories should appear on your Codecov dashboard. Select the desired repository and copy the token. In GitHub Actions, define the `CODECOV_TOKEN` environment variable and paste the token. 376 | 377 | Make sure to create `CODECOV_TOKEN` as a GitHub Actions secret, do not paste it directly into your source code. 378 | 379 | ### Logging 380 | 381 | The project uses Pino.js for logging. In the development environment, logs are displayed in the console by default. 382 | 383 | For production, the project is already integrated with [Better Stack](https://betterstack.com/?utm_source=github&utm_medium=sponsorship&utm_campaign=next-js-boilerplate) to manage and query your logs using SQL. To use Better Stack, you need to create a [Better Stack](https://betterstack.com/?utm_source=github&utm_medium=sponsorship&utm_campaign=next-js-boilerplate) account and create a new source: go to your Better Stack Logs Dashboard > Sources > Connect source. Then, you need to give a name to your source and select Node.js as the platform. 384 | 385 | After creating the source, you will be able to view and copy your source token. In your environment variables, paste the token into the `LOGTAIL_SOURCE_TOKEN` variable. Now, all logs will automatically be sent to and ingested by Better Stack. 386 | 387 | ### Checkly monitoring 388 | 389 | The project uses [Checkly](https://www.checklyhq.com/?utm_source=github&utm_medium=sponsorship&utm_campaign=next-js-boilerplate) to ensure that your production environment is always up and running. At regular intervals, Checkly runs the tests ending with `*.check.e2e.ts` extension and notifies you if any of the tests fail. Additionally, you have the flexibility to execute tests from multiple locations to ensure that your application is available worldwide. 390 | 391 | To use Checkly, you must first create an account on [their website](https://www.checklyhq.com/?utm_source=github&utm_medium=sponsorship&utm_campaign=next-js-boilerplate). After creating an account, generate a new API key in the Checkly Dashboard and set the `CHECKLY_API_KEY` environment variable in GitHub Actions. Additionally, you will need to define the `CHECKLY_ACCOUNT_ID`, which can also be found in your Checkly Dashboard under User Settings > General. 392 | 393 | To complete the setup, update the `checkly.config.ts` file with your own email address and production URL. 394 | 395 | ### Arcjet security and bot protection 396 | 397 | The project uses [Arcjet](https://launch.arcjet.com/Q6eLbRE), a security as code product that includes several features that can be used individually or combined to provide defense in depth for your site. 398 | 399 | To set up Arcjet, [create a free account](https://launch.arcjet.com/Q6eLbRE) and get your API key. Then add it to the `ARCJET_KEY` environment variable. 400 | 401 | Arcjet is configured with two main features: bot detection and the Arcjet Shield WAF: 402 | 403 | - [Bot detection](https://docs.arcjet.com/bot-protection/concepts) is configured to allow search engines, preview link generators e.g. Slack and Twitter previews, and to allow common uptime monitoring services. All other bots, such as scrapers and AI crawlers, will be blocked. You can [configure additional bot types](https://docs.arcjet.com/bot-protection/identifying-bots) to allow or block. 404 | - [Arcjet Shield WAF](https://docs.arcjet.com/shield/concepts) will detect and block common attacks such as SQL injection, cross-site scripting, and other OWASP Top 10 vulnerabilities. 405 | 406 | Arcjet is configured with a central client at `src/libs/Arcjet.ts` that includes the Shield WAF rules. Additional rules are applied when Arcjet is called in `middleware.ts`. 407 | 408 | ### Useful commands 409 | 410 | #### Bundle Analyzer 411 | 412 | Next.js Boilerplate includes a built-in bundle analyzer. It can be used to analyze the size of your JavaScript bundles. To begin, run the following command: 413 | 414 | ```shell 415 | npm run build-stats 416 | ``` 417 | 418 | By running the command, it'll automatically open a new browser window with the results. 419 | 420 | #### Database Studio 421 | 422 | The project is already configured with Drizzle Studio to explore the database. You can run the following command to open the database studio: 423 | 424 | ```shell 425 | npm run db:studio 426 | ``` 427 | 428 | Then, you can open https://local.drizzle.studio with your favorite browser to explore your database. 429 | 430 | ### VSCode information (optional) 431 | 432 | If you are VSCode user, you can have a better integration with VSCode by installing the suggested extension in `.vscode/extension.json`. The starter code comes up with Settings for a seamless integration with VSCode. The Debug configuration is also provided for frontend and backend debugging experience. 433 | 434 | With the plugins installed in your VSCode, ESLint and Prettier can automatically fix the code and display errors. The same applies to testing: you can install the VSCode Vitest extension to automatically run your tests, and it also shows the code coverage in context. 435 | 436 | Pro tips: if you need a project wide-type checking with TypeScript, you can run a build with Cmd + Shift + B on Mac. 437 | 438 | ### Contributions 439 | 440 | Everyone is welcome to contribute to this project. Feel free to open an issue if you have any questions or find a bug. Totally open to suggestions and improvements. 441 | 442 | ### License 443 | 444 | Licensed under the MIT License, Copyright © 2025 445 | 446 | See [LICENSE](LICENSE) for more information. 447 | 448 | ## Sponsors 449 | 450 | 451 | 452 | 461 | 470 | 486 | 487 | 488 | 497 | 506 | 515 | 516 | 517 | 526 | 535 | 544 | 545 | 546 | 551 | 556 | 557 |
453 | 454 | 455 | 456 | 457 | Clerk – Authentication & User Management for Next.js 458 | 459 | 460 | 462 | 463 | 464 | 465 | 466 | CodeRabbit 467 | 468 | 469 | 471 | 472 | 473 | 474 | 475 | Sentry 476 | 477 | 478 | 479 | 480 | 481 | 482 | Codecov 483 | 484 | 485 |
489 | 490 | 491 | 492 | 493 | Arcjet 494 | 495 | 496 | 498 | 499 | 500 | 501 | 502 | Sevalla 503 | 504 | 505 | 507 | 508 | 509 | 510 | 511 | Crowdin 512 | 513 | 514 |
518 | 519 | 520 | 521 | 522 | PostHog 523 | 524 | 525 | 527 | 528 | 529 | 530 | 531 | Better Stack 532 | 533 | 534 | 536 | 537 | 538 | 539 | 540 | Checkly 541 | 542 | 543 |
547 | 548 | Next.js SaaS Boilerplate with React 549 | 550 | 552 | 553 | Add your logo here 554 | 555 |
558 | 559 | --- 560 | 561 | Made with ♥ by [CreativeDesignsGuru](https://creativedesignsguru.com) [![Twitter](https://img.shields.io/twitter/url/https/twitter.com/cloudposse.svg?style=social&label=Follow%20%40Ixartz)](https://twitter.com/ixartz) 562 | 563 | Looking for a custom boilerplate to kick off your project? I'd be glad to discuss how I can help you build one. Feel free to reach out anytime at contact@creativedesignsguru.com! 564 | 565 | [![Sponsor Next JS Boilerplate](https://cdn.buymeacoffee.com/buttons/default-red.png)](https://github.com/sponsors/ixartz) 566 | -------------------------------------------------------------------------------- /checkly.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'checkly'; 2 | import { EmailAlertChannel, Frequency } from 'checkly/constructs'; 3 | 4 | const sendDefaults = { 5 | sendFailure: true, 6 | sendRecovery: true, 7 | sendDegraded: true, 8 | }; 9 | 10 | // FIXME: Add your production URL 11 | const productionURL = 'https://demo.nextjs-boilerplate.com'; 12 | 13 | const emailChannel = new EmailAlertChannel('email-channel-1', { 14 | // FIXME: add your own email address, Checkly will send you an email notification if a check fails 15 | address: 'contact@creativedesignsguru.com', 16 | ...sendDefaults, 17 | }); 18 | 19 | export const config = defineConfig({ 20 | // FIXME: Add your own project name, logical ID, and repository URL 21 | projectName: 'Next.js Boilerplate', 22 | logicalId: 'nextjs-boilerplate', 23 | repoUrl: 'https://github.com/ixartz/Next-js-Boilerplate', 24 | checks: { 25 | locations: ['us-east-1', 'eu-west-1'], 26 | tags: ['website'], 27 | runtimeId: '2024.02', 28 | browserChecks: { 29 | frequency: Frequency.EVERY_24H, 30 | testMatch: '**/tests/e2e/**/*.check.e2e.ts', 31 | alertChannels: [emailChannel], 32 | }, 33 | playwrightConfig: { 34 | use: { 35 | baseURL: process.env.ENVIRONMENT_URL || productionURL, 36 | extraHTTPHeaders: { 37 | 'x-vercel-protection-bypass': process.env.VERCEL_BYPASS_TOKEN, 38 | }, 39 | }, 40 | }, 41 | }, 42 | cli: { 43 | runLocation: 'us-east-1', 44 | reporters: ['list'], 45 | }, 46 | }); 47 | 48 | export default config; 49 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | patch: off 4 | -------------------------------------------------------------------------------- /commitlint.config.ts: -------------------------------------------------------------------------------- 1 | import type { UserConfig } from '@commitlint/types'; 2 | 3 | const Configuration: UserConfig = { 4 | extends: ['@commitlint/config-conventional'], 5 | ignores: [message => message.startsWith('chore: bump')], // Ignore dependabot commits 6 | }; 7 | 8 | export default Configuration; 9 | -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Your Crowdin credentials 3 | # 4 | # No need modify CROWDIN_PROJECT_ID and CROWDIN_PERSONAL_TOKEN, you can set them in GitHub Actions secrets 5 | project_id_env: CROWDIN_PROJECT_ID 6 | api_token_env: CROWDIN_PERSONAL_TOKEN 7 | base_path: . 8 | base_url: 'https://api.crowdin.com' # https://{organization-name}.crowdin.com for Crowdin Enterprise 9 | 10 | # 11 | # Choose file structure in Crowdin 12 | # e.g. true or false 13 | # 14 | preserve_hierarchy: true 15 | 16 | # 17 | # Files configuration 18 | # 19 | files: 20 | - source: /src/locales/en.json 21 | 22 | # 23 | # Where translations will be placed 24 | # e.g. "/resources/%two_letters_code%/%original_file_name%" 25 | # 26 | translation: '/src/locales/%two_letters_code%.json' 27 | 28 | # 29 | # File type 30 | # e.g. "json" 31 | # 32 | type: json 33 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'drizzle-kit'; 2 | 3 | export default defineConfig({ 4 | out: './migrations', 5 | schema: './src/models/Schema.ts', 6 | dialect: 'postgresql', 7 | dbCredentials: { 8 | url: process.env.DATABASE_URL ?? '', 9 | }, 10 | verbose: true, 11 | strict: true, 12 | }); 13 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config'; 2 | import nextPlugin from '@next/eslint-plugin-next'; 3 | import jestDom from 'eslint-plugin-jest-dom'; 4 | import jsxA11y from 'eslint-plugin-jsx-a11y'; 5 | import playwright from 'eslint-plugin-playwright'; 6 | 7 | export default antfu( 8 | { 9 | react: true, 10 | typescript: true, 11 | 12 | // Configuration preferences 13 | lessOpinionated: true, 14 | isInEditor: false, 15 | 16 | // Code style 17 | stylistic: { 18 | semi: true, 19 | }, 20 | 21 | // Format settings 22 | formatters: { 23 | css: true, 24 | }, 25 | 26 | // Ignored paths 27 | ignores: [ 28 | 'migrations/**/*', 29 | ], 30 | }, 31 | // --- Next.js Specific Rules --- 32 | { 33 | plugins: { 34 | '@next/next': nextPlugin, 35 | }, 36 | rules: { 37 | ...nextPlugin.configs.recommended.rules, 38 | ...nextPlugin.configs['core-web-vitals'].rules, 39 | }, 40 | }, 41 | // --- Accessibility Rules --- 42 | jsxA11y.flatConfigs.recommended, 43 | // --- Testing Rules --- 44 | { 45 | files: [ 46 | '**/*.test.ts?(x)', 47 | ], 48 | ...jestDom.configs['flat/recommended'], 49 | }, 50 | // --- E2E Testing Rules --- 51 | { 52 | files: [ 53 | '**/*.spec.ts', 54 | '**/*.e2e.ts', 55 | ], 56 | ...playwright.configs['flat/recommended'], 57 | }, 58 | // --- Custom Rule Overrides --- 59 | { 60 | rules: { 61 | 'antfu/no-top-level-await': 'off', // Allow top-level await 62 | 'style/brace-style': ['error', '1tbs'], // Use the default brace style 63 | 'ts/consistent-type-definitions': ['error', 'type'], // Use `type` instead of `interface` 64 | 'react/prefer-destructuring-assignment': 'off', // Vscode doesn't support automatically destructuring, it's a pain to add a new variable 65 | 'node/prefer-global/process': 'off', // Allow using `process.env` 66 | 'test/padding-around-all': 'error', // Add padding in test files 67 | 'test/prefer-lowercase-title': 'off', // Allow using uppercase titles in test titles 68 | }, 69 | }, 70 | ); 71 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | # Validate commit messages 2 | commit-msg: 3 | commands: 4 | commitlint: 5 | run: npx --no -- commitlint --edit {1} 6 | 7 | # Validate content before committing 8 | pre-commit: 9 | commands: 10 | lint: 11 | glob: '*' 12 | run: npx --no -- eslint --fix --no-warn-ignored 13 | stage_fixed: true 14 | priority: 1 15 | check-types: 16 | glob: '*.{ts,tsx}' 17 | run: npm run check-types 18 | priority: 2 19 | -------------------------------------------------------------------------------- /migrations/0000_init-db.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "counter" ( 2 | "id" serial PRIMARY KEY NOT NULL, 3 | "count" integer DEFAULT 0, 4 | "updated_at" timestamp DEFAULT now() NOT NULL, 5 | "created_at" timestamp DEFAULT now() NOT NULL 6 | ); 7 | -------------------------------------------------------------------------------- /migrations/meta/0000_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "0896e842-e142-406c-99b2-a602f7fa8731", 3 | "prevId": "00000000-0000-0000-0000-000000000000", 4 | "version": "7", 5 | "dialect": "postgresql", 6 | "tables": { 7 | "public.counter": { 8 | "name": "counter", 9 | "schema": "", 10 | "columns": { 11 | "id": { 12 | "name": "id", 13 | "type": "serial", 14 | "primaryKey": true, 15 | "notNull": true 16 | }, 17 | "count": { 18 | "name": "count", 19 | "type": "integer", 20 | "primaryKey": false, 21 | "notNull": false, 22 | "default": 0 23 | }, 24 | "updated_at": { 25 | "name": "updated_at", 26 | "type": "timestamp", 27 | "primaryKey": false, 28 | "notNull": true, 29 | "default": "now()" 30 | }, 31 | "created_at": { 32 | "name": "created_at", 33 | "type": "timestamp", 34 | "primaryKey": false, 35 | "notNull": true, 36 | "default": "now()" 37 | } 38 | }, 39 | "indexes": {}, 40 | "foreignKeys": {}, 41 | "compositePrimaryKeys": {}, 42 | "uniqueConstraints": {}, 43 | "policies": {}, 44 | "checkConstraints": {}, 45 | "isRLSEnabled": false 46 | } 47 | }, 48 | "enums": {}, 49 | "schemas": {}, 50 | "sequences": {}, 51 | "roles": {}, 52 | "policies": {}, 53 | "views": {}, 54 | "_meta": { 55 | "columns": {}, 56 | "schemas": {}, 57 | "tables": {} 58 | } 59 | } -------------------------------------------------------------------------------- /migrations/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "postgresql", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "7", 8 | "when": 1745518076143, 9 | "tag": "0000_init-db", 10 | "breakpoints": true 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from 'next'; 2 | import withBundleAnalyzer from '@next/bundle-analyzer'; 3 | import { withSentryConfig } from '@sentry/nextjs'; 4 | import createNextIntlPlugin from 'next-intl/plugin'; 5 | import './src/libs/Env'; 6 | 7 | // Define the base Next.js configuration 8 | const baseConfig: NextConfig = { 9 | eslint: { 10 | dirs: ['.'], 11 | }, 12 | poweredByHeader: false, 13 | reactStrictMode: true, 14 | serverExternalPackages: ['@electric-sql/pglite'], 15 | }; 16 | 17 | // Initialize the Next-Intl plugin 18 | let configWithPlugins = createNextIntlPlugin('./src/libs/i18n.ts')(baseConfig); 19 | 20 | // Conditionally enable bundle analysis 21 | if (process.env.ANALYZE === 'true') { 22 | configWithPlugins = withBundleAnalyzer()(configWithPlugins); 23 | } 24 | 25 | // Conditionally enable Sentry configuration 26 | if (!process.env.NEXT_PUBLIC_SENTRY_DISABLED) { 27 | configWithPlugins = withSentryConfig(configWithPlugins, { 28 | // For all available options, see: 29 | // https://www.npmjs.com/package/@sentry/webpack-plugin#options 30 | // FIXME: Add your Sentry organization and project names 31 | org: 'nextjs-boilerplate-org', 32 | project: 'nextjs-boilerplate', 33 | 34 | // Only print logs for uploading source maps in CI 35 | silent: !process.env.CI, 36 | 37 | // For all available options, see: 38 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ 39 | 40 | // Upload a larger set of source maps for prettier stack traces (increases build time) 41 | widenClientFileUpload: true, 42 | 43 | // Upload a larger set of source maps for prettier stack traces (increases build time) 44 | reactComponentAnnotation: { 45 | enabled: true, 46 | }, 47 | 48 | // Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers. 49 | // This can increase your server load as well as your hosting bill. 50 | // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client- 51 | // side errors will fail. 52 | tunnelRoute: '/monitoring', 53 | 54 | // Automatically tree-shake Sentry logger statements to reduce bundle size 55 | disableLogger: true, 56 | 57 | // Disable Sentry telemetry 58 | telemetry: false, 59 | }); 60 | } 61 | 62 | const nextConfig = configWithPlugins; 63 | export default nextConfig; 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-js-boilerplate", 3 | "version": "3.68.0", 4 | "author": "Ixartz (https://github.com/ixartz)", 5 | "engines": { 6 | "node": ">=20" 7 | }, 8 | "scripts": { 9 | "dev:spotlight": "spotlight-sidecar", 10 | "dev:next": "next dev --turbopack", 11 | "dev": "run-p dev:*", 12 | "build": "next build", 13 | "start": "next start", 14 | "build-stats": "cross-env ANALYZE=true npm run build", 15 | "clean": "rimraf .next out coverage", 16 | "lint": "eslint .", 17 | "lint:fix": "eslint . --fix", 18 | "check-types": "tsc --noEmit --pretty", 19 | "test": "vitest run --browser.headless", 20 | "test:e2e": "playwright test", 21 | "db:generate": "drizzle-kit generate", 22 | "db:migrate": "dotenv -c production -- drizzle-kit migrate", 23 | "db:studio": "dotenv -c production -- drizzle-kit studio", 24 | "storybook": "storybook dev -p 6006", 25 | "storybook:build": "storybook build", 26 | "storybook:serve": "http-server storybook-static --port 6006 --silent", 27 | "serve-storybook": "run-s storybook:*", 28 | "test-storybook:ci": "start-server-and-test serve-storybook http://127.0.0.1:6006 test-storybook" 29 | }, 30 | "dependencies": { 31 | "@arcjet/next": "^1.0.0-beta.8", 32 | "@clerk/localizations": "^3.16.3", 33 | "@clerk/nextjs": "^6.20.2", 34 | "@electric-sql/pglite": "^0.3.2", 35 | "@hookform/resolvers": "^5.0.1", 36 | "@logtail/pino": "^0.5.5", 37 | "@sentry/nextjs": "^9.24.0", 38 | "@t3-oss/env-nextjs": "^0.13.6", 39 | "drizzle-orm": "^0.44.1", 40 | "next": "^15.3.3", 41 | "next-intl": "^4.1.0", 42 | "pg": "^8.16.0", 43 | "pino": "^9.7.0", 44 | "pino-pretty": "^13.0.0", 45 | "posthog-js": "^1.249.0", 46 | "react": "19.1.0", 47 | "react-dom": "19.1.0", 48 | "react-hook-form": "^7.57.0", 49 | "zod": "^3.25.46" 50 | }, 51 | "devDependencies": { 52 | "@antfu/eslint-config": "^4.13.2", 53 | "@commitlint/cli": "^19.8.1", 54 | "@commitlint/config-conventional": "^19.8.1", 55 | "@commitlint/cz-commitlint": "^19.8.1", 56 | "@eslint-react/eslint-plugin": "^1.50.0", 57 | "@faker-js/faker": "^9.8.0", 58 | "@next/bundle-analyzer": "^15.3.3", 59 | "@next/eslint-plugin-next": "^15.3.3", 60 | "@percy/cli": "1.30.11", 61 | "@percy/playwright": "^1.0.8", 62 | "@playwright/test": "^1.52.0", 63 | "@semantic-release/changelog": "^6.0.3", 64 | "@semantic-release/git": "^10.0.1", 65 | "@spotlightjs/spotlight": "^2.13.3", 66 | "@storybook/addon-essentials": "^8.6.14", 67 | "@storybook/addon-interactions": "^8.6.14", 68 | "@storybook/addon-links": "^8.6.14", 69 | "@storybook/addon-onboarding": "^8.6.14", 70 | "@storybook/blocks": "^8.6.14", 71 | "@storybook/nextjs": "^8.6.14", 72 | "@storybook/react": "^8.6.14", 73 | "@storybook/test": "^8.6.14", 74 | "@storybook/test-runner": "^0.22.0", 75 | "@tailwindcss/postcss": "^4.1.8", 76 | "@types/node": "^22.15.29", 77 | "@types/pg": "^8.15.4", 78 | "@types/react": "^19.1.6", 79 | "@vitejs/plugin-react": "^4.5.0", 80 | "@vitest/browser": "^3.1.4", 81 | "@vitest/coverage-v8": "^3.1.4", 82 | "@vitest/expect": "^3.1.4", 83 | "checkly": "^5.5.0", 84 | "commitizen": "^4.3.1", 85 | "cross-env": "^7.0.3", 86 | "dotenv-cli": "^8.0.0", 87 | "drizzle-kit": "^0.31.1", 88 | "eslint": "^9.28.0", 89 | "eslint-plugin-format": "^1.0.1", 90 | "eslint-plugin-jest-dom": "^5.5.0", 91 | "eslint-plugin-jsx-a11y": "^6.10.2", 92 | "eslint-plugin-playwright": "^2.2.0", 93 | "eslint-plugin-react-hooks": "^5.2.0", 94 | "eslint-plugin-react-refresh": "^0.4.20", 95 | "http-server": "^14.1.1", 96 | "lefthook": "^1.11.13", 97 | "npm-run-all": "^4.1.5", 98 | "postcss": "^8.5.4", 99 | "postcss-load-config": "^6.0.1", 100 | "rimraf": "^6.0.1", 101 | "semantic-release": "^24.2.5", 102 | "start-server-and-test": "^2.0.12", 103 | "storybook": "^8.6.14", 104 | "tailwindcss": "^4.1.8", 105 | "typescript": "^5.8.3", 106 | "vite-tsconfig-paths": "^5.1.4", 107 | "vitest": "^3.1.4", 108 | "vitest-browser-react": "^0.2.0" 109 | }, 110 | "config": { 111 | "commitizen": { 112 | "path": "@commitlint/cz-commitlint" 113 | } 114 | }, 115 | "release": { 116 | "branches": [ 117 | "main" 118 | ], 119 | "plugins": [ 120 | [ 121 | "@semantic-release/commit-analyzer", 122 | { 123 | "preset": "conventionalcommits" 124 | } 125 | ], 126 | "@semantic-release/release-notes-generator", 127 | "@semantic-release/changelog", 128 | [ 129 | "@semantic-release/npm", 130 | { 131 | "npmPublish": false 132 | } 133 | ], 134 | "@semantic-release/git", 135 | "@semantic-release/github" 136 | ] 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test'; 2 | 3 | // Use process.env.PORT by default and fallback to port 3000 4 | const PORT = process.env.PORT || 3000; 5 | 6 | // Set webServer.url and use.baseURL with the location of the WebServer respecting the correct set port 7 | const baseURL = `http://localhost:${PORT}`; 8 | 9 | /** 10 | * See https://playwright.dev/docs/test-configuration. 11 | */ 12 | export default defineConfig({ 13 | testDir: './tests', 14 | // Look for files with the .spec.js or .e2e.js extension 15 | testMatch: '*.@(spec|e2e).?(c|m)[jt]s?(x)', 16 | // Timeout per test 17 | timeout: 30 * 1000, 18 | // Fail the build on CI if you accidentally left test.only in the source code. 19 | forbidOnly: !!process.env.CI, 20 | // Reporter to use. See https://playwright.dev/docs/test-reporters 21 | reporter: process.env.CI ? 'github' : 'list', 22 | 23 | expect: { 24 | // Set timeout for async expect matchers 25 | timeout: 10 * 1000, 26 | }, 27 | 28 | // Run your local dev server before starting the tests: 29 | // https://playwright.dev/docs/test-advanced#launching-a-development-web-server-during-the-tests 30 | webServer: { 31 | command: process.env.CI ? 'npm run start' : 'npm run dev:next', 32 | url: baseURL, 33 | timeout: 2 * 60 * 1000, 34 | reuseExistingServer: !process.env.CI, 35 | }, 36 | 37 | // Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. 38 | use: { 39 | // Use baseURL so to make navigations relative. 40 | // More information: https://playwright.dev/docs/api/class-testoptions#test-options-base-url 41 | baseURL, 42 | 43 | // Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer 44 | trace: 'retain-on-failure', 45 | 46 | // Record videos when retrying the failed test. 47 | video: process.env.CI ? 'retain-on-failure' : undefined, 48 | }, 49 | 50 | projects: [ 51 | { 52 | name: 'chromium', 53 | use: { ...devices['Desktop Chrome'] }, 54 | }, 55 | ...(process.env.CI 56 | ? [ 57 | { 58 | name: 'firefox', 59 | use: { ...devices['Desktop Firefox'] }, 60 | }, 61 | ] 62 | : []), 63 | ], 64 | }); 65 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * PostCSS Configuration 3 | * @type {import('postcss-load-config').Config} 4 | * 5 | * This file configures the PostCSS processor which transforms CSS with JavaScript plugins. 6 | * It's used in the build process to process CSS files before they're served to the browser. 7 | */ 8 | const config = { 9 | plugins: { 10 | // Add Tailwind CSS support 11 | '@tailwindcss/postcss': {}, 12 | }, 13 | }; 14 | 15 | export default config; 16 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixartz/Next-js-Boilerplate/01c4608bd01e67e7a9d9d8c56a3419e977beabeb/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/assets/images/arcjet-dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/images/arcjet-light.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/images/better-stack-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixartz/Next-js-Boilerplate/01c4608bd01e67e7a9d9d8c56a3419e977beabeb/public/assets/images/better-stack-dark.png -------------------------------------------------------------------------------- /public/assets/images/better-stack-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixartz/Next-js-Boilerplate/01c4608bd01e67e7a9d9d8c56a3419e977beabeb/public/assets/images/better-stack-white.png -------------------------------------------------------------------------------- /public/assets/images/checkly-logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixartz/Next-js-Boilerplate/01c4608bd01e67e7a9d9d8c56a3419e977beabeb/public/assets/images/checkly-logo-dark.png -------------------------------------------------------------------------------- /public/assets/images/checkly-logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixartz/Next-js-Boilerplate/01c4608bd01e67e7a9d9d8c56a3419e977beabeb/public/assets/images/checkly-logo-light.png -------------------------------------------------------------------------------- /public/assets/images/clerk-logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixartz/Next-js-Boilerplate/01c4608bd01e67e7a9d9d8c56a3419e977beabeb/public/assets/images/clerk-logo-dark.png -------------------------------------------------------------------------------- /public/assets/images/codecov-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /public/assets/images/codecov-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /public/assets/images/coderabbit-logo-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /public/assets/images/coderabbit-logo-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /public/assets/images/crowdin-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixartz/Next-js-Boilerplate/01c4608bd01e67e7a9d9d8c56a3419e977beabeb/public/assets/images/crowdin-dark.png -------------------------------------------------------------------------------- /public/assets/images/crowdin-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixartz/Next-js-Boilerplate/01c4608bd01e67e7a9d9d8c56a3419e977beabeb/public/assets/images/crowdin-white.png -------------------------------------------------------------------------------- /public/assets/images/nextjs-boilerplate-saas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixartz/Next-js-Boilerplate/01c4608bd01e67e7a9d9d8c56a3419e977beabeb/public/assets/images/nextjs-boilerplate-saas.png -------------------------------------------------------------------------------- /public/assets/images/nextjs-boilerplate-sign-in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixartz/Next-js-Boilerplate/01c4608bd01e67e7a9d9d8c56a3419e977beabeb/public/assets/images/nextjs-boilerplate-sign-in.png -------------------------------------------------------------------------------- /public/assets/images/nextjs-boilerplate-sign-up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixartz/Next-js-Boilerplate/01c4608bd01e67e7a9d9d8c56a3419e977beabeb/public/assets/images/nextjs-boilerplate-sign-up.png -------------------------------------------------------------------------------- /public/assets/images/nextjs-starter-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixartz/Next-js-Boilerplate/01c4608bd01e67e7a9d9d8c56a3419e977beabeb/public/assets/images/nextjs-starter-banner.png -------------------------------------------------------------------------------- /public/assets/images/sentry-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixartz/Next-js-Boilerplate/01c4608bd01e67e7a9d9d8c56a3419e977beabeb/public/assets/images/sentry-dark.png -------------------------------------------------------------------------------- /public/assets/images/sentry-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixartz/Next-js-Boilerplate/01c4608bd01e67e7a9d9d8c56a3419e977beabeb/public/assets/images/sentry-white.png -------------------------------------------------------------------------------- /public/assets/images/sevalla-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixartz/Next-js-Boilerplate/01c4608bd01e67e7a9d9d8c56a3419e977beabeb/public/assets/images/sevalla-dark.png -------------------------------------------------------------------------------- /public/assets/images/sevalla-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixartz/Next-js-Boilerplate/01c4608bd01e67e7a9d9d8c56a3419e977beabeb/public/assets/images/sevalla-light.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixartz/Next-js-Boilerplate/01c4608bd01e67e7a9d9d8c56a3419e977beabeb/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixartz/Next-js-Boilerplate/01c4608bd01e67e7a9d9d8c56a3419e977beabeb/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixartz/Next-js-Boilerplate/01c4608bd01e67e7a9d9d8c56a3419e977beabeb/public/favicon.ico -------------------------------------------------------------------------------- /src/app/[locale]/(auth)/(center)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { setRequestLocale } from 'next-intl/server'; 2 | 3 | export default async function CenteredLayout(props: { 4 | children: React.ReactNode; 5 | params: Promise<{ locale: string }>; 6 | }) { 7 | const { locale } = await props.params; 8 | setRequestLocale(locale); 9 | 10 | return ( 11 |
12 | {props.children} 13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/app/[locale]/(auth)/(center)/sign-in/[[...sign-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from '@clerk/nextjs'; 2 | import { getTranslations, setRequestLocale } from 'next-intl/server'; 3 | import { getI18nPath } from '@/utils/Helpers'; 4 | 5 | type ISignInPageProps = { 6 | params: Promise<{ locale: string }>; 7 | }; 8 | 9 | export async function generateMetadata(props: ISignInPageProps) { 10 | const { locale } = await props.params; 11 | const t = await getTranslations({ 12 | locale, 13 | namespace: 'SignIn', 14 | }); 15 | 16 | return { 17 | title: t('meta_title'), 18 | description: t('meta_description'), 19 | }; 20 | } 21 | 22 | export default async function SignInPage(props: ISignInPageProps) { 23 | const { locale } = await props.params; 24 | setRequestLocale(locale); 25 | 26 | return ( 27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /src/app/[locale]/(auth)/(center)/sign-up/[[...sign-up]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUp } from '@clerk/nextjs'; 2 | import { getTranslations, setRequestLocale } from 'next-intl/server'; 3 | import { getI18nPath } from '@/utils/Helpers'; 4 | 5 | type ISignUpPageProps = { 6 | params: Promise<{ locale: string }>; 7 | }; 8 | 9 | export async function generateMetadata(props: ISignUpPageProps) { 10 | const { locale } = await props.params; 11 | const t = await getTranslations({ 12 | locale, 13 | namespace: 'SignUp', 14 | }); 15 | 16 | return { 17 | title: t('meta_title'), 18 | description: t('meta_description'), 19 | }; 20 | } 21 | 22 | export default async function SignUpPage(props: ISignUpPageProps) { 23 | const { locale } = await props.params; 24 | setRequestLocale(locale); 25 | 26 | return ( 27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /src/app/[locale]/(auth)/dashboard/layout.tsx: -------------------------------------------------------------------------------- 1 | import { SignOutButton } from '@clerk/nextjs'; 2 | import { getTranslations, setRequestLocale } from 'next-intl/server'; 3 | import Link from 'next/link'; 4 | import { LocaleSwitcher } from '@/components/LocaleSwitcher'; 5 | import { BaseTemplate } from '@/templates/BaseTemplate'; 6 | 7 | export default async function DashboardLayout(props: { 8 | children: React.ReactNode; 9 | params: Promise<{ locale: string }>; 10 | }) { 11 | const { locale } = await props.params; 12 | setRequestLocale(locale); 13 | const t = await getTranslations({ 14 | locale, 15 | namespace: 'DashboardLayout', 16 | }); 17 | 18 | return ( 19 | 22 |
  • 23 | 27 | {t('dashboard_link')} 28 | 29 |
  • 30 |
  • 31 | 35 | {t('user_profile_link')} 36 | 37 |
  • 38 | 39 | )} 40 | rightNav={( 41 | <> 42 |
  • 43 | 44 | 47 | 48 |
  • 49 | 50 |
  • 51 | 52 |
  • 53 | 54 | )} 55 | > 56 | {props.children} 57 |
    58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /src/app/[locale]/(auth)/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | import { getTranslations } from 'next-intl/server'; 2 | import { Hello } from '@/components/Hello'; 3 | 4 | export async function generateMetadata(props: { 5 | params: Promise<{ locale: string }>; 6 | }) { 7 | const { locale } = await props.params; 8 | const t = await getTranslations({ 9 | locale, 10 | namespace: 'Dashboard', 11 | }); 12 | 13 | return { 14 | title: t('meta_title'), 15 | }; 16 | } 17 | 18 | export default function Dashboard() { 19 | return ( 20 |
    21 | 22 |
    23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/app/[locale]/(auth)/dashboard/user-profile/[[...user-profile]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { UserProfile } from '@clerk/nextjs'; 2 | import { getTranslations, setRequestLocale } from 'next-intl/server'; 3 | import { getI18nPath } from '@/utils/Helpers'; 4 | 5 | type IUserProfilePageProps = { 6 | params: Promise<{ locale: string }>; 7 | }; 8 | 9 | export async function generateMetadata(props: IUserProfilePageProps) { 10 | const { locale } = await props.params; 11 | const t = await getTranslations({ 12 | locale, 13 | namespace: 'UserProfile', 14 | }); 15 | 16 | return { 17 | title: t('meta_title'), 18 | }; 19 | } 20 | 21 | export default async function UserProfilePage(props: IUserProfilePageProps) { 22 | const { locale } = await props.params; 23 | setRequestLocale(locale); 24 | 25 | return ( 26 |
    27 | 30 |
    31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/app/[locale]/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ClerkProvider } from '@clerk/nextjs'; 2 | import { setRequestLocale } from 'next-intl/server'; 3 | import { routing } from '@/libs/i18nRouting'; 4 | import { ClerkLocalizations } from '@/utils/AppConfig'; 5 | 6 | export default async function AuthLayout(props: { 7 | children: React.ReactNode; 8 | params: Promise<{ locale: string }>; 9 | }) { 10 | const { locale } = await props.params; 11 | setRequestLocale(locale); 12 | 13 | const clerkLocale = ClerkLocalizations.supportedLocales[locale] ?? ClerkLocalizations.defaultLocale; 14 | let signInUrl = '/sign-in'; 15 | let signUpUrl = '/sign-up'; 16 | let dashboardUrl = '/dashboard'; 17 | let afterSignOutUrl = '/'; 18 | 19 | if (locale !== routing.defaultLocale) { 20 | signInUrl = `/${locale}${signInUrl}`; 21 | signUpUrl = `/${locale}${signUpUrl}`; 22 | dashboardUrl = `/${locale}${dashboardUrl}`; 23 | afterSignOutUrl = `/${locale}${afterSignOutUrl}`; 24 | } 25 | 26 | return ( 27 | 35 | {props.children} 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/app/[locale]/(marketing)/about/page.tsx: -------------------------------------------------------------------------------- 1 | import { getTranslations, setRequestLocale } from 'next-intl/server'; 2 | import Image from 'next/image'; 3 | 4 | type IAboutProps = { 5 | params: Promise<{ locale: string }>; 6 | }; 7 | 8 | export async function generateMetadata(props: IAboutProps) { 9 | const { locale } = await props.params; 10 | const t = await getTranslations({ 11 | locale, 12 | namespace: 'About', 13 | }); 14 | 15 | return { 16 | title: t('meta_title'), 17 | description: t('meta_description'), 18 | }; 19 | } 20 | 21 | export default async function About(props: IAboutProps) { 22 | const { locale } = await props.params; 23 | setRequestLocale(locale); 24 | const t = await getTranslations({ 25 | locale, 26 | namespace: 'About', 27 | }); 28 | 29 | return ( 30 | <> 31 |

    {t('about_paragraph')}

    32 | 33 |
    34 | {`${t('translation_powered_by')} `} 35 | 39 | Crowdin 40 | 41 |
    42 | 43 | 44 | Crowdin Translation Management System 51 | 52 | 53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /src/app/[locale]/(marketing)/api/counter/route.ts: -------------------------------------------------------------------------------- 1 | import { sql } from 'drizzle-orm'; 2 | import { headers } from 'next/headers'; 3 | import { NextResponse } from 'next/server'; 4 | import { db } from '@/libs/DB'; 5 | import { logger } from '@/libs/Logger'; 6 | import { counterSchema } from '@/models/Schema'; 7 | import { CounterValidation } from '@/validations/CounterValidation'; 8 | 9 | export const PUT = async (request: Request) => { 10 | const json = await request.json(); 11 | const parse = CounterValidation.safeParse(json); 12 | 13 | if (!parse.success) { 14 | return NextResponse.json(parse.error.format(), { status: 422 }); 15 | } 16 | 17 | // `x-e2e-random-id` is used for end-to-end testing to make isolated requests 18 | // The default value is 0 when there is no `x-e2e-random-id` header 19 | const id = Number((await headers()).get('x-e2e-random-id')) ?? 0; 20 | 21 | const count = await db 22 | .insert(counterSchema) 23 | .values({ id, count: parse.data.increment }) 24 | .onConflictDoUpdate({ 25 | target: counterSchema.id, 26 | set: { count: sql`${counterSchema.count} + ${parse.data.increment}` }, 27 | }) 28 | .returning(); 29 | 30 | logger.info('Counter has been incremented'); 31 | 32 | return NextResponse.json({ 33 | count: count[0]?.count, 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /src/app/[locale]/(marketing)/counter/page.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslations } from 'next-intl'; 2 | import { getTranslations } from 'next-intl/server'; 3 | import Image from 'next/image'; 4 | import { Suspense } from 'react'; 5 | import { CounterForm } from '@/components/CounterForm'; 6 | import { CurrentCount } from '@/components/CurrentCount'; 7 | 8 | export async function generateMetadata(props: { 9 | params: Promise<{ locale: string }>; 10 | }) { 11 | const { locale } = await props.params; 12 | const t = await getTranslations({ 13 | locale, 14 | namespace: 'Counter', 15 | }); 16 | 17 | return { 18 | title: t('meta_title'), 19 | description: t('meta_description'), 20 | }; 21 | } 22 | 23 | export default function Counter() { 24 | const t = useTranslations('Counter'); 25 | 26 | return ( 27 | <> 28 | 29 | 30 |
    31 | {t('loading_counter')}

    }> 32 | 33 |
    34 |
    35 | 36 |
    37 | {`${t('security_powered_by')} `} 38 | 42 | Arcjet 43 | 44 |
    45 | 46 | 49 | Arcjet 56 | 57 | 58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /src/app/[locale]/(marketing)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { getTranslations, setRequestLocale } from 'next-intl/server'; 2 | import Link from 'next/link'; 3 | import { DemoBanner } from '@/components/DemoBanner'; 4 | import { LocaleSwitcher } from '@/components/LocaleSwitcher'; 5 | import { BaseTemplate } from '@/templates/BaseTemplate'; 6 | 7 | export default async function Layout(props: { 8 | children: React.ReactNode; 9 | params: Promise<{ locale: string }>; 10 | }) { 11 | const { locale } = await props.params; 12 | setRequestLocale(locale); 13 | const t = await getTranslations({ 14 | locale, 15 | namespace: 'RootLayout', 16 | }); 17 | 18 | return ( 19 | <> 20 | 21 | 24 |
  • 25 | 29 | {t('home_link')} 30 | 31 |
  • 32 |
  • 33 | 37 | {t('about_link')} 38 | 39 |
  • 40 |
  • 41 | 45 | {t('counter_link')} 46 | 47 |
  • 48 |
  • 49 | 53 | {t('portfolio_link')} 54 | 55 |
  • 56 |
  • 57 | 61 | GitHub 62 | 63 |
  • 64 | 65 | )} 66 | rightNav={( 67 | <> 68 |
  • 69 | 73 | {t('sign_in_link')} 74 | 75 |
  • 76 | 77 |
  • 78 | 82 | {t('sign_up_link')} 83 | 84 |
  • 85 | 86 |
  • 87 | 88 |
  • 89 | 90 | )} 91 | > 92 |
    {props.children}
    93 |
    94 | 95 | ); 96 | } 97 | -------------------------------------------------------------------------------- /src/app/[locale]/(marketing)/page.tsx: -------------------------------------------------------------------------------- 1 | import { getTranslations, setRequestLocale } from 'next-intl/server'; 2 | import { Sponsors } from '@/components/Sponsors'; 3 | 4 | type IIndexProps = { 5 | params: Promise<{ locale: string }>; 6 | }; 7 | 8 | export async function generateMetadata(props: IIndexProps) { 9 | const { locale } = await props.params; 10 | const t = await getTranslations({ 11 | locale, 12 | namespace: 'Index', 13 | }); 14 | 15 | return { 16 | title: t('meta_title'), 17 | description: t('meta_description'), 18 | }; 19 | } 20 | 21 | export default async function Index(props: IIndexProps) { 22 | const { locale } = await props.params; 23 | setRequestLocale(locale); 24 | const t = await getTranslations({ 25 | locale, 26 | namespace: 'Index', 27 | }); 28 | 29 | return ( 30 | <> 31 |

    32 | {`Follow `} 33 | 39 | @Ixartz on Twitter 40 | 41 | {` for updates and more information about the boilerplate.`} 42 |

    43 |

    44 | Boilerplate Code for Your Next.js Project with Tailwind CSS 45 |

    46 |

    47 | Next.js Boilerplate is a developer-friendly starter code for Next.js projects, built with Tailwind CSS and TypeScript. 48 | {' '} 49 | 50 | ⚡️ 51 | 52 | {' '} 53 | Designed with developer experience in mind, it includes: 54 |

    55 |
      56 |
    • 🚀 Next.js with App Router support
    • 57 |
    • 🔥 TypeScript for type checking
    • 58 |
    • 💎 Tailwind CSS integration
    • 59 |
    • 60 | 🔒 Authentication with 61 | {' '} 62 | 66 | Clerk 67 | 68 | {' '} 69 | (includes passwordless, social, and multi-factor auth) 70 |
    • 71 |
    • 📦 ORM with DrizzleORM (PostgreSQL, SQLite, MySQL support)
    • 72 |
    • 73 | 🌐 Multi-language support (i18n) with next-intl and 74 | {' '} 75 | 79 | Crowdin 80 | 81 |
    • 82 |
    • 🔴 Form handling (React Hook Form) and validation (Zod)
    • 83 |
    • 📏 Linting and formatting (ESLint, Prettier)
    • 84 |
    • 🦊 Git hooks and commit linting (Husky, Commitlint)
    • 85 |
    • 🦺 Testing suite (Vitest, React Testing Library, Playwright)
    • 86 |
    • 🎉 Storybook for UI development
    • 87 |
    • 88 | 🐰 AI-powered code reviews with 89 | {' '} 90 | 94 | CodeRabbit 95 | 96 |
    • 97 |
    • 98 | 🚨 Error monitoring ( 99 | 103 | Sentry 104 | 105 | ) and logging (Pino.js) 106 |
    • 107 |
    • 🖥️ Monitoring as Code (Checkly)
    • 108 |
    • 109 | 🔐 Security and bot protection ( 110 | 114 | Arcjet 115 | 116 | ) 117 |
    • 118 |
    • 🤖 SEO optimization (metadata, JSON-LD, Open Graph tags)
    • 119 |
    • ⚙️ Development tools (VSCode config, bundler analyzer, changelog generation)
    • 120 |
    121 |

    122 | Our sponsors' exceptional support has made this project possible. 123 | Their services integrate seamlessly with the boilerplate, and we 124 | recommend trying them out. 125 |

    126 |

    {t('sponsors_title')}

    127 | 128 | 129 | ); 130 | }; 131 | -------------------------------------------------------------------------------- /src/app/[locale]/(marketing)/portfolio/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import { getTranslations, setRequestLocale } from 'next-intl/server'; 2 | import Image from 'next/image'; 3 | import { routing } from '@/libs/i18nRouting'; 4 | 5 | type IPortfolioDetailProps = { 6 | params: Promise<{ slug: string; locale: string }>; 7 | }; 8 | 9 | export function generateStaticParams() { 10 | return routing.locales 11 | .map(locale => 12 | Array.from(Array.from({ length: 6 }).keys()).map(elt => ({ 13 | slug: `${elt}`, 14 | locale, 15 | })), 16 | ) 17 | .flat(1); 18 | } 19 | 20 | export async function generateMetadata(props: IPortfolioDetailProps) { 21 | const { locale, slug } = await props.params; 22 | const t = await getTranslations({ 23 | locale, 24 | namespace: 'PortfolioSlug', 25 | }); 26 | 27 | return { 28 | title: t('meta_title', { slug }), 29 | description: t('meta_description', { slug }), 30 | }; 31 | } 32 | 33 | export default async function PortfolioDetail(props: IPortfolioDetailProps) { 34 | const { locale, slug } = await props.params; 35 | setRequestLocale(locale); 36 | const t = await getTranslations({ 37 | locale, 38 | namespace: 'PortfolioSlug', 39 | }); 40 | 41 | return ( 42 | <> 43 |

    {t('header', { slug })}

    44 |

    {t('content')}

    45 | 46 |
    47 | {`${t('code_review_powered_by')} `} 48 | 52 | CodeRabbit 53 | 54 |
    55 | 56 | 59 | CodeRabbit 66 | 67 | 68 | ); 69 | }; 70 | 71 | export const dynamicParams = false; 72 | -------------------------------------------------------------------------------- /src/app/[locale]/(marketing)/portfolio/page.tsx: -------------------------------------------------------------------------------- 1 | import { getTranslations, setRequestLocale } from 'next-intl/server'; 2 | import Image from 'next/image'; 3 | import Link from 'next/link'; 4 | 5 | type IPortfolioProps = { 6 | params: Promise<{ locale: string }>; 7 | }; 8 | 9 | export async function generateMetadata(props: IPortfolioProps) { 10 | const { locale } = await props.params; 11 | const t = await getTranslations({ 12 | locale, 13 | namespace: 'Portfolio', 14 | }); 15 | 16 | return { 17 | title: t('meta_title'), 18 | description: t('meta_description'), 19 | }; 20 | } 21 | 22 | export default async function Portfolio(props: IPortfolioProps) { 23 | const { locale } = await props.params; 24 | setRequestLocale(locale); 25 | const t = await getTranslations({ 26 | locale, 27 | namespace: 'Portfolio', 28 | }); 29 | 30 | return ( 31 | <> 32 |

    {t('presentation')}

    33 | 34 |
    35 | {Array.from(Array.from({ length: 6 }).keys()).map(elt => ( 36 | 41 | {t('portfolio_name', { name: elt })} 42 | 43 | ))} 44 |
    45 | 46 |
    47 | {`${t('error_reporting_powered_by')} `} 48 | 52 | Sentry 53 | 54 | {` - ${t('coverage_powered_by')} `} 55 | 59 | Codecov 60 | 61 |
    62 | 63 | 66 | Sentry 73 | 74 | 75 | ); 76 | }; 77 | -------------------------------------------------------------------------------- /src/app/[locale]/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import { hasLocale, NextIntlClientProvider } from 'next-intl'; 3 | import { setRequestLocale } from 'next-intl/server'; 4 | import { notFound } from 'next/navigation'; 5 | import { PostHogProvider } from '@/components/analytics/PostHogProvider'; 6 | import { DemoBadge } from '@/components/DemoBadge'; 7 | import { routing } from '@/libs/i18nRouting'; 8 | import '@/styles/global.css'; 9 | 10 | export const metadata: Metadata = { 11 | icons: [ 12 | { 13 | rel: 'apple-touch-icon', 14 | url: '/apple-touch-icon.png', 15 | }, 16 | { 17 | rel: 'icon', 18 | type: 'image/png', 19 | sizes: '32x32', 20 | url: '/favicon-32x32.png', 21 | }, 22 | { 23 | rel: 'icon', 24 | type: 'image/png', 25 | sizes: '16x16', 26 | url: '/favicon-16x16.png', 27 | }, 28 | { 29 | rel: 'icon', 30 | url: '/favicon.ico', 31 | }, 32 | ], 33 | }; 34 | 35 | export function generateStaticParams() { 36 | return routing.locales.map(locale => ({ locale })); 37 | } 38 | 39 | export default async function RootLayout(props: { 40 | children: React.ReactNode; 41 | params: Promise<{ locale: string }>; 42 | }) { 43 | const { locale } = await props.params; 44 | 45 | if (!hasLocale(routing.locales, locale)) { 46 | notFound(); 47 | } 48 | 49 | setRequestLocale(locale); 50 | 51 | return ( 52 | 53 | 54 | 55 | 56 | {props.children} 57 | 58 | 59 | 60 | 61 | 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /src/app/global-error.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as Sentry from '@sentry/nextjs'; 4 | import NextError from 'next/error'; 5 | import { useEffect } from 'react'; 6 | import { routing } from '@/libs/i18nRouting'; 7 | 8 | export default function GlobalError(props: { 9 | error: Error & { digest?: string }; 10 | }) { 11 | useEffect(() => { 12 | Sentry.captureException(props.error); 13 | }, [props.error]); 14 | 15 | return ( 16 | 17 | 18 | {/* `NextError` is the default Next.js error page component. Its type 19 | definition requires a `statusCode` prop. However, since the App Router 20 | does not expose status codes for errors, we simply pass 0 to render a 21 | generic error message. */} 22 | 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/app/robots.ts: -------------------------------------------------------------------------------- 1 | import type { MetadataRoute } from 'next'; 2 | import { getBaseUrl } from '@/utils/Helpers'; 3 | 4 | export default function robots(): MetadataRoute.Robots { 5 | return { 6 | rules: { 7 | userAgent: '*', 8 | allow: '/', 9 | disallow: '/dashboard/', 10 | }, 11 | sitemap: `${getBaseUrl()}/sitemap.xml`, 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/app/sitemap.ts: -------------------------------------------------------------------------------- 1 | import type { MetadataRoute } from 'next'; 2 | import { getBaseUrl } from '@/utils/Helpers'; 3 | 4 | export default function sitemap(): MetadataRoute.Sitemap { 5 | return [ 6 | { 7 | url: `${getBaseUrl()}/`, 8 | lastModified: new Date(), 9 | changeFrequency: 'daily', 10 | priority: 0.7, 11 | }, 12 | // Add more URLs here 13 | ]; 14 | } 15 | -------------------------------------------------------------------------------- /src/components/CounterForm.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { zodResolver } from '@hookform/resolvers/zod'; 4 | import { useTranslations } from 'next-intl'; 5 | import { useRouter } from 'next/navigation'; 6 | import { useForm } from 'react-hook-form'; 7 | import { CounterValidation } from '@/validations/CounterValidation'; 8 | 9 | export const CounterForm = () => { 10 | const t = useTranslations('CounterForm'); 11 | const form = useForm({ 12 | resolver: zodResolver(CounterValidation), 13 | defaultValues: { 14 | increment: 0, 15 | }, 16 | }); 17 | const router = useRouter(); 18 | 19 | const handleIncrement = form.handleSubmit(async (data) => { 20 | await fetch(`/api/counter`, { 21 | method: 'PUT', 22 | headers: { 23 | 'Content-Type': 'application/json', 24 | }, 25 | body: JSON.stringify(data), 26 | }); 27 | 28 | form.reset(); 29 | router.refresh(); 30 | }); 31 | 32 | return ( 33 |
    34 |

    {t('presentation')}

    35 |
    36 | 45 | 46 | {form.formState.errors.increment?.message && ( 47 |
    {form.formState.errors.increment?.message}
    48 | )} 49 |
    50 | 51 |
    52 | 59 |
    60 |
    61 | ); 62 | }; 63 | -------------------------------------------------------------------------------- /src/components/CurrentCount.tsx: -------------------------------------------------------------------------------- 1 | import { eq } from 'drizzle-orm'; 2 | import { getTranslations } from 'next-intl/server'; 3 | import { headers } from 'next/headers'; 4 | import { db } from '@/libs/DB'; 5 | import { logger } from '@/libs/Logger'; 6 | import { counterSchema } from '@/models/Schema'; 7 | 8 | export const CurrentCount = async () => { 9 | const t = await getTranslations('CurrentCount'); 10 | 11 | // `x-e2e-random-id` is used for end-to-end testing to make isolated requests 12 | // The default value is 0 when there is no `x-e2e-random-id` header 13 | const id = Number((await headers()).get('x-e2e-random-id')) ?? 0; 14 | const result = await db.query.counterSchema.findMany({ 15 | where: eq(counterSchema.id, id), 16 | }); 17 | const count = result[0]?.count ?? 0; 18 | 19 | logger.info('Counter fetched successfully'); 20 | 21 | return ( 22 |
    23 | {t('count', { count })} 24 |
    25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/components/DemoBadge.tsx: -------------------------------------------------------------------------------- 1 | export const DemoBadge = () => ( 2 | 12 | ); 13 | -------------------------------------------------------------------------------- /src/components/DemoBanner.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | export const DemoBanner = () => ( 4 |
    5 | Live Demo of Next.js Boilerplate - 6 | {' '} 7 | Explore the Authentication 8 |
    9 | ); 10 | -------------------------------------------------------------------------------- /src/components/Hello.tsx: -------------------------------------------------------------------------------- 1 | import { currentUser } from '@clerk/nextjs/server'; 2 | import { getTranslations } from 'next-intl/server'; 3 | import { Sponsors } from './Sponsors'; 4 | 5 | export const Hello = async () => { 6 | const t = await getTranslations('Dashboard'); 7 | const user = await currentUser(); 8 | 9 | return ( 10 | <> 11 |

    12 | {`👋 `} 13 | {t('hello_message', { email: user?.primaryEmailAddress?.emailAddress ?? '' })} 14 |

    15 |

    16 | {t.rich('alternative_message', { 17 | url: () => ( 18 | 22 | Next.js Boilerplate SaaS 23 | 24 | ), 25 | })} 26 |

    27 | 28 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/components/LocaleSwitcher.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import type { ChangeEventHandler } from 'react'; 4 | import { useLocale } from 'next-intl'; 5 | import { useRouter } from 'next/navigation'; 6 | import { usePathname } from '@/libs/i18nNavigation'; 7 | import { routing } from '@/libs/i18nRouting'; 8 | 9 | export const LocaleSwitcher = () => { 10 | const router = useRouter(); 11 | const pathname = usePathname(); 12 | const locale = useLocale(); 13 | 14 | const handleChange: ChangeEventHandler = (event) => { 15 | router.push(`/${event.target.value}${pathname}`); 16 | router.refresh(); // Ensure the page takes the new locale into account related to the issue #395 17 | }; 18 | 19 | return ( 20 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/components/Sponsors.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-dom/no-unsafe-target-blank */ 2 | import Image from 'next/image'; 3 | 4 | export const Sponsors = () => ( 5 | 6 | 7 | 8 | 22 | 32 | 46 | 47 | 48 | 58 | 68 | 78 | 79 | 80 | 94 | 108 | 122 | 123 | 124 | 134 | 135 | 136 |
    9 | 14 | Clerk – Authentication & User Management for Next.js 20 | 21 | 23 | 24 | CodeRabbit 30 | 31 | 33 | 38 | Sentry 44 | 45 |
    49 | 50 | Arcjet 56 | 57 | 59 | 60 | Sevalla 66 | 67 | 69 | 70 | Crowdin 76 | 77 |
    81 | 86 | PostHog 92 | 93 | 95 | 100 | Better Stack 106 | 107 | 109 | 114 | Checkly 120 | 121 |
    125 | 126 | Next.js SaaS Boilerplate 132 | 133 |
    137 | ); 138 | -------------------------------------------------------------------------------- /src/components/analytics/PostHogPageView.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { usePathname, useSearchParams } from 'next/navigation'; 4 | import { usePostHog } from 'posthog-js/react'; 5 | import { Suspense, useEffect } from 'react'; 6 | 7 | const PostHogPageView = () => { 8 | const pathname = usePathname(); 9 | const searchParams = useSearchParams(); 10 | const posthog = usePostHog(); 11 | 12 | // Track pageviews 13 | useEffect(() => { 14 | if (pathname && posthog) { 15 | let url = window.origin + pathname; 16 | if (searchParams.toString()) { 17 | url = `${url}?${searchParams.toString()}`; 18 | } 19 | 20 | posthog.capture('$pageview', { $current_url: url }); 21 | } 22 | }, [pathname, searchParams, posthog]); 23 | 24 | return null; 25 | }; 26 | 27 | // Wrap this in Suspense to avoid the `useSearchParams` usage above 28 | // from de-opting the whole app into client-side rendering 29 | // See: https://nextjs.org/docs/messages/deopted-into-client-rendering 30 | export const SuspendedPostHogPageView = () => { 31 | return ( 32 | 33 | 34 | 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /src/components/analytics/PostHogProvider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import posthog from 'posthog-js'; 4 | import { PostHogProvider as PHProvider } from 'posthog-js/react'; 5 | import { useEffect } from 'react'; 6 | import { Env } from '@/libs/Env'; 7 | import { SuspendedPostHogPageView } from './PostHogPageView'; 8 | 9 | export const PostHogProvider = (props: { children: React.ReactNode }) => { 10 | useEffect(() => { 11 | if (Env.NEXT_PUBLIC_POSTHOG_KEY) { 12 | posthog.init(Env.NEXT_PUBLIC_POSTHOG_KEY, { 13 | api_host: Env.NEXT_PUBLIC_POSTHOG_HOST, 14 | capture_pageview: false, // Disable automatic pageview capture, as we capture manually 15 | capture_pageleave: true, // Enable pageleave capture 16 | }); 17 | } 18 | }, []); 19 | 20 | if (!Env.NEXT_PUBLIC_POSTHOG_KEY) { 21 | return props.children; 22 | } 23 | 24 | return ( 25 | 26 | 27 | {props.children} 28 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/instrumentation-client.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry on the client. 2 | // The added config here will be used whenever a users loads a page in their browser. 3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 4 | import * as Sentry from '@sentry/nextjs'; 5 | import * as Spotlight from '@spotlightjs/spotlight'; 6 | 7 | if (!process.env.NEXT_PUBLIC_SENTRY_DISABLED) { 8 | Sentry.init({ 9 | dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, 10 | 11 | // Add optional integrations for additional features 12 | integrations: [ 13 | Sentry.replayIntegration(), 14 | ], 15 | 16 | // Adds request headers and IP for users, for more info visit 17 | sendDefaultPii: true, 18 | 19 | // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. 20 | tracesSampleRate: 1, 21 | 22 | // Define how likely Replay events are sampled. 23 | // This sets the sample rate to be 10%. You may want this to be 100% while 24 | // in development and sample at a lower rate in production 25 | replaysSessionSampleRate: 0.1, 26 | 27 | // Define how likely Replay events are sampled when an error occurs. 28 | replaysOnErrorSampleRate: 1.0, 29 | 30 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 31 | debug: false, 32 | }); 33 | 34 | if (process.env.NODE_ENV === 'development') { 35 | Spotlight.init(); 36 | } 37 | } 38 | 39 | export const onRouterTransitionStart = Sentry.captureRouterTransitionStart; 40 | -------------------------------------------------------------------------------- /src/instrumentation.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/nextjs'; 2 | 3 | const sentryOptions: Sentry.NodeOptions | Sentry.EdgeOptions = { 4 | // Sentry DSN 5 | dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, 6 | 7 | // Enable Spotlight in development 8 | spotlight: process.env.NODE_ENV === 'development', 9 | 10 | // Adds request headers and IP for users, for more info visit 11 | sendDefaultPii: true, 12 | 13 | // Adjust this value in production, or use tracesSampler for greater control 14 | tracesSampleRate: 1, 15 | 16 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 17 | debug: false, 18 | }; 19 | 20 | export async function register() { 21 | if (!process.env.NEXT_PUBLIC_SENTRY_DISABLED) { 22 | if (process.env.NEXT_RUNTIME === 'nodejs') { 23 | // Node.js Sentry configuration 24 | Sentry.init(sentryOptions); 25 | } 26 | 27 | if (process.env.NEXT_RUNTIME === 'edge') { 28 | // Edge Sentry configuration 29 | Sentry.init(sentryOptions); 30 | } 31 | } 32 | } 33 | 34 | export const onRequestError = Sentry.captureRequestError; 35 | -------------------------------------------------------------------------------- /src/libs/Arcjet.ts: -------------------------------------------------------------------------------- 1 | import arcjet, { shield } from '@arcjet/next'; 2 | 3 | // Create a base Arcjet instance which can be imported and extended in each route. 4 | export default arcjet({ 5 | // Get your site key from https://launch.arcjet.com/Q6eLbRE 6 | // Use `process.env` instead of Env to reduce bundle size in middleware 7 | key: process.env.ARCJET_KEY ?? '', 8 | // Identify the user by their IP address 9 | characteristics: ['ip.src'], 10 | rules: [ 11 | // Protect against common attacks with Arcjet Shield 12 | shield({ 13 | mode: 'LIVE', // will block requests. Use "DRY_RUN" to log only 14 | }), 15 | // Other rules are added in different routes 16 | ], 17 | }); 18 | -------------------------------------------------------------------------------- /src/libs/DB.ts: -------------------------------------------------------------------------------- 1 | import type { PgliteDatabase } from 'drizzle-orm/pglite'; 2 | import path from 'node:path'; 3 | import { PGlite } from '@electric-sql/pglite'; 4 | import { drizzle as drizzlePg } from 'drizzle-orm/node-postgres'; 5 | import { migrate as migratePg } from 'drizzle-orm/node-postgres/migrator'; 6 | import { drizzle as drizzlePglite } from 'drizzle-orm/pglite'; 7 | import { migrate as migratePglite } from 'drizzle-orm/pglite/migrator'; 8 | import { PHASE_PRODUCTION_BUILD } from 'next/dist/shared/lib/constants'; 9 | import { Client } from 'pg'; 10 | import * as schema from '@/models/Schema'; 11 | import { Env } from './Env'; 12 | 13 | let client; 14 | let drizzle; 15 | 16 | if (process.env.NEXT_PHASE !== PHASE_PRODUCTION_BUILD && Env.DATABASE_URL) { 17 | client = new Client({ 18 | connectionString: Env.DATABASE_URL, 19 | }); 20 | await client.connect(); 21 | 22 | drizzle = drizzlePg(client, { schema }); 23 | await migratePg(drizzle, { 24 | migrationsFolder: path.join(process.cwd(), 'migrations'), 25 | }); 26 | } else { 27 | // Stores the db connection in the global scope to prevent multiple instances due to hot reloading with Next.js 28 | const global = globalThis as unknown as { client: PGlite; drizzle: PgliteDatabase }; 29 | 30 | if (!global.client) { 31 | global.client = new PGlite(); 32 | await global.client.waitReady; 33 | 34 | global.drizzle = drizzlePglite(global.client, { schema }); 35 | } 36 | 37 | drizzle = global.drizzle; 38 | await migratePglite(global.drizzle, { 39 | migrationsFolder: path.join(process.cwd(), 'migrations'), 40 | }); 41 | } 42 | 43 | export const db = drizzle; 44 | -------------------------------------------------------------------------------- /src/libs/Env.ts: -------------------------------------------------------------------------------- 1 | import { createEnv } from '@t3-oss/env-nextjs'; 2 | import { z } from 'zod'; 3 | 4 | export const Env = createEnv({ 5 | server: { 6 | ARCJET_KEY: z.string().startsWith('ajkey_').optional(), 7 | CLERK_SECRET_KEY: z.string().min(1), 8 | DATABASE_URL: z.string().optional(), 9 | LOGTAIL_SOURCE_TOKEN: z.string().optional(), 10 | }, 11 | client: { 12 | NEXT_PUBLIC_APP_URL: z.string().optional(), 13 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string().min(1), 14 | NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(), 15 | NEXT_PUBLIC_POSTHOG_HOST: z.string().optional(), 16 | }, 17 | shared: { 18 | NODE_ENV: z.enum(['test', 'development', 'production']).optional(), 19 | }, 20 | // You need to destructure all the keys manually 21 | runtimeEnv: { 22 | ARCJET_KEY: process.env.ARCJET_KEY, 23 | CLERK_SECRET_KEY: process.env.CLERK_SECRET_KEY, 24 | DATABASE_URL: process.env.DATABASE_URL, 25 | LOGTAIL_SOURCE_TOKEN: process.env.LOGTAIL_SOURCE_TOKEN, 26 | NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL, 27 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: 28 | process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, 29 | NODE_ENV: process.env.NODE_ENV, 30 | NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY, 31 | NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST, 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /src/libs/Logger.ts: -------------------------------------------------------------------------------- 1 | import type { DestinationStream } from 'pino'; 2 | import logtail from '@logtail/pino'; 3 | import pino from 'pino'; 4 | import pretty from 'pino-pretty'; 5 | import { Env } from './Env'; 6 | 7 | let stream: DestinationStream; 8 | 9 | if (Env.LOGTAIL_SOURCE_TOKEN) { 10 | stream = pino.multistream([ 11 | await logtail({ 12 | sourceToken: Env.LOGTAIL_SOURCE_TOKEN, 13 | options: { 14 | sendLogsToBetterStack: true, 15 | }, 16 | }), 17 | { 18 | stream: pretty(), // Prints logs to the console 19 | }, 20 | ]); 21 | } else { 22 | stream = pretty({ 23 | colorize: true, 24 | }); 25 | } 26 | 27 | export const logger = pino({ base: undefined }, stream); 28 | -------------------------------------------------------------------------------- /src/libs/i18n.ts: -------------------------------------------------------------------------------- 1 | import { hasLocale } from 'next-intl'; 2 | import { getRequestConfig } from 'next-intl/server'; 3 | import { routing } from './i18nRouting'; 4 | 5 | // NextJS Boilerplate uses Crowdin as the localization software. 6 | // As a developer, you only need to take care of the English (or another default language) version. 7 | // Other languages are automatically generated and handled by Crowdin. 8 | 9 | // The localisation files are synced with Crowdin using GitHub Actions. 10 | // By default, there are 3 ways to sync the message files: 11 | // 1. Automatically sync on push to the `main` branch 12 | // 2. Run manually the workflow on GitHub Actions 13 | // 3. Every 24 hours at 5am, the workflow will run automatically 14 | 15 | export default getRequestConfig(async ({ requestLocale }) => { 16 | // Typically corresponds to the `[locale]` segment 17 | const requested = await requestLocale; 18 | const locale = hasLocale(routing.locales, requested) 19 | ? requested 20 | : routing.defaultLocale; 21 | 22 | return { 23 | locale, 24 | messages: (await import(`../locales/${locale}.json`)).default, 25 | }; 26 | }); 27 | -------------------------------------------------------------------------------- /src/libs/i18nNavigation.ts: -------------------------------------------------------------------------------- 1 | import { createNavigation } from 'next-intl/navigation'; 2 | import { routing } from './i18nRouting'; 3 | 4 | export const { usePathname } = createNavigation(routing); 5 | -------------------------------------------------------------------------------- /src/libs/i18nRouting.ts: -------------------------------------------------------------------------------- 1 | import { defineRouting } from 'next-intl/routing'; 2 | import { AppConfig } from '@/utils/AppConfig'; 3 | 4 | export const routing = defineRouting({ 5 | locales: AppConfig.locales, 6 | localePrefix: AppConfig.localePrefix, 7 | defaultLocale: AppConfig.defaultLocale, 8 | }); 9 | -------------------------------------------------------------------------------- /src/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "RootLayout": { 3 | "home_link": "Home", 4 | "about_link": "About", 5 | "counter_link": "Counter", 6 | "portfolio_link": "Portfolio", 7 | "sign_in_link": "Sign in", 8 | "sign_up_link": "Sign up" 9 | }, 10 | "BaseTemplate": { 11 | "description": "Starter code for your Nextjs Boilerplate with Tailwind CSS", 12 | "made_with": "Made with ." 13 | }, 14 | "Index": { 15 | "meta_title": "Next.js Boilerplate Presentation", 16 | "meta_description": "Next js Boilerplate is the perfect starter code for your project. Build your React application with the Next.js framework.", 17 | "sponsors_title": "Sponsors" 18 | }, 19 | "Counter": { 20 | "meta_title": "Counter", 21 | "meta_description": "An example of DB operation", 22 | "loading_counter": "Loading counter...", 23 | "security_powered_by": "Security, bot detection and rate limiting powered by" 24 | }, 25 | "CounterForm": { 26 | "presentation": "The counter is stored in the database and incremented by the value you provide.", 27 | "label_increment": "Increment by", 28 | "button_increment": "Increment" 29 | }, 30 | "CurrentCount": { 31 | "count": "Count: {count}" 32 | }, 33 | "About": { 34 | "meta_title": "About", 35 | "meta_description": "About page description", 36 | "about_paragraph": "Welcome to our About page! We are a team of passionate individuals dedicated to creating amazing software.", 37 | "translation_powered_by": "Translation powered by" 38 | }, 39 | "Portfolio": { 40 | "meta_title": "Portfolio", 41 | "meta_description": "Welcome to my portfolio page!", 42 | "presentation": "Welcome to my portfolio page! Here you will find a carefully curated collection of my work and accomplishments. Through this portfolio, I'm to showcase my expertise, creativity, and the value I can bring to your projects.", 43 | "portfolio_name": "Portfolio {name}", 44 | "error_reporting_powered_by": "Error reporting powered by", 45 | "coverage_powered_by": "Code coverage powered by" 46 | }, 47 | "PortfolioSlug": { 48 | "meta_title": "Portfolio {slug}", 49 | "meta_description": "Portfolio {slug} description", 50 | "header": "Portfolio {slug}", 51 | "content": "Created a set of promotional materials and branding elements for a corporate event. Crafted a visually unified theme, encompassing a logo, posters, banners, and digital assets. Integrated the client's brand identity while infusing it with a contemporary and innovative approach. Garnered favorable responses from event attendees, resulting in a successful event with heightened participant engagement and increased brand visibility.", 52 | "code_review_powered_by": "Code review powered by" 53 | }, 54 | "SignIn": { 55 | "meta_title": "Sign in", 56 | "meta_description": "Seamlessly sign in to your account with our user-friendly login process." 57 | }, 58 | "SignUp": { 59 | "meta_title": "Sign up", 60 | "meta_description": "Effortlessly create an account through our intuitive sign-up process." 61 | }, 62 | "Dashboard": { 63 | "meta_title": "Dashboard", 64 | "hello_message": "Hello {email}!", 65 | "alternative_message": "Want to build your SaaS faster using the same stack? Try ." 66 | }, 67 | "UserProfile": { 68 | "meta_title": "User Profile" 69 | }, 70 | "DashboardLayout": { 71 | "dashboard_link": "Dashboard", 72 | "user_profile_link": "Manage your account", 73 | "sign_out": "Sign out" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/locales/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "RootLayout": { 3 | "home_link": "Accueil", 4 | "about_link": "A propos", 5 | "counter_link": "Compteur", 6 | "portfolio_link": "Portfolio", 7 | "sign_in_link": "Se connecter", 8 | "sign_up_link": "S'inscrire" 9 | }, 10 | "BaseTemplate": { 11 | "description": "Code de démarrage pour Next.js avec Tailwind CSS", 12 | "made_with": "Fait avec ." 13 | }, 14 | "Index": { 15 | "meta_title": "Présentation de Next.js Boilerplate", 16 | "meta_description": "Next js Boilerplate est le code de démarrage parfait pour votre projet. Construisez votre application React avec le framework Next.js.", 17 | "sponsors_title": "Partenaires" 18 | }, 19 | "Counter": { 20 | "meta_title": "Compteur", 21 | "meta_description": "Un exemple d'opération DB", 22 | "loading_counter": "Chargement du compteur...", 23 | "security_powered_by": "Sécurité, détection de bot et rate limiting propulsés par" 24 | }, 25 | "CounterForm": { 26 | "presentation": "Le compteur est stocké dans la base de données et incrémenté par la valeur que vous fournissez.", 27 | "label_increment": "Incrémenter de", 28 | "button_increment": "Incrémenter" 29 | }, 30 | "CurrentCount": { 31 | "count": "Nombre : {count}" 32 | }, 33 | "About": { 34 | "meta_title": "A propos", 35 | "meta_description": "A propos description", 36 | "about_paragraph": "Bienvenue sur notre page À propos ! Nous sommes une équipe de passionnés et dévoués à la création de logiciels.", 37 | "translation_powered_by": "Traduction propulsée par" 38 | }, 39 | "Portfolio": { 40 | "meta_title": "Portfolio", 41 | "meta_description": "Bienvenue sur la page de mon portfolio !", 42 | "presentation": "Bienvenue sur ma page portfolio ! Vous trouverez ici une collection soigneusement organisée de mon travail et de mes réalisations. À travers ce portfolio, je mets en valeur mon expertise, ma créativité et la valeur que je peux apporter à vos projets.", 43 | "portfolio_name": "Portfolio {name}", 44 | "error_reporting_powered_by": "Rapport d'erreur propulsé par", 45 | "coverage_powered_by": "Couverture de code propulsée par" 46 | }, 47 | "PortfolioSlug": { 48 | "meta_title": "Portfolio {slug}", 49 | "meta_description": "Description du Portfolio {slug}", 50 | "header": "Portfolio {slug}", 51 | "content": "Créé un ensemble de matériel promotionnel et d'éléments de marquage pour un événement d'entreprise. Conçu un thème visuellement unifié, englobant un logo, des affiches, des bannières et des actifs numériques. Intégrer l'identité de marque du client tout en l'insufflant à une approche contemporaine et innovante. Des réponses favorables de la part des participants ont été obtenues, ce qui a donné lieu à un événement réussi avec un engagement accru des participants et une meilleure visibilité de la marque.", 52 | "code_review_powered_by": "Code review propulsé par" 53 | }, 54 | "SignIn": { 55 | "meta_title": "Se connecter", 56 | "meta_description": "Connectez-vous à votre compte avec facilité." 57 | }, 58 | "SignUp": { 59 | "meta_title": "S'inscrire", 60 | "meta_description": "Créez un compte facilement grâce à notre processus d'inscription intuitif." 61 | }, 62 | "Dashboard": { 63 | "meta_title": "Tableau de bord", 64 | "hello_message": "Bonjour {email}!", 65 | "alternative_message": "Vous voulez créer votre SaaS plus rapidement en utilisant la même stack ? Essayez ." 66 | }, 67 | "UserProfile": { 68 | "meta_title": "Profil de l'utilisateur" 69 | }, 70 | "DashboardLayout": { 71 | "dashboard_link": "Tableau de bord", 72 | "user_profile_link": "Gérer votre compte", 73 | "sign_out": "Se déconnecter" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import type { NextFetchEvent, NextRequest } from 'next/server'; 2 | import { detectBot } from '@arcjet/next'; 3 | import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'; 4 | import createMiddleware from 'next-intl/middleware'; 5 | import { NextResponse } from 'next/server'; 6 | import arcjet from '@/libs/Arcjet'; 7 | import { routing } from './libs/i18nRouting'; 8 | 9 | const handleI18nRouting = createMiddleware(routing); 10 | 11 | const isProtectedRoute = createRouteMatcher([ 12 | '/dashboard(.*)', 13 | '/:locale/dashboard(.*)', 14 | ]); 15 | 16 | const isAuthPage = createRouteMatcher([ 17 | '/sign-in(.*)', 18 | '/:locale/sign-in(.*)', 19 | '/sign-up(.*)', 20 | '/:locale/sign-up(.*)', 21 | ]); 22 | 23 | // Improve security with Arcjet 24 | const aj = arcjet.withRule( 25 | detectBot({ 26 | mode: 'LIVE', 27 | // Block all bots except the following 28 | allow: [ 29 | // See https://docs.arcjet.com/bot-protection/identifying-bots 30 | 'CATEGORY:SEARCH_ENGINE', // Allow search engines 31 | 'CATEGORY:PREVIEW', // Allow preview links to show OG images 32 | 'CATEGORY:MONITOR', // Allow uptime monitoring services 33 | ], 34 | }), 35 | ); 36 | 37 | export default async function middleware( 38 | request: NextRequest, 39 | event: NextFetchEvent, 40 | ) { 41 | // Verify the request with Arcjet 42 | // Use `process.env` instead of Env to reduce bundle size in middleware 43 | if (process.env.ARCJET_KEY) { 44 | const decision = await aj.protect(request); 45 | 46 | if (decision.isDenied()) { 47 | return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); 48 | } 49 | } 50 | 51 | // Clerk keyless mode doesn't work with i18n, this is why we need to run the middleware conditionally 52 | if ( 53 | isAuthPage(request) || isProtectedRoute(request) 54 | ) { 55 | return clerkMiddleware(async (auth, req) => { 56 | if (isProtectedRoute(req)) { 57 | const locale = req.nextUrl.pathname.match(/(\/.*)\/dashboard/)?.at(1) ?? ''; 58 | 59 | const signInUrl = new URL(`${locale}/sign-in`, req.url); 60 | 61 | await auth.protect({ 62 | unauthenticatedUrl: signInUrl.toString(), 63 | }); 64 | } 65 | 66 | return handleI18nRouting(request); 67 | })(request, event); 68 | } 69 | 70 | return handleI18nRouting(request); 71 | } 72 | 73 | export const config = { 74 | // Match all pathnames except for 75 | // - … if they start with `/api`, `/trpc`, `/_next` or `/_vercel` 76 | // - … the ones containing a dot (e.g. `favicon.ico`) 77 | matcher: '/((?!_next|_vercel|monitoring|.*\\..*).*)', 78 | }; 79 | -------------------------------------------------------------------------------- /src/models/Schema.ts: -------------------------------------------------------------------------------- 1 | import { integer, pgTable, serial, timestamp } from 'drizzle-orm/pg-core'; 2 | 3 | // This file defines the structure of your database tables using the Drizzle ORM. 4 | 5 | // To modify the database schema: 6 | // 1. Update this file with your desired changes. 7 | // 2. Generate a new migration by running: `npm run db:generate` 8 | 9 | // The generated migration file will reflect your schema changes. 10 | // The migration is automatically applied during the next database interaction, 11 | // so there's no need to run it manually or restart the Next.js server. 12 | 13 | export const counterSchema = pgTable('counter', { 14 | id: serial('id').primaryKey(), 15 | count: integer('count').default(0), 16 | updatedAt: timestamp('updated_at', { mode: 'date' }) 17 | .defaultNow() 18 | .$onUpdate(() => new Date()) 19 | .notNull(), 20 | createdAt: timestamp('created_at', { mode: 'date' }).defaultNow().notNull(), 21 | }); 22 | -------------------------------------------------------------------------------- /src/styles/global.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | -------------------------------------------------------------------------------- /src/templates/BaseTemplate.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { userEvent, within } from '@storybook/test'; 3 | import { NextIntlClientProvider } from 'next-intl'; 4 | import messages from '@/locales/en.json'; 5 | import { BaseTemplate } from './BaseTemplate'; 6 | 7 | const meta = { 8 | title: 'Example/BaseTemplate', 9 | component: BaseTemplate, 10 | parameters: { 11 | layout: 'fullscreen', 12 | }, 13 | tags: ['autodocs'], 14 | decorators: [ 15 | Story => ( 16 | 17 | 18 | 19 | ), 20 | ], 21 | } satisfies Meta; 22 | 23 | export default meta; 24 | type Story = StoryObj; 25 | 26 | export const BaseWithReactComponent = { 27 | args: { 28 | children:
    Children node
    , 29 | leftNav: ( 30 | <> 31 |
  • Link 1
  • 32 |
  • Link 2
  • 33 | 34 | ), 35 | }, 36 | } satisfies Story; 37 | 38 | export const BaseWithString = { 39 | args: { 40 | children: 'String', 41 | leftNav: ( 42 | <> 43 |
  • Link 1
  • 44 |
  • Link 2
  • 45 | 46 | ), 47 | }, 48 | } satisfies Story; 49 | 50 | // More on interaction testing: https://storybook.js.org/docs/7.0/react/writing-tests/interaction-testing 51 | export const BaseWithHomeLink: Story = { 52 | args: { 53 | children:
    Children node
    , 54 | leftNav: ( 55 | <> 56 |
  • Link 1
  • 57 |
  • Link 2
  • 58 | 59 | ), 60 | }, 61 | play: async ({ canvasElement }) => { 62 | const canvas = within(canvasElement); 63 | const link = canvas.getByText('Link 1'); 64 | 65 | await userEvent.click(link); 66 | }, 67 | } satisfies Story; 68 | -------------------------------------------------------------------------------- /src/templates/BaseTemplate.test.tsx: -------------------------------------------------------------------------------- 1 | import { page } from '@vitest/browser/context'; 2 | import { NextIntlClientProvider } from 'next-intl'; 3 | import { describe, expect, it } from 'vitest'; 4 | import { render } from 'vitest-browser-react'; 5 | import messages from '@/locales/en.json'; 6 | import { BaseTemplate } from './BaseTemplate'; 7 | 8 | describe('Base template', () => { 9 | describe('Render method', () => { 10 | it('should have 3 menu items', () => { 11 | render( 12 | 13 | 16 |
  • link 1
  • 17 |
  • link 2
  • 18 |
  • link 3
  • 19 | 20 | )} 21 | > 22 | {null} 23 |
    24 |
    , 25 | ); 26 | 27 | const menuItemList = page.getByRole('listitem'); 28 | 29 | expect(menuItemList.elements()).toHaveLength(3); 30 | }); 31 | 32 | it('should have a link to support creativedesignsguru.com', () => { 33 | render( 34 | 35 | 1}>{null} 36 | , 37 | ); 38 | 39 | const copyrightSection = page.getByText(/© Copyright/); 40 | const copyrightLink = copyrightSection.getByRole('link'); 41 | 42 | /* 43 | * PLEASE READ THIS SECTION 44 | * We'll really appreciate if you could have a link to our website 45 | * The link doesn't need to appear on every pages, one link on one page is enough. 46 | * Thank you for your support it'll mean a lot for us. 47 | */ 48 | expect(copyrightLink).toHaveAttribute( 49 | 'href', 50 | 'https://creativedesignsguru.com', 51 | ); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/templates/BaseTemplate.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslations } from 'next-intl'; 2 | import { AppConfig } from '@/utils/AppConfig'; 3 | 4 | export const BaseTemplate = (props: { 5 | leftNav: React.ReactNode; 6 | rightNav?: React.ReactNode; 7 | children: React.ReactNode; 8 | }) => { 9 | const t = useTranslations('BaseTemplate'); 10 | 11 | return ( 12 |
    13 |
    14 |
    15 |
    16 |

    17 | {AppConfig.name} 18 |

    19 |

    {t('description')}

    20 |
    21 | 22 |
    23 | 28 | 29 | 34 |
    35 |
    36 | 37 |
    {props.children}
    38 | 39 |
    40 | {`© Copyright ${new Date().getFullYear()} ${AppConfig.name}. `} 41 | {t.rich('made_with', { 42 | author: () => ( 43 | 47 | CreativeDesignsGuru 48 | 49 | ), 50 | })} 51 | {/* 52 | * PLEASE READ THIS SECTION 53 | * I'm an indie maker with limited resources and funds, I'll really appreciate if you could have a link to my website. 54 | * The link doesn't need to appear on every pages, one link on one page is enough. 55 | * For example, in the `About` page. Thank you for your support, it'll mean a lot to me. 56 | */} 57 |
    58 |
    59 |
    60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /src/types/i18n.ts: -------------------------------------------------------------------------------- 1 | import type { routing } from '@/libs/i18nRouting'; 2 | import type messages from '@/locales/en.json'; 3 | 4 | declare module 'next-intl' { 5 | // eslint-disable-next-line ts/consistent-type-definitions 6 | interface AppConfig { 7 | Locale: (typeof routing.locales)[number]; 8 | Messages: typeof messages; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/AppConfig.ts: -------------------------------------------------------------------------------- 1 | import type { LocalizationResource } from '@clerk/types'; 2 | import type { LocalePrefixMode } from 'next-intl/routing'; 3 | import { enUS, frFR } from '@clerk/localizations'; 4 | 5 | const localePrefix: LocalePrefixMode = 'as-needed'; 6 | 7 | // FIXME: Update this configuration file based on your project information 8 | export const AppConfig = { 9 | name: 'Nextjs Starter', 10 | locales: ['en', 'fr'], 11 | defaultLocale: 'en', 12 | localePrefix, 13 | }; 14 | 15 | const supportedLocales: Record = { 16 | en: enUS, 17 | fr: frFR, 18 | }; 19 | 20 | export const ClerkLocalizations = { 21 | defaultLocale: enUS, 22 | supportedLocales, 23 | }; 24 | -------------------------------------------------------------------------------- /src/utils/Helpers.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { routing } from '@/libs/i18nRouting'; 3 | import { getI18nPath } from './Helpers'; 4 | 5 | describe('Helpers', () => { 6 | describe('getI18nPath function', () => { 7 | it('should not change the path for default language', () => { 8 | const url = '/random-url'; 9 | const locale = routing.defaultLocale; 10 | 11 | expect(getI18nPath(url, locale)).toBe(url); 12 | }); 13 | 14 | it('should prepend the locale to the path for non-default language', () => { 15 | const url = '/random-url'; 16 | const locale = 'fr'; 17 | 18 | expect(getI18nPath(url, locale)).toMatch(/^\/fr/); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/utils/Helpers.ts: -------------------------------------------------------------------------------- 1 | import { routing } from '@/libs/i18nRouting'; 2 | 3 | export const getBaseUrl = () => { 4 | if (process.env.NEXT_PUBLIC_APP_URL) { 5 | return process.env.NEXT_PUBLIC_APP_URL; 6 | } 7 | 8 | if ( 9 | process.env.VERCEL_ENV === 'production' 10 | && process.env.VERCEL_PROJECT_PRODUCTION_URL 11 | ) { 12 | return `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`; 13 | } 14 | 15 | if (process.env.VERCEL_URL) { 16 | return `https://${process.env.VERCEL_URL}`; 17 | } 18 | 19 | return 'http://localhost:3000'; 20 | }; 21 | 22 | export const getI18nPath = (url: string, locale: string) => { 23 | if (locale === routing.defaultLocale) { 24 | return url; 25 | } 26 | 27 | return `/${locale}${url}`; 28 | }; 29 | -------------------------------------------------------------------------------- /src/validations/CounterValidation.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const CounterValidation = z.object({ 4 | increment: z.coerce.number().min(1).max(3), 5 | }); 6 | -------------------------------------------------------------------------------- /tests/e2e/Counter.e2e.ts: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import { faker } from '@faker-js/faker'; 3 | import { expect, test } from '@playwright/test'; 4 | 5 | test.describe('Counter', () => { 6 | test.describe('Increment operation', () => { 7 | test('should display error message when incrementing with negative number', async ({ 8 | page, 9 | }) => { 10 | await page.goto('/counter'); 11 | 12 | const count = page.getByText('Count:'); 13 | const countText = await count.textContent(); 14 | 15 | assert(countText !== null, 'Count should not be null'); 16 | 17 | await page.getByLabel('Increment by').fill('-1'); 18 | await page.getByRole('button', { name: 'Increment' }).click(); 19 | 20 | await expect(page.getByText('Number must be greater than or equal to 1')).toBeVisible(); 21 | await expect(page.getByText('Count:')).toHaveText(countText); 22 | }); 23 | 24 | test('should increment the counter and validate the count', async ({ 25 | page, 26 | }) => { 27 | // `x-e2e-random-id` is used for end-to-end testing to make isolated requests 28 | // The default value is 0 when there is no `x-e2e-random-id` header 29 | const e2eRandomId = faker.number.int({ max: 1000000 }); 30 | await page.setExtraHTTPHeaders({ 31 | 'x-e2e-random-id': e2eRandomId.toString(), 32 | }); 33 | await page.goto('/counter'); 34 | 35 | const count = page.getByText('Count:'); 36 | const countText = await count.textContent(); 37 | 38 | assert(countText !== null, 'Count should not be null'); 39 | 40 | const countNumber = Number(countText.split(' ')[1]); 41 | 42 | await page.getByLabel('Increment by').fill('2'); 43 | await page.getByRole('button', { name: 'Increment' }).click(); 44 | 45 | await expect(page.getByText('Count:')).toHaveText(`Count: ${countNumber + 2}`); 46 | 47 | await page.getByLabel('Increment by').fill('3'); 48 | await page.getByRole('button', { name: 'Increment' }).click(); 49 | 50 | await expect(page.getByText('Count:')).toHaveText(`Count: ${countNumber + 5}`); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /tests/e2e/I18n.e2e.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | test.describe('I18n', () => { 4 | test.describe('Language Switching', () => { 5 | test('should switch language from English to French using dropdown and verify text on the homepage', async ({ page }) => { 6 | await page.goto('/'); 7 | 8 | await expect( 9 | page.getByRole('heading', { name: 'Boilerplate Code for Your Next.js Project with Tailwind CSS' }), 10 | ).toBeVisible(); 11 | 12 | await page.getByLabel('lang-switcher').selectOption('fr'); 13 | 14 | await expect( 15 | page.getByRole('heading', { name: 'Code de démarrage pour Next.js avec Tailwind CSS' }), 16 | ).toBeVisible(); 17 | }); 18 | 19 | test('should switch language from English to French using URL and verify text on the sign-in page', async ({ page }) => { 20 | await page.goto('/sign-in'); 21 | 22 | await expect(page.getByText('Email address')).toBeVisible(); 23 | 24 | await page.goto('/fr/sign-in'); 25 | 26 | await expect(page.getByText('Adresse e-mail')).toBeVisible(); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/e2e/Sanity.check.e2e.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | // Checkly is a tool used to monitor deployed environments, such as production or preview environments. 4 | // It runs end-to-end tests with the `.check.e2e.ts` extension after each deployment to ensure that the environment is up and running. 5 | // With Checkly, you can monitor your production environment and run `*.check.e2e.ts` tests regularly at a frequency of your choice. 6 | // If the tests fail, Checkly will notify you via email, Slack, or other channels of your choice. 7 | // On the other hand, E2E tests ending with `*.e2e.ts` are only run before deployment. 8 | // You can run them locally or on CI to ensure that the application is ready for deployment. 9 | 10 | // BaseURL needs to be explicitly defined in the test file. 11 | // Otherwise, Checkly runtime will throw an exception: `CHECKLY_INVALID_URL: Only URL's that start with http(s)` 12 | // You can't use `goto` function directly with a relative path like with other *.e2e.ts tests. 13 | // Check the example at https://feedback.checklyhq.com/changelog/new-changelog-436 14 | 15 | test.describe('Sanity', () => { 16 | test.describe('Static pages', () => { 17 | test('should display the homepage', async ({ page, baseURL }) => { 18 | await page.goto(`${baseURL}/`); 19 | 20 | await expect( 21 | page.getByRole('heading', { name: 'Boilerplate Code for Your Next.js Project with Tailwind CSS' }), 22 | ).toBeVisible(); 23 | }); 24 | 25 | test('should navigate to the about page', async ({ page, baseURL }) => { 26 | await page.goto(`${baseURL}/`); 27 | 28 | await page.getByRole('link', { name: 'About' }).click(); 29 | 30 | await expect(page).toHaveURL(/about$/); 31 | 32 | await expect( 33 | page.getByText('Welcome to our About page', { exact: false }), 34 | ).toBeVisible(); 35 | }); 36 | 37 | test('should navigate to the portfolio page', async ({ page, baseURL }) => { 38 | await page.goto(`${baseURL}/`); 39 | 40 | await page.getByRole('link', { name: 'Portfolio' }).click(); 41 | 42 | await expect(page).toHaveURL(/portfolio$/); 43 | 44 | await expect( 45 | page.locator('main').getByRole('link', { name: /^Portfolio/ }), 46 | ).toHaveCount(6); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /tests/e2e/Visual.e2e.ts: -------------------------------------------------------------------------------- 1 | import percySnapshot from '@percy/playwright'; 2 | import { expect, test } from '@playwright/test'; 3 | 4 | test.describe('Visual testing', () => { 5 | test.describe('Static pages', () => { 6 | test('should take screenshot of the homepage', async ({ page }) => { 7 | await page.goto('/'); 8 | 9 | await expect( 10 | page.getByRole('heading', { name: 'Boilerplate Code for Your Next.js Project with Tailwind CSS' }), 11 | ).toBeVisible(); 12 | 13 | await percySnapshot(page, 'Homepage'); 14 | }); 15 | 16 | test('should take screenshot of the about page', async ({ page }) => { 17 | await page.goto('/about'); 18 | 19 | await expect( 20 | page.getByRole('link', { name: 'About' }), 21 | ).toBeVisible(); 22 | 23 | await percySnapshot(page, 'About'); 24 | }); 25 | 26 | test('should take screenshot of the portfolio page', async ({ page }) => { 27 | await page.goto('/portfolio'); 28 | 29 | await expect( 30 | page.getByText('Welcome to my portfolio page!'), 31 | ).toBeVisible(); 32 | 33 | await percySnapshot(page, 'Portfolio'); 34 | }); 35 | 36 | test('should take screenshot of the portfolio details page', async ({ page }) => { 37 | await page.goto('/portfolio'); 38 | 39 | await page.getByRole('link', { name: 'Portfolio 2' }).click(); 40 | 41 | await expect( 42 | page.getByText('Created a set of promotional'), 43 | ).toBeVisible(); 44 | 45 | await percySnapshot(page, 'Portfolio details'); 46 | }); 47 | 48 | test('should take screenshot of the French homepage', async ({ page }) => { 49 | await page.goto('/fr'); 50 | 51 | await expect( 52 | page.getByRole('heading', { name: 'Code de démarrage pour Next.js avec Tailwind CSS' }), 53 | ).toBeVisible(); 54 | 55 | await percySnapshot(page, 'Homepage - French'); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /tests/integration/Counter.spec.ts: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker'; 2 | import { expect, test } from '@playwright/test'; 3 | 4 | test.describe('Counter', () => { 5 | test.describe('Basic database operations', () => { 6 | test('shouldn\'t increment the counter with an invalid input', async ({ page }) => { 7 | const counter = await page.request.put('/api/counter', { 8 | data: { 9 | increment: 'incorrect', 10 | }, 11 | }); 12 | 13 | expect(counter.status()).toBe(422); 14 | }); 15 | 16 | test('shouldn\'t increment the counter with a negative number', async ({ page }) => { 17 | const counter = await page.request.put('/api/counter', { 18 | data: { 19 | increment: -1, 20 | }, 21 | }); 22 | 23 | expect(counter.status()).toBe(422); 24 | }); 25 | 26 | test('shouldn\'t increment the counter with a number greater than 3', async ({ page }) => { 27 | const counter = await page.request.put('/api/counter', { 28 | data: { 29 | increment: 5, 30 | }, 31 | }); 32 | 33 | expect(counter.status()).toBe(422); 34 | }); 35 | 36 | test('should increment the counter and update the counter correctly', async ({ page }) => { 37 | // `x-e2e-random-id` is used for end-to-end testing to make isolated requests 38 | // The default value is 0 when there is no `x-e2e-random-id` header 39 | const e2eRandomId = faker.number.int({ max: 1000000 }); 40 | 41 | let counter = await page.request.put('/api/counter', { 42 | data: { 43 | increment: 1, 44 | }, 45 | headers: { 46 | 'x-e2e-random-id': e2eRandomId.toString(), 47 | }, 48 | }); 49 | let counterJson = await counter.json(); 50 | 51 | expect(counter.status()).toBe(200); 52 | 53 | // Save the current count 54 | const count = counterJson.count; 55 | 56 | counter = await page.request.put('/api/counter', { 57 | data: { 58 | increment: 2, 59 | }, 60 | headers: { 61 | 'x-e2e-random-id': e2eRandomId.toString(), 62 | }, 63 | }); 64 | counterJson = await counter.json(); 65 | 66 | expect(counter.status()).toBe(200); 67 | expect(counterJson.count).toEqual(count + 2); 68 | 69 | counter = await page.request.put('/api/counter', { 70 | data: { 71 | increment: 1, 72 | }, 73 | headers: { 74 | 'x-e2e-random-id': e2eRandomId.toString(), 75 | }, 76 | }); 77 | counterJson = await counter.json(); 78 | 79 | expect(counter.status()).toBe(200); 80 | expect(counterJson.count).toEqual(count + 3); 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsonc/sort-keys */ 2 | { 3 | "compilerOptions": { 4 | // ====================================================================== 5 | // Language & Environment 6 | // Defines JavaScript version and runtime environment 7 | // ====================================================================== 8 | "target": "ES2017", 9 | "module": "esnext", 10 | "lib": ["dom", "dom.iterable", "esnext"], 11 | "moduleResolution": "bundler", 12 | "isolatedModules": true, 13 | 14 | // ====================================================================== 15 | // Type Safety - Foundation 16 | // Core type checking settings for a robust codebase 17 | // ====================================================================== 18 | "strict": true, 19 | "alwaysStrict": true, 20 | "strictNullChecks": true, 21 | "noImplicitAny": true, 22 | "noImplicitThis": true, 23 | 24 | // ====================================================================== 25 | // Type Safety - Advanced 26 | // Additional checks for higher code quality 27 | // ====================================================================== 28 | "noUncheckedIndexedAccess": true, 29 | "noImplicitReturns": true, 30 | "noUnusedLocals": true, 31 | "noUnusedParameters": true, 32 | "noFallthroughCasesInSwitch": true, 33 | "allowUnreachableCode": false, 34 | "useUnknownInCatchVariables": true, 35 | "noImplicitOverride": true, 36 | 37 | // ====================================================================== 38 | // Interoperability 39 | // Settings for working with different file types and modules 40 | // ====================================================================== 41 | "allowJs": true, 42 | "checkJs": true, 43 | "esModuleInterop": true, 44 | "resolveJsonModule": true, 45 | 46 | // ====================================================================== 47 | // Build & Performance 48 | // Settings that affect compilation output and build performance 49 | // ====================================================================== 50 | "skipLibCheck": true, 51 | "removeComments": true, 52 | "preserveConstEnums": true, 53 | "forceConsistentCasingInFileNames": true, 54 | 55 | // ====================================================================== 56 | // Project Structure 57 | // Configure import paths and module resolution 58 | // ====================================================================== 59 | "baseUrl": ".", 60 | "paths": { 61 | "@/*": ["./src/*"], 62 | "@/public/*": ["./public/*"] 63 | }, 64 | 65 | // ====================================================================== 66 | // Next.js Project Configuration 67 | // Controls settings specific to Next.js framework 68 | // ====================================================================== 69 | "jsx": "preserve", // Preserve JSX for Next.js transformation 70 | "incremental": true, // Enable faster incremental builds 71 | "noEmit": true, // Skip emitting files (Next.js handles this) 72 | "plugins": [{ "name": "next" }] // Enable Next.js TypeScript plugin 73 | }, 74 | 75 | // Files to include/exclude from the project 76 | "exclude": ["node_modules", "**/*.spec.ts", "**/*.e2e.ts"], 77 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".storybook/*.ts", ".next/types/**/*.ts", "**/*.mts"] 78 | } 79 | -------------------------------------------------------------------------------- /vitest.config.mts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react'; 2 | import { loadEnv } from 'vite'; 3 | import tsconfigPaths from 'vite-tsconfig-paths'; 4 | import { defineConfig } from 'vitest/config'; 5 | 6 | export default defineConfig({ 7 | plugins: [react(), tsconfigPaths()], 8 | test: { 9 | coverage: { 10 | include: ['src/**/*'], 11 | exclude: ['src/**/*.stories.{js,jsx,ts,tsx}'], 12 | }, 13 | workspace: [ 14 | { 15 | extends: true, 16 | test: { 17 | name: 'unit', 18 | include: ['src/**/*.test.{js,ts}'], 19 | exclude: ['src/hooks/**/*.test.ts'], 20 | environment: 'node', 21 | }, 22 | }, 23 | { 24 | extends: true, 25 | test: { 26 | name: 'ui', 27 | include: ['**/*.test.tsx', 'src/hooks/**/*.test.ts'], 28 | browser: { 29 | provider: 'playwright', // or 'webdriverio' 30 | enabled: true, 31 | screenshotDirectory: 'vitest-test-results', 32 | instances: [ 33 | { browser: 'chromium' }, 34 | ], 35 | }, 36 | }, 37 | }, 38 | ], 39 | env: loadEnv('', process.cwd(), ''), 40 | }, 41 | }); 42 | --------------------------------------------------------------------------------