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.
Writing compelling project case studies
The difference between a portfolio that gets replies and one that gets passed over is almost always the writing, not the design. A project card with a name and a screenshot says nothing about your thinking. A case study that explains the problem, your approach, and the outcome tells an employer or client exactly how you work.
For each project, answer four questions in prose: What problem did this solve? What constraints shaped your decisions? What did you build, and why did you make the key choices you made? What happened after launch? Concrete outcomes matter β βreduced load time by 40%β is better than βimproved performance.β Even βlaunched with zero bugs and received positive user feedbackβ is better than no outcome at all.
Keep case studies focused and readable. Two to three paragraphs per project is usually ideal. Use the content from your project Markdown file for the full case study, and the description front matter field for the one-sentence summary shown on cards and in meta tags.
Customising typography and colour
A portfolio communicates your taste before anyone reads a word. The visual identity you choose β typography, colour palette, spacing β forms the first impression. If you are using a theme, you can usually override these without editing the theme source.
Create _sass/overrides.scss (or modify assets/css/main.scss) to set CSS custom properties before the themeβs defaults apply:
:root {
--color-primary: #0f4c81;
--color-text: #1a1a2e;
--font-sans: 'Inter', system-ui, -apple-system, sans-serif;
}
For typography, use Google Fonts or a self-hosted font. Self-hosting is better for performance and privacy β download the font files, place them in assets/fonts/, and reference them with @font-face in your Sass. This eliminates the third-party network request and the associated Cumulative Layout Shift that Google Fonts can cause if the font loads slowly.
For a portfolio, choose one display font for headings and one body font for prose. Two fonts is the maximum β adding more looks unprofessional and hurts readability. A popular combination for developer portfolios is Inter (body) with a mono font like Fira Code for code samples.
SEO for your portfolio
Your portfolio competes with thousands of similar sites for visibility on search engines. A few targeted optimisations make a meaningful difference.
Set descriptive title tags and meta descriptions for every page via _config.yml and front matter. Install jekyll-seo-tag if your theme does not already include it β it handles Open Graph, Twitter Card, and basic meta tags automatically from your front matter values.
For the homepage, include your full name, location, and primary skills in the page copy. Search engines need this text to associate your portfolio with relevant searches. Avoid putting all this information only in images or hero graphics β text the crawler cannot read does not help.
Create a dedicated /projects/ listing page and individual project pages for your most significant work. Each project page is an opportunity to rank for the project name, the technology stack, and problem keywords. If you built a βtask management app with React,β that phrase should appear naturally in your projectβs title, description, and body content.
Add jekyll-sitemap to generate a sitemap.xml automatically. Submit it to Google Search Console after deployment. This is a five-minute step that accelerates how quickly Google indexes your pages.
Adding a blog section
A portfolio blog improves search visibility, demonstrates expertise, and gives repeat visitors a reason to return. Jekyll makes this trivial β _posts/ already exists, and your theme likely already includes a blog layout.
Write about what you learn while building projects. Tutorials, problem-solving posts, and βhow I built Xβ articles are highly searchable and position you as a practitioner rather than just someone listing credentials. Even one post per month compounds significantly over a year.
Keep a consistent format: a clear title, an estimated reading time, the date, and tags. Tags become navigable category pages over time, building topical authority around your areas of expertise.
Use your blog posts to link internally to your project pages and to other posts. Internal links distribute ranking authority across your portfolio and help search engines understand the structure of your site.
Setting up analytics
Understanding who visits your portfolio and which projects attract the most attention helps you prioritise what to write and build next. Two good analytics options for Jekyll are Google Analytics 4 and Plausible.
Google Analytics 4 is free and widely used. Add the tracking snippet via a _includes/analytics.html file and include it in your layout inside a {% if jekyll.environment == "production" %} guard so it only fires on your live site, not during local development.
{% if jekyll.environment == "production" and site.google_analytics %}
{% include analytics.html %}
{% endif %}
Plausible is a privacy-focused, paid alternative with a simpler interface and no cookie banner requirement under GDPR. For a portfolio with modest traffic, the Plausible Starter plan covers typical usage. The setup is identical β a single script tag in your layout.
Monitor which project pages have the highest traffic, which blog posts attract external links, and how visitors navigate from the homepage to specific projects. Use this data to decide which projects to write fuller case studies for and which technologies to emphasise.
Keeping your portfolio updated
The most common portfolio failure is abandonment. A portfolio with a last commit date from three years ago and projects that no longer have working live links sends a negative signal. Build the habit of updating it on the same schedule you would a professional social profile.
Set a recurring reminder to review your portfolio every quarter. Check that all live links still work. Add any significant recent projects. Update your skills list if you have learned new technologies. Refresh the design if the current look feels dated.
Jekyllβs Git-based workflow makes updates fast. You do not log into a CMS β you open your editor, edit a Markdown file, and push. A bundle exec jekyll serve preview takes seconds. The friction of updating is lower than almost any other platform, which removes the main excuse for letting a portfolio go stale.
Use html-proofer in your CI/CD pipeline to catch broken links automatically on every push. Add it to your GitHub Actions workflow:
- name: Check links
run: bundle exec htmlproofer _site --disable-external
This fails the build if any internal link is broken, ensuring your portfolio never ships with dead links.
Browse Jekyll portfolio themes on JekyllHub with live demos and filter by the Portfolio category to find the right visual foundation before you start building.
Optimising for performance
A slow portfolio hurts your credibility β you are demonstrating technical work on a page that performs poorly. Aim for a Lighthouse performance score above 90 on every page.
The most impactful optimisations for Jekyll portfolios are image compression, proper font loading, and minimal JavaScript. Use WebP format for all project images and compress them before committing. Tools like imagemin-webp or the jekyll-imagemagick plugin can automate this in your build process. Always include width and height attributes on your <img> tags to prevent Cumulative Layout Shift β the browser needs these to reserve the correct space before the image loads.
For fonts, load only the weights you use. A developer portfolio typically needs Regular (400) and Bold (700) at most. Each additional weight adds an HTTP request and delays rendering. Consider system font stacks (system-ui, -apple-system, sans-serif) if font choice is not a strong priority β they render instantly with no network requests.
Keep JavaScript to a minimum. A portfolio rarely needs a full JavaScript framework. Alpine.js (15kb) is sufficient for most interactive elements β a mobile menu, a dark mode toggle, or a simple lightbox. Every unnecessary script tag is a render-blocking or execution cost that your visitors pay on every visit.
Run bundle exec jekyll build and then npx pagespeed-insights _site/index.html (or simply test with Googleβs PageSpeed Insights tool) before publishing. Fix anything below 90 before launch.