Skip to main content

Getting Started

Creating a custom template for GitFolio involves building a React component that receives standardized user data and renders a portfolio website. This guide walks you through the entire process.

Prerequisites

Before creating a template, ensure you have:
  • Node.js (version 20 or higher)
  • pnpm (version 10.4.1 or higher)
  • TypeScript knowledge
  • React fundamentals
  • Tailwind CSS familiarity

Development Setup

1. Clone and Install

git clone https://github.com/Skb3611/GitFolio.git
cd GitFolio
pnpm install

2. Build the Renderer

pnpm run build --filter=renderer

3. Start Development Server

pnpm run dev --filter=renderer
The renderer package at packages/renderer provides live preview of templates during development.

Template Structure

Directory Layout

Create your template directory in packages/templates/src/Templates/:
packages/templates/src/Templates/
└── MyAwesomeTemplate/
    ├── components/
    │   ├── Hero.tsx
    │   ├── Projects.tsx
    │   ├── Experience.tsx
    │   ├── Skills.tsx
    │   └── Footer.tsx
    └── template.tsx

Main Template File

Create template.tsx as your template’s entry point:
packages/templates/src/Templates/MyAwesomeTemplate/template.tsx
"use client";
import React from "react";
import { DummyData } from "../dummyData";
import { DATA } from "@workspace/types";
import { useTheme } from "next-themes";

const template = ({ data = DummyData }: { data?: DATA }) => {
  const { setTheme } = useTheme();

  React.useEffect(() => {
    setTheme("dark"); // Set your preferred default theme
  }, []);

  return (
    <div className="min-h-screen bg-background">
      {/* Your template content */}
      <h1>{data.personalInfo.full_name}</h1>
    </div>
  );
};

export default template;
GitFolio uses Next.js, and templates often need client-side features like hooks, event handlers, and theme switching. The "use client" directive marks this component as a Client Component.

Understanding the DATA Interface

Your template receives a DATA object with all user information:
import { DATA } from "@workspace/types";

interface DATA {
  personalInfo: PersonalInformation;
  projects: Projects[];
  experience: Experience[];
  education: Education[];
  socialLinks: SocialLinks;
  skills: string[];
}

Personal Information

interface PersonalInformation {
  profileImg: string;
  full_name: string;
  username: string;
  email: string;
  location: string | null;
  tagline: string | null;
  bio: string | null;
  website: string | null;
  githubLink: string;
  followers: number;
  following: number;
  activeTemplateId?: string;
}
Usage example:
<img src={data.personalInfo.profileImg} alt={data.personalInfo.full_name} />
<h1>{data.personalInfo.full_name}</h1>
<p>{data.personalInfo.tagline}</p>

Projects

interface Projects {
  id: string;
  name: string;
  description: string;
  thumbnail: string;
  repoLink: string;
  topics: string[];
  liveLink: string;
  languages: Object;
  stars: number;
  forks: number;
  isIncluded: boolean;
}
Usage example:
{data.projects
  .filter(p => p.isIncluded)
  .map(project => (
    <div key={project.id}>
      <h3>{project.name}</h3>
      <p>{project.description}</p>
      <a href={project.liveLink}>View Live</a>
    </div>
  ))}

Experience

interface Experience {
  id: string;
  logo: string;
  company: string;
  role: string;
  description: string;
  start_date: string;
  end_date: string;
  onGoing: boolean;
}

Education

interface Education {
  id: string;
  logo: string;
  title: string;
  institution: string;
  description: string;
  start_date: string;
  end_date: string;
  onGoing: boolean;
}
interface SocialLinks {
  github: string | null;
  linkedin: string | null;
  twitter: string | null;
  website: string | null;
  instagram: string | null;
  facebook: string | null;
  behance: string | null;
  youtube: string | null;
}

Building Components

Hero Component Example

packages/templates/src/Templates/MyAwesomeTemplate/components/Hero.tsx
import { PersonalInformation } from "@workspace/types";

interface HeroProps {
  data: PersonalInformation;
}

export default function Hero({ data }: HeroProps) {
  return (
    <section className="min-h-screen flex items-center justify-center">
      <div className="text-center space-y-4">
        <img
          src={data.profileImg}
          alt={data.full_name}
          className="w-32 h-32 rounded-full mx-auto"
        />
        <h1 className="text-5xl font-bold">{data.full_name}</h1>
        {data.tagline && (
          <p className="text-xl text-muted-foreground">{data.tagline}</p>
        )}
        {data.bio && <p className="max-w-2xl mx-auto">{data.bio}</p>}
      </div>
    </section>
  );
}

Projects Section Example

packages/templates/src/Templates/MyAwesomeTemplate/components/Projects.tsx
import { Projects } from "@workspace/types";

interface ProjectsSectionProps {
  data: Projects[];
}

