Home Blog How to Build a Portfolio Website with Jekyll
Tutorial

How to Build a Portfolio Website with Jekyll

Build a professional Jekyll portfolio site — showcase projects, add a blog, set up contact forms, and deploy to GitHub Pages. Includes theme recommendations.

How to Build a Portfolio Website with Jekyll

A Jekyll portfolio site is fast, free to host, and lives in a GitHub repository — which itself signals professionalism to employers and clients. This guide covers building a portfolio from the ground up.


Why Jekyll for a Portfolio?

  • GitHub Pages hosting — free, reliable, with your custom domain
  • Markdown writing — write project case studies in plain text
  • No backend — nothing to hack, nothing to maintain, nothing to pay for
  • Impressive to employers — a portfolio on GitHub shows you can use version control and static site tools

Choosing a Portfolio Theme

Rather than building from scratch, start with a theme. Here are the best Jekyll portfolio themes:

Minimal Mistakes — the most flexible option. Supports portfolio layouts, blog, and a huge range of customisation options. 27k+ GitHub stars.

Chirpy — beautiful and minimal, excellent for developer portfolios with a blog. Strong dark mode.

Huxpro — full-screen hero, clean typography. Popular for personal sites.

Browse all portfolio-ready themes at JekyllHub — filter by the Portfolio category.


Project Structure for a Portfolio

my-portfolio/
├── _config.yml
├── _layouts/
│   ├── default.html
│   ├── home.html
│   └── project.html
├── _includes/
│   ├── header.html
│   ├── footer.html
│   └── project-card.html
├── _projects/              # Collection of case studies
│   ├── my-web-app.md
│   ├── mobile-app.md
│   └── open-source-tool.md
├── _posts/                 # Optional blog
├── _data/
│   ├── skills.yml          # Your skills list
│   └── experience.yml      # Work history
├── assets/
│   ├── images/projects/    # Screenshots and mockups
│   └── css/
├── index.md                # Homepage
├── about.md                # About page
└── contact.md              # Contact page

Setting Up the Projects Collection

# _config.yml
collections:
  projects:
    output: true
    permalink: /projects/:name/
    sort_by: order

defaults:
  - scope:
      type: projects
    values:
      layout: project

A project file (_projects/my-web-app.md):

---
title: "Task Management App"
description: "A full-stack task manager built with React and Node.js. 2,000+ active users."
order: 1
year: 2025
tech:
  - React
  - Node.js
  - PostgreSQL
  - AWS
role: "Lead Developer"
image: /assets/images/projects/task-app-hero.jpg
screenshots:
  - /assets/images/projects/task-app-1.jpg
  - /assets/images/projects/task-app-2.jpg
github_url: https://github.com/username/task-app
live_url: https://taskapp.io
featured: true
---

## Overview

Task App is a collaborative task management tool I built to scratch my own itch — existing tools were either too complex or too simple.

## The Challenge

The main technical challenge was real-time synchronisation across multiple users without a WebSocket server...

## What I Built

- Real-time updates using server-sent events
- Drag-and-drop task ordering
- Team spaces with role-based permissions
- Mobile-first responsive design

## Results

- 2,000 active users within 3 months of launch
- 4.8/5 star rating on Product Hunt

The Project Layout

Create _layouts/project.html:


---
layout: default
---

