Skills Generation System¶
Navigation: Documentation Home > Content Management > Skills Generation
Table of Contents¶
- Introduction
- How It Works
- Generation Algorithm
- Skill ID Generation
- Deduplication Strategy
- Project Association
- Sorting and Ordering
- Usage Examples
- Best Practices
- Common Issues
- Advanced Scenarios
- See Also
- Next Steps
Introduction¶
The skills system is automatically generated from project technologies. There are no manual skill definitions—skills are dynamically created by analyzing the technologies array in every project.
Key Concepts¶
- Source of Truth: Project
technologiesarrays - Auto-Generation: Skills computed at runtime/build time
- Deduplication: Same technology (case-insensitive ID) groups projects
- Full References: Each skill contains complete project objects, not just IDs
- Alphabetical Sorting: Skills sorted by name for consistency
Benefits¶
- ✅ No Duplication: Single source of truth in projects
- ✅ Always in Sync: Skills automatically update when projects change
- ✅ Zero Maintenance: No manual skill management required
- ✅ Type Safe: Full TypeScript support
- ✅ Flexible: Easy to query and filter
How It Works¶
High-Level Overview¶
flowchart LR
A[Project JSON Files] --> B[technologies Arrays]
B --> C[Extract All Technologies]
C --> D[Generate IDs]
D --> E[Group by ID]
E --> F[Create Skill Objects]
F --> G[Associate Projects]
G --> H[Sort Alphabetically]
H --> I[Return Skills Array]
style A fill:#e3f2fd
style B fill:#e8f5e9
style C fill:#fff3e0
style D fill:#f3e5f5
style E fill:#fce4ec
style F fill:#e0f2f1
style G fill:#fff9c4
style H fill:#e1bee7
style I fill:#c5e1a5
Example Transformation¶
Input: Project Files
// signapse.json
{
"technologies": ["AI", "Python", "React Native", "TypeScript"]
}
// pop-a-loon.json
{
"technologies": ["React", "TypeScript", "MongoDB"]
}
// chitchatz.json
{
"technologies": ["React", "TypeScript", "WebSocket"]
}
Output: Generated Skills
[
{
id: "ai",
name: "AI",
projects: [
/* Signapse */
],
},
{
id: "mongodb",
name: "MongoDB",
projects: [
/* Pop-a-loon */
],
},
{
id: "python",
name: "Python",
projects: [
/* Signapse */
],
},
{
id: "react",
name: "React",
projects: [
/* Pop-a-loon, ChitChatz */
],
},
{
id: "react-native",
name: "React Native",
projects: [
/* Signapse */
],
},
{
id: "typescript",
name: "TypeScript",
projects: [
/* Signapse, Pop-a-loon, ChitChatz */
],
},
{
id: "websocket",
name: "WebSocket",
projects: [
/* ChitChatz */
],
},
];
Generation Algorithm¶
Complete Flow Diagram¶
flowchart TD
Start([Start: getSkills Called]) --> A[Get All Projects]
A --> B[Create Empty Map<string, Skill>]
B --> C{For Each Project}
C -->|Next Project| D{For Each Technology in Project}
D -->|Next Tech| E[Generate Skill ID from Tech Name]
E --> F{Does Skill ID Exist in Map?}
F -->|Yes| G[Get Existing Skill from Map]
F -->|No| H[Create New Skill Object]
G --> I{Is Project Already in Skill.projects?}
I -->|No| J[Add Project to Skill.projects]
I -->|Yes| K[Skip Duplicate]
H --> L[Set skill.id = generated ID]
L --> M[Set skill.name = original tech name]
M --> N[Set skill.projects = empty array]
N --> O[Add Project to Skill.projects]
O --> P[Add Skill to Map]
J --> Q{More Technologies?}
K --> Q
P --> Q
Q -->|Yes| D
Q -->|No| R{More Projects?}
R -->|Yes| C
R -->|No| S[Convert Map to Array]
S --> T[Sort Array by skill.name Alphabetically]
T --> End([Return Skills Array])
style Start fill:#e8f5e9
style End fill:#c8e6c9
style B fill:#fff9c4
style E fill:#ffe0b2
style F fill:#f3e5f5
style H fill:#e1f5ff
style S fill:#fce4ec
style T fill:#e1bee7
Code Implementation¶
// lib/skills.ts
import { getProjects, Project } from "@/lib/projects";
export interface Skill {
id: string;
name: string;
projects: Project[];
}
export async function getSkills(): Promise<Skill[]> {
// Step 1: Get all projects
const projects = await getProjects();
// Step 2: Create a map to collect unique skills and their projects
const skillsMap = new Map<string, Skill>();
// Step 3: Iterate through each project
projects.forEach((project) => {
// Step 4: Iterate through each technology in the project
project.technologies.forEach((tech) => {
// Step 5: Generate skill ID
const skillId = generateSkillId(tech);
// Step 6: Check if skill already exists
if (skillsMap.has(skillId)) {
// Add project to existing skill
const existingSkill = skillsMap.get(skillId)!;
if (!existingSkill.projects.includes(project)) {
existingSkill.projects.push(project);
}
} else {
// Create new skill
skillsMap.set(skillId, {
id: skillId,
name: tech, // Preserve original capitalization
projects: [project],
});
}
});
});
// Step 7: Convert map to array and sort by name
return Array.from(skillsMap.values()).sort((a, b) => {
return a.name.localeCompare(b.name);
});
}
Step-by-Step Example¶
Let's trace through the algorithm with 2 projects:
Input:
const projects = [
{
slug: "project-a",
title: "Project A",
technologies: ["React", "TypeScript"],
},
{
slug: "project-b",
title: "Project B",
technologies: ["react", "Node.js"], // Note: lowercase "react"
},
];
Execution:
// Initial state
skillsMap = new Map()
// Process Project A, Technology "React"
skillId = generateSkillId("React") // "react"
skillsMap.has("react") // false
skillsMap.set("react", {
id: "react",
name: "React", // Preserves original "React"
projects: [Project A]
})
// Process Project A, Technology "TypeScript"
skillId = generateSkillId("TypeScript") // "typescript"
skillsMap.has("typescript") // false
skillsMap.set("typescript", {
id: "typescript",
name: "TypeScript",
projects: [Project A]
})
// Process Project B, Technology "react" (lowercase)
skillId = generateSkillId("react") // "react" (same ID!)
skillsMap.has("react") // true (found existing!)
existingSkill = skillsMap.get("react")
// existingSkill.name is still "React" (from first occurrence)
existingSkill.projects.push(Project B) // Add to existing
// Process Project B, Technology "Node.js"
skillId = generateSkillId("Node.js") // "nodejs"
skillsMap.has("nodejs") // false
skillsMap.set("nodejs", {
id: "nodejs",
name: "Node.js",
projects: [Project B]
})
// Convert to array and sort
skills = [
{
id: "nodejs",
name: "Node.js",
projects: [Project B]
},
{
id: "react",
name: "React", // First occurrence capitalization
projects: [Project A, Project B]
},
{
id: "typescript",
name: "TypeScript",
projects: [Project A]
}
]
// Sorted alphabetically: "Node.js", "React", "TypeScript"
Skill ID Generation¶
Algorithm¶
export function generateSkillId(skillName: string): string {
return skillName
.toLowerCase() // Convert to lowercase
.replace(/[^\w\s-]/g, "") // Remove special characters (keep word chars, spaces, hyphens)
.replace(/\s+/g, "-") // Replace spaces with hyphens
.replace(/-+/g, "-") // Replace multiple hyphens with single hyphen
.replace(/^-|-$/g, ""); // Remove leading/trailing hyphens
}
Transformation Examples¶
| Input | Step 1: Lowercase | Step 2: Remove Special | Step 3: Spaces→Hyphens | Step 4: Collapse Hyphens | Step 5: Trim | Output |
|---|---|---|---|---|---|---|
React |
react |
react |
react |
react |
react |
react |
Next.js |
next.js |
nextjs |
nextjs |
nextjs |
nextjs |
nextjs |
Tailwind CSS |
tailwind css |
tailwind css |
tailwind-css |
tailwind-css |
tailwind-css |
tailwind-css |
C++ |
c++ |
c |
c |
c |
c |
c |
ASP.NET Core |
asp.net core |
aspnet core |
aspnet-core |
aspnet-core |
aspnet-core |
aspnet-core |
Node.js (Express) |
node.js (express) |
nodejs express |
nodejs-express |
nodejs-express |
nodejs-express |
nodejs-express |
Vue.js 3.0 |
vue.js 3.0 |
vuejs 30 |
vuejs-30 |
vuejs-30 |
vuejs-30 |
vuejs-30 |
React |
react |
react |
--react-- |
-react- |
react |
react |
Regex Breakdown¶
// Step 2: Remove special characters
.replace(/[^\w\s-]/g, "")
// Breakdown:
// [^\w\s-] - Match any character that is NOT:
// \w - Word character (a-z, A-Z, 0-9, _)
// \s - Whitespace
// - - Hyphen
// g - Global flag (replace all occurrences)
// Examples:
"Next.js" → "Nextjs" (. removed)
"C++" → "C" (++ removed)
"React (v18)" → "React v18" (parentheses removed)
Collision Cases¶
Some technology names generate the same ID:
// These all generate id "c"
generateSkillId("C"); // "c"
generateSkillId("C++"); // "c" (++ removed)
generateSkillId("C#"); // "c" (# removed)
// Solution: Use more specific names in technologies array
"technologies": ["C++"] // Better: Specific
"technologies": ["C-Sharp"] // Better: Specific
"technologies": ["C Language"] // Better: Specific
// These generate different IDs
generateSkillId("React"); // "react"
generateSkillId("React Native"); // "react-native" (different!)
Deduplication Strategy¶
Case-Insensitive Matching¶
The system uses ID-based deduplication, which is case-insensitive:
// These are treated as the SAME skill
"React" → ID: "react"
"react" → ID: "react"
"REACT" → ID: "react"
"ReAcT" → ID: "react"
// They all map to the same skill
{
id: "react",
name: "React", // First occurrence's capitalization preserved
projects: [/* all projects using any variant */]
}
First Occurrence Wins¶
The name field preserves the capitalization from the first occurrence:
// Project 1 (processed first)
{
"technologies": ["react"] // lowercase
}
// Project 2
{
"technologies": ["React"] // proper case
}
// Result
{
id: "react",
name: "react", // ⚠️ Uses first occurrence (lowercase)
projects: [Project 1, Project 2]
}
// Best Practice: Use consistent capitalization in all projects
// All projects should use:
{
"technologies": ["React"] // Consistent!
}
Example: Inconsistent Capitalization¶
// Projects with inconsistent capitalization
const projects = [
{ slug: "p1", technologies: ["TypeScript", "Docker"] },
{ slug: "p2", technologies: ["typescript", "docker"] },
{ slug: "p3", technologies: ["TYPESCRIPT", "DOCKER"] },
];
// Generated skills
const skills = [
{
id: "docker",
name: "Docker", // First occurrence
projects: [
/* p1, p2, p3 */
],
},
{
id: "typescript",
name: "TypeScript", // First occurrence
projects: [
/* p1, p2, p3 */
],
},
];
// ✅ Correct grouping (all variants included)
// ⚠️ Display name uses first occurrence capitalization
Project Association¶
Full Project Objects¶
Skills contain complete project objects, not just slugs or IDs:
const skill: Skill = {
id: "react",
name: "React",
projects: [
{
slug: "project-a",
title: "Project A",
shortDescription: "...",
description: "...",
technologies: ["React", "TypeScript"],
images: [...],
// ... all project fields
},
{
slug: "project-b",
title: "Project B",
// ... all project fields
}
]
};
// You can access any project field directly
skill.projects.forEach(project => {
console.log(project.title);
console.log(project.technologies);
console.log(project.images[0].src);
});
Duplicate Prevention¶
The algorithm prevents the same project from being added twice:
if (skillsMap.has(skillId)) {
const existingSkill = skillsMap.get(skillId)!;
// Check if project is already in the array
if (!existingSkill.projects.includes(project)) {
existingSkill.projects.push(project);
}
}
Why needed? Edge cases where same project might be processed multiple times (though unlikely with current implementation).
Sorting and Ordering¶
Alphabetical by Name¶
Skills are always sorted alphabetically by the name field (case-insensitive):
Locale-Aware Sorting¶
localeCompare() provides proper internationalized sorting:
// Example
const skills = [{ name: "Ångström" }, { name: "Zebra" }, { name: "Apple" }];
skills.sort((a, b) => a.name.localeCompare(b.name));
// Result: ["Ångström", "Apple", "Zebra"]
// Without localeCompare (incorrect):
skills.sort((a, b) => (a.name < b.name ? -1 : 1));
// Result: ["Apple", "Zebra", "Ångström"]
Custom Sorting¶
You can re-sort skills for specific use cases:
const skills = await getSkills();
// Sort by number of projects (descending)
const byPopularity = [...skills].sort((a, b) => b.projects.length - a.projects.length);
// Sort by most recent project
const byRecency = [...skills].sort((a, b) => {
const maxOrderA = Math.min(...a.projects.map((p) => p.order ?? Infinity));
const maxOrderB = Math.min(...b.projects.map((p) => p.order ?? Infinity));
return maxOrderA - maxOrderB;
});
// Filter and sort
const popularSkills = skills
.filter((s) => s.projects.length >= 3)
.sort((a, b) => b.projects.length - a.projects.length);
Usage Examples¶
Display All Skills¶
import { getSkills } from "@/lib/skills";
export default async function SkillsPage() {
const skills = await getSkills();
return (
<div>
<h1>Skills ({skills.length})</h1>
<ul>
{skills.map(skill => (
<li key={skill.id}>
<strong>{skill.name}</strong> - {skill.projects.length} project(s)
</li>
))}
</ul>
</div>
);
}
Skills Table¶
import { getSkills } from "@/lib/skills";
export default async function SkillsTable() {
const skills = await getSkills();
return (
<table>
<thead>
<tr>
<th>Skill</th>
<th>Projects</th>
<th>Project Names</th>
</tr>
</thead>
<tbody>
{skills.map(skill => (
<tr key={skill.id}>
<td>{skill.name}</td>
<td>{skill.projects.length}</td>
<td>
{skill.projects.map(p => p.title).join(", ")}
</td>
</tr>
))}
</tbody>
</table>
);
}
Find Projects by Skill¶
import { getSkills } from "@/lib/skills";
async function findProjectsBySkill(skillName: string) {
const skills = await getSkills();
const skill = skills.find((s) => s.name.toLowerCase() === skillName.toLowerCase());
return skill?.projects ?? [];
}
// Usage
const reactProjects = await findProjectsBySkill("React");
console.log(reactProjects.map((p) => p.title));
Related Projects by Skills¶
import { getSkills, Skill } from "@/lib/skills";
import { Project } from "@/lib/projects";
async function getRelatedProjects(currentProject: Project, limit: number = 3): Promise<Project[]> {
const skills = await getSkills();
// Find skills used by current project
const projectSkills = skills.filter((skill) =>
skill.projects.some((p) => p.slug === currentProject.slug)
);
// Collect all related projects
const relatedMap = new Map<string, { project: Project; sharedCount: number }>();
projectSkills.forEach((skill) => {
skill.projects.forEach((project) => {
if (project.slug !== currentProject.slug) {
const existing = relatedMap.get(project.slug);
if (existing) {
existing.sharedCount++;
} else {
relatedMap.set(project.slug, {
project,
sharedCount: 1,
});
}
}
});
});
// Sort by shared skills count and return
return Array.from(relatedMap.values())
.sort((a, b) => b.sharedCount - a.sharedCount)
.slice(0, limit)
.map((item) => item.project);
}
Best Practices¶
1. Consistent Capitalization¶
Always use the same capitalization across all projects:
// ✅ Good - consistent
// project-a.json
{"technologies": ["React", "TypeScript", "Docker"]}
// project-b.json
{"technologies": ["React", "Node.js", "MongoDB"]}
// ❌ Bad - inconsistent
// project-a.json
{"technologies": ["React", "TypeScript", "Docker"]}
// project-b.json
{"technologies": ["react", "typescript", "docker"]}
// Creates display names with lowercase (if processed first)
2. Specific Technology Names¶
Use specific names to avoid ID collisions:
// ✅ Good - specific
{"technologies": ["React Native", "React"]} // Different IDs: "react-native", "react"
// ✅ Good - specific
{"technologies": ["C++", "C#", "C"]} // Use "C-Plus-Plus", "C-Sharp", "C"
// ❌ Bad - ambiguous
{"technologies": ["C++", "C"]} // Both generate "c"
3. Standard Names¶
Use widely recognized technology names:
// ✅ Good - standard names
{
"technologies": [
"TypeScript", // Not "TS"
"JavaScript", // Not "JS"
"React", // Not "React.js"
"Next.js", // Not "NextJS"
"Node.js", // Not "NodeJS"
"PostgreSQL", // Not "Postgres"
"MongoDB" // Not "Mongo"
]
}
4. Avoid Redundancy¶
Don't list both framework and underlying language unless both are significant:
// ⚠️ Redundant
{"technologies": ["React", "JavaScript"]} // React implies JavaScript
// ✅ Better
{"technologies": ["React", "TypeScript"]} // TypeScript is significant
// ⚠️ Redundant
{"technologies": ["Next.js", "React"]} // Next.js implies React
// ✅ Better (if both are showcased)
{"technologies": ["Next.js", "React", "TypeScript"]}
5. Order by Importance¶
List technologies from most to least important:
{
"technologies": [
"React", // Primary framework
"TypeScript", // Primary language
"Next.js", // Framework extension
"Tailwind CSS", // Styling
"PostgreSQL", // Database
"Docker" // Deployment
]
}
Common Issues¶
Issue 1: Duplicate Skills with Different Names¶
Problem: Skills appear multiple times with slight variations
Cause: Inconsistent capitalization across projects
Solution:
// Find inconsistencies
const skills = await getSkills();
const names = skills.map((s) => s.name);
const lowercase = names.map((n) => n.toLowerCase());
const duplicates = lowercase.filter((n, i) => lowercase.indexOf(n) !== i);
// Fix by updating all project JSON files to use consistent capitalization
Issue 2: Skill Has Wrong Display Name¶
Problem: Skill displays as "react" instead of "React"
Cause: First project processed uses lowercase
Solution: Update the first project file (alphabetically by filename) to use proper capitalization
Issue 3: Too Many Skills¶
Problem: Too many granular skills (e.g., "React", "React Hooks", "React Router")
Solution: Use broader terms
// ❌ Too granular
{"technologies": ["React", "React Hooks", "React Router", "React Context"]}
// ✅ Better
{"technologies": ["React"]}
// ✅ Or mention specific advanced features
{"technologies": ["React", "React Router"]}
Advanced Scenarios¶
Custom Skill Grouping¶
Create skill categories:
const skills = await getSkills();
const categories = {
frontend: skills.filter((s) => ["React", "Vue.js", "Angular", "Svelte"].includes(s.name)),
backend: skills.filter((s) => ["Node.js", "Python", "Java", "Go"].includes(s.name)),
database: skills.filter((s) => ["PostgreSQL", "MongoDB", "MySQL", "Redis"].includes(s.name)),
devops: skills.filter((s) => ["Docker", "Kubernetes", "CI/CD", "AWS"].includes(s.name)),
};
Skill Statistics¶
const skills = await getSkills();
const stats = {
total: skills.length,
mostUsed: skills.reduce((max, s) => (s.projects.length > max.projects.length ? s : max)),
average: skills.reduce((sum, s) => sum + s.projects.length, 0) / skills.length,
single: skills.filter((s) => s.projects.length === 1).length,
multiple: skills.filter((s) => s.projects.length > 1).length,
};
See Also¶
- Data Fetching API -
getSkills()function details - TypeScript Interfaces -
Skillinterface definition - Adding Projects - How to add technologies to projects
- JSON Schema -
technologiesfield specification
Next Steps¶
- Review Projects: Check all project
technologiesarrays for consistency - Test Generation: Run
getSkills()and examine output - Build Skills Page: Create a page displaying all skills
- Add Filtering: Implement skill-based project filtering
- Create Categories: Group skills into meaningful categories
Last Updated: February 2026
Maintainer: Development Team
Related Files: lib/skills.ts, lib/projects.ts