Related Projects Algorithm¶
Navigation: Home → Features & Components → Related Projects
Table of Contents¶
- Introduction
- Algorithm Overview
- Implementation
- Algorithm Flowchart
- Matching Logic
- Edge Cases
- Performance Considerations
- Customization
- Usage Examples
- See Also
- Next Steps
Introduction¶
The Related Projects component (components/related-projects.tsx) implements a recommendation algorithm that suggests projects similar to the one currently being viewed. It uses technology overlap as the primary matching criterion, helping visitors discover related work.
Key Features¶
- ✅ Technology-Based Matching: Projects sharing technologies are considered related
- ✅ Smart Filtering: Excludes current project from suggestions
- ✅ Configurable Limit: Control number of suggestions (default: 3)
- ✅ Graceful Degradation: Returns null when no matches found
- ✅ Server Component: No client-side JavaScript overhead
- ✅ Type-Safe: Full TypeScript support
Algorithm Overview¶
High-Level Logic¶
1. Receive current project and all projects as input
2. Filter projects that share at least one technology with current project
3. Exclude the current project itself
4. Limit results to maxProjects (default: 3)
5. Return matched projects or null if none found
Matching Criteria¶
A project is considered "related" if:
- It's not the current project
- It shares at least one technology with the current project
Example:
Current Project: "Portfolio Website"
Technologies: ["Next.js", "TypeScript", "Tailwind CSS"]
All Projects:
- "E-commerce App" → ["Next.js", "React", "Stripe"] ✅ Matches (Next.js)
- "Mobile Game" → ["Unity", "C#"] ❌ No match
- "Blog Platform" → ["TypeScript", "Node.js"] ✅ Matches (TypeScript)
- "Portfolio Website" → ["Next.js", "TypeScript", "Tailwind CSS"] ❌ Excluded (current)
Implementation¶
Component Code¶
```typescript:1:56:components/related-projects.tsx import Link from "next/link"; import type { ClassValue } from "clsx"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardFooter, CardTitle } from "@/components/ui/card"; import { cn } from "@/lib/utils"; import type { Project } from "@/lib/projects";
interface RelatedProjectsProps { currentProject: Project; allProjects: Project[]; maxProjects?: number; className?: ClassValue; }
export default function RelatedProjects({ currentProject, allProjects, maxProjects = 3, className, }: RelatedProjectsProps) { // Find projects that share at least one technology with the current project const relatedProjects = allProjects .filter( (project) => project.slug !== currentProject.slug && // Exclude current project project.technologies.some((tech) => currentProject.technologies.includes(tech)) ) .slice(0, maxProjects);
if (relatedProjects.length === 0) { return null; }
return (
Related Projects
### Props Interface
```typescript
interface RelatedProjectsProps {
currentProject: Project; // The project being viewed
allProjects: Project[]; // All available projects
maxProjects?: number; // Max suggestions (default: 3)
className?: ClassValue; // Optional styling
}
Algorithm Flowchart¶
Visual Representation¶
flowchart TD
Start([Start: Related Projects Algorithm]) --> Input[/Input: currentProject, allProjects, maxProjects/]
Input --> Filter1{For each project in allProjects}
Filter1 --> CheckSlug{project.slug ≠ currentProject.slug?}
CheckSlug -->|No| Exclude1[❌ Exclude - Same project]
CheckSlug -->|Yes| CheckTech{Share at least 1 technology?}
CheckTech -->|No| Exclude2[❌ Exclude - No match]
CheckTech -->|Yes| Include[✅ Include in matches]
Exclude1 --> Filter1
Exclude2 --> Filter1
Include --> Filter1
Filter1 -->|All processed| Matches[Matched Projects Array]
Matches --> Limit[Slice to maxProjects items]
Limit --> Count{matches.length > 0?}
Count -->|No| ReturnNull[Return null]
Count -->|Yes| ReturnMatches[Return matched projects]
ReturnNull --> End([End: No suggestions shown])
ReturnMatches --> Render[Render cards in grid]
Render --> End2([End: Display suggestions])
style Start fill:#e1f5ff
style Matches fill:#d4edda
style ReturnMatches fill:#d4edda
style ReturnNull fill:#f8d7da
style Include fill:#d4edda
style Exclude1 fill:#f8d7da
style Exclude2 fill:#f8d7da
Detailed Step-by-Step¶
- Input Validation
- Receive current project
- Receive all projects array
-
Receive max results limit (default 3)
-
Filtering Phase
allProjects.filter((project) => {
// Step 2a: Exclude current project
if (project.slug === currentProject.slug) return false;
// Step 2b: Check technology overlap
return project.technologies.some((tech) => currentProject.technologies.includes(tech));
});
- Limiting Phase
Takes first N matches
- Return Phase
Matching Logic¶
Technology Comparison¶
The matching uses JavaScript's Array.some() and Array.includes():
Breakdown:
// Example data
currentProject.technologies = ["React", "TypeScript", "Next.js"];
project.technologies = ["Vue", "TypeScript", "Vite"];
// Check if ANY tech in project exists in currentProject
project.technologies.some(
(
tech // For each: "Vue", "TypeScript", "Vite"
) => currentProject.technologies.includes(tech) // Check if exists in current
);
// "Vue" → includes? false
// "TypeScript" → includes? true ✅ (stops here, returns true)
Match Strength¶
The current algorithm treats all matches equally (binary: match or no match). It doesn't consider:
- Number of shared technologies
- Importance of technologies
- Recency of projects
Enhancement Opportunity: Implement weighted matching (see Next Steps)
Array.slice() Behavior¶
- Returns first
maxProjectsitems - If fewer matches than limit, returns all matches
- Original array remains unchanged
- Empty array if no matches (handled by length check)
Edge Cases¶
No Matches Found¶
Result: Component renders nothing, no empty state shown.
Alternative Approach:
Single Match¶
When only one related project exists:
- Component still renders
- Grid adapts responsively
- Single card displayed
Excessive Matches¶
If 20 projects match but maxProjects = 3:
- Only first 3 returned
- No sorting applied (order depends on input array)
- No prioritization logic
Enhancement: Add sorting by relevance or date
Current Project in AllProjects¶
The algorithm assumes currentProject exists in allProjects array and correctly filters it out:
Duplicate Technologies¶
If current project has duplicate tech entries (data error):
The algorithm still works correctly due to includes() behavior, but data should be cleaned.
Performance Considerations¶
Time Complexity¶
O(n × m × p) where:
- n = number of all projects
- m = average technologies per project
- p = average technologies in current project
Breakdown:
allProjects.filter(
(
project // O(n)
) =>
project.slug !== currentProject.slug && // O(1)
project.technologies.some(
(
tech // O(m)
) => currentProject.technologies.includes(tech) // O(p)
)
);
Optimization Opportunities¶
- Early Exit on Limit Reached
const relatedProjects: Project[] = [];
for (const project of allProjects) {
if (relatedProjects.length >= maxProjects) break;
if (/* match logic */) {
relatedProjects.push(project);
}
}
- Use Set for Technology Lookup
const currentTechs = new Set(currentProject.technologies);
project.technologies.some((tech) => currentTechs.has(tech)); // O(1) lookup
- Memoization Cache results for repeat views (if client-side)
Memory Usage¶
Minimal - only stores filtered array slice:
- 3 projects (default) × ~1KB each = ~3KB
- Total component overhead: negligible
Customization¶
Change Maximum Results¶
<RelatedProjects
currentProject={project}
allProjects={allProjects}
maxProjects={5} // Show 5 instead of 3
/>
Custom Grid Layout¶
<RelatedProjects
currentProject={project}
allProjects={allProjects}
className="grid gap-4 md:grid-cols-4" // 4 columns on desktop
/>
Add Sorting¶
Modify the algorithm to sort by match strength:
const relatedProjects = allProjects
.filter(/* existing filter */)
.map((project) => ({
project,
matchCount: project.technologies.filter((tech) => currentProject.technologies.includes(tech))
.length,
}))
.sort((a, b) => b.matchCount - a.matchCount)
.slice(0, maxProjects)
.map((item) => item.project);
Technology Weights¶
Assign importance to technologies:
const techWeights = {
React: 3,
TypeScript: 2,
CSS: 1,
};
const score = project.technologies.reduce((sum, tech) => sum + (techWeights[tech] || 1), 0);
Usage Examples¶
Basic Usage on Project Page¶
import { getProjects, getProjectBySlug } from "@/lib/projects";
import RelatedProjects from "@/components/related-projects";
export default async function ProjectPage({ params }: { params: { slug: string } }) {
const project = await getProjectBySlug(params.slug);
const allProjects = await getProjects();
return (
<main>
{/* Project content */}
<RelatedProjects
currentProject={project}
allProjects={allProjects}
/>
</main>
);
}
With Custom Limit¶
<RelatedProjects
currentProject={project}
allProjects={allProjects}
maxProjects={6} // Show up to 6 suggestions
/>
With Wrapper Section¶
<section className="bg-muted/30 py-16">
<div className="container mx-auto max-w-6xl px-4">
<RelatedProjects
currentProject={project}
allProjects={allProjects}
className="mx-auto"
/>
</div>
</section>
Conditional Rendering¶
const relatedComponent = (
<RelatedProjects
currentProject={project}
allProjects={allProjects}
/>
);
// Component returns null if no matches, so this works:
{relatedComponent && <Separator className="my-12" />}
{relatedComponent}
See Also¶
- Project Data Structure - Project schema
- Project Card Component - Card design
- Skills Generation - Similar technology analysis
- Performance Optimization - Performance tips
Next Steps¶
- Weighted Matching: Score projects by number of shared technologies
- Category Matching: Add project categories for better matching
- User Preferences: Track viewed projects and personalize suggestions
- Featured Prioritization: Boost featured projects in results
- Date Sorting: Prefer recent projects in suggestions
- A/B Testing: Test different matching algorithms
- Analytics: Track which suggestions are clicked
Enhanced Algorithm Example¶
function getRelatedProjects(
currentProject: Project,
allProjects: Project[],
options: {
maxProjects?: number;
sortBy?: "relevance" | "date" | "featured";
minSharedTech?: number;
}
) {
const { maxProjects = 3, sortBy = "relevance", minSharedTech = 1 } = options;
// Calculate match scores
const scoredProjects = allProjects
.filter((p) => p.slug !== currentProject.slug)
.map((project) => {
const sharedTech = project.technologies.filter((tech) =>
currentProject.technologies.includes(tech)
);
return {
project,
score: sharedTech.length,
isFeatured: project.order !== undefined,
date: new Date(project.createdAt || 0),
};
})
.filter((item) => item.score >= minSharedTech);
// Sort based on criteria
if (sortBy === "relevance") {
scoredProjects.sort((a, b) => {
if (a.score !== b.score) return b.score - a.score;
if (a.isFeatured !== b.isFeatured) return a.isFeatured ? -1 : 1;
return b.date.getTime() - a.date.getTime();
});
} else if (sortBy === "date") {
scoredProjects.sort((a, b) => b.date.getTime() - a.date.getTime());
} else if (sortBy === "featured") {
scoredProjects.sort((a, b) => {
if (a.isFeatured !== b.isFeatured) return a.isFeatured ? -1 : 1;
return b.score - a.score;
});
}
return scoredProjects.slice(0, maxProjects).map((item) => item.project);
}
Last Updated: 2024-02-09
Related Docs: Components | Data Flow | Project Card