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.


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.

Share LinkedIn