A modern monorepo built with Turborepo, pnpm workspaces, and Next.js 16, following individual package architecture for optimal build caching and performance.
website/
โโโ apps/
โ โโโ web/ # Next.js 16 app with App Router
โ โโโ content/ # MDX content (blog, projects)
โโโ configs/
โ โโโ eslint/ # Shared ESLint configs (base, Next.js, React library)
โ โโโ prettier/ # Shared Prettier config with Tailwind plugin
โ โโโ tailwind/ # Shared Tailwind config with design tokens
โ โโโ typescript/ # Shared TypeScript configs
โ โโโ vitest/ # Shared Vitest configs (base, React)
โโโ packages/
โโโ components/
โ โโโ design-system/
โ โ โโโ button/ # components.design-system.button
โ โโโ layout/
โ โโโ header/ # components.layout.header
โ โโโ footer/ # components.layout.footer
โโโ pages/
โ โโโ home/ # pages.home
โ โโโ about/ # pages.about
โ โโโ blog-list/ # pages.blog-list
โ โโโ blog-post/ # pages.blog-post
โโโ utils/
โโโ core/
โโโ cn/ # utils.core.cn
โโโ format/ # utils.core.format
Each component, utility, and page is its own package for optimal Turborepo performance:
- Granular caching - Only rebuild packages that changed
- Better parallelization - Independent packages build in parallel
- Precise cache invalidation - Unchanged packages stay cached
Packages follow dot notation matching their folder hierarchy:
| Folder Path | Package Name |
|---|---|
packages/components/design-system/button/ |
components.design-system.button |
packages/utils/core/cn/ |
utils.core.cn |
packages/pages/home/ |
pages.home |
- Node.js 22.x (see
.nvmrc) - pnpm 9.x
# Use correct Node version
nvm use
# Install pnpm if needed
npm install -g pnpm# Install all dependencies
pnpm install
# Start development server
pnpm dev
# Build all packages and apps
pnpm build
# Type check all packages
pnpm typecheck
# Lint all packages
pnpm lint
# Run tests
pnpm test
# Format code
pnpm format- Build System: Turborepo 2.x with remote caching support
- Package Manager: pnpm 9.x with workspaces and catalogs
- Framework: Next.js 16 with App Router and Turbopack
- Language: TypeScript 5.7 with strict mode
- Styling: Tailwind CSS 3.4 with design system tokens
- Content: Velite for type-safe MDX content collections
- Testing: Vitest with React Testing Library
- Linting: ESLint 9 with flat config format
- Formatting: Prettier 3.x with Tailwind CSS plugin
- CI/CD: GitHub Actions for automated checks
- CSS variables for light/dark mode theming
- Theme toggle with system preference detection via next-themes
- Consistent border radius tokens
- shadcn/ui compatible color scheme
- lucide-react icons throughout the UI
The web app uses Velite for type-safe content collections, enabling MDX authoring with full TypeScript support.
Content files live in apps/web/content/:
apps/web/content/
โโโ blog/ # Blog posts
โ โโโ *.mdx
โโโ changelog/ # Site changelog entries
โ โโโ *.mdx
โโโ projects/ # Project showcases
โโโ *.mdx
Create a new file in apps/web/content/blog/:
---
title: My Post Title
description: A brief description of the post
date: 2025-01-26
tags:
- example
- tutorial
draft: false # Set to true to hide from production
---
Your MDX content here...Create a new file in apps/web/content/projects/:
---
title: Project Name
description: What the project does
url: https://example.com # Live URL (optional)
repo: https://github.com/... # Repository URL (optional)
tech:
- Next.js
- TypeScript
featured: true # Highlight on homepage
order: 1 # Sort order for featured projects
---
Project description and details...Create a new file in apps/web/content/changelog/:
---
version: "0.1.0"
date: 2025-01-26
title: Initial Release
description: First release with core features # Optional
breaking: false # Mark breaking changes
tags:
- feature # feature, fix, improvement, breaking, docs
---
Release notes and changes...Import content collections via the #content alias:
import { blog, projects, changelog } from "#content";
// All posts (typed as Post[])
const posts = blog;
// All projects (typed as Project[])
const allProjects = projects;
// All changelog entries (typed as ChangelogEntry[])
const entries = changelog;
// Filter examples
const published = blog.filter((post) => !post.draft);
const featured = projects.filter((p) => p.featured);
const features = changelog.filter((e) => e.tags.includes("feature"));Velite runs automatically during development and build:
# Development - Velite watches for content changes
pnpm dev
# Build - Velite processes content before Next.js build
pnpm buildOutput is generated to apps/web/.velite/ (git-ignored) and includes full TypeScript types.
# Create directory structure
mkdir -p packages/components/design-system/[name]/src
# Create package.json, tsconfig.json, eslint.config.mjs
# Follow existing patterns in packages/components/design-system/button/# Create directory structure
mkdir -p packages/utils/core/[name]/src
# Create package.json, tsconfig.json, eslint.config.mjs
# Follow existing patterns in packages/utils/core/cn/# Create directory structure
mkdir -p packages/pages/[name]/src
# Create package.json, tsconfig.json, eslint.config.mjs
# Follow existing patterns in packages/pages/home/GitHub Actions workflow runs on all PRs to main:
- โ ESLint checks
- โ TypeScript type checking
- โ Unit tests (Vitest)
- โ E2E tests (Playwright)
- โ Build verification
Playwright HTML reports are attached to CI summaries for easy debugging. All checks must pass before merging.
Each package includes its own README with:
- Usage examples
- API documentation
- Features and benefits
We use individual packages rather than grouped packages because:
- Turborepo caches each package separately
- Changes to one component don't rebuild unrelated components
- Parallel builds are more efficient with many small packages
- Clear dependency boundaries between components
Page packages (packages/pages/*) enable:
- Sharing complete pages across multiple apps
- Single source of truth for page implementations
- Consistent user experience across frontends
- Easy micro-frontend architecture in the future
Centralized configs (configs/*) ensure:
- Consistent code style across all packages
- Easy updates to linting/formatting rules
- Type safety with shared TypeScript configs
- Single dependency management via pnpm catalogs
Unit tests use Vitest with shared configs from configs/vitest/.
# Run all unit tests
pnpm test
# Run tests in watch mode (per-package)
cd packages/utils/core/cn && pnpm test:watchEach testable package includes:
vitest.config.ts- Extends shared configsrc/*.test.ts(x)- Test files colocated with source
| Config | Environment | Use Case |
|---|---|---|
configs.vitest |
Node | Utility packages |
configs.vitest/react |
jsdom | React components |
React component tests use @testing-library/react for accessible, behavior-focused testing.
End-to-end tests use Playwright for cross-browser testing.
# Run E2E tests
pnpm --filter web e2e
# Run E2E tests with UI
pnpm --filter web e2e --uiE2E tests are located in apps/web/e2e/ and cover:
- Theme toggle functionality
- Mobile navigation
- Accessibility (skip-to-content links)
- Set up shadcn/ui components
- Add contact page
- Build project portfolio pages
- Explore other frameworks (Vue, Svelte) in component packages