├── codecov.yml ├── public ├── favicon.ico ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png └── assets │ └── images │ ├── crowdin-dark.png │ ├── crowdin-white.png │ ├── sentry-dark.png │ ├── sentry-white.png │ ├── sevalla-dark.png │ ├── sevalla-light.png │ ├── clerk-logo-dark.png │ ├── better-stack-dark.png │ ├── better-stack-white.png │ ├── checkly-logo-dark.png │ ├── checkly-logo-light.png │ ├── nextjs-boilerplate-saas.png │ ├── nextjs-starter-banner.png │ ├── nextjs-boilerplate-sign-in.png │ ├── nextjs-boilerplate-sign-up.png │ ├── arcjet-dark.svg │ ├── arcjet-light.svg │ ├── codecov-dark.svg │ ├── codecov-white.svg │ ├── coderabbit-logo-dark.svg │ └── coderabbit-logo-light.svg ├── .github ├── FUNDING.yml ├── workflows │ ├── release.yml │ ├── crowdin.yml │ ├── checkly.yml │ └── CI.yml ├── actions │ └── setup-project │ │ └── action.yml └── dependabot.yml ├── src ├── styles │ └── global.css ├── validations │ └── CounterValidation.ts ├── libs │ ├── I18nNavigation.ts │ ├── I18nRouting.ts │ ├── Arcjet.ts │ ├── DB.ts │ ├── I18n.ts │ ├── Logger.ts │ └── Env.ts ├── types │ └── I18n.ts ├── app │ ├── robots.ts │ ├── sitemap.ts │ ├── [locale] │ │ ├── (auth) │ │ │ ├── (center) │ │ │ │ ├── layout.tsx │ │ │ │ ├── sign-in │ │ │ │ │ └── [[...sign-in]] │ │ │ │ │ │ └── page.tsx │ │ │ │ └── sign-up │ │ │ │ │ └── [[...sign-up]] │ │ │ │ │ └── page.tsx │ │ │ ├── dashboard │ │ │ │ ├── page.tsx │ │ │ │ ├── user-profile │ │ │ │ │ └── [[...user-profile]] │ │ │ │ │ │ └── page.tsx │ │ │ │ └── layout.tsx │ │ │ └── layout.tsx │ │ ├── api │ │ │ └── counter │ │ │ │ └── route.ts │ │ ├── (marketing) │ │ │ ├── about │ │ │ │ └── page.tsx │ │ │ ├── counter │ │ │ │ └── page.tsx │ │ │ ├── portfolio │ │ │ │ ├── [slug] │ │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ └── layout.tsx │ └── global-error.tsx ├── components │ ├── DemoBanner.tsx │ ├── DemoBadge.tsx │ ├── Hello.tsx │ ├── CurrentCount.tsx │ ├── analytics │ │ ├── PostHogProvider.tsx │ │ └── PostHogPageView.tsx │ ├── LocaleSwitcher.tsx │ ├── CounterForm.tsx │ └── Sponsors.tsx ├── utils │ ├── DBConnection.ts │ ├── AppConfig.ts │ ├── Helpers.test.ts │ └── Helpers.ts ├── templates │ ├── BaseTemplate.stories.tsx │ ├── BaseTemplate.test.tsx │ └── BaseTemplate.tsx ├── models │ └── Schema.ts ├── instrumentation.ts ├── instrumentation-client.ts ├── proxy.ts └── locales │ ├── en.json │ └── fr.json ├── migrations ├── 0000_init-db.sql └── meta │ ├── _journal.json │ └── 0000_snapshot.json ├── commitlint.config.ts ├── drizzle.config.ts ├── .vscode ├── extensions.json ├── tasks.json ├── launch.json └── settings.json ├── postcss.config.mjs ├── lefthook.yml ├── .storybook ├── vitest.setup.ts ├── main.ts ├── preview.ts └── vitest.config.mts ├── knip.config.ts ├── .coderabbit.yaml ├── crowdin.yml ├── .gitignore ├── tests ├── e2e │ ├── I18n.e2e.ts │ ├── Visual.e2e.ts │ ├── Counter.e2e.ts │ └── Sanity.check.e2e.ts └── integration │ └── Counter.spec.ts ├── LICENSE ├── vitest.config.mts ├── checkly.config.ts ├── .env ├── .env.production ├── eslint.config.mjs ├── next.config.ts ├── playwright.config.ts ├── tsconfig.json ├── package.json └── README.md /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | patch: off 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixartz/Next-js-Boilerplate/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixartz/Next-js-Boilerplate/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixartz/Next-js-Boilerplate/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixartz/Next-js-Boilerplate/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/assets/images/crowdin-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixartz/Next-js-Boilerplate/HEAD/public/assets/images/crowdin-dark.png -------------------------------------------------------------------------------- /public/assets/images/crowdin-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixartz/Next-js-Boilerplate/HEAD/public/assets/images/crowdin-white.png -------------------------------------------------------------------------------- /public/assets/images/sentry-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixartz/Next-js-Boilerplate/HEAD/public/assets/images/sentry-dark.png -------------------------------------------------------------------------------- /public/assets/images/sentry-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixartz/Next-js-Boilerplate/HEAD/public/assets/images/sentry-white.png -------------------------------------------------------------------------------- /public/assets/images/sevalla-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixartz/Next-js-Boilerplate/HEAD/public/assets/images/sevalla-dark.png -------------------------------------------------------------------------------- /public/assets/images/sevalla-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixartz/Next-js-Boilerplate/HEAD/public/assets/images/sevalla-light.png -------------------------------------------------------------------------------- /public/assets/images/clerk-logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixartz/Next-js-Boilerplate/HEAD/public/assets/images/clerk-logo-dark.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ixartz 2 | custom: 3 | - 'https://nextjs-boilerplate.com/pro-saas-starter-kit' 4 | - 'https://nextlessjs.com' 5 | -------------------------------------------------------------------------------- /public/assets/images/better-stack-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixartz/Next-js-Boilerplate/HEAD/public/assets/images/better-stack-dark.png -------------------------------------------------------------------------------- /public/assets/images/better-stack-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixartz/Next-js-Boilerplate/HEAD/public/assets/images/better-stack-white.png -------------------------------------------------------------------------------- /public/assets/images/checkly-logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixartz/Next-js-Boilerplate/HEAD/public/assets/images/checkly-logo-dark.png -------------------------------------------------------------------------------- /public/assets/images/checkly-logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixartz/Next-js-Boilerplate/HEAD/public/assets/images/checkly-logo-light.png -------------------------------------------------------------------------------- /public/assets/images/nextjs-boilerplate-saas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixartz/Next-js-Boilerplate/HEAD/public/assets/images/nextjs-boilerplate-saas.png -------------------------------------------------------------------------------- /public/assets/images/nextjs-starter-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixartz/Next-js-Boilerplate/HEAD/public/assets/images/nextjs-starter-banner.png -------------------------------------------------------------------------------- /src/styles/global.css: -------------------------------------------------------------------------------- 1 | @layer theme, base, clerk, components, utilities; /* Ensure Clerk is compatible with Tailwind CSS v4 */ 2 | 3 | @import 'tailwindcss'; 4 | -------------------------------------------------------------------------------- /public/assets/images/nextjs-boilerplate-sign-in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixartz/Next-js-Boilerplate/HEAD/public/assets/images/nextjs-boilerplate-sign-in.png -------------------------------------------------------------------------------- /public/assets/images/nextjs-boilerplate-sign-up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ixartz/Next-js-Boilerplate/HEAD/public/assets/images/nextjs-boilerplate-sign-up.png -------------------------------------------------------------------------------- /src/validations/CounterValidation.ts: -------------------------------------------------------------------------------- 1 | import * as z from 'zod'; 2 | 3 | export const CounterValidation = z.object({ 4 | increment: z.number().min(1).max(3), 5 | }); 6 | -------------------------------------------------------------------------------- /src/libs/I18nNavigation.ts: -------------------------------------------------------------------------------- 1 | import { createNavigation } from 'next-intl/navigation'; 2 | import { routing } from './I18nRouting'; 3 | 4 | export const { usePathname } = createNavigation(routing); 5 | -------------------------------------------------------------------------------- /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/_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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /.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 | "ms-ossdata.vscode-pgsql" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /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/DemoBadge.tsx: -------------------------------------------------------------------------------- 1 | export const DemoBadge = () => ( 2 |
3 | 6 |
7 | Demo of 8 | {` Next.js Boilerplate`} 9 |
10 |
11 |
12 | ); 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 {staged_files} 13 | stage_fixed: true 14 | priority: 1 15 | check-types: 16 | glob: '*.{ts,tsx}' 17 | run: npm run check:types 18 | priority: 2 19 | -------------------------------------------------------------------------------- /.storybook/vitest.setup.ts: -------------------------------------------------------------------------------- 1 | import * as a11yAddonAnnotations from '@storybook/addon-a11y/preview'; 2 | import { setProjectAnnotations } from '@storybook/nextjs-vite'; 3 | import * as projectAnnotations from './preview'; 4 | 5 | // This is an important step to apply the right configuration when testing your stories. 6 | // More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations 7 | setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]); 8 | -------------------------------------------------------------------------------- /src/utils/DBConnection.ts: -------------------------------------------------------------------------------- 1 | import { drizzle } from 'drizzle-orm/node-postgres'; 2 | import { Pool } from 'pg'; 3 | import { Env } from '@/libs/Env'; 4 | import * as schema from '@/models/Schema'; 5 | 6 | // Need a database for production? Just claim it by running `npm run neon:claim`. 7 | // Tested and compatible with Next.js Boilerplate 8 | export const createDbConnection = () => { 9 | const pool = new Pool({ 10 | connectionString: Env.DATABASE_URL, 11 | max: 1, 12 | }); 13 | 14 | return drizzle({ 15 | client: pool, 16 | schema, 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/nextjs-vite'; 2 | 3 | const config: StorybookConfig = { 4 | stories: [ 5 | '../src/**/*.mdx', 6 | '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)', 7 | ], 8 | addons: [ 9 | '@storybook/addon-docs', 10 | '@storybook/addon-a11y', 11 | ], 12 | framework: { 13 | name: '@storybook/nextjs-vite', 14 | options: {}, 15 | }, 16 | staticDirs: [ 17 | '../public', 18 | ], 19 | features: { 20 | experimentalRSC: true, 21 | }, 22 | core: { 23 | disableTelemetry: true, 24 | }, 25 | }; 26 | export default config; 27 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from '@storybook/nextjs-vite'; 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, // Enable App Router support 14 | }, 15 | docs: { 16 | toc: true, // Enable table of contents 17 | }, 18 | a11y: { 19 | test: 'todo', // Make a11y tests optional 20 | }, 21 | }, 22 | tags: ['autodocs'], 23 | }; 24 | 25 | export default preview; 26 | -------------------------------------------------------------------------------- /src/app/[locale]/(auth)/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import { getTranslations } from 'next-intl/server'; 3 | import { Hello } from '@/components/Hello'; 4 | 5 | export async function generateMetadata(props: { 6 | params: Promise<{ locale: string }>; 7 | }): Promise { 8 | const { locale } = await props.params; 9 | const t = await getTranslations({ 10 | locale, 11 | namespace: 'Dashboard', 12 | }); 13 | 14 | return { 15 | title: t('meta_title'), 16 | }; 17 | } 18 | 19 | export default function Dashboard() { 20 | return ( 21 |
22 | 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /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 { NodePgDatabase } from 'drizzle-orm/node-postgres'; 2 | import type * as schema from '@/models/Schema'; 3 | import { createDbConnection } from '@/utils/DBConnection'; 4 | import { Env } from './Env'; 5 | 6 | // Stores the db connection in the global scope to prevent multiple instances due to hot reloading with Next.js 7 | const globalForDb = globalThis as unknown as { 8 | drizzle: NodePgDatabase; 9 | }; 10 | 11 | const db = globalForDb.drizzle || createDbConnection(); 12 | 13 | // Only store in global during development to prevent hot reload issues 14 | if (Env.NODE_ENV !== 'production') { 15 | globalForDb.drizzle = db; 16 | } 17 | 18 | export { db }; 19 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /knip.config.ts: -------------------------------------------------------------------------------- 1 | import type { KnipConfig } from 'knip'; 2 | 3 | const config: KnipConfig = { 4 | // Files to exclude from Knip analysis 5 | ignore: [ 6 | 'checkly.config.ts', 7 | 'src/libs/I18n.ts', 8 | 'src/types/I18n.ts', 9 | 'src/utils/Helpers.ts', 10 | 'tests/**/*.ts', 11 | ], 12 | // Dependencies to ignore during analysis 13 | ignoreDependencies: [ 14 | '@commitlint/types', 15 | '@clerk/types', 16 | 'conventional-changelog-conventionalcommits', 17 | 'vite', 18 | ], 19 | // Binaries to ignore during analysis 20 | ignoreBinaries: [ 21 | 'production', // False positive raised with dotenv-cli 22 | ], 23 | compilers: { 24 | css: (text: string) => [...text.matchAll(/(?<=@)import[^;]+/g)].join('\n'), 25 | }, 26 | }; 27 | 28 | export default config; 29 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 30 | export const isServer = () => { 31 | return typeof window === 'undefined'; 32 | }; 33 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 Pro 23 | 24 | ), 25 | })} 26 |

