Coding Conventions & Standards¶
Navigation: Documentation Home → Architecture → Conventions
Table of Contents¶
- Overview
- File Naming Conventions
- Import Standards
- TypeScript Guidelines
- React Patterns
- Styling Conventions
- Git Workflow
- Testing Standards
- See Also
- Next Steps
Overview¶
This project follows strict conventions for consistency, maintainability, and developer experience. All code is automatically validated through ESLint, Prettier, and TypeScript checks.
Enforcement¶
Conventions are enforced through:
- Pre-commit hooks (Husky + lint-staged)
- CI/CD pipeline (GitHub Actions)
- Editor integration (ESLint + Prettier extensions)
Pre-commit Hook:
{
"lint-staged": {
"*.{js,jsx,ts,tsx}": ["prettier --write", "eslint --fix"],
"*.{json,css,md}": ["prettier --write"]
}
}
File Naming Conventions¶
Directory Names¶
Use kebab-case for all directories:
✅ Good
components/layout/
app/projects/
content/projects/
❌ Bad
components/Layout/
app/Projects/
content/project_files/
Component Files¶
Option 1: kebab-case (recommended for new files)
components / project - card.tsx;
components / achievement - card.tsx;
components / email - copy - button.tsx;
Option 2: PascalCase (acceptable, used in some existing files)
Rule: Be consistent within the same directory.
Utility Files¶
Use kebab-case for utilities and libraries:
Special Next.js Files¶
Next.js requires specific names (lowercase):
app / page.tsx; // Route page
app / layout.tsx; // Layout wrapper
app / error.tsx; // Error boundary
app / not - found.tsx; // 404 page
app / loading.tsx; // Loading UI
Content Files¶
Content files use kebab-case matching the slug:
content/projects/audionome.json → slug: "audionome"
content/projects/cerm-mcp-poc.json → slug: "cerm-mcp-poc"
content/achievements/rotary-award.json → slug: "rotary-award"
Slug Rules:
- Lowercase only
- Use hyphens for spaces
- No special characters except hyphens
- Should be URL-safe
Test Files¶
Mirror source file naming with .test.ts(x) suffix:
lib/projects.ts → tests/lib/projects.test.ts
components/button.tsx → tests/components/button.test.tsx
app/page.tsx → tests/app/page.test.tsx
Import Standards¶
Absolute Imports¶
Always use absolute imports with the @/ prefix:
✅ Good
import { Button } from "@/components/ui/button"
import { getProjects } from "@/lib/projects"
import { siteConfig } from "@/lib/config"
import { cn } from "@/lib/utils"
❌ Bad
import { Button } from "../../components/ui/button"
import { getProjects } from "../../../lib/projects"
Configuration:
```20:24:/workspaces/website/tsconfig.json "paths": { "@/": ["./"] } }, "include": ["next-env.d.ts", "/*.ts", "/.tsx", ".next/types//.ts"],
### Import Order
Organize imports in this order:
1. **React and Next.js**
2. **Third-party libraries**
3. **Internal utilities and types**
4. **Internal components**
5. **Styles and assets**
**Example:**
```typescript
// 1. React/Next.js
import { Metadata } from "next";
import Link from "next/link";
import Image from "next/image";
// 2. Third-party
import { ArrowLeft } from "lucide-react";
// 3. Internal utilities/types
import { getProjects, type Project } from "@/lib/projects";
import { cn } from "@/lib/utils";
import { siteConfig } from "@/lib/config";
// 4. Internal components
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import ProjectCard from "@/components/project-card";
// 5. Styles (if needed)
import "./globals.css";
Named vs Default Exports¶
Prefer named exports for utilities and data:
✅ Good
// lib/projects.ts
export async function getProjects() { ... }
export async function getProjectBySlug(slug: string) { ... }
// Usage
import { getProjects, getProjectBySlug } from "@/lib/projects"
Use default exports for components (Next.js pages require it):
// app/page.tsx
export default async function Home() {
return <div>...</div>
}
// components/project-card.tsx
export default function ProjectCard({ project }: { project: Project }) {
return <Card>...</Card>
}
TypeScript Guidelines¶
Type Everything¶
All functions, parameters, and return values must be typed:
✅ Good
export async function getProjects(): Promise<Project[]> {
const fileNames = fs.readdirSync(projectsDirectory);
return projects;
}
export interface Project {
slug: string;
title: string;
technologies: string[];
}
❌ Bad
export async function getProjects() {
const fileNames = fs.readdirSync(projectsDirectory);
return projects;
}
Interface vs Type¶
Use interface for object shapes (preferred):
Use type for unions, intersections, or primitives:
export type AchievementType = "certification" | "award" | "achievement";
export type ClassValue = string | number | boolean | undefined | null;
Avoid any¶
Use proper types instead of any:
✅ Good
export async function getProjects(): Promise<Project[]>
const projectData: unknown = JSON.parse(fileContent)
❌ Bad
export async function getProjects(): Promise<any>
const projectData: any = JSON.parse(fileContent)
Use unknown for JSON parsing, then validate:
``44:56:/workspaces/website/lib/projects.ts
// Parse the JSON data with error handling
let projectData: unknown;
try {
projectData = JSON.parse(fileContent);
} catch (error) {
console.error(Error parsing JSON for project ${slug}`, { error, fileName });
return null; // Skip invalid files
}
// Ensure it's an object and not an array
if (typeof projectData !== "object" || projectData === null || Array.isArray(projectData)) {
console.error(`Invalid JSON structure for project ${slug}`, { projectData });
return null;
```
TypeScript Configuration¶
Strict mode is enabled:
1:27:/workspaces/website/tsconfig.json
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
Key Settings:
strict: true- All strict checks enablednoEmit: true- TypeScript for checking only (Next.js handles compilation)isolatedModules: true- Required for Next.jsresolveJsonModule: true- Import JSON files
React Patterns¶
Server Components (Default)¶
Use Server Components by default for optimal performance:
// app/page.tsx
export default async function Home() {
// Data fetching on server
const projects = await getFeaturedProjects()
const achievements = await getAchievements()
return (
<div>
{projects.map(project => (
<ProjectCard key={project.slug} project={project} />
))}
</div>
)
}
Benefits:
- No JavaScript sent to client
- Can use server-only APIs (fs, database)
- SEO-friendly
- Better performance
Client Components (When Needed)¶
Use 'use client' directive when you need:
- Event handlers (
onClick,onChange) - State (
useState,useReducer) - Effects (
useEffect) - Browser APIs
- React hooks
'use client'
import { useState } from 'react'
export function ThemeToggle() {
const [theme, setTheme] = useState('light')
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
)
}
Component Props¶
Always type component props:
✅ Good
interface ProjectCardProps {
project: Project;
featured?: boolean;
}
export default function ProjectCard({ project, featured = false }: ProjectCardProps) {
return <Card>...</Card>
}
// Or inline
export default function ProjectCard({ project }: { project: Project }) {
return <Card>...</Card>
}
❌ Bad
export default function ProjectCard({ project, featured }) {
return <Card>...</Card>
}
Async Server Components¶
Fetch data directly in components:
```30:34:/workspaces/website/app/page.tsx export default async function Home() { const projects = await getFeaturedProjects(); const achievements = await getAchievements(); const skills = await getSkills();
**No need for:**
- `getServerSideProps`
- `getStaticProps`
- `useEffect` for data fetching
### Component Composition
Break down complex components:
```typescript
// Instead of one large component
export default function ProjectPage() {
return (
<div>
{/* 200 lines of JSX */}
</div>
)
}
// Split into smaller components
export default function ProjectPage({ project }: { project: Project }) {
return (
<div>
<ProjectHeader project={project} />
<ProjectImages images={project.images} />
<ProjectContent description={project.description} />
<ProjectTechnologies technologies={project.technologies} />
<ProjectActions demoUrl={project.demoUrl} githubUrl={project.githubUrl} />
</div>
)
}
Example from codebase:
```155:181:/workspaces/website/app/projects/[slug]/page.tsx function ProjectContent({ description, technologies, }: { description: string; technologies: string[]; }) { return (
Project Overview
{description.split("\n").map((line, index) => ({line}
))} <h2 className="mt-8 mb-4 text-2xl font-bold">Technologies Used</h2>
<ul className="mb-8 flex flex-wrap gap-2">
{technologies.map((tech) => (
<Badge key={tech} variant="secondary" asChild>
<li>{tech}</li>
</Badge>
))}
</ul>
</div>
); } ```
Styling Conventions¶
Tailwind CSS¶
Use utility classes directly in JSX:
typescript
<div className="flex min-h-screen flex-col">
<h1 className="text-4xl font-extrabold md:text-6xl">
Title
</h1>
</div>
Class Merging with cn()¶
Use the cn() utility for conditional classes:
import { cn } from "@/lib/utils"
<div className={cn(
"base-class",
isActive && "active-class",
isPrimary ? "primary-class" : "secondary-class"
)}>
Content
</div>
The cn() utility:
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
Benefits:
- Merges class lists
- Resolves Tailwind conflicts (later wins)
- Handles conditional classes
Responsive Design¶
Use Tailwind breakpoints:
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
{/* Mobile: 1 column, Tablet: 2 columns, Desktop: 3 columns */}
</div>
Breakpoints:
sm:- 640px and upmd:- 768px and uplg:- 1024px and upxl:- 1280px and up2xl:- 1536px and up
Dark Mode¶
Use Tailwind's dark mode classes:
<div className="bg-white dark:bg-gray-900">
<p className="text-gray-900 dark:text-gray-100">
Content adapts to theme
</p>
</div>
Configuration:
```3:10:/workspaces/website/tailwind.config.ts const config = { darkMode: "class", content: [ "./pages//*.{ts,tsx}", "./components//.{ts,tsx}", "./app//.{ts,tsx}", "./src/*/.{ts,tsx}", ],
### Avoid Inline Styles
```typescript
❌ Bad
<div style={{ color: 'red', fontSize: '24px' }}>
✅ Good
<div className="text-red-500 text-2xl">
Git Workflow¶
Commit Messages¶
Use conventional commit format:
Types:
feat:- New featurefix:- Bug fixdocs:- Documentation changesstyle:- Formatting (no code change)refactor:- Code restructuringtest:- Adding testschore:- Build/config changes
Examples:
git commit -m "feat(projects): add video support for project images"
git commit -m "fix(skills): correct skill ID generation for special characters"
git commit -m "docs(architecture): add data flow diagrams"
git commit -m "refactor(components): extract ProjectActions component"
Branch Strategy¶
- main-v2 - Production branch
- feature/* - Feature branches
- fix/* - Bug fix branches
- docs/* - Documentation branches
Pre-commit Hooks¶
Husky runs checks before commit:
What gets checked:
- TypeScript compilation
- ESLint rules
- Prettier formatting
- Import order
If checks fail, commit is blocked until fixed.
Pull Requests¶
Before submitting a PR:
npm run lint # Check linting
npm run format:check # Check formatting
npm test # Run tests
npm run build # Ensure build succeeds
CI will run the same checks on push.
Testing Standards¶
Test File Location¶
Mirror source structure:
lib/projects.ts → tests/lib/projects.test.ts
components/button.tsx → tests/components/button.test.tsx
app/page.tsx → tests/app/page.test.tsx
Test Naming¶
Use describe and it blocks:
import { getProjects } from "@/lib/projects";
describe("getProjects", () => {
it("should return all projects", async () => {
const projects = await getProjects();
expect(projects).toBeDefined();
expect(Array.isArray(projects)).toBe(true);
});
it("should sort projects by order field", async () => {
const projects = await getProjects();
const orderedProjects = projects.filter((p) => p.order !== undefined);
for (let i = 0; i < orderedProjects.length - 1; i++) {
expect(orderedProjects[i].order!).toBeLessThanOrEqual(orderedProjects[i + 1].order!);
}
});
});
Component Testing¶
Use React Testing Library:
import { render, screen } from "@testing-library/react"
import ProjectCard from "@/components/project-card"
describe("ProjectCard", () => {
const mockProject = {
slug: "test-project",
title: "Test Project",
shortDescription: "A test project",
// ... other fields
}
it("renders project title", () => {
render(<ProjectCard project={mockProject} />)
expect(screen.getByText("Test Project")).toBeInTheDocument()
})
})
Running Tests¶
Test Coverage¶
Jest is configured to collect coverage:
17:28:/workspaces/website/jest.config.js
collectCoverageFrom: [
"**/*.{js,jsx,ts,tsx}",
"!**/*.d.ts",
"!**/node_modules/**",
"!**/.next/**",
"!**/coverage/**",
"!jest.config.js",
"!jest.setup.js",
],
coverageDirectory: "coverage",
coverageReporters: ["text", "lcov", "html"],
};
View coverage report: open coverage/index.html
See Also¶
Related documentation:
- Architecture Overview - Understanding the system
- Directory Structure - Where files belong
- Configuration Reference - Tool configurations
- Development Workflow - Day-to-day development
- Testing Guide - Writing and running tests
Next Steps¶
- Configure editor: Install ESLint and Prettier extensions
- Review code: Study existing components for patterns
- Run checks: Execute
npm run lintandnpm test - Start coding: Follow these conventions in your work