Written by Findoora Team
Development
Learn how to customize, extend, and develop with Findoora Docs.
Development Setup
Prerequisites
Ensure you have the required tools installed:
bash# Check Node.js version (18+ required)
node --version
# Check pnpm version (10.7.0+ recommended)
pnpm --version
# Install if missing
npm install -g pnpm
Getting Started
bash# Clone the repository
git clone https://github.com/muralitmuthuhotmail/findoora-docs.git
cd findoora-docs
# Install dependencies
pnpm install
# Start development server
pnpm dev
# Open http://localhost:3000
Development Scripts
bash# Development
pnpm dev # Start development server with Turbopack
pnpm dev:debug # Start with debugging enabled
pnpm dev:turbo # Force Turbopack (default in dev)
# Building
pnpm build # Production build
pnpm start # Start production server
pnpm preview # Preview production build locally
# Code Quality
pnpm lint # Run ESLint
pnpm lint:fix # Fix ESLint issues automatically
pnpm typecheck # Run TypeScript type checking
pnpm format # Format code with Prettier
pnpm format:check # Check code formatting
# Testing
pnpm test # Run tests
pnpm test:watch # Run tests in watch mode
pnpm test:coverage # Generate coverage report
pnpm test:ui # Run tests with UI
# Analysis
pnpm analyze # Analyze bundle size
pnpm build:analyze # Build and analyze bundle
Project Architecture
Directory Structure
bashfindoora-docs/
├── app/ # Next.js App Router
│ ├── [category]/ # Dynamic category routes
│ │ ├── page.tsx # Category listing page
│ │ └── [slug]/ # Individual content pages
│ │ ├── page.tsx # Content page
│ │ └── loading.tsx # Loading UI
│ ├── api/ # API routes
│ │ ├── search/route.ts # Search functionality
│ │ ├── navigation/route.ts # Navigation data
│ │ └── health/route.ts # Health check
│ ├── globals.css # Global styles
│ ├── layout.tsx # Root layout
│ ├── page.tsx # Homepage
│ ├── loading.tsx # Global loading UI
│ ├── error.tsx # Error boundary
│ └── not-found.tsx # 404 page
├── components/ # React components
│ ├── blocks/ # Content blocks
│ │ ├── content-list.tsx # Content listing
│ │ ├── table-of-contents.tsx # TOC component
│ │ └── mdx/ # MDX components
│ │ ├── custom-mdx.tsx # MDX provider
│ │ ├── mdx-code.tsx # Code blocks
│ │ ├── mdx-heading.tsx # Headings
│ │ └── ... # Other MDX components
│ ├── features/ # Feature components
│ │ ├── search-dialog.tsx # Search interface
│ │ ├── back-to-top.tsx # Back to top button
│ │ └── social-share.tsx # Social sharing
│ ├── layout/ # Layout components
│ │ ├── app-bar.tsx # Top navigation
│ │ ├── navigation-sidebar.tsx # Sidebar navigation
│ │ ├── mobile-navigation.tsx # Mobile nav
│ │ └── footer.tsx # Site footer
│ ├── pages/ # Page-specific components
│ │ ├── home/ # Homepage components
│ │ └── content/ # Content page components
│ ├── providers/ # Context providers
│ │ └── search-provider.tsx # Search state
│ ├── seo/ # SEO components
│ │ └── structured-data.tsx # Schema.org data
│ └── ui/ # Reusable UI components
│ ├── button.tsx # Button component
│ ├── dialog.tsx # Modal dialogs
│ ├── input.tsx # Form inputs
│ └── ... # Other UI components
├── lib/ # Utility functions
│ ├── content.ts # Content management
│ ├── config.ts # App configuration
│ ├── metadata.ts # SEO metadata
│ ├── utils.ts # General utilities
│ └── utils/ # Specific utilities
│ ├── content-processor.ts # Content processing
│ ├── markdown-parser.ts # Markdown parsing
│ ├── search.ts # Search utilities
│ └── security.ts # Security helpers
├── posts/ # MDX content files
├── md-content/ # Alternative content directory
├── public/ # Static assets
├── styles/ # CSS and styling
│ ├── globals.css # Global styles
│ ├── theme.css # Theme variables
│ └── components.css # Component styles
└── types/ # TypeScript definitions
└── content.types.ts # Content type definitions
Component Development
Creating Custom Components
1. Basic UI Component
tsx// components/ui/custom-button.tsx
import { cn } from "@/lib/utils";
import { ButtonHTMLAttributes, forwardRef } from "react";
interface CustomButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary" | "outline";
size?: "sm" | "md" | "lg";
isLoading?: boolean;
}
export const CustomButton = forwardRef<HTMLButtonElement, CustomButtonProps>(
(
{
className,
variant = "primary",
size = "md",
isLoading,
children,
...props
},
ref,
) => {
const baseStyles =
"inline-flex items-center justify-center rounded-md font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none";
const variants = {
primary:
"bg-primary text-primary-foreground hover:bg-primary/90 focus:ring-primary",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80 focus:ring-secondary",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground focus:ring-accent",
};
const sizes = {
sm: "h-9 px-3 text-sm",
md: "h-10 px-4 py-2",
lg: "h-11 px-6 text-lg",
};
return (
<button
className={cn(baseStyles, variants[variant], sizes[size], className)}
ref={ref}
disabled={isLoading}
{...props}
>
{isLoading && (
<svg className="mr-2 h-4 w-4 animate-spin" viewBox="0 0 24 24">
<circle
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
className="opacity-25"
/>
<path
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
className="opacity-75"
/>
</svg>
)}
{children}
</button>
);
},
);
CustomButton.displayName = "CustomButton";
2. Content Block Component
tsx// components/blocks/feature-grid.tsx
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
interface Feature {
title: string;
description: string;
icon: React.ReactNode;
href?: string;
}
interface FeatureGridProps {
features: Feature[];
columns?: 2 | 3 | 4;
className?: string;
}
export function FeatureGrid({
features,
columns = 3,
className,
}: FeatureGridProps) {
const gridCols = {
2: "md:grid-cols-2",
3: "md:grid-cols-3",
4: "md:grid-cols-4",
};
return (
<div className={cn("grid grid-cols-1 gap-6", gridCols[columns], className)}>
{features.map((feature, index) => (
<Card key={index} className="group hover:shadow-lg transition-shadow">
<CardHeader className="pb-4">
<div className="mb-2 text-primary">{feature.icon}</div>
<CardTitle className="text-lg">{feature.title}</CardTitle>
</CardHeader>
<CardContent>
<CardDescription className="text-sm leading-relaxed">
{feature.description}
</CardDescription>
</CardContent>
</Card>
))}
</div>
);
}
3. MDX Component
tsx// components/blocks/mdx/code-showcase.tsx
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { CodeBlock } from "./mdx-code";
interface CodeShowcaseProps {
title?: string;
examples: {
label: string;
language: string;
code: string;
}[];
}
export function CodeShowcase({ title, examples }: CodeShowcaseProps) {
return (
<div className="my-6">
{title && <h3 className="text-lg font-semibold mb-4">{title}</h3>}
<Tabs defaultValue={examples[0]?.label} className="w-full">
<TabsList className="grid w-full grid-cols-2 lg:grid-cols-4">
{examples.map((example) => (
<TabsTrigger key={example.label} value={example.label}>
{example.label}
</TabsTrigger>
))}
</TabsList>
{examples.map((example) => (
<TabsContent key={example.label} value={example.label}>
<CodeBlock
language={example.language}
code={example.code}
showLineNumbers
/>
</TabsContent>
))}
</Tabs>
</div>
);
}
Registering MDX Components
Add your custom components to the MDX provider:
tsx// components/blocks/mdx/custom-mdx.tsx
import { FeatureGrid } from "../feature-grid";
import { CodeShowcase } from "./code-showcase";
const comp {
// Existing components...
FeatureGrid,
CodeShowcase,
// Custom HTML elements
h1: (props: any) => <h1 className="text-4xl font-bold mb-6" {...props} />,
h2: (props: any) => (
<h2 className="text-3xl font-semibold mb-4 mt-8" {...props} />
),
p: (props: any) => <p className="mb-4 leading-relaxed" {...props} />,
// Add more as needed...
};
Styling & Theming
CSS Architecture
The project uses a layered CSS architecture:
- Base Layer (
styles/base.css) - Reset and base styles - Component Layer (
styles/components.css) - Component-specific styles - Utility Layer (Tailwind) - Utility classes
- Theme Layer (
styles/theme.css) - CSS custom properties
Theme Customization
CSS Variables
css/* styles/theme.css */
:root {
/* Base colors */
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
/* Primary colors */
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
/* Brand colors */
--brand-primary: 200 100% 50%;
--brand-secondary: 160 100% 45%;
--brand-accent: 280 100% 60%;
/* Component colors */
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--muted: 210 40% 96%;
--muted-foreground: 215.4 16.3% 46.9%;
/* Interactive colors */
--hover: 210 40% 96%;
--active: 210 40% 92%;
/* Border and spacing */
--border: 214.3 31.8% 91.4%;
--radius: 0.5rem;
}
[data-theme="dark"] {
/* Dark theme overrides */
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 84% 4.9%;
/* ... more dark theme colors */
}
Tailwind Configuration
javascript// tailwind.config.js
const { fontFamily } = require("tailwindcss/defaultTheme");
module.exports = {
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
],
darkMode: ["class", '[data-theme="dark"]'],
theme: {
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
brand: {
primary: "hsl(var(--brand-primary))",
secondary: "hsl(var(--brand-secondary))",
accent: "hsl(var(--brand-accent))",
},
},
fontFamily: {
sans: ["Inter", ...fontFamily.sans],
mono: ["JetBrains Mono", ...fontFamily.mono],
heading: ["Cal Sans", "Inter", ...fontFamily.sans],
},
typography: {
DEFAULT: {
css: {
maxWidth: "none",
color: "hsl(var(--foreground))",
h1: { color: "hsl(var(--foreground))" },
h2: { color: "hsl(var(--foreground))" },
h3: { color: "hsl(var(--foreground))" },
h4: { color: "hsl(var(--foreground))" },
code: { color: "hsl(var(--primary))" },
"blockquote p:first-of-type::before": { content: "none" },
"blockquote p:first-of-type::after": { content: "none" },
},
},
},
},
},
plugins: [require("@tailwindcss/typography"), require("tailwindcss-animate")],
};
Component Styling Patterns
1. Compound Components
tsx// components/ui/card.tsx
import { cn } from "@/lib/utils";
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className,
)}
{...props}
/>
));
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
));
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className,
)}
{...props}
/>
));
const CardC React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
));
export { Card, CardHeader, CardTitle, CardContent };
2. Responsive Design Patterns
tsx// Responsive grid component
export function ResponsiveGrid({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) {
return (
<div
className={cn(
"grid gap-4",
"grid-cols-1", // Mobile: 1 column
"sm:grid-cols-2", // Small: 2 columns
"md:grid-cols-3", // Medium: 3 columns
"lg:grid-cols-4", // Large: 4 columns
"xl:grid-cols-5", // Extra large: 5 columns
className,
)}
>
{children}
</div>
);
}
Advanced Development
Content Processing
Custom Content Transformers
typescript// lib/utils/content-processor.ts
import { remark } from "remark";
import remarkGfm from "remark-gfm";
import remarkMath from "remark-math";
import { rehype } from "rehype";
import rehypeSlug from "rehype-slug";
import rehypeAutolinkHeadings from "rehype-autolink-headings";
export interface ContentProcessor {
processMarkdown(content: string): Promise<string>;
extractMetadata(content: string): ContentMetadata;
generateTableOfContents(content: string): TableOfContents[];
}
class MDXContentProcessor implements ContentProcessor {
private remarkProcessor = remark().use(remarkGfm).use(remarkMath);
private rehypeProcessor = rehype()
.use(rehypeSlug)
.use(rehypeAutolinkHeadings, {
behavior: "wrap",
properties: {
className: ["anchor-link"],
"aria-label": "Link to heading",
},
});
async processMarkdown(content: string): Promise<string> {
const remarkResult = await this.remarkProcessor.process(content);
const rehypeResult = await this.rehypeProcessor.process(remarkResult);
return String(rehypeResult);
}
extractMetadata(content: string): ContentMetadata {
const fr /^---\n([\s\S]*?)\n---/;
const match = content.match(frontmatterRegex);
if (!match) return {};
const fr match[1];
const metadata: C {};
frontmatter.split("\n").forEach((line) => {
const [key, ...valueParts] = line.split(":");
if (key && valueParts.length > 0) {
const value = valueParts.join(":").trim();
metadata[key.trim()] = this.parseValue(value);
}
});
return metadata;
}
generateTableOfContents(content: string): TableOfContents[] {
const headingRegex = /^(#{1,6})\s+(.+)$/gm;
const toc: TableOfContents[] = [];
let match;
while ((match = headingRegex.exec(content)) !== null) {
const level = match[1].length;
const title = match[2].trim();
const slug = title
.toLowerCase()
.replace(/\s+/g, "-")
.replace(/[^\w-]/g, "");
toc.push({
level,
title,
slug,
children: [],
});
}
return this.buildTocHierarchy(toc);
}
private parseValue(value: string): any {
// Parse arrays
if (value.startsWith("[") && value.endsWith("]")) {
return value
.slice(1, -1)
.split(",")
.map((item) => item.trim().replace(/['"]/g, ""));
}
// Parse booleans
if (value === "true") return true;
if (value === "false") return false;
// Parse strings
return value.replace(/['"]/g, "");
}
private buildTocHierarchy(flatToc: TableOfContents[]): TableOfContents[] {
const hierarchy: TableOfContents[] = [];
const stack: TableOfContents[] = [];
flatToc.forEach((item) => {
// Find parent level
while (stack.length > 0 && stack[stack.length - 1].level >= item.level) {
stack.pop();
}
if (stack.length === 0) {
hierarchy.push(item);
} else {
stack[stack.length - 1].children.push(item);
}
stack.push(item);
});
return hierarchy;
}
}
export const c new MDXContentProcessor();
Search Implementation
Advanced Search Features
typescript// lib/utils/search.ts
import Fuse from "fuse.js";
export interface SearchResult {
item: ContentPost;
score: number;
matches: Fuse.FuseResultMatch[];
highlights: string[];
}
export class AdvancedSearch {
private fuse: Fuse<ContentPost>;
private searchConfig: Fuse.IFuseOptions<ContentPost> = {
includeScore: true,
includeMatches: true,
threshold: 0.3,
minMatchCharLength: 2,
keys: [
{ name: "title", weight: 5 },
{ name: "summary", weight: 3 },
{ name: "content", weight: 1 },
{ name: "tags", weight: 2 },
{ name: "category", weight: 2 },
],
};
constructor(posts: ContentPost[]) {
this.fuse = new Fuse(posts, this.searchConfig);
}
search(query: string, filters?: SearchFilters): SearchResult[] {
if (!query.trim()) return [];
let results = this.fuse.search(query);
// Apply filters
if (filters) {
results = this.applyFilters(results, filters);
}
// Transform results
return results.map((result) => ({
item: result.item,
score: result.score || 0,
matches: result.matches || [],
highlights: this.generateHighlights(result),
}));
}
private applyFilters(
results: Fuse.FuseResult<ContentPost>[],
filters: SearchFilters,
): Fuse.FuseResult<ContentPost>[] {
return results.filter((result) => {
if (
filters.categories &&
!filters.categories.includes(result.item.category)
) {
return false;
}
if (
filters.tags &&
!filters.tags.some((tag) => result.item.tags?.includes(tag))
) {
return false;
}
if (filters.dateRange) {
const publishedDate = new Date(result.item.publishedAt);
if (
publishedDate < filters.dateRange.start ||
publishedDate > filters.dateRange.end
) {
return false;
}
}
return true;
});
}
private generateHighlights(result: Fuse.FuseResult<ContentPost>): string[] {
const highlights: string[] = [];
result.matches?.forEach((match) => {
if (match.indices && match.value) {
match.indices.forEach(([start, end]) => {
const before = match.value!.substring(Math.max(0, start - 20), start);
const highlighted = match.value!.substring(start, end + 1);
const after = match.value!.substring(
end + 1,
Math.min(match.value!.length, end + 21),
);
highlights.push(`...${before}<mark>${highlighted}</mark>${after}...`);
});
}
});
return highlights;
}
}
Performance Optimization
Code Splitting
tsx// Dynamic imports for better performance
import dynamic from "next/dynamic";
// Lazy load heavy components
const MermaidDiagram = dynamic(() => import("./mermaid-diagram"), {
loading: () => <div className="animate-pulse bg-muted h-48 rounded-lg" />,
ssr: false,
});
const CodePlayground = dynamic(() => import("./code-playground"), {
loading: () => <div className="animate-pulse bg-muted h-96 rounded-lg" />,
});
// Component with lazy loading
export function ContentRenderer({ content }: { content: ContentPost }) {
return (
<article>
<MDXContent comp{{ MermaidDiagram, CodePlayground }} />
</article>
);
}
Image Optimization
tsx// components/ui/optimized-image.tsx
import Image from "next/image";
import { useState } from "react";
interface OptimizedImageProps {
src: string;
alt: string;
width?: number;
height?: number;
className?: string;
priority?: boolean;
}
export function OptimizedImage({
src,
alt,
width = 800,
height = 600,
className,
priority = false,
}: OptimizedImageProps) {
const [isLoading, setIsLoading] = useState(true);
const [hasError, setHasError] = useState(false);
return (
<div className={cn("relative overflow-hidden rounded-lg", className)}>
{isLoading && <div className="absolute inset-0 bg-muted animate-pulse" />}
{hasError ? (
<div className="flex items-center justify-center bg-muted text-muted-foreground">
<span>Failed to load image</span>
</div>
) : (
<Image
src={src}
alt={alt}
width={width}
height={height}
priority={priority}
className={cn(
"transition-opacity duration-300",
isLoading ? "opacity-0" : "opacity-100",
)}
{() => setIsLoading(false)}
{() => {
setIsLoading(false);
setHasError(true);
}}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAIAAoDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAhEAACAQMDBQAAAAAAAAAAAAABAgMABAUGIWGRkrHB0f/EABUBAQEAAAAAAAAAAAAAAAAAAAMF/8QAGhEAAgIDAAAAAAAAAAAAAAAAAAECEgMRkf/aAAwDAQACEQMRAD8AltJagyeH0AthI5xdrLcNM91BF5pX2HaH9bcfaSXWGaRmknyJckliyjqTzSlT54b6bk+h0R//2Q=="
/>
)}
</div>
);
}
Testing
Unit Testing
typescript// __tests__/components/ui/button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { CustomButton } from '@/components/ui/custom-button';
describe('CustomButton', () => {
it('renders correctly', () => {
render(<CustomButton>Click me</CustomButton>);
const button = screen.getByRole('button', { name: /click me/i });
expect(button).toBeInTheDocument();
});
it('applies the correct variant styles', () => {
render(<CustomButton variant="outline">Outline Button</CustomButton>);
const button = screen.getByRole('button');
expect(button).toHaveClass('border');
});
it('shows loading state', () => {
render(<CustomButton isLoading>Loading</CustomButton>);
const button = screen.getByRole('button');
expect(button).toBeDisabled();
expect(screen.getByRole('button')).toHaveTextContent('Loading');
});
it('handles click events', () => {
const handleClick = jest.fn();
render(<CustomButton {handleClick}>Click me</CustomButton>);
fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
});
Integration Testing
typescript// __tests__/pages/content.test.tsx
import { render, screen } from '@testing-library/react';
import ContentPage from '@/app/[category]/[slug]/page';
import { getContentPost } from '@/lib/content';
// Mock the content function
jest.mock('@/lib/content', () => ({
getContentPost: jest.fn(),
}));
const mockPost = {
slug: 'test-post',
title: 'Test Post',
summary: 'This is a test post',
content: '# Test Content\n\nThis is test content.',
category: 'blog',
publishedAt: '2025-07-26',
tags: ['test'],
};
describe('ContentPage', () => {
beforeEach(() => {
(getContentPost as jest.Mock).mockReturnValue(mockPost);
});
it('renders content correctly', () => {
render(<ContentPage params={{ category: 'blog', slug: 'test-post' }} />);
expect(screen.getByText('Test Post')).toBeInTheDocument();
expect(screen.getByText('This is a test post')).toBeInTheDocument();
});
it('shows 404 for non-existent content', () => {
(getContentPost as jest.Mock).mockReturnValue(null);
render(<ContentPage params={{ category: 'blog', slug: 'non-existent' }} />);
// Should render not-found component
expect(screen.getByText(/not found/i)).toBeInTheDocument();
});
});
E2E Testing
typescript// __tests__/e2e/navigation.spec.ts
import { test, expect } from "@playwright/test";
test.describe("Navigation", () => {
test("should navigate between pages", async ({ page }) => {
await page.goto("/");
// Check homepage loads
await expect(page).toHaveTitle(/Findoora Docs/);
// Navigate to documentation
await page.click("text=Documentation");
await expect(page).toHaveURL(/\/docs/);
// Check sidebar navigation
await page.click("text=Getting Started");
await expect(page).toHaveURL(/\/docs\/getting-started/);
});
test("should search for content", async ({ page }) => {
await page.goto("/");
// Open search dialog
await page.keyboard.press("Meta+K");
await expect(page.locator('[role="dialog"]')).toBeVisible();
// Type search query
await page.fill('[placeholder="Search documentation..."]', "installation");
// Check search results
await expect(page.locator("text=Installation")).toBeVisible();
// Click on result
await page.click("text=Installation");
await expect(page).toHaveURL(/\/docs\/installation/);
});
});
Build Optimization
Bundle Analysis
bash# Analyze bundle size
pnpm build:analyze
# View bundle analyzer report
open .next/analyze/client.html
Performance Monitoring
typescript// lib/performance.ts
export function measurePerformance(name: string) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor,
) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
const start = performance.now();
const result = await originalMethod.apply(this, args);
const end = performance.now();
console.log(`${name} took ${end - start} milliseconds`);
// Send to analytics in production
if (process.env.NODE_ENV === "production") {
// Analytics tracking code
}
return result;
};
return descriptor;
};
}
// Usage
class ContentService {
@measurePerformance("getContentPosts")
async getContentPosts() {
// Method implementation
}
}
Development Best Practices
Code Organization
- Use barrel exports for cleaner imports
- Follow the feature-based directory structure
- Keep components small and focused
- Use TypeScript strictly
Performance
- Implement lazy loading for heavy components
- Use React.memo for expensive renders
- Optimize images and assets
- Monitor bundle size regularly
Accessibility
- Use semantic HTML elements
- Provide proper ARIA labels
- Test with screen readers
- Ensure keyboard navigation works
SEO
- Generate proper meta tags
- Use structured data
- Optimize for Core Web Vitals
- Implement proper heading hierarchy
Development Workflow
Git Workflow
bash# Feature development
git checkout -b feature/new-component
git add .
git commit -m "feat: add new component with accessibility features"
# Code review preparation
pnpm lint
pnpm typecheck
pnpm test
pnpm build
# Push and create PR
git push origin feature/new-component
Pre-commit Hooks
json// package.json
{
"lint-staged": {
"*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{md,mdx}": ["prettier --write"]
},
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"pre-push": "pnpm typecheck && pnpm test"
}
}
}
Development Troubleshooting
Common Issues
TypeScript errors:
bash# Clear TypeScript cache
rm -rf .next
pnpm typecheck
# Check for missing types
pnpm add -D @types/node @types/react
Build failures:
bash# Clear all caches
rm -rf .next node_modules
pnpm install
pnpm build
Hot reload not working:
bash# Check file watchers limit (Linux)
echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
Next Steps
Ready to take your development further?
- Architecture Guide - Deep dive into the system architecture
- Deployment Guide - Deploy your customizations
- Contributing Guide - Contribute back to the project
Happy coding! Build amazing documentation experiences with Findoora Docs. 🚀