Jekyll themes provide a powerful way to create consistent, maintainable websites while separating content from presentation. This comprehensive guide covers everything from basic theme usage to advanced API integration patterns.
Jekyll themes are reusable packages that contain:
# _config.yml
theme: minima
# _config.yml
remote_theme: jekyll/minima
# Gemfile
gem "theme-name", "~> 1.0"
# _config.yml
theme: theme-name
bundle install
bundle exec jekyll serve
# _config.yml
remote_theme: username/theme-repository
plugins:
- jekyll-remote-theme
# Clone theme repository
git clone https://github.com/username/theme-name.git my-site
cd my-site
# Install dependencies
bundle install
# Customize and build
bundle exec jekyll serve
theme-name/
├── _layouts/
│ ├── default.html # Base layout
│ ├── page.html # Page layout
│ ├── post.html # Blog post layout
│ └── home.html # Homepage layout
├── _includes/
│ ├── header.html # Site header
│ ├── footer.html # Site footer
│ ├── head.html # HTML head section
│ └── navigation.html # Navigation menu
├── _sass/
│ ├── _variables.scss # Theme variables
│ ├── _base.scss # Base styles
│ └── _layout.scss # Layout styles
├── assets/
│ ├── main.scss # Main stylesheet
│ ├── js/ # JavaScript files
│ └── images/ # Theme images
├── _config.yml # Theme configuration
└── theme-name.gemspec # Gem specification
---
layout: default # Inherits from default.html
---
<!-- _layouts/default.html -->
<!DOCTYPE html>
<html>
<head>
<% include head.html %%}gt;
</head>
<body>
<% include header.html %%}gt;
<main><{{lt; content >}}gt;</main>
<% include footer.html %%}gt;
</body>
</html>
<!-- _layouts/post.html -->
---
layout: default
---
<article class="post">
<h1><{{lt; page.title >}}gt;</h1>
<div class="post-content"><{{lt; content >}}gt;</div>
</article>
Create files with the same path in your site to override theme files:
your-site/
├── _layouts/
│ └── post.html # Overrides theme's post.html
├── _includes/
│ └── header.html # Overrides theme's header.html
└── _sass/
└── _variables.scss # Overrides theme variables
// _sass/_variables.scss
$primary-color: #2c5aa0;
$font-family: 'Helvetica Neue', Arial, sans-serif;
$content-width: 800px;
// Import theme styles after variables
@import 'theme-name';
// assets/main.scss
---
---
@import "theme-name";
// Custom styles
.custom-section {
background-color: var(--custom-bg);
padding: 2rem;
}
# _config.yml
title: 'My Custom Site'
description: 'Powered by Jekyll Theme'
# Theme-specific settings
theme_settings:
navigation:
- title: 'Home'
url: '/'
- title: 'About'
url: '/about/'
social_links:
github: 'username'
twitter: 'username'
# Override theme defaults
permalink: /:categories/:year/:month/:day/:title/
paginate: 10
<!-- _layouts/api-docs.html -->
---
layout: default
---
<div class="api-documentation">
<aside class="api-sidebar"><!-- <% include api-navigation.html %%}gt; --></aside>
<main class="api-content">
<header class="api-header">
<h1><{{lt; page.title >}}gt;</h1>
<% if page.api_version %%}gt;
<span class="api-version"><{{lt; page.api_version >}}gt;</span>
<% endif %%}gt;
</header>
<div class="api-body"><{{lt; content >}}gt;</div>
<% if page.code_examples %%}gt;
<section class="code-examples">
<% for example in page.code_examples %%}gt;
<div class="code-example">
<h3><{{lt; example.title >}}gt;</h3>
<pre><code class="<{{lt; example.language >}}gt;"><{{lt; example.code >}}gt;</code></pre>
</div>
<% endfor %%}gt;
</section>
<% endif %%}gt;
</main>
</div>
# theme-name.gemspec
Gem::Specification.new do |spec|
spec.name = "theme-name"
spec.version = "1.0.0"
spec.authors = ["Your Name"]
spec.email = ["your.email@example.com"]
spec.summary = "A Jekyll theme for documentation"
spec.homepage = "https://github.com/username/theme-name"
spec.license = "MIT"
spec.files = `git ls-files -z`.split("\x0").select do |f|
f.match(%r{^(assets|_layouts|_includes|_sass|LICENSE|README)}i)
end
spec.add_runtime_dependency "jekyll", "~> 4.0"
spec.add_runtime_dependency "jekyll-feed", "~> 0.9"
spec.add_runtime_dependency "jekyll-sitemap", "~> 1.0"
end
# _config.yml
collections:
tooltips:
output: false
api_docs:
output: true
permalink: /:collection/:name/
# _data/definitions.yml
api_key: 'A unique identifier used to authenticate API requests. Keep this secure and never expose it in client-side code.'
response_codes: 'HTTP status codes returned by the API. 200 indicates success, 4xx indicates client errors, 5xx indicates server errors.'
rate_limiting: 'The practice of limiting the number of API requests a client can make within a specific time period to prevent abuse and ensure fair usage.'
<!-- _tooltips/api-key.md -->
---
doc_id: api_key
product: mijug_api
category: authentication
---
<{{lt; site.data.definitions.api_key >}}gt;
---
layout: null
search: exclude
---
{
"entries": [
<% for page in site.tooltips %%}gt;
<% if page.product == "mijug_api" %%}gt;
{
"doc_id": "<{{lt; page.doc_id >}}gt;",
"category": "<{{lt; page.category >}}gt;",
"body": "<{{lt; page.content | strip_newlines | replace: '\', '\\\\' | replace: '"', '\\"' >}}gt;"
}<% unless forloop.last %%}gt;,<% endunless %%}gt;
<% endif %%}gt;
<% endfor %%}gt;
],
"metadata": {
"generated": "<{{lt; site.time | date_to_xmlschema >}}gt;",
"version": "<{{lt; site.data.api.version >}}gt;",
"total_entries": <{{lt; site.tooltips | where: "product", "mijug_api" | size >}}gt;
}
}
// assets/js/theme-api.js
class ThemeAPIHelper {
constructor(apiUrl) {
this.apiUrl = apiUrl;
this.cache = new Map();
}
async loadTooltips() {
if (this.cache.has('tooltips')) {
return this.cache.get('tooltips');
}
try {
const response = await fetch(`${this.apiUrl}/tooltips.json`);
const data = await response.json();
this.cache.set('tooltips', data);
return data;
} catch (error) {
console.error('Failed to load tooltips:', error);
return { entries: [] };
}
}
async initializeTooltips() {
const data = await this.loadTooltips();
data.entries.forEach(entry => {
const elements = document.querySelectorAll(
`[data-tooltip="${entry.doc_id}"]`
);
elements.forEach(element => {
this.attachTooltip(element, entry.body);
});
});
}
attachTooltip(element, content) {
// Bootstrap popover integration
if (typeof bootstrap !== 'undefined') {
new bootstrap.Popover(element, {
content: content,
html: true,
placement: 'top',
trigger: 'hover focus',
});
}
}
}
// Initialize API helper
document.addEventListener('DOMContentLoaded', () => {
const apiHelper = new ThemeAPIHelper('/api');
apiHelper.initializeTooltips();
});
# _config.yml
collections:
docs_en:
output: true
permalink: /en/:name/
docs_es:
output: true
permalink: /es/:name/
defaults:
- scope:
path: '_docs_en'
values:
lang: 'en'
layout: 'docs'
- scope:
path: '_docs_es'
values:
lang: 'es'
layout: 'docs'
# _data/navigation.yml
main:
- title: 'Documentation'
url: '/docs/'
children:
- title: 'Getting Started'
url: '/docs/getting-started/'
- title: 'API Reference'
url: '/docs/api/'
children:
- title: 'Authentication'
url: '/docs/api/auth/'
- title: 'Endpoints'
url: '/docs/api/endpoints/'
footer:
- title: 'Resources'
links:
- title: 'GitHub'
url: 'https://github.com/username/repo'
- title: 'Issues'
url: 'https://github.com/username/repo/issues'
<!-- _includes/api-navigation.html -->
<nav class="api-navigation">
<% assign api_pages = site.api_docs | sort: 'order' %%}gt;
<% for page in api_pages %%}gt;
<div class="nav-section">
<h3><{{lt; page.section >}}gt;</h3>
<ul>
<% assign section_pages = site.api_docs | where: 'section', page.section | sort: 'order' %%}gt;
<% for section_page in section_pages %%}gt;
<li>
<a href="<{{lt; section_page.url | relative_url >}}gt;"
<% if section_page.url == page.url %%}gt;class="active"<% endif %%}gt;>
<{{lt; section_page.title >}}gt;
</a>
</li>
<% endfor %%}gt;
</ul>
</div>
<% endfor %%}gt;
</nav>
// Optimize CSS loading
@import "variables";
@import "mixins";
// Load critical styles first
@import "base";
@import "layout";
// Load component styles conditionally
<% if page.layout == "post" %%}gt;
@import "post";
<% endif %%}gt;
<!-- _includes/accessible-navigation.html -->
<nav role="navigation" aria-label="Main navigation">
<button
class="nav-toggle"
aria-expanded="false"
aria-controls="nav-menu"
aria-label="Toggle navigation menu"
>
<span class="sr-only">Menu</span>
</button>
<ul id="nav-menu" class="nav-menu">
<% for item in site.data.navigation.main %%}gt;
<li>
<a
href="<{{lt; item.url | relative_url >}}gt;"
<% if item.url == page.url %%}gt;aria-current="page"<% endif %%}gt;
>
<{{lt; item.title >}}gt;
</a>
</li>
<% endfor %%}gt;
</ul>
</nav>
<!-- _includes/seo.html -->
<meta
name="description"
content="<{{lt; page.description | default: site.description | escape >}}gt;"
/>
<meta name="keywords" content="<{{lt; page.tags | join: ', ' >}}gt;" />
<!-- Open Graph -->
<meta property="og:title" content="<{{lt; page.title | default: site.title >}}gt;" />
<meta
property="og:description"
content="<{{lt; page.description | default: site.description >}}gt;"
/>
<meta property="og:url" content="<{{lt; page.url | absolute_url >}}gt;" />
<!-- Schema.org -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "<{{lt; page.schema_type | default: 'WebPage' >}}gt;",
"name": "<{{lt; page.title | escape >}}gt;",
"description": "<{{lt; page.description | escape >}}gt;"
}
</script>
For themes that serve APIs or data files:
# .htaccess
Header set Access-Control-Allow-Origin "*"
Header set Access-Control-Allow-Methods "GET, POST, OPTIONS"
Header set Access-Control-Allow-Headers "Content-Type"
# Check if theme is installed
bundle list | grep theme-name
# Reinstall theme
bundle clean --force
bundle install
# _config.yml
theme: correct-theme-name # Check spelling
# Check file paths match exactly
bundle info theme-name --path
your-site/
├── _layouts/
│ └── post.html # Must match theme path exactly
Liquid Exception: undefined method `title' for nil:NilClass
<% if page.title %%}gt;
<h1><{{lt; page.title >}}gt;</h1>
<% endif %%}gt;
# _config.yml
incremental: true
profile: true
<!-- Cache expensive operations -->
<% assign sorted_posts = site.posts | sort: 'date' | reverse %%}gt;
<% for post in sorted_posts limit: 10 %%}gt;
<!-- Content -->
<% endfor %%}gt;
Jekyll themes provide a powerful foundation for creating maintainable, scalable websites. By understanding theme structure, customization patterns, and advanced features like API integration, you can create sophisticated documentation sites that serve both human readers and automated systems.
The integration of help APIs through collections and JSON generation allows themes to extend beyond static content, creating dynamic, interactive experiences while maintaining the simplicity and performance benefits of static site generation.
Whether you're using an existing theme or developing your own, following these best practices will ensure your Jekyll site is accessible, performant, and maintainable for years to come.
This document follows the Chicago Manual of Style for web resources. All URLs were verified as accessible on July 31, 2025. For the most current information, please refer to the original sources as web content may change over time.
Special Acknowledgment: The API integration patterns and collection-based help system concepts in this guide are significantly influenced by the MyDoc Help API documentation by SuperScary, which provides excellent examples of Jekyll-based API generation and tooltip systems.
This guide is part of the MIJUG.NET documentation series. For updates and additional resources, visit MIJUG.NET.