27 | 28 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /.storybook/vitest.config.mts: -------------------------------------------------------------------------------- 1 | import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; 2 | import { playwright } from '@vitest/browser-playwright'; 3 | import { defineConfig } from 'vitest/config'; 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | // The plugin will run tests for the stories defined in your Storybook config 8 | // See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest 9 | storybookTest(), 10 | ], 11 | test: { 12 | projects: [ 13 | { 14 | extends: true, 15 | test: { 16 | name: 'storybook', 17 | browser: { 18 | enabled: true, 19 | headless: true, 20 | provider: playwright(), 21 | instances: [{ browser: 'chromium' }], 22 | }, 23 | setupFiles: ['.storybook/vitest.setup.ts'], 24 | }, 25 | }, 26 | ], 27 | }, 28 | }); 29 | -------------------------------------------------------------------------------- /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.findFirst({ 15 | where: eq(counterSchema.id, id), 16 | }); 17 | const count = result?.count ?? 0; 18 | 19 | logger.info('Counter fetched successfully'); 20 | 21 | return ( 22 |
23 | {t('count', { count })} 24 |
25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/app/[locale]/(auth)/(center)/sign-in/[[...sign-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import { SignIn } from '@clerk/nextjs'; 3 | import { getTranslations, setRequestLocale } from 'next-intl/server'; 4 | import { getI18nPath } from '@/utils/Helpers'; 5 | 6 | type ISignInPageProps = { 7 | params: Promise<{ locale: string }>; 8 | }; 9 | 10 | export async function generateMetadata(props: ISignInPageProps): Promise { 11 | const { locale } = await props.params; 12 | const t = await getTranslations({ 13 | locale, 14 | namespace: 'SignIn', 15 | }); 16 | 17 | return { 18 | title: t('meta_title'), 19 | description: t('meta_description'), 20 | }; 21 | } 22 | 23 | export default async function SignInPage(props: ISignInPageProps) { 24 | const { locale } = await props.params; 25 | setRequestLocale(locale); 26 | 27 | return ( 28 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/app/[locale]/(auth)/(center)/sign-up/[[...sign-up]]/page.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import { SignUp } from '@clerk/nextjs'; 3 | import { getTranslations, setRequestLocale } from 'next-intl/server'; 4 | import { getI18nPath } from '@/utils/Helpers'; 5 | 6 | type ISignUpPageProps = { 7 | params: Promise<{ locale: string }>; 8 | }; 9 | 10 | export async function generateMetadata(props: ISignUpPageProps): Promise { 11 | const { locale } = await props.params; 12 | const t = await getTranslations({ 13 | locale, 14 | namespace: 'SignUp', 15 | }); 16 | 17 | return { 18 | title: t('meta_title'), 19 | description: t('meta_description'), 20 | }; 21 | } 22 | 23 | export default async function SignUpPage(props: ISignUpPageProps) { 24 | const { locale } = await props.params; 25 | setRequestLocale(locale); 26 | 27 | return ( 28 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /.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 | build-archive.log 58 | 59 | # playwright 60 | /test-results/ 61 | /playwright-report/ 62 | /playwright/.cache/ 63 | -------------------------------------------------------------------------------- /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/app/[locale]/(auth)/dashboard/user-profile/[[...user-profile]]/page.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import { UserProfile } from '@clerk/nextjs'; 3 | import { getTranslations, setRequestLocale } from 'next-intl/server'; 4 | import { getI18nPath } from '@/utils/Helpers'; 5 | 6 | type IUserProfilePageProps = { 7 | params: Promise<{ locale: string }>; 8 | }; 9 | 10 | export async function generateMetadata(props: IUserProfilePageProps): Promise { 11 | const { locale } = await props.params; 12 | const t = await getTranslations({ 13 | locale, 14 | namespace: 'UserProfile', 15 | }); 16 | 17 | return { 18 | title: t('meta_title'), 19 | }; 20 | } 21 | 22 | export default async function UserProfilePage(props: IUserProfilePageProps) { 23 | const { locale } = await props.params; 24 | setRequestLocale(locale); 25 | 26 | return ( 27 |
28 | 31 |
32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/templates/BaseTemplate.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/nextjs-vite'; 2 | import { NextIntlClientProvider } from 'next-intl'; 3 | import messages from '@/locales/en.json'; 4 | import { BaseTemplate } from './BaseTemplate'; 5 | 6 | const meta = { 7 | title: 'Example/BaseTemplate', 8 | component: BaseTemplate, 9 | parameters: { 10 | layout: 'fullscreen', 11 | }, 12 | decorators: [ 13 | Story => ( 14 | 15 | 16 | 17 | ), 18 | ], 19 | } satisfies Meta; 20 | 21 | export default meta; 22 | type Story = StoryObj; 23 | 24 | export const BaseWithReactComponent: Story = { 25 | args: { 26 | children:
Children node
, 27 | leftNav: ( 28 | <> 29 |
  • Link 1
  • 30 |
  • Link 2
  • 31 | 32 | ), 33 | }, 34 | }; 35 | 36 | export const BaseWithString: Story = { 37 | args: { 38 | ...BaseWithReactComponent.args, 39 | children: 'String', 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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 | // It automatically run the command `db-server:file`, which apply the migration before Next.js starts in development mode, 11 | // Alternatively, if your database is running, you can run `npm run db:migrate` and there is no need to restart the server. 12 | 13 | // Need a database for production? Just claim it by running `npm run neon:claim`. 14 | // Tested and compatible with Next.js Boilerplate 15 | 16 | export const counterSchema = pgTable('counter', { 17 | id: serial('id').primaryKey(), 18 | count: integer('count').default(0), 19 | updatedAt: timestamp('updated_at', { mode: 'date' }) 20 | .defaultNow() 21 | .$onUpdate(() => new Date()) 22 | .notNull(), 23 | createdAt: timestamp('created_at', { mode: 'date' }).defaultNow().notNull(), 24 | }); 25 | -------------------------------------------------------------------------------- /.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: [22.x] 16 | 17 | name: Create a new release 18 | runs-on: ubuntu-latest 19 | timeout-minutes: 10 20 | 21 | permissions: 22 | contents: write # to be able to publish a GitHub release 23 | issues: write # to be able to comment on released issues 24 | pull-requests: write # to be able to comment on released pull requests 25 | 26 | steps: 27 | - uses: actions/checkout@v6 28 | with: 29 | fetch-depth: 0 30 | 31 | - name: Set up Node.js environment 32 | uses: ./.github/actions/setup-project 33 | with: 34 | node-version: ${{ matrix.node-version }} 35 | 36 | - name: Verify the integrity of provenance attestations and registry signatures for installed dependencies 37 | run: npm audit signatures 38 | 39 | - name: Release 40 | run: npx semantic-release 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | -------------------------------------------------------------------------------- /src/libs/Logger.ts: -------------------------------------------------------------------------------- 1 | import type { AsyncSink } from '@logtape/logtape'; 2 | import { configure, fromAsyncSink, getConsoleSink, getJsonLinesFormatter, getLogger } from '@logtape/logtape'; 3 | import { Env } from './Env'; 4 | 5 | const betterStackSink: AsyncSink = async (record) => { 6 | await fetch(`https://${Env.NEXT_PUBLIC_BETTER_STACK_INGESTING_HOST}`, { 7 | method: 'POST', 8 | headers: { 9 | 'Content-Type': 'application/json', 10 | 'Authorization': `Bearer ${Env.NEXT_PUBLIC_BETTER_STACK_SOURCE_TOKEN}`, 11 | }, 12 | body: JSON.stringify(record), 13 | }); 14 | }; 15 | 16 | await configure({ 17 | sinks: { 18 | console: getConsoleSink({ formatter: getJsonLinesFormatter() }), 19 | betterStack: fromAsyncSink(betterStackSink), 20 | }, 21 | loggers: [ 22 | { category: ['logtape', 'meta'], sinks: ['console'], lowestLevel: 'warning' }, 23 | { 24 | category: ['app'], 25 | sinks: Env.NEXT_PUBLIC_BETTER_STACK_SOURCE_TOKEN && Env.NEXT_PUBLIC_BETTER_STACK_INGESTING_HOST 26 | ? ['console', 'betterStack'] 27 | : ['console'], 28 | lowestLevel: 'debug', 29 | }, 30 | ], 31 | }); 32 | 33 | export const logger = getLogger(['app']); 34 | -------------------------------------------------------------------------------- /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 | integrations: [ 11 | Sentry.consoleLoggingIntegration(), 12 | ], 13 | 14 | // Adds request headers and IP for users, for more info visit 15 | sendDefaultPii: true, 16 | 17 | // Adjust this value in production, or use tracesSampler for greater control 18 | tracesSampleRate: 1, 19 | 20 | // Enable logs to be sent to Sentry 21 | enableLogs: true, 22 | 23 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 24 | debug: false, 25 | }; 26 | 27 | export async function register() { 28 | if (!process.env.NEXT_PUBLIC_SENTRY_DISABLED) { 29 | if (process.env.NEXT_RUNTIME === 'nodejs') { 30 | // Node.js Sentry configuration 31 | Sentry.init(sentryOptions); 32 | } 33 | 34 | if (process.env.NEXT_RUNTIME === 'edge') { 35 | // Edge Sentry configuration 36 | Sentry.init(sentryOptions); 37 | } 38 | } 39 | } 40 | 41 | export const onRequestError = Sentry.captureRequestError; 42 | -------------------------------------------------------------------------------- /.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 | timeout-minutes: 10 15 | 16 | steps: 17 | - uses: actions/checkout@v6 18 | 19 | - name: Crowdin action 20 | uses: crowdin/github-action@v2 21 | with: 22 | upload_sources: true 23 | upload_translations: true 24 | download_translations: true 25 | localization_branch_name: l10n_crowdin_translations 26 | create_pull_request: true 27 | pull_request_title: New Crowdin Translations 28 | pull_request_body: 'New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)' 29 | pull_request_base_branch_name: main 30 | commit_message: 'chore: new Crowdin translations by GitHub Action' 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} 34 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} 35 | -------------------------------------------------------------------------------- /vitest.config.mts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react'; 2 | import { playwright } from '@vitest/browser-playwright'; 3 | import { loadEnv } from 'vite'; 4 | import tsconfigPaths from 'vite-tsconfig-paths'; 5 | import { defineConfig } from 'vitest/config'; 6 | 7 | export default defineConfig({ 8 | plugins: [react(), tsconfigPaths()], 9 | test: { 10 | coverage: { 11 | include: ['src/**/*'], 12 | exclude: ['src/**/*.stories.{js,jsx,ts,tsx}'], 13 | }, 14 | projects: [ 15 | { 16 | extends: true, 17 | test: { 18 | name: 'unit', 19 | include: ['src/**/*.test.{js,ts}'], 20 | exclude: ['src/hooks/**/*.test.ts'], 21 | environment: 'node', 22 | }, 23 | }, 24 | { 25 | extends: true, 26 | test: { 27 | name: 'ui', 28 | include: ['**/*.test.tsx', 'src/hooks/**/*.test.ts'], 29 | browser: { 30 | enabled: true, 31 | headless: true, 32 | provider: playwright(), 33 | screenshotDirectory: 'vitest-test-results', 34 | instances: [ 35 | { browser: 'chromium' }, 36 | ], 37 | }, 38 | }, 39 | }, 40 | ], 41 | env: loadEnv('', process.cwd(), ''), 42 | }, 43 | }); 44 | -------------------------------------------------------------------------------- /.github/actions/setup-project/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup Node.js and dependencies 2 | description: Setup Node.js environment with npm dependencies 3 | 4 | inputs: 5 | node-version: 6 | description: Node.js version 7 | required: true 8 | restore-nextjs-cache: 9 | description: Whether to restore Next.js build cache 10 | required: false 11 | default: 'false' 12 | 13 | runs: 14 | using: composite 15 | steps: 16 | - name: Use Node.js ${{ inputs.node-version }} 17 | uses: actions/setup-node@v6 18 | with: 19 | node-version: ${{ inputs.node-version }} 20 | cache: npm 21 | 22 | - name: Restore or cache node_modules 23 | id: cache-node-modules 24 | uses: actions/cache@v4 25 | with: 26 | path: node_modules 27 | key: node-modules-${{ inputs.node-version }}-${{ hashFiles('package-lock.json') }} 28 | 29 | - name: Install dependencies 30 | if: steps.cache-node-modules.outputs.cache-hit != 'true' 31 | shell: bash 32 | run: npm ci 33 | 34 | - name: Restore Next.js build output 35 | if: inputs.restore-nextjs-cache == 'true' 36 | uses: actions/cache/restore@v4 37 | with: 38 | path: | 39 | .next 40 | key: nextjs-build-${{ inputs.node-version }}-${{ github.sha }} 41 | fail-on-cache-miss: true 42 | -------------------------------------------------------------------------------- /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 | const emailChannel = new EmailAlertChannel('email-channel-1', { 11 | address: process.env.CHECKLY_EMAIL_ADDRESS ?? '', 12 | ...sendDefaults, 13 | }); 14 | 15 | export const config = defineConfig({ 16 | projectName: process.env.CHECKLY_PROJECT_NAME ?? '', 17 | logicalId: process.env.CHECKLY_LOGICAL_ID ?? '', 18 | repoUrl: 'https://github.com/ixartz/Next-js-Boilerplate', 19 | checks: { 20 | locations: ['us-east-1', 'eu-central-1'], 21 | tags: ['website'], 22 | runtimeId: '2024.02', 23 | browserChecks: { 24 | frequency: Frequency.EVERY_24H, 25 | testMatch: '**/tests/e2e/**/*.check.e2e.ts', 26 | alertChannels: [emailChannel], 27 | }, 28 | playwrightConfig: { 29 | use: { 30 | baseURL: process.env.ENVIRONMENT_URL || process.env.NEXT_PUBLIC_APP_URL, 31 | extraHTTPHeaders: { 32 | 'x-vercel-protection-bypass': process.env.VERCEL_BYPASS_TOKEN, 33 | }, 34 | }, 35 | }, 36 | }, 37 | cli: { 38 | runLocation: 'us-east-1', 39 | reporters: ['list'], 40 | }, 41 | }); 42 | 43 | export default config; 44 | -------------------------------------------------------------------------------- /src/app/[locale]/api/counter/route.ts: -------------------------------------------------------------------------------- 1 | import { sql } from 'drizzle-orm'; 2 | import { headers } from 'next/headers'; 3 | import { NextResponse } from 'next/server'; 4 | import * as z from 'zod'; 5 | import { db } from '@/libs/DB'; 6 | import { logger } from '@/libs/Logger'; 7 | import { counterSchema } from '@/models/Schema'; 8 | import { CounterValidation } from '@/validations/CounterValidation'; 9 | 10 | export const PUT = async (request: Request) => { 11 | const json = await request.json(); 12 | const parse = CounterValidation.safeParse(json); 13 | 14 | if (!parse.success) { 15 | return NextResponse.json(z.treeifyError(parse.error), { status: 422 }); 16 | } 17 | 18 | // `x-e2e-random-id` is used for end-to-end testing to make isolated requests 19 | // The default value is 0 when there is no `x-e2e-random-id` header 20 | const id = Number((await headers()).get('x-e2e-random-id')) || 0; 21 | 22 | const count = await db 23 | .insert(counterSchema) 24 | .values({ id, count: parse.data.increment }) 25 | .onConflictDoUpdate({ 26 | target: counterSchema.id, 27 | set: { count: sql`${counterSchema.count} + ${parse.data.increment}` }, 28 | }) 29 | .returning(); 30 | 31 | logger.info('Counter has been incremented'); 32 | 33 | return NextResponse.json({ 34 | count: count[0]?.count, 35 | }); 36 | }; 37 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # FIXME: Configure environment variables for your project 2 | 3 | # Need advanced features? Multi-tenancy & Teams, Roles & Permissions, Shadcn UI, End-to-End Typesafety with oRPC, 4 | # Stripe Payment, Light / Dark mode. Try Next.js Boilerplate Pro: 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 | # Database 14 | # Need a database for production? Just claim it by running `npm run neon:claim`. 15 | # Tested and compatible with Next.js Boilerplate 16 | 17 | # `DATABASE_URL` is a placeholder, you can find your connection string in `.env.local` file. 18 | DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:5432/postgres 19 | 20 | # Next.js 21 | NEXT_TELEMETRY_DISABLED=1 22 | 23 | ######## [BEGIN] SENSITIVE DATA ######## For security reason, don't update the following variables (secret key) directly in this file. 24 | ######## Please create a new file named `.env.local`, all environment files ending with `.local` won't be tracked by Git. 25 | ######## After creating the file, you can add the following variables. 26 | # Arcjet security 27 | # Get your key from https://launch.arcjet.com/Q6eLbRE 28 | # ARCJET_KEY= 29 | 30 | # Clerk authentication 31 | CLERK_SECRET_KEY=your_clerk_secret_key 32 | ######## [END] SENSITIVE DATA 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 | 38 | {props.children} 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /.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 month 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 | 27 | # Enable version updates for GitHub Actions 28 | - package-ecosystem: github-actions 29 | # Look for `.github/workflows` in the root directory 30 | directory: / 31 | # Check GitHub Actions for updates every month 32 | schedule: 33 | interval: monthly 34 | time: '06:05' 35 | # Limit the number of open pull requests for version updates to 1 36 | open-pull-requests-limit: 1 37 | # Commit message format 38 | commit-message: 39 | # Prefix all commit messages and pull request titles with "chore" 40 | prefix: chore 41 | -------------------------------------------------------------------------------- /src/libs/Env.ts: -------------------------------------------------------------------------------- 1 | import { createEnv } from '@t3-oss/env-nextjs'; 2 | import * as 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().min(1), 9 | }, 10 | client: { 11 | NEXT_PUBLIC_APP_URL: z.string().optional(), 12 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string().min(1), 13 | NEXT_PUBLIC_BETTER_STACK_SOURCE_TOKEN: z.string().optional(), 14 | NEXT_PUBLIC_BETTER_STACK_INGESTING_HOST: z.string().optional(), 15 | NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(), 16 | NEXT_PUBLIC_POSTHOG_HOST: z.string().optional(), 17 | }, 18 | shared: { 19 | NODE_ENV: z.enum(['test', 'development', 'production']).optional(), 20 | }, 21 | // You need to destructure all the keys manually 22 | runtimeEnv: { 23 | ARCJET_KEY: process.env.ARCJET_KEY, 24 | CLERK_SECRET_KEY: process.env.CLERK_SECRET_KEY, 25 | DATABASE_URL: process.env.DATABASE_URL, 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 | NEXT_PUBLIC_BETTER_STACK_SOURCE_TOKEN: process.env.NEXT_PUBLIC_BETTER_STACK_SOURCE_TOKEN, 30 | NEXT_PUBLIC_BETTER_STACK_INGESTING_HOST: process.env.NEXT_PUBLIC_BETTER_STACK_INGESTING_HOST, 31 | NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY, 32 | NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST, 33 | NODE_ENV: process.env.NODE_ENV, 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /src/app/[locale]/(marketing)/about/page.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import { getTranslations, setRequestLocale } from 'next-intl/server'; 3 | import Image from 'next/image'; 4 | 5 | type IAboutProps = { 6 | params: Promise<{ locale: string }>; 7 | }; 8 | 9 | export async function generateMetadata(props: IAboutProps): Promise { 10 | const { locale } = await props.params; 11 | const t = await getTranslations({ 12 | locale, 13 | namespace: 'About', 14 | }); 15 | 16 | return { 17 | title: t('meta_title'), 18 | description: t('meta_description'), 19 | }; 20 | } 21 | 22 | export default async function About(props: IAboutProps) { 23 | const { locale } = await props.params; 24 | setRequestLocale(locale); 25 | const t = await getTranslations({ 26 | locale, 27 | namespace: 'About', 28 | }); 29 | 30 | return ( 31 | <> 32 |

    {t('about_paragraph')}

    33 | 34 |
    35 | {`${t('translation_powered_by')} `} 36 | 40 | Crowdin 41 | 42 |
    43 | 44 | 45 | Crowdin Translation Management System 52 | 53 | 54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /src/app/[locale]/(marketing)/counter/page.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import { useTranslations } from 'next-intl'; 3 | import { getTranslations } from 'next-intl/server'; 4 | import Image from 'next/image'; 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 | }): Promise { 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 | 32 |
    33 | 34 |
    35 | {`${t('security_powered_by')} `} 36 | 40 | Arcjet 41 | 42 |
    43 | 44 | 47 | Arcjet 54 | 55 | 56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # FIXME: Configure environment variables for production 2 | 3 | # Need advanced features? Multi-tenancy & Teams, Roles & Permissions, Shadcn UI, End-to-End Typesafety with oRPC, 4 | # Stripe Payment, Light / Dark mode. Try Next.js Boilerplate Pro: https://nextjs-boilerplate.com/pro-saas-starter-kit 5 | 6 | # Hosting 7 | # Replace by your domain name 8 | NEXT_PUBLIC_APP_URL=https://demo.nextjs-boilerplate.com 9 | 10 | # Sentry DSN 11 | # NEXT_PUBLIC_SENTRY_DSN= 12 | 13 | # SENTRY_ORGANIZATION= 14 | # SENTRY_PROJECT= 15 | 16 | # Checkly 17 | CHECKLY_EMAIL_ADDRESS=contact@creativedesignsguru.com 18 | CHECKLY_PROJECT_NAME=Next.js Boilerplate 19 | CHECKLY_LOGICAL_ID=nextjs-boilerplate 20 | 21 | ######## [BEGIN] SENSITIVE DATA ######## For security reason, don't update the following variables (secret key) directly in this file. 22 | ######## Please create a new file named `.env.production.local`, all environment files ending with `.local` won't be tracked by Git. 23 | ######## After creating the file, you can add the following variables. 24 | # Arcjet security 25 | # Get your key from https://launch.arcjet.com/Q6eLbRE 26 | # ARCJET_KEY= 27 | 28 | # Database 29 | # 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" 30 | # Need a database for production? Just claim it by running `npm run neon:claim`. 31 | # Tested and compatible with Next.js Boilerplate 32 | # DATABASE_URL=postgresql://postgres@localhost:5432/postgres 33 | 34 | # Error monitoring 35 | # SENTRY_AUTH_TOKEN= 36 | 37 | # Logging ingestion 38 | # NEXT_PUBLIC_BETTER_STACK_SOURCE_TOKEN= 39 | # NEXT_PUBLIC_BETTER_STACK_INGESTING_HOST= 40 | ######## [END] SENSITIVE DATA 41 | -------------------------------------------------------------------------------- /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 | } 65 | -------------------------------------------------------------------------------- /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 | 6 | if (!process.env.NEXT_PUBLIC_SENTRY_DISABLED) { 7 | Sentry.init({ 8 | dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, 9 | 10 | // Add optional integrations for additional features 11 | integrations: [ 12 | Sentry.replayIntegration(), 13 | Sentry.consoleLoggingIntegration(), 14 | Sentry.browserTracingIntegration(), 15 | 16 | ...(process.env.NODE_ENV === 'development' 17 | ? [Sentry.spotlightBrowserIntegration()] 18 | : []), 19 | ], 20 | 21 | // Adds request headers and IP for users, for more info visit 22 | sendDefaultPii: true, 23 | 24 | // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. 25 | tracesSampleRate: 1, 26 | 27 | // Define how likely Replay events are sampled. 28 | // This sets the sample rate to be 10%. You may want this to be 100% while 29 | // in development and sample at a lower rate in production 30 | replaysSessionSampleRate: 0.1, 31 | 32 | // Define how likely Replay events are sampled when an error occurs. 33 | replaysOnErrorSampleRate: 1.0, 34 | 35 | // Enable logs to be sent to Sentry 36 | enableLogs: true, 37 | 38 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 39 | debug: false, 40 | }); 41 | } 42 | 43 | export const onRouterTransitionStart = Sentry.captureRouterTransitionStart; 44 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/e2e/Visual.e2e.ts: -------------------------------------------------------------------------------- 1 | import { expect, takeSnapshot, test } from '@chromatic-com/playwright'; 2 | 3 | test.describe('Visual testing', () => { 4 | test.describe('Static pages', () => { 5 | test('should take screenshot of the homepage', async ({ page }, testInfo) => { 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 takeSnapshot(page, testInfo); 13 | }); 14 | 15 | test('should take screenshot of the portfolio page', async ({ page }, testInfo) => { 16 | await page.goto('/portfolio'); 17 | 18 | await expect( 19 | page.getByText('Welcome to my portfolio page!'), 20 | ).toBeVisible(); 21 | 22 | await takeSnapshot(page, testInfo); 23 | }); 24 | 25 | test('should take screenshot of the about page', async ({ page }, testInfo) => { 26 | await page.goto('/about'); 27 | 28 | await expect( 29 | page.getByText('Welcome to our About page!'), 30 | ).toBeVisible(); 31 | 32 | await takeSnapshot(page, testInfo); 33 | }); 34 | 35 | test('should take screenshot of the portfolio details page', async ({ page }, testInfo) => { 36 | await page.goto('/portfolio/2'); 37 | 38 | await expect( 39 | page.getByText('Created a set of promotional'), 40 | ).toBeVisible(); 41 | 42 | await takeSnapshot(page, testInfo); 43 | }); 44 | 45 | test('should take screenshot of the French homepage', async ({ page }, testInfo) => { 46 | await page.goto('/fr'); 47 | 48 | await expect( 49 | page.getByRole('heading', { name: 'Code de démarrage pour Next.js avec Tailwind CSS' }), 50 | ).toBeVisible(); 51 | 52 | await takeSnapshot(page, testInfo); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/templates/BaseTemplate.test.tsx: -------------------------------------------------------------------------------- 1 | import { NextIntlClientProvider } from 'next-intl'; 2 | import { describe, expect, it } from 'vitest'; 3 | import { render } from 'vitest-browser-react'; 4 | import { page } from 'vitest/browser'; 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 nextjs-boilerplate.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://nextjs-boilerplate.com', 51 | ); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /.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 | // Use Zod v4 instead of v3 19 | "zod/v3", 20 | // Sentry is imported with `import *` 21 | "@sentry/nextjs" 22 | ], 23 | 24 | // Vitest 25 | "testing.automaticallyOpenTestResults": "neverOpen", // Don't open the test results automatically 26 | 27 | // I18n 28 | "i18n-ally.localesPaths": ["src/locales"], 29 | "i18n-ally.keystyle": "nested", 30 | 31 | // Disable the default formatter, use ESLint instead 32 | "prettier.enable": false, 33 | "editor.formatOnSave": false, 34 | 35 | // Auto fix with ESLint on save 36 | "editor.codeActionsOnSave": { 37 | "source.addMissingImports": "explicit", 38 | "source.fixAll.eslint": "explicit" 39 | }, 40 | 41 | // Enable eslint for all supported languages 42 | "eslint.validate": [ 43 | "javascript", 44 | "javascriptreact", 45 | "typescript", 46 | "typescriptreact", 47 | "vue", 48 | "html", 49 | "markdown", 50 | "json", 51 | "jsonc", 52 | "yaml", 53 | "toml", 54 | "xml", 55 | "gql", 56 | "graphql", 57 | "astro", 58 | "svelte", 59 | "css", 60 | "less", 61 | "scss", 62 | "pcss", 63 | "postcss", 64 | "github-actions-workflow" 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import antfu from '@antfu/eslint-config'; 4 | import jsxA11y from 'eslint-plugin-jsx-a11y'; 5 | import playwright from 'eslint-plugin-playwright'; 6 | import storybook from 'eslint-plugin-storybook'; 7 | import tailwind from 'eslint-plugin-tailwindcss'; 8 | 9 | export default antfu( 10 | { 11 | react: true, 12 | nextjs: true, 13 | typescript: true, 14 | 15 | // Configuration preferences 16 | lessOpinionated: true, 17 | isInEditor: false, 18 | 19 | // Code style 20 | stylistic: { 21 | semi: true, 22 | }, 23 | 24 | // Format settings 25 | formatters: { 26 | css: true, 27 | }, 28 | 29 | // Ignored paths 30 | ignores: [ 31 | 'migrations/**/*', 32 | ], 33 | }, 34 | // --- Accessibility Rules --- 35 | jsxA11y.flatConfigs.recommended, 36 | // --- Tailwind CSS Rules --- 37 | ...tailwind.configs['flat/recommended'], 38 | { 39 | settings: { 40 | tailwindcss: { 41 | config: `${dirname(fileURLToPath(import.meta.url))}/src/styles/global.css`, 42 | }, 43 | }, 44 | }, 45 | // --- E2E Testing Rules --- 46 | { 47 | files: [ 48 | '**/*.spec.ts', 49 | '**/*.e2e.ts', 50 | ], 51 | ...playwright.configs['flat/recommended'], 52 | }, 53 | // --- Storybook Rules --- 54 | ...storybook.configs['flat/recommended'], 55 | // --- Custom Rule Overrides --- 56 | { 57 | rules: { 58 | 'antfu/no-top-level-await': 'off', // Allow top-level await 59 | 'style/brace-style': ['error', '1tbs'], // Use the default brace style 60 | 'ts/consistent-type-definitions': ['error', 'type'], // Use `type` instead of `interface` 61 | 'react/prefer-destructuring-assignment': 'off', // Vscode doesn't support automatically destructuring, it's a pain to add a new variable 62 | 'node/prefer-global/process': 'off', // Allow using `process.env` 63 | 'test/padding-around-all': 'error', // Add padding in test files 64 | 'test/prefer-lowercase-title': 'off', // Allow using uppercase titles in test titles 65 | }, 66 | }, 67 | ); 68 | -------------------------------------------------------------------------------- /.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: [22.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@v6 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 | - name: Set up Node.js environment 34 | uses: ./.github/actions/setup-project 35 | with: 36 | node-version: ${{ matrix.node-version }} 37 | 38 | - name: Run checks # run the checks passing in the ENVIRONMENT_URL and recording a test session. 39 | id: run-checks 40 | run: npx dotenv -c production -- npx checkly test --reporter=github --record 41 | env: 42 | VERCEL_BYPASS_TOKEN: ${{ secrets.VERCEL_BYPASS_TOKEN }} 43 | ENVIRONMENT_URL: ${{ github.event.deployment_status.environment_url }} 44 | 45 | - name: Create summary # export the markdown report to the job summary. 46 | id: create-summary 47 | run: cat checkly-github-report.md > $GITHUB_STEP_SUMMARY 48 | 49 | - name: Deploy checks # if the test run was successful and we are on Production, deploy the checks 50 | id: deploy-checks 51 | if: steps.run-checks.outcome == 'success' && github.event.deployment_status.environment == 'Production' 52 | run: npx dotenv -c production -- npx checkly deploy --force 53 | -------------------------------------------------------------------------------- /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: 1, 15 | }, 16 | }); 17 | const router = useRouter(); 18 | 19 | const handleIncrement = form.handleSubmit(async (data) => { 20 | const response = await fetch(`/api/counter`, { 21 | method: 'PUT', 22 | headers: { 23 | 'Content-Type': 'application/json', 24 | }, 25 | body: JSON.stringify(data), 26 | }); 27 | await response.json(); 28 | 29 | router.refresh(); 30 | }); 31 | 32 | return ( 33 |
    34 |

    {t('presentation')}

    35 |
    36 | 45 | 46 | {form.formState.errors.increment && ( 47 |
    48 | {t('error_increment_range')} 49 |
    50 | )} 51 |
    52 | 53 |
    54 | 61 |
    62 |
    63 | ); 64 | }; 65 | -------------------------------------------------------------------------------- /src/app/[locale]/(marketing)/portfolio/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import { getTranslations, setRequestLocale } from 'next-intl/server'; 3 | import Image from 'next/image'; 4 | import { routing } from '@/libs/I18nRouting'; 5 | 6 | type IPortfolioDetailProps = { 7 | params: Promise<{ slug: string; locale: string }>; 8 | }; 9 | 10 | export function generateStaticParams() { 11 | return routing.locales 12 | .map(locale => 13 | Array.from(Array.from({ length: 6 }).keys()).map(elt => ({ 14 | slug: `${elt}`, 15 | locale, 16 | })), 17 | ) 18 | .flat(1); 19 | } 20 | 21 | export async function generateMetadata(props: IPortfolioDetailProps): Promise { 22 | const { locale, slug } = await props.params; 23 | const t = await getTranslations({ 24 | locale, 25 | namespace: 'PortfolioSlug', 26 | }); 27 | 28 | return { 29 | title: t('meta_title', { slug }), 30 | description: t('meta_description', { slug }), 31 | }; 32 | } 33 | 34 | export default async function PortfolioDetail(props: IPortfolioDetailProps) { 35 | const { locale, slug } = await props.params; 36 | setRequestLocale(locale); 37 | const t = await getTranslations({ 38 | locale, 39 | namespace: 'PortfolioSlug', 40 | }); 41 | 42 | return ( 43 | <> 44 |

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

    45 |

    {t('content')}

    46 | 47 |
    48 | {`${t('code_review_powered_by')} `} 49 | 53 | CodeRabbit 54 | 55 |
    56 | 57 | 60 | CodeRabbit 67 | 68 | 69 | ); 70 | }; 71 | 72 | export const dynamicParams = false; 73 | -------------------------------------------------------------------------------- /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('Value must be between 1 and 3')).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' }).isEnabled(); 44 | await page.getByRole('button', { name: 'Increment' }).click(); 45 | 46 | await expect(page.getByText('Count:')).toHaveText(`Count: ${countNumber + 2}`); 47 | 48 | await page.getByLabel('Increment by').fill('3'); 49 | await page.getByRole('button', { name: 'Increment' }).isEnabled(); 50 | await page.getByRole('button', { name: 'Increment' }).click(); 51 | 52 | await expect(page.getByText('Count:')).toHaveText(`Count: ${countNumber + 5}`); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /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 | Next.js Boilerplate 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | devIndicators: { 10 | position: 'bottom-right', 11 | }, 12 | poweredByHeader: false, 13 | reactStrictMode: true, 14 | reactCompiler: true, 15 | outputFileTracingIncludes: { 16 | '/': ['./migrations/**/*'], 17 | }, 18 | experimental: { 19 | turbopackFileSystemCacheForDev: true, 20 | }, 21 | }; 22 | 23 | // Initialize the Next-Intl plugin 24 | let configWithPlugins = createNextIntlPlugin('./src/libs/I18n.ts')(baseConfig); 25 | 26 | // Conditionally enable bundle analysis 27 | if (process.env.ANALYZE === 'true') { 28 | configWithPlugins = withBundleAnalyzer()(configWithPlugins); 29 | } 30 | 31 | // Conditionally enable Sentry configuration 32 | if (!process.env.NEXT_PUBLIC_SENTRY_DISABLED) { 33 | configWithPlugins = withSentryConfig(configWithPlugins, { 34 | // For all available options, see: 35 | // https://www.npmjs.com/package/@sentry/webpack-plugin#options 36 | org: process.env.SENTRY_ORGANIZATION, 37 | project: process.env.SENTRY_PROJECT, 38 | 39 | // Only print logs for uploading source maps in CI 40 | silent: !process.env.CI, 41 | 42 | // For all available options, see: 43 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ 44 | 45 | // Upload a larger set of source maps for prettier stack traces (increases build time) 46 | widenClientFileUpload: true, 47 | 48 | // Upload a larger set of source maps for prettier stack traces (increases build time) 49 | reactComponentAnnotation: { 50 | enabled: true, 51 | }, 52 | 53 | // Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers. 54 | // This can increase your server load as well as your hosting bill. 55 | // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client- 56 | // side errors will fail. 57 | tunnelRoute: '/monitoring', 58 | 59 | // Automatically tree-shake Sentry logger statements to reduce bundle size 60 | disableLogger: true, 61 | 62 | // Disable Sentry telemetry 63 | telemetry: false, 64 | }); 65 | } 66 | 67 | const nextConfig = configWithPlugins; 68 | export default nextConfig; 69 | -------------------------------------------------------------------------------- /public/assets/images/arcjet-dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/images/arcjet-light.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/proxy.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 proxy( 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(req); 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 `/_next`, `/_vercel` or `monitoring` 76 | // - … the ones containing a dot (e.g. `favicon.ico`) 77 | matcher: '/((?!_next|_vercel|monitoring|.*\\..*).*)', 78 | }; 79 | -------------------------------------------------------------------------------- /src/app/[locale]/(marketing)/portfolio/page.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import { getTranslations, setRequestLocale } from 'next-intl/server'; 3 | import Image from 'next/image'; 4 | import Link from 'next/link'; 5 | 6 | type IPortfolioProps = { 7 | params: Promise<{ locale: string }>; 8 | }; 9 | 10 | export async function generateMetadata(props: IPortfolioProps): Promise { 11 | const { locale } = await props.params; 12 | const t = await getTranslations({ 13 | locale, 14 | namespace: 'Portfolio', 15 | }); 16 | 17 | return { 18 | title: t('meta_title'), 19 | description: t('meta_description'), 20 | }; 21 | } 22 | 23 | export default async function Portfolio(props: IPortfolioProps) { 24 | const { locale } = await props.params; 25 | setRequestLocale(locale); 26 | const t = await getTranslations({ 27 | locale, 28 | namespace: 'Portfolio', 29 | }); 30 | 31 | return ( 32 | <> 33 |

    {t('presentation')}

    34 | 35 |
    36 | {Array.from(Array.from({ length: 6 }).keys()).map(elt => ( 37 | 42 | {t('portfolio_name', { name: elt })} 43 | 44 | ))} 45 |
    46 | 47 |
    48 | {`${t('error_reporting_powered_by')} `} 49 | 53 | Sentry 54 | 55 | {` - ${t('coverage_powered_by')} `} 56 | 60 | Codecov 61 | 62 |
    63 | 64 | 67 | Sentry 74 | 75 | 76 | ); 77 | }; 78 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import type { ChromaticConfig } from '@chromatic-com/playwright'; 2 | import { defineConfig, devices } from '@playwright/test'; 3 | 4 | // Use process.env.PORT by default and fallback to port 3000 5 | const PORT = process.env.PORT || 3000; 6 | 7 | // Set webServer.url and use.baseURL with the location of the WebServer respecting the correct set port 8 | const baseURL = `http://localhost:${PORT}`; 9 | 10 | /** 11 | * See https://playwright.dev/docs/test-configuration. 12 | */ 13 | export default defineConfig({ 14 | testDir: './tests', 15 | // Look for files with the .spec.js or .e2e.js extension 16 | testMatch: '*.@(spec|e2e).?(c|m)[jt]s?(x)', 17 | // Timeout per test, test running locally are slower due to database connections with PGLite 18 | timeout: 60 * 1000, 19 | // Fail the build on CI if you accidentally left test.only in the source code. 20 | forbidOnly: !!process.env.CI, 21 | // Reporter to use. See https://playwright.dev/docs/test-reporters 22 | reporter: process.env.CI ? 'github' : 'list', 23 | 24 | expect: { 25 | // Set timeout for async expect matchers 26 | timeout: 20 * 1000, 27 | }, 28 | 29 | // Run your local dev server before starting the tests: 30 | // https://playwright.dev/docs/test-advanced#launching-a-development-web-server-during-the-tests 31 | webServer: { 32 | command: process.env.CI ? 'npx run-p db-server:memory start' : 'npx run-p db-server:memory dev:next', 33 | url: baseURL, 34 | timeout: 2 * 60 * 1000, 35 | reuseExistingServer: !process.env.CI, 36 | env: { 37 | NEXT_PUBLIC_SENTRY_DISABLED: 'true', 38 | }, 39 | }, 40 | 41 | // Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. 42 | use: { 43 | // Use baseURL so to make navigations relative. 44 | // More information: https://playwright.dev/docs/api/class-testoptions#test-options-base-url 45 | baseURL, 46 | 47 | // Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer 48 | trace: process.env.CI ? 'on' : 'retain-on-failure', 49 | 50 | // Record videos when retrying the failed test. 51 | video: process.env.CI ? 'retain-on-failure' : undefined, 52 | 53 | // Disable automatic screenshots at test completion when using Chromatic test fixture. 54 | disableAutoSnapshot: true, 55 | }, 56 | 57 | projects: [ 58 | { 59 | name: 'chromium', 60 | use: { ...devices['Desktop Chrome'] }, 61 | }, 62 | ...(process.env.CI 63 | ? [ 64 | { 65 | name: 'firefox', 66 | use: { ...devices['Desktop Firefox'] }, 67 | }, 68 | ] 69 | : []), 70 | ], 71 | }); 72 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | "security_powered_by": "Security, bot detection and rate limiting powered by" 23 | }, 24 | "CounterForm": { 25 | "presentation": "The counter is stored in the database and incremented by the value you provide.", 26 | "label_increment": "Increment by", 27 | "button_increment": "Increment", 28 | "error_increment_range": "Value must be between 1 and 3" 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.", 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": "Need advanced features? Multi-tenancy & Teams, Roles & Permissions, Shadcn UI, End-to-End Typesafety with oRPC, Stripe Payment, Light / Dark mode. 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 | -------------------------------------------------------------------------------- /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": [ 11 | "dom", 12 | "dom.iterable", 13 | "esnext" 14 | ], 15 | "moduleResolution": "bundler", 16 | "isolatedModules": true, 17 | // ====================================================================== 18 | // Type Safety - Foundation 19 | // Core type checking settings for a robust codebase 20 | // ====================================================================== 21 | "strict": true, 22 | "alwaysStrict": true, 23 | "strictNullChecks": true, 24 | "noImplicitAny": true, 25 | "noImplicitThis": true, 26 | // ====================================================================== 27 | // Type Safety - Advanced 28 | // Additional checks for higher code quality 29 | // ====================================================================== 30 | "noUncheckedIndexedAccess": true, 31 | "noImplicitReturns": true, 32 | "noUnusedLocals": true, 33 | "noUnusedParameters": true, 34 | "noFallthroughCasesInSwitch": true, 35 | "allowUnreachableCode": false, 36 | "useUnknownInCatchVariables": true, 37 | "noImplicitOverride": true, 38 | // ====================================================================== 39 | // Interoperability 40 | // Settings for working with different file types and modules 41 | // ====================================================================== 42 | "allowJs": true, 43 | "checkJs": true, 44 | "esModuleInterop": true, 45 | "resolveJsonModule": true, 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 | // Project Structure 56 | // Configure import paths and module resolution 57 | // ====================================================================== 58 | "baseUrl": ".", 59 | "paths": { 60 | "@/*": [ 61 | "./src/*" 62 | ], 63 | "@/public/*": [ 64 | "./public/*" 65 | ] 66 | }, 67 | // ====================================================================== 68 | // Next.js Project Configuration 69 | // Controls settings specific to Next.js framework 70 | // ====================================================================== 71 | "jsx": "react-jsx", // Uses the React automatic runtime 72 | "incremental": true, // Enable faster incremental builds 73 | "noEmit": true, // Skip emitting files (Next.js handles this) 74 | "plugins": [ 75 | { 76 | "name": "next" 77 | } 78 | ] // Enable Next.js TypeScript plugin 79 | }, 80 | // Files to include/exclude from the project 81 | "exclude": [ 82 | "node_modules", 83 | "**/*.spec.ts", 84 | "**/*.e2e.ts" 85 | ], 86 | "include": [ 87 | "next-env.d.ts", 88 | "**/*.ts", 89 | "**/*.tsx", 90 | ".next/types/**/*.ts", 91 | ".next/dev/types/**/*.ts", 92 | "**/*.mts" 93 | ] 94 | } 95 | -------------------------------------------------------------------------------- /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 | "security_powered_by": "Sécurité, détection de bot et rate limiting propulsés par" 23 | }, 24 | "CounterForm": { 25 | "presentation": "Le compteur est stocké dans la base de données et incrémenté par la valeur que vous fournissez.", 26 | "label_increment": "Incrémenter de", 27 | "button_increment": "Incrémenter", 28 | "error_increment_range": "La valeur doit être entre 1 et 3" 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.", 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": "Besoin de fonctionnalités avancées ? Multi-tenant et équipes, rôles et permissions, Shadcn UI, typage de bout en bout avec oRPC, paiement Stripe, mode clair / sombre. 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-js-boilerplate", 3 | "author": "Ixartz (https://github.com/ixartz)", 4 | "engines": { 5 | "node": ">=20" 6 | }, 7 | "scripts": { 8 | "dev:spotlight": "npx @spotlightjs/spotlight", 9 | "dev:next": "next dev", 10 | "dev": "npm-run-all db-remote:neon db:migrate --parallel dev:*", 11 | "build:next": "next build --webpack", 12 | "build-local": "run-p db-server:memory build:next --race", 13 | "build": "run-s db:migrate build:next", 14 | "start": "next start", 15 | "build-stats": "cross-env ANALYZE=true npm run build", 16 | "clean": "rimraf .next out coverage", 17 | "lint": "eslint .", 18 | "lint:fix": "eslint . --fix", 19 | "check:types": "tsc --noEmit --pretty", 20 | "check:deps": "knip", 21 | "check:i18n": "i18n-check -l src/locales -s en -u src -f next-intl", 22 | "commit": "commit", 23 | "test": "vitest run", 24 | "test:e2e": "playwright test", 25 | "db-remote:neon": "npx get-db --yes --env .env.local --ref nextjs-boilerplate", 26 | "neon:claim": "npx get-db claim --env .env.local --ref nextjs-boilerplate", 27 | "db-server:file": "pglite-server --db=local.db --run 'npm run db:migrate'", 28 | "db-server:memory": "pglite-server --run 'npm run db:migrate'", 29 | "db:generate": "drizzle-kit generate", 30 | "db:migrate": "dotenv -c -- drizzle-kit migrate", 31 | "db:studio": "drizzle-kit studio", 32 | "storybook": "storybook dev -p 6006", 33 | "storybook:test": "vitest run --config .storybook/vitest.config.mts", 34 | "build-storybook": "storybook build" 35 | }, 36 | "dependencies": { 37 | "@arcjet/next": "^1.0.0-beta.15", 38 | "@clerk/localizations": "^3.29.0", 39 | "@clerk/nextjs": "^6.35.6", 40 | "@hookform/resolvers": "^5.2.2", 41 | "@logtape/logtape": "^1.2.2", 42 | "@sentry/nextjs": "^10.28.0", 43 | "@t3-oss/env-nextjs": "^0.13.8", 44 | "drizzle-orm": "^0.44.7", 45 | "next": "^16.0.10", 46 | "next-intl": "^4.5.8", 47 | "pg": "^8.16.3", 48 | "posthog-js": "^1.300.0", 49 | "react": "^19.2.3", 50 | "react-dom": "^19.2.3", 51 | "react-hook-form": "^7.67.0", 52 | "zod": "^4.1.13" 53 | }, 54 | "devDependencies": { 55 | "@antfu/eslint-config": "^6.3.0", 56 | "@chromatic-com/playwright": "^0.12.7", 57 | "@commitlint/cli": "^20.1.0", 58 | "@commitlint/config-conventional": "^20.0.0", 59 | "@commitlint/prompt-cli": "^20.1.0", 60 | "@electric-sql/pglite-socket": "^0.0.19", 61 | "@eslint-react/eslint-plugin": "^2.3.12", 62 | "@faker-js/faker": "^10.1.0", 63 | "@lingual/i18n-check": "^0.8.14", 64 | "@next/bundle-analyzer": "^16.0.10", 65 | "@next/eslint-plugin-next": "^16.0.10", 66 | "@playwright/test": "^1.57.0", 67 | "@spotlightjs/spotlight": "4.5.1", 68 | "@storybook/addon-a11y": "^10.1.4", 69 | "@storybook/addon-docs": "^10.1.4", 70 | "@storybook/addon-vitest": "^10.1.4", 71 | "@storybook/nextjs-vite": "^10.1.4", 72 | "@tailwindcss/postcss": "^4.1.17", 73 | "@types/node": "^24.10.1", 74 | "@types/pg": "^8.15.6", 75 | "@types/react": "^19.2.7", 76 | "@vitejs/plugin-react": "^5.1.1", 77 | "@vitest/browser": "^4.0.15", 78 | "@vitest/browser-playwright": "^4.0.15", 79 | "@vitest/coverage-v8": "^4.0.15", 80 | "babel-plugin-react-compiler": "^1.0.0", 81 | "checkly": "^6.9.5", 82 | "conventional-changelog-conventionalcommits": "^9.1.0", 83 | "cross-env": "^10.1.0", 84 | "dotenv-cli": "^11.0.0", 85 | "drizzle-kit": "^0.31.7", 86 | "eslint": "^9.39.1", 87 | "eslint-plugin-format": "^1.0.2", 88 | "eslint-plugin-jsx-a11y": "^6.10.2", 89 | "eslint-plugin-playwright": "^2.4.0", 90 | "eslint-plugin-react-hooks": "^7.0.1", 91 | "eslint-plugin-react-refresh": "^0.4.24", 92 | "eslint-plugin-storybook": "^10.1.4", 93 | "eslint-plugin-tailwindcss": "^4.0.0-beta.0", 94 | "get-db": "^0.11.0", 95 | "knip": "^5.71.0", 96 | "lefthook": "^2.0.7", 97 | "npm-run-all": "^4.1.5", 98 | "postcss": "^8.5.6", 99 | "postcss-load-config": "^6.0.1", 100 | "rimraf": "^6.1.2", 101 | "semantic-release": "^25.0.2", 102 | "storybook": "^10.1.4", 103 | "tailwindcss": "^4.1.17", 104 | "typescript": "^5.9.3", 105 | "vite-tsconfig-paths": "^5.1.4", 106 | "vitest": "^4.0.15", 107 | "vitest-browser-react": "^2.0.2" 108 | }, 109 | "release": { 110 | "branches": [ 111 | "main" 112 | ], 113 | "plugins": [ 114 | [ 115 | "@semantic-release/commit-analyzer", 116 | { 117 | "preset": "conventionalcommits" 118 | } 119 | ], 120 | "@semantic-release/release-notes-generator", 121 | "@semantic-release/github" 122 | ] 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /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 | Better Stack 92 | 93 | 95 | 100 | PostHog 106 | 107 | 109 | 114 | Checkly 120 | 121 |
    125 | 126 | Next.js SaaS Boilerplate 132 | 133 |
    137 | ); 138 | -------------------------------------------------------------------------------- /src/app/[locale]/(marketing)/page.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import { getTranslations, setRequestLocale } from 'next-intl/server'; 3 | import { Sponsors } from '@/components/Sponsors'; 4 | 5 | type IIndexProps = { 6 | params: Promise<{ locale: string }>; 7 | }; 8 | 9 | export async function generateMetadata(props: IIndexProps): Promise { 10 | const { locale } = await props.params; 11 | const t = await getTranslations({ 12 | locale, 13 | namespace: 'Index', 14 | }); 15 | 16 | return { 17 | title: t('meta_title'), 18 | description: t('meta_description'), 19 | }; 20 | } 21 | 22 | export default async function Index(props: IIndexProps) { 23 | const { locale } = await props.params; 24 | setRequestLocale(locale); 25 | const t = await getTranslations({ 26 | locale, 27 | namespace: 'Index', 28 | }); 29 | 30 | return ( 31 | <> 32 |

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

    44 |

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

    47 |

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

    56 |
      57 |
    • 🚀 Next.js with App Router support
    • 58 |
    • 🔥 TypeScript for type checking
    • 59 |
    • 💎 Tailwind CSS integration
    • 60 |
    • 61 | 🔒 Authentication with 62 | {' '} 63 | 67 | Clerk 68 | 69 | {' '} 70 | (includes passwordless, social, and multi-factor auth) 71 |
    • 72 |
    • 📦 ORM with DrizzleORM (PostgreSQL, SQLite, MySQL support)
    • 73 |
    • 74 | 💽 Dev database with PGlite and production with Neon (PostgreSQL) 75 |
    • 76 |
    • 77 | 🌐 Multi-language support (i18n) with next-intl and 78 | {' '} 79 | 83 | Crowdin 84 | 85 |
    • 86 |
    • 🔴 Form handling (React Hook Form) and validation (Zod)
    • 87 |
    • 📏 Linting and formatting (ESLint, Prettier)
    • 88 |
    • 🦊 Git hooks and commit linting (Husky, Commitlint)
    • 89 |
    • 🦺 Testing suite (Vitest, React Testing Library, Playwright)
    • 90 |
    • 🎉 Storybook for UI development
    • 91 |
    • 92 | 🐰 AI-powered code reviews with 93 | {' '} 94 | 98 | CodeRabbit 99 | 100 |
    • 101 |
    • 102 | 🚨 Error monitoring ( 103 | 107 | Sentry 108 | 109 | ) and logging (LogTape, an alternative to Pino.js) 110 |
    • 111 |
    • 🖥️ Monitoring as Code (Checkly)
    • 112 |
    • 113 | 🔐 Security and bot protection ( 114 | 118 | Arcjet 119 | 120 | ) 121 |
    • 122 |
    • 🤖 SEO optimization (metadata, JSON-LD, Open Graph tags)
    • 123 |
    • ⚙️ Development tools (VSCode config, bundler analyzer, changelog generation)
    • 124 |
    125 |

    126 | Our sponsors' exceptional support has made this project possible. 127 | Their services integrate seamlessly with the boilerplate, and we 128 | recommend trying them out. 129 |

    130 |

    {t('sponsors_title')}

    131 | 132 | 133 | ); 134 | }; 135 | -------------------------------------------------------------------------------- /.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: [22.x, 24.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 | timeout-minutes: 10 19 | 20 | steps: 21 | - uses: actions/checkout@v6 22 | 23 | - name: Set up Node.js environment 24 | uses: ./.github/actions/setup-project 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | 28 | - name: Restore or cache Next.js build 29 | uses: actions/cache@v4 30 | with: 31 | path: | 32 | .next/cache 33 | # Generate a new cache whenever packages or source files change. 34 | key: nextjs-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('src/**') }} 35 | 36 | - name: Build Next.js 37 | run: npm run build-local 38 | env: 39 | NEXT_PUBLIC_SENTRY_DISABLED: 'true' # Only upload Sentry source maps in deployment 40 | 41 | - if: matrix.node-version == '22.x' && success() 42 | name: Cache Next.js build output 43 | uses: actions/cache/save@v4 44 | with: 45 | path: | 46 | .next 47 | key: nextjs-build-${{ matrix.node-version }}-${{ github.sha }} 48 | 49 | static: 50 | strategy: 51 | matrix: 52 | node-version: [22.x] 53 | 54 | name: Run static checks 55 | runs-on: ubuntu-latest 56 | timeout-minutes: 10 57 | 58 | steps: 59 | - uses: actions/checkout@v6 60 | with: 61 | fetch-depth: 0 # Retrieve Git history, needed to verify commits 62 | 63 | - name: Set up Node.js environment 64 | uses: ./.github/actions/setup-project 65 | with: 66 | node-version: ${{ matrix.node-version }} 67 | 68 | - if: github.event_name == 'pull_request' 69 | name: Validate all commits from PR 70 | run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose 71 | 72 | - name: Linter 73 | run: npm run lint 74 | 75 | - name: Type checking 76 | run: npm run check:types 77 | 78 | - name: Check dependencies 79 | run: npm run check:deps 80 | 81 | - name: I18n check 82 | run: npm run check:i18n 83 | 84 | unit: 85 | strategy: 86 | matrix: 87 | node-version: [22.x] 88 | 89 | name: Run unit tests 90 | runs-on: ubuntu-latest 91 | timeout-minutes: 10 92 | needs: [build] 93 | 94 | steps: 95 | - uses: actions/checkout@v6 96 | 97 | - name: Set up Node.js environment 98 | uses: ./.github/actions/setup-project 99 | with: 100 | node-version: ${{ matrix.node-version }} 101 | 102 | - name: Run unit tests 103 | uses: docker://mcr.microsoft.com/playwright:v1.57.0 104 | with: 105 | args: npm run test -- --coverage 106 | 107 | - name: Upload coverage reports to Codecov 108 | uses: codecov/codecov-action@v5 109 | env: 110 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 111 | 112 | storybook: 113 | strategy: 114 | matrix: 115 | node-version: [22.x] 116 | 117 | name: Run Storybook 118 | runs-on: ubuntu-latest 119 | timeout-minutes: 10 120 | needs: [build] 121 | 122 | steps: 123 | - uses: actions/checkout@v6 124 | 125 | - name: Set up Node.js environment 126 | uses: ./.github/actions/setup-project 127 | with: 128 | node-version: ${{ matrix.node-version }} 129 | 130 | - name: Run storybook tests 131 | uses: docker://mcr.microsoft.com/playwright:v1.57.0 132 | with: 133 | args: npm run storybook:test 134 | 135 | e2e: 136 | strategy: 137 | matrix: 138 | node-version: [22.x] 139 | 140 | name: Run E2E tests 141 | runs-on: ubuntu-latest 142 | timeout-minutes: 10 143 | needs: [build] 144 | 145 | steps: 146 | - uses: actions/checkout@v6 147 | with: 148 | fetch-depth: 0 # For chromatic 149 | 150 | - name: Set up Node.js environment 151 | uses: ./.github/actions/setup-project 152 | with: 153 | node-version: ${{ matrix.node-version }} 154 | restore-nextjs-cache: true 155 | 156 | - name: Run E2E tests 157 | uses: docker://mcr.microsoft.com/playwright:v1.57.0 158 | with: 159 | args: sh -c "HOME=/root npm run test:e2e" # Set HOME to /root to avoid Playwright error with Firebox 160 | env: 161 | CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }} 162 | 163 | - name: Fix test results permission # Give permissions to test results needed by Chromatic 164 | run: | 165 | sudo chmod -R 777 test-results 166 | 167 | - name: Run visual regression tests 168 | uses: chromaui/action@v13 169 | with: 170 | playwright: true 171 | exitOnceUploaded: true # Speed up by skipping the build results 172 | outputDir: storybook-static 173 | projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} 174 | 175 | - name: Upload test results 176 | uses: actions/upload-artifact@v5 177 | if: always() 178 | with: 179 | name: test-results 180 | path: test-results/ 181 | retention-days: 7 182 | 183 | synchronize-with-crowdin: 184 | name: GitHub PR synchronize with Crowdin 185 | runs-on: ubuntu-latest 186 | timeout-minutes: 10 187 | 188 | needs: [build, static] 189 | if: github.event_name == 'pull_request' 190 | 191 | steps: 192 | - uses: actions/checkout@v6 193 | with: 194 | ref: ${{ github.event.pull_request.head.sha }} # Crowdin Actions needs to push commits to the PR branch, checkout HEAD commit instead of merge commit 195 | fetch-depth: 0 196 | 197 | - name: Crowdin action 198 | uses: crowdin/github-action@v2 199 | with: 200 | upload_sources: true 201 | upload_translations: true 202 | download_translations: true 203 | create_pull_request: false 204 | localization_branch_name: ${{ github.head_ref || github.ref_name }} # explanation here: https://stackoverflow.com/a/71158878 205 | commit_message: 'chore: new Crowdin translations by GitHub Action' 206 | env: 207 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 208 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} 209 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} 210 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Boilerplate and Starter for Next.js 16+, 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, Lefthook (replacing 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), Local database with PGlite and production with Neon (PostgreSQL), 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 LogTape (replacing Pino.js) and Log Management, Monitoring as Code, Storybook, Multi-language (i18n), AI-powered code reviews with CodeRabbit, 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 | Better Stack 92 | 93 | 94 | 96 | 97 | 98 | 99 | 100 | PostHog 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 | - ☁️ Remote and production database with Neon (PostgreSQL) 149 | - 🌐 Multi-language (i18n) with next-intl and [Crowdin](https://l.crowdin.com/next-js) 150 | - ♻️ Type-safe environment variables with T3 Env 151 | - ⌨️ Form handling with React Hook Form 152 | - 🔴 Validation library with Zod 153 | - 📏 Linter with [ESLint](https://eslint.org) (default Next.js, Next.js Core Web Vitals, Tailwind CSS and Antfu configuration) 154 | - 💖 Code Formatter with Prettier 155 | - 🦊 Husky for Git Hooks (replaced by Lefthook) 156 | - 🚫 Lint-staged for running linters on Git staged files 157 | - 🚓 Lint git commit with Commitlint 158 | - 📓 Write standard compliant commit messages with Commitizen 159 | - 🔍 Unused files and dependencies detection with Knip 160 | - 🌍 I18n validation and missing translation detection with i18n-check 161 | - 🦺 Unit Testing with Vitest and Browser mode (replacing React Testing Library) 162 | - 🧪 Integration and E2E Testing with Playwright 163 | - 👷 Run tests on pull request with GitHub Actions 164 | - 🎉 Storybook for UI development 165 | - 🐰 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) 166 | - 🚨 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) 167 | - 🔍 Local development error monitoring with Sentry Spotlight 168 | - ☂️ 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) 169 | - 📝 Logging with LogTape and Log Management with [Better Stack](https://betterstack.com/?utm_source=github&utm_medium=sponsorship&utm_campaign=next-js-boilerplate) 170 | - 🖥️ Monitoring as Code with [Checkly](https://www.checklyhq.com/?utm_source=github&utm_medium=sponsorship&utm_campaign=next-js-boilerplate) 171 | - 🔐 Security and bot protection ([Arcjet](https://launch.arcjet.com/Q6eLbRE)) 172 | - 📊 Analytics with PostHog 173 | - 🎁 Automatic changelog generation with Semantic Release 174 | - 🔍 Visual regression testing 175 | - 💡 Absolute Imports using `@` prefix 176 | - 🗂 VSCode configuration: Debug, Settings, Tasks and Extensions 177 | - 🤖 SEO metadata, JSON-LD and Open Graph tags 178 | - 🗺️ Sitemap.xml and robots.txt 179 | - 👷 Automatic dependency updates with Dependabot 180 | - ⌘ Database exploration with Drizzle Studio and CLI migration tool with Drizzle Kit 181 | - ⚙️ Bundler Analyzer 182 | - 🌈 Include a FREE minimalist theme 183 | - 💯 Maximize lighthouse score 184 | 185 | Built-in features from Next.js: 186 | 187 | - ☕ Minify HTML & CSS 188 | - 💨 Live reload 189 | - ✅ Cache busting 190 | 191 | Optional features (easy to add): 192 | 193 | - 🔑 Multi-tenancy, Role-based access control (RBAC) 194 | - 🔐 OAuth for Single Sign-On (SSO), Enterprise SSO, SAML, OpenID Connect (OIDC), EASIE 195 | - 🔗 Web 3 (Base, MetaMask, Coinbase Wallet, OKX Wallet) 196 | 197 | ### Philosophy 198 | 199 | - Nothing is hidden from you, allowing you to make any necessary adjustments to suit your requirements and preferences. 200 | - Dependencies are regularly updated on a monthly basis 201 | - Start for free without upfront costs 202 | - Easy to customize 203 | - Minimal code 204 | - Unstyled template 205 | - SEO-friendly 206 | - 🚀 Production-ready 207 | 208 | ### Requirements 209 | 210 | - Node.js 22+ and npm 211 | 212 | ### Getting started 213 | 214 | Run the following command on your local environment: 215 | 216 | ```shell 217 | git clone --depth=1 https://github.com/ixartz/Next-js-Boilerplate.git my-project-name 218 | cd my-project-name 219 | npm install 220 | ``` 221 | 222 | For your information, all dependencies are updated every month. 223 | 224 | Then, you can run the project locally in development mode with live reload by executing: 225 | 226 | ```shell 227 | npm run dev 228 | ``` 229 | 230 | Open http://localhost:3000 with your favorite browser to see your project. For your information, the project is already pre-configured with a database. 231 | 232 | > [!WARNING] 233 | > Next.js Boilerplate ships with a fully working Postgres database for your local environment. This database is **temporary** and will expire after **72 hours** if you don't claim it. 234 | > 235 | > Once expired, the project won't be able to connect to the database, and it'll throw connection errors. 236 | > 237 | > To avoid the connection errors and make the database **persistent**, run `npm run neon:claim`. After claiming it, the database becomes persistent and suitable for production use as well. 238 | 239 | > [!CAUTION] 240 | > The authentication system requires environment variables to be set up. Please refer to the [Set up authentication](#set-up-authentication) section. 241 | 242 | Need advanced features? Multi-tenancy & Teams, Roles & Permissions, Shadcn UI, End-to-End Typesafety with oRPC, Stripe Payment, Light / Dark mode. Try [Next.js Boilerplate Pro](https://nextjs-boilerplate.com/pro-saas-starter-kit). 243 | 244 | ### Set up authentication 245 | 246 | 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): 247 | 248 | ```shell 249 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_clerk_pub_key 250 | CLERK_SECRET_KEY=your_clerk_secret_key 251 | ``` 252 | 253 | 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. 254 | 255 | ### Set up remote database 256 | 257 | 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. 258 | 259 | When you launch the project locally for the first time, it automatically creates a temporary PostgreSQL database. This allows you to work with a PostgreSQL database without Docker or any additional setup. 260 | 261 | This temporary database will **expire after 72 hours** if you don't claim it. To avoid connection errors and **make the database persistent**, simply run the following command: 262 | 263 | ```shell 264 | npm run neon:claim 265 | ``` 266 | 267 | Then, follow the instructions provided in the terminal to complete the claiming process. 268 | 269 | Once claimed, the database is suitable for production use. You can create separate database branches for development, staging, and production environments to keep data isolated. 270 | 271 | #### Create a fresh and empty database 272 | 273 | If you want to create a fresh and empty database, you just need to remove the following environment variables: `DATABASE_URL`, `DATABASE_URL_DIRECT` and `PUBLIC_INSTAGRES_CLAIM_URL` from the `.env.local` file. 274 | 275 | Then, run the following command to create a new temporary database: 276 | 277 | ```shell 278 | npm run dev 279 | ``` 280 | 281 | After the database is created, the connection strings will be automatically added to your `.env.local` file. And, don't forget to claim the database with `npm run neon:claim`. 282 | 283 | ### Translation (i18n) setup 284 | 285 | 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. 286 | 287 | 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`. 288 | 289 | 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. 290 | 291 | ### Project structure 292 | 293 | ```shell 294 | . 295 | ├── README.md # README file 296 | ├── .github # GitHub folder 297 | │ ├── actions # Reusable actions 298 | │ └── workflows # GitHub Actions workflows 299 | ├── .storybook # Storybook folder 300 | ├── .vscode # VSCode configuration 301 | ├── migrations # Database migrations 302 | ├── public # Public assets folder 303 | ├── src 304 | │ ├── app # Next JS App (App Router) 305 | │ ├── components # React components 306 | │ ├── libs # 3rd party libraries configuration 307 | │ ├── locales # Locales folder (i18n messages) 308 | │ ├── models # Database models 309 | │ ├── styles # Styles folder 310 | │ ├── templates # Templates folder 311 | │ ├── types # Type definitions 312 | │ ├── utils # Utilities folder 313 | │ └── validations # Validation schemas 314 | ├── tests 315 | │ ├── e2e # E2E tests, also includes Monitoring as Code 316 | │ └── integration # Integration tests 317 | ├── next.config.ts # Next JS configuration 318 | └── tsconfig.json # TypeScript configuration 319 | ``` 320 | 321 | ### Customization 322 | 323 | 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: 324 | 325 | - `public/apple-touch-icon.png`, `public/favicon.ico`, `public/favicon-16x16.png` and `public/favicon-32x32.png`: your website favicon 326 | - `src/utils/AppConfig.ts`: configuration file 327 | - `src/templates/BaseTemplate.tsx`: default theme 328 | - `next.config.ts`: Next.js configuration 329 | - `.env`: default environment variables 330 | 331 | 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 🚀. 332 | 333 | ### Change database schema 334 | 335 | 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. 336 | 337 | After making changes to the schema, generate a migration by running the following command: 338 | 339 | ```shell 340 | npm run db:generate 341 | ``` 342 | 343 | This will create a migration file that reflects your schema changes. 344 | 345 | After making sure your database is running, you can apply the generated migration using: 346 | 347 | ```shell 348 | npm run db:migrate 349 | ``` 350 | 351 | There is no need to restart the Next.js server for the changes to take effect. 352 | 353 | ### Commit Message Format 354 | 355 | 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 provides an interactive CLI that guides you through the commit process. To use it, run the following command: 356 | 357 | ```shell 358 | npm run commit 359 | ``` 360 | 361 | One of the benefits of using Conventional Commits is the ability to automatically generate GitHub releases. It also allows us to automatically determine the next version number based on the types of commits that are included in a release. 362 | 363 | ### CodeRabbit AI Code Reviews 364 | 365 | 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. 366 | 367 | 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! 368 | 369 | ### Testing 370 | 371 | All unit tests are located alongside the source code in the same directory, making them easier to find. The unit test files follow this format: `*.test.ts` or `*.test.tsx`. The project uses Vitest and React Testing Library for unit testing. You can run the tests with the following command: 372 | 373 | ```shell 374 | npm run test 375 | ``` 376 | 377 | ### Integration & E2E Testing 378 | 379 | The project uses Playwright for integration and end-to-end (E2E) testing. Integration test files use the `*.spec.ts` extension, while E2E test files use the `*.e2e.ts` extension. You can run the tests with the following commands: 380 | 381 | ```shell 382 | npx playwright install # Only for the first time in a new environment 383 | npm run test:e2e 384 | ``` 385 | 386 | ### Storybook 387 | 388 | Storybook is configured for UI component development and testing. The project uses Storybook with Next.js and Vite integration, including accessibility testing and documentation features. 389 | 390 | Stories are located alongside your components in the `src` directory and follow the pattern `*.stories.ts` or `*.stories.tsx`. 391 | 392 | You can run Storybook in development mode with: 393 | 394 | ```shell 395 | npm run storybook 396 | ``` 397 | 398 | This will start Storybook on http://localhost:6006 where you can view and interact with your UI components in isolation. 399 | 400 | To run Storybook tests in headless mode, you can use the following command: 401 | 402 | ```shell 403 | npm run storybook:test 404 | ``` 405 | 406 | ### Local Production Build 407 | 408 | Generate an optimized production build locally using a temporary in-memory Postgres database: 409 | 410 | ```shell 411 | npm run build-local 412 | ``` 413 | 414 | This command: 415 | 416 | - Starts a temporary in-memory Database server 417 | - Runs database migrations with Drizzle Kit 418 | - Builds the Next.js app for production 419 | - Shuts down the temporary DB when the build finishes 420 | 421 | Notes: 422 | 423 | - By default, it uses a local database, but you can also use `npm run build` with a remote database. 424 | - This only creates the build, it doesn't start the server. To run the build locally, use `npm run start`. 425 | 426 | ### Deploy to production 427 | 428 | 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. 429 | 430 | Then, you can generate a production build with: 431 | 432 | ```shell 433 | $ npm run build 434 | ``` 435 | 436 | It generates an optimized production build of the boilerplate. To test the generated build, run: 437 | 438 | ```shell 439 | $ npm run start 440 | ``` 441 | 442 | You also need to defined the environment variables `CLERK_SECRET_KEY` using your own key. 443 | 444 | 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. 445 | 446 | ### Deploy to Sevalla 447 | 448 | You can deploy a Next.js application along with its database on a single platform. First, create an account on [Sevalla](https://sevalla.com). 449 | 450 | 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. 451 | 452 | 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`. 453 | 454 | 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`. 455 | 456 | 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`. 457 | 458 | 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. 459 | 460 | ### Error Monitoring 461 | 462 | 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. 463 | 464 | #### Local development with Sentry and Spotlight 465 | 466 | In the development environment, no additional setup is required: Next.js Boilerplate comes pre-configured with Sentry and Spotlight (Sentry for Development). All errors are automatically captured by your local Spotlight instance, enabling testing without sending data to Sentry Cloud. 467 | 468 | You can inspect captured events, view stack traces, and analyze errors in the Spotlight UI at `http://localhost:8969`. 469 | 470 | #### Production setup with Sentry 471 | 472 | For production environment, you'll need to create a Sentry account and a new project. Then, in `.env.production`, you need to update the following environment variables: 473 | 474 | ```shell 475 | NEXT_PUBLIC_SENTRY_DSN= 476 | SENTRY_ORGANIZATION= 477 | SENTRY_PROJECT= 478 | ``` 479 | 480 | You also need to create a environment variable `SENTRY_AUTH_TOKEN` in your hosting provider's dashboard. 481 | 482 | ### Code coverage 483 | 484 | 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. 485 | 486 | Make sure to create `CODECOV_TOKEN` as a GitHub Actions secret, do not paste it directly into your source code. 487 | 488 | ### Logging 489 | 490 | The project uses LogTape for logging. In the development environment, logs are displayed in the console by default. 491 | 492 | 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. 493 | 494 | After creating the source, you will be able to view and copy your source token. In your environment variables, paste the token into the `NEXT_PUBLIC_BETTER_STACK_SOURCE_TOKEN` variable. You'll also need to define the `NEXT_PUBLIC_BETTER_STACK_INGESTING_HOST` variable, which can be found in the same place as the source token. 495 | 496 | Now, all logs will automatically be sent to and ingested by Better Stack. 497 | 498 | ### Checkly monitoring 499 | 500 | 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. 501 | 502 | 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. 503 | 504 | To complete the setup, update the `checkly.config.ts` file with your own email address and production URL. 505 | 506 | ### Arcjet security and bot protection 507 | 508 | 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. 509 | 510 | 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. 511 | 512 | Arcjet is configured with two main features: bot detection and the Arcjet Shield WAF: 513 | 514 | - [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. 515 | - [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. 516 | 517 | 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 `proxy.ts`. 518 | 519 | ### Useful commands 520 | 521 | ### Code Quality and Validation 522 | 523 | The project includes several commands to ensure code quality and consistency. You can run: 524 | 525 | - `npm run lint` to check for linting errors 526 | - `npm run lint:fix` to automatically fix fixable issues from the linter 527 | - `npm run check:types` to verify type safety across the entire project 528 | - `npm run check:deps` help identify unused dependencies and files 529 | - `npm run check:i18n` ensures all translations are complete and properly formatted 530 | 531 | #### Bundle Analyzer 532 | 533 | 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: 534 | 535 | ```shell 536 | npm run build-stats 537 | ``` 538 | 539 | By running the command, it'll automatically open a new browser window with the results. 540 | 541 | #### Database Studio 542 | 543 | The project is already configured with Drizzle Studio to explore the database. You can run the following command to open the database studio: 544 | 545 | ```shell 546 | npm run db:studio 547 | ``` 548 | 549 | Then, you can open https://local.drizzle.studio with your favorite browser to explore your database. 550 | 551 | ### VSCode information (optional) 552 | 553 | 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. 554 | 555 | 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. 556 | 557 | Pro tips: if you need a project wide-type checking with TypeScript, you can run a build with Cmd + Shift + B on Mac. 558 | 559 | ### Contributions 560 | 561 | 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. 562 | 563 | ### License 564 | 565 | Licensed under the MIT License, Copyright © 2025 566 | 567 | See [LICENSE](LICENSE) for more information. 568 | 569 | ## Sponsors 570 | 571 | 572 | 573 | 582 | 591 | 607 | 608 | 609 | 618 | 627 | 636 | 637 | 638 | 647 | 656 | 665 | 666 | 667 | 672 | 677 | 678 |
    574 | 575 | 576 | 577 | 578 | Clerk – Authentication & User Management for Next.js 579 | 580 | 581 | 583 | 584 | 585 | 586 | 587 | CodeRabbit 588 | 589 | 590 | 592 | 593 | 594 | 595 | 596 | Sentry 597 | 598 | 599 | 600 | 601 | 602 | 603 | Codecov 604 | 605 | 606 |
    610 | 611 | 612 | 613 | 614 | Arcjet 615 | 616 | 617 | 619 | 620 | 621 | 622 | 623 | Sevalla 624 | 625 | 626 | 628 | 629 | 630 | 631 | 632 | Crowdin 633 | 634 | 635 |
    639 | 640 | 641 | 642 | 643 | Better Stack 644 | 645 | 646 | 648 | 649 | 650 | 651 | 652 | PostHog 653 | 654 | 655 | 657 | 658 | 659 | 660 | 661 | Checkly 662 | 663 | 664 |
    668 | 669 | Next.js SaaS Boilerplate with React 670 | 671 | 673 | 674 | Add your logo here 675 | 676 |
    679 | 680 | --- 681 | 682 | 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) 683 | 684 | 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! 685 | 686 | [![Sponsor Next JS Boilerplate](https://cdn.buymeacoffee.com/buttons/default-red.png)](https://github.com/sponsors/ixartz) 687 | --------------------------------------------------------------------------------