<article class="project-detail">
  <header class="project-header">
    <h1>{{ page.title }}</h1>
    <p class="project-description">{{ page.description }}</p>
    
    <div class="project-meta">
      {% if page.role %}<span><strong>Role:</strong> {{ page.role }}</span>{% endif %}
      {% if page.year %}<span><strong>Year:</strong> {{ page.year }}</span>{% endif %}
    </div>
    
    <div class="project-tech">
      {% for tech in page.tech %}
        <span class="tech-badge">{{ tech }}</span>
      {% endfor %}
    </div>
    
    <div class="project-links">
      {% if page.live_url %}
        <a href="{{ page.live_url }}" class="btn btn--primary" target="_blank">
          View Live →
        </a>
      {% endif %}
      {% if page.github_url %}
        <a href="{{ page.github_url }}" class="btn btn--secondary" target="_blank">
          GitHub →
        </a>
      {% endif %}
    </div>
  </header>
  
  {% if page.image %}
    <img src="{{ page.image | relative_url }}" alt="{{ page.title }}" class="project-hero-image">
  {% endif %}
  
  <div class="project-content">
    {{ content }}
  </div>
  
  {% if page.screenshots.size > 0 %}
    <div class="project-screenshots">
      {% for screenshot in page.screenshots %}
        <img src="{{ screenshot | relative_url }}" alt="{{ page.title }} screenshot" loading="lazy">
      {% endfor %}
    </div>
  {% endif %}
</article>


The Homepage


<!-- _layouts/home.html -->
---
layout: default
---

<!-- Hero Section -->
<section class="hero">
  <div class="container">
    <h1>{{ site.author.name }}</h1>
    <p class="hero-tagline">{{ site.author.bio }}</p>
    <div class="hero-cta">
      <a href="#projects" class="btn btn--primary">View Projects</a>
      <a href="/contact/" class="btn btn--secondary">Get in Touch</a>
    </div>
  </div>
</section>

<!-- Featured Projects -->
<section class="projects" id="projects">
  <div class="container">
    <h2>Projects</h2>
    <div class="projects-grid">
      {% assign featured = site.projects | where: "featured", true | sort: "order" %}
      {% for project in featured %}
        {% include project-card.html project=project %}
      {% endfor %}
    </div>
    <a href="/projects/" class="view-all">View all projects →</a>
  </div>
</section>

<!-- Skills -->
<section class="skills">
  <div class="container">
    <h2>Skills</h2>
    <div class="skills-grid">
      {% for skill_group in site.data.skills %}
        <div class="skill-group">
          <h3>{{ skill_group.category }}</h3>
          <ul>
            {% for skill in skill_group.items %}
              <li>{{ skill }}</li>
            {% endfor %}
          </ul>
        </div>
      {% endfor %}
    </div>
  </div>
</section>

{{ content }}


Skills Data File

Create _data/skills.yml:

- category: Frontend
  items:
    - HTML/CSS
    - JavaScript
    - React
    - Vue.js
    - Tailwind CSS

- category: Backend
  items:
    - Node.js
    - Python
    - Ruby on Rails
    - PostgreSQL
    - Redis

- category: Tools
  items:
    - Git
    - Docker
    - AWS
    - Jekyll
    - Figma

Adding a Contact Form

Jekyll is static, so you need a form service for contact forms. Formspree is the most popular — free for basic use, no backend required.

<!-- contact.md -->
---
layout: page
title: Contact
---

<form action="https://formspree.io/f/YOUR_FORM_ID" method="POST" class="contact-form">
  <div class="form-group">
    <label for="name">Name</label>
    <input type="text" id="name" name="name" required>
  </div>
  
  <div class="form-group">
    <label for="email">Email</label>
    <input type="email" id="email" name="email" required>
  </div>
  
  <div class="form-group">
    <label for="message">Message</label>
    <textarea id="message" name="message" rows="6" required></textarea>
  </div>
  
  <button type="submit" class="btn btn--primary">Send Message</button>
</form>

Sign up at Formspree, create a form, and use the form ID in the action URL. Submissions are emailed to you.


Deploying to GitHub Pages

git init
git add .
git commit -m "Initial portfolio"
git remote add origin https://github.com/username/username.github.io
git push -u origin main

Go to repository Settings → Pages and enable GitHub Pages. Your portfolio is live at https://username.github.io.

For a custom domain, add CNAME with your domain and update your DNS.


Ready to find the right design? Browse Jekyll portfolio themes on JekyllHub with live demos to see them in action before you start.

Share LinkedIn