export default function ProjectsSection({ data }: ProjectsSectionProps) {
  const includedProjects = data.filter(p => p.isIncluded);

  if (includedProjects.length === 0) return null;

  return (
    <section className="py-20">
      <h2 className="text-3xl font-bold mb-8">Projects</h2>
      <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
        {includedProjects.map(project => (
          <div key={project.id} className="border rounded-lg p-6">
            <img
              src={project.thumbnail}
              alt={project.name}
              className="w-full h-48 object-cover rounded mb-4"
            />
            <h3 className="text-xl font-semibold mb-2">{project.name}</h3>
            <p className="text-muted-foreground mb-4">
              {project.description}
            </p>
            <div className="flex gap-2 flex-wrap mb-4">
              {project.topics.map(topic => (
                <span
                  key={topic}
                  className="px-2 py-1 bg-secondary rounded text-sm"
                >
                  {topic}
                </span>
              ))}
            </div>
            <div className="flex gap-4">
              <a href={project.liveLink} className="text-blue-500">
                Live Demo
              </a>
              <a href={project.repoLink} className="text-blue-500">
                View Code
              </a>
            </div>
          </div>
        ))}
      </div>
    </section>
  );
}

Styling with Tailwind

GitFolio uses Tailwind CSS for styling. Use Tailwind’s utility classes:
<div className="max-w-6xl mx-auto px-4 py-20">
  <h1 className="text-4xl font-bold text-foreground">
    Title
  </h1>
  <p className="text-muted-foreground mt-4">
    Description
  </p>
</div>

Theme-aware Styling

// Light mode: white background, dark text
// Dark mode: dark background, light text
<div className="bg-background text-foreground">
  
// Conditional styling based on theme
<div className="bg-white dark:bg-gray-900 text-black dark:text-white">
Use Tailwind’s semantic color classes like bg-background, text-foreground, and text-muted-foreground for automatic theme switching.

Theme Integration

Setting Default Theme

import { useTheme } from "next-themes";

const template = ({ data = DummyData }: { data?: DATA }) => {
  const { setTheme } = useTheme();

  React.useEffect(() => {
    setTheme("dark"); // or "light"
  }, []);

  return <div>...</div>;
};

Adding Theme Toggle

import { useTheme } from "next-themes";
import { Moon, Sun } from "lucide-react";

function ThemeToggle() {
  const { theme, setTheme } = useTheme();

  return (
    <button
      onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
      className="p-2 rounded-lg hover:bg-secondary"
    >
      {theme === "dark" ? <Sun /> : <Moon />}
    </button>
  );
}

Using Shared Components

Leverage GitFolio’s shared components from @workspace/ui:
import { Button } from "@workspace/ui/components/button";
import { Card } from "@workspace/ui/components/card";
import { Avatar, AvatarImage } from "@workspace/ui/components/avatar";

// Use in your template
<Button variant="default" size="lg">
  Contact Me
</Button>

<Card className="p-6">
  <Avatar>
    <AvatarImage src={data.personalInfo.profileImg} />
  </Avatar>
</Card>

Adding Animations

Use Motion (Framer Motion) for animations:
import { motion } from "motion/react";

<motion.div
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ duration: 0.5 }}
>
  <h1>Animated Content</h1>
</motion.div>

Exporting Your Template

1. Export from Template Directory

Add to packages/templates/src/Templates/index.ts:
export { default as MyAwesomeTemplate } from "./MyAwesomeTemplate/template";

2. Test in Renderer

Update packages/renderer/app/page.tsx:
import { MyAwesomeTemplate } from "@workspace/templates";

export default function Page() {
  return <MyAwesomeTemplate />;
}

3. Run Preview

pnpm run dev --filter=renderer
Visit http://localhost:3000 to see your template.

Best Practices

Use optional chaining and provide fallbacks:
{data.personalInfo.tagline ?? "Add a tagline"}
{data.projects.length > 0 && <Projects data={data.projects} />}
Each component should have a single responsibility. Don’t create monolithic components.
Test on mobile, tablet, and desktop. Use Tailwind’s responsive classes:
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
Use proper HTML5 elements for accessibility:
<header>, <nav>, <main>, <section>, <article>, <footer>
Always add alt text and use appropriate image sizing:
<img src={src} alt="Descriptive text" className="w-full h-auto" />

Testing Your Template

Build Test

Ensure your template builds without errors:
pnpm run build --filter=renderer

Responsive Testing

Test your template at these breakpoints:
  • Mobile: 375px, 428px
  • Tablet: 768px, 1024px
  • Desktop: 1440px, 1920px

Theme Testing

Verify both light and dark themes:
// In your browser console
localStorage.setItem('theme', 'dark')
localStorage.setItem('theme', 'light')

Common Patterns

Conditional Rendering

// Only render section if data exists
{data.experience.length > 0 && (
  <ExperienceSection data={data.experience} />
)}

// Provide fallback
{data.personalInfo.bio || "No bio available"}

Date Formatting

import { format } from "date-fns";

const formattedDate = format(new Date(experience.start_date), "MMM yyyy");

Filtering Projects

const includedProjects = data.projects.filter(p => p.isIncluded);

Next Steps

Contribute Your Template

Submit your template to GitFolio

Template Architecture

Deep dive into template architecture

Build docs developers (and LLMs) love