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.
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.