If you’ve ever wanted to share your Jekyll site’s look-and-feel as a reusable theme, you’ve probably found the docs thin on “the whole process.”
This post is a practical, repeatable playbook for converting a real Jekyll site into a theme gem—covering structure, portability, a local sandbox, versioning, and release. It’s based on extracting the Purelog theme into a gem, but it works for any Jekyll site following similar conventions.
What you’ll get by the end:
- A theme gem living under
theme/
with_layouts/
,_includes/
, andassets/
. - A working sandbox site under
sandbox/
to iterate quickly. - A clean assets strategy (goodbye placeholder files, hello single CSS entrypoint).
- Versioning, a changelog, and a release checklist you can reuse.
Who it’s for:
- Site owners who want to share their style.
- Maintainers who need repeatable packaging.
- Teams standardizing multiple themes across sites.
NOTE: I am using my own Jekyll theme Purelog as an example throughout this guide.
1) Plan your repo structure
Keep your original site in the repo root; add a theme folder and a sandbox site.
/ (repo root)
_config.yml
_layouts/
_includes/
assets/
...
theme/ # the theme gem lives here (what consumers will install)
sandbox/ # a test site that uses the theme during development
docs/ # changelog, guide, deployment notes
Tip: Exclude theme/
and sandbox/
from your production build if your root is still a live site.
2) Scaffold the theme gem under theme/
Inside theme/
:
-
_layouts/
—default.html
,home.html
,post.html
, etc. -
_includes/
—head.html
,sidebar.html
,footer.html
,analytics.html
, etc. -
assets/
— real CSS/JS (no placeholders). -
assets/css/purelog.css
— your single CSS entrypoint that imports actual CSS. -
lib/purelog.rb
andlib/purelog/version.rb
— Ruby entrypoint + version. -
purelog.gemspec
— gemspec that packages everything. -
README.md
andLICENSE
— for consumers and legal clarity.
Example: theme/lib/purelog/version.rb
# frozen_string_literal: true
module Purelog
VERSION = "0.1.0"
end
Example: theme/purelog.gemspec
(key parts)
Gem::Specification.new do |spec|
spec.name = "purelog"
spec.version = Purelog::VERSION
spec.summary = "Purelog Jekyll theme"
spec.description = "A reusable Jekyll theme extracted from a site."
spec.homepage = "https://github.com/you/your-repo"
spec.license = "MIT"
spec.files = Dir.chdir(__dir__) do
Dir["lib/**/*", "_layouts/**/*", "_includes/**/*", "_sass/**/*", "assets/**/*", "README*", "LICENSE*"]
.select { |f| File.file?(f) }
end
spec.add_runtime_dependency "jekyll", "~> 4.3"
spec.required_ruby_version = ">= 3.0"
end
Why pin jekyll
to ~> 4.3
? It respects semver and silences a warning about wide-open version ranges.
3) Make paths portable with relative_url
Ensure all theme links work regardless of baseurl
. In includes and layouts, prefer:
<link rel="stylesheet" href="{{ "/assets/main.css" | relative_url }}">
<link rel="stylesheet" href="{{ "/assets/code.css" | relative_url }}">
<a href="{{ "/" | relative_url }}">Home</a>
Bonus: Load third-party CSS with SRI + crossorigin attributes, and add SEO/Feed tags if you use jekyll-seo-tag
and jekyll-feed
.
4) Assets strategy that “just works”
- Copy real assets into
theme/assets/
. - Create one entrypoint CSS file to keep things predictable:
theme/assets/css/purelog.css
/* Theme entrypoint */
@import url("../main.css");
@import url("../code.css");
Had an SCSS entrypoint? Two options:
- Convert to CSS entrypoint and stop compiling the SCSS (remove front matter/imports so Jekyll ignores it).
- Or delete the SCSS file to avoid duplicate builds and conflicts.
5) Spin up a sandbox site for quick feedback
In sandbox/
, create a minimal Jekyll site that uses your theme gem:
sandbox/_config.yml
title: Theme Sandbox
baseurl: ""
url: ""
theme: purelog
plugins:
- jekyll-feed
- jekyll-seo-tag
- jekyll-paginate-v2
- jekyll-sitemap
# Pagination defaults (v2 config)
pagination:
enabled: true
per_page: 5
permalink: "/page/:num/"
sort_field: "date"
sort_reverse: true
sandbox/index.md
---
layout: home
title: Home
pagination:
enabled: true
---
Commands:
bundle install
bundle exec jekyll build
bundle exec jekyll serve --detach --port 4001
Pro tip: Add a handful of posts to test multi-page pagination under sandbox/_posts/
.
6) Versioning, docs, and changelog
- Version: bump
theme/lib/purelog/version.rb
per release. - Changelog: keep a
docs/CHANGELOG.md
(Keep a Changelog style works great). - Docs: a
theme/README.md
for consumers, and a “Theme Gem Guide” for maintainers with your exact process and checklists.
A short, repeatable release checklist pays dividends.
7) Build and verify the gem
Run from theme/
:
gem build purelog.gemspec
# => Successfully built RubyGem: purelog-0.1.0.gem
Common pitfalls and fixes:
- Building from repo root → File lists don’t resolve. Fix: run from
theme/
. - Placeholder assets → Broken styles. Fix: copy real CSS/JS.
- SCSS entrypoint conflict → Two files write to same CSS path. Fix: disable/convert the SCSS entrypoint.
8) Release checklist you can paste into issues
- [ ] All internal links/assets use
relative_url
. - [ ]
theme/_includes/head.html
loads CSS/JS viarelative_url
. - [ ]
theme/assets/
contains real, complete assets. - [ ] Single CSS entrypoint present at
assets/css/purelog.css
. - [ ] Any legacy SCSS entrypoint does not compile (or is removed).
- [ ]
theme/purelog.gemspec
pinsjekyll
to~> 4.3
and includes all files. - [ ]
theme/lib/purelog/version.rb
bumped (e.g.,0.1.0
). - [ ]
theme/README.md
andtheme/LICENSE
present. - [ ] Sandbox builds and serves (
bundle exec jekyll build
, thenserve
). - [ ] Changelog updated with the new version entry.
9) Optional niceties
- CI workflow to build the sandbox on PRs.
- Netlify or GitHub Pages for a public theme demo.
- Lighthouse and accessibility passes.
- Document “minimal config” defaults in the README for quick starts.
10) Ready-to-reuse snippets
Sandbox Gemfile (local path during development):
source "https://rubygems.org"
gem "jekyll", "~> 4.3"
group :jekyll_plugins do
gem "jekyll-feed"
gem "jekyll-seo-tag"
gem "jekyll-paginate-v2"
gem "jekyll-sitemap"
end
# Use local theme gem
gem "purelog", path: "../theme"
Search initialization (if you include a simple search):
<script src="{{ "/assets/simple-jekyll-search.js" | relative_url }}"></script>
<script>
SimpleJekyllSearch({
searchInput: document.getElementById('search-input'),
resultsContainer: document.getElementById('results-container'),
json: '{{ "/search.json" | relative_url }}'
})
</script>
Conclusion
Turning a Jekyll site into a reusable theme gem is less about magic and more about discipline:
- Organize your repo with a clear
theme/
andsandbox/
. - Make paths portable with
relative_url
. - Centralize styling with a single CSS entrypoint.
- Keep your gemspec honest and versioning intentional.
Once you’ve done it once, the next dozen themes become “copy, adapt, ship.”
If you’d like to see this playbook in action, check out the Purelog theme gem’s structure:
-
theme/_layouts/
,theme/_includes/
,theme/assets/
-
theme/lib/purelog.rb
,theme/lib/purelog/version.rb
theme/purelog.gemspec
-
sandbox/
with pagination and sample content
Happy theming—and if you ship your own theme gem using this method, share it in the comments. I’d love to see what you build!
Top comments (0)