├── 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 |
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 |
43 |
44 |
45 |
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 |
43 |
44 |
47 |
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 |
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 |
56 |
57 |
60 |
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 |
36 |
37 |
{props.children}
38 |
39 |
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 |
63 |
64 |
67 |
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 |
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 |
24 |
--------------------------------------------------------------------------------
/public/assets/images/codecov-white.svg:
--------------------------------------------------------------------------------
1 |
29 |
--------------------------------------------------------------------------------
/public/assets/images/coderabbit-logo-dark.svg:
--------------------------------------------------------------------------------
1 |
22 |
--------------------------------------------------------------------------------
/public/assets/images/coderabbit-logo-light.svg:
--------------------------------------------------------------------------------
1 |
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Boilerplate and Starter for Next.js 16+, Tailwind CSS 4, and TypeScript.
2 |
3 |
4 |
5 |
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 |
127 |
128 | ### Demo
129 |
130 | **Live demo: [Next.js Boilerplate](https://demo.nextjs-boilerplate.com)**
131 |
132 | | Sign Up | Sign In |
133 | | --- | --- |
134 | | [](https://demo.nextjs-boilerplate.com/sign-up) | [](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 |
679 |
680 | ---
681 |
682 | Made with ♥ by [CreativeDesignsGuru](https://creativedesignsguru.com) [](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 | [](https://github.com/sponsors/ixartz)
687 |
--------------------------------------------------------------------------------