Minimal Mistakes Plus
As I’ve been using Jekyll and Minimal Mistakes for this blog and added my own customizations quite a lot, I figure maybe I should release them as a standalone Gem so that others can use it painlessly. It provides or integrates more features besides everything in Jekyll and Minimal Mistakes—like dark mode toggling, drafting in Org Mode, numbering headings automatically, managing Kramdown attributes in Org files, and encrypting posts with a password, etc.
Library, Plugin and Theme
Some basic concepts to know before packaging up the codes.
The Core Difference
Technically, every Jekyll plugin is packaged and distributed as a Ruby gem, which is why you used the bundle gem command to generate the scaffolding. However, its intended use case and architecture make it a plugin rather than a general-purpose library.
- A general Ruby library, like
org-rubyornokogiri, is framework-agnostic. You feed it inputs, and it gives you outputs. It doesn’t know anything about Jekyll, static sites, or front matter. - A Jekyll plugin is framework-dependent. It specifically injects itself into a host environment. Because your code uses
Jekyll::Hooks.registerand inherits fromJekyll::Converter, it fundamentally requires the Jekyll ecosystem to run. If someone tried to require your gem in a standard Ruby script or a Ruby on Rails application, it wouldn’t do anything because it is specifically listening for Jekyll’s build events.
In short, you are using the Ruby library packaging system, RubyGems, to distribute a Jekyll plugin.
Theme Gem
While Minimal Mistakes is packaged and distributed as a Ruby gem (which is why you can easily install it via your Gemfile), it uses a specific pattern in the Jekyll ecosystem called a “Theme Gem.” It is fundamentally a Jekyll Theme, not a standard Ruby application or a custom Jekyll Plugin.
If you observe the Minimal Mistakes repository you will find there are not the lib and spec directories, nor any .rb files. It’s because it is fundamentally a Jekyll Theme, not a standard Ruby application or a custom Jekyll Plugin.
-
No Custom Ruby Logic
A standard Ruby gem uses the
lib/directory to store its executable Ruby code (.rbfiles) and thespec/directory to store the testing framework for that code. Minimal Mistakes does not contain any custom Ruby backend scripts. It is built entirely out of Liquid templating, HTML, Sass, and JavaScript. Because there is no raw Ruby code to execute or test, those directories are simply omitted. -
The Jekyll Theme Convention
When you package a Jekyll theme as a gem, RubyGems and Jekyll look for a completely different set of files. Instead of loading code from
lib/, Jekyll looks for the standard frontend directories at the root of the repository:_layouts/ _includes/ _sass/ assets/When you declare
theme: minimal-mistakes-jekyllin your_config.yml, Jekyll finds the gem on your computer and virtually merges those frontend folders into your local project during the build process. -
Reliance on External Plugins
If a theme developer wants to add backend functionality (like pagination or sitemaps), they have two choices: write custom
.rbplugins (which would require alib/folder) or rely on existing ones. Minimal Mistakes takes the second route. It achieves its advanced functionality by requiring established, external plugins as dependencies in its.gemspecfile (such asjekyll-paginateandjekyll-include-cache).
The Loading and Deploying of Gems
In you Gemfile there is the block grouping plugins:
group :jekyll_plugins do
gem "jekyll-paginate"
gem "jekyll-sitemap"
...
endThis is a specific feature designed to automatically load Jekyll plugins during the build process.
When Jekyll boots up, it asks Bundler to find all the gems listed specifically inside the :jekyll_plugins group and requires them automatically. This saves you from having to manually type out every single plugin name into the plugins: array inside your _config.yml file.
You should put ~gem “org-ruby”~ outside the block. Here is why: org-ruby is not actually a Jekyll plugin. It is a standalone Ruby text parser. Because it doesn’t contain Jekyll-specific hooks or generators, Jekyll doesn’t need to (and can’t) automatically load it as a plugin. Instead, your custom script (org_converter.rb) is the actual plugin that manually requires and uses org-ruby under the hood. Placing it outside the group simply tells Bundler “install this gem on the system so my custom scripts can use it,” which is the exact and correct behavior for a standard dependency.
You absolutely must list a plugin in _config.yml (under the plugins: array) in two specific scenarios:
-
When you are deploying to standard GitHub Pages
If you are pushing your site to GitHub Pages without using a custom GitHub Actions workflow, GitHub completely ignores your Gemfile and the
:jekyll_pluginsgroup. Instead, GitHub Pages strictly reads your_config.ymlfile to check if the plugins you want are on their “safe whitelist” (likejekyll-paginateorjekyll-sitemap). If they aren’t listed in_config.yml, GitHub Pages will not load them. -
When a gem is installed globally but not in the
:jekyll_pluginsgroupWhen you run
jekyll build, Jekyll automatically asks Bundler to require any gem inside the group:jekyll_pluginsdo block.However, if you have a Jekyll plugin listed normally in your Gemfile (outside of that group), Jekyll won’t know it’s supposed to hook into the build process. Adding it to the
plugins:array in_config.ymlforces Jekyll to execute require “that-plugin” during the boot sequence.
Why org-ruby works without being listed?
The reason org-ruby works regardless of where you put it—and without being in _config.yml —is because org-ruby is not a Jekyll plugin. It is just a standard Ruby library (a parser). Jekyll does not know what org-ruby is and never attempts to load it.
Instead, the plugin is actually your custom org_converter.rb script (which sits in your _plugins/ directory, a folder Jekyll does auto-load). Inside that script, you explicitly wrote ~require ‘org-ruby’~. When Jekyll runs your script, your script manually fetches the org-ruby library from your system.
The Rule of Thumb for Jekyll:
- If it modifies Jekyll’s build process (adds tags, commands, generators, or converters like
jekyll-sitemapor your newminimal-mistakes-plusgem): It must be required by Jekyll either via the:jekyll_pluginsGemfile group OR the_config.ymlarray. - If it is just a helper library (like
nokogiri,rouge, ororg-ruby): It just needs to be in your standard Gemfile so it is installed on your machine, and your custom Ruby code will manually require it when needed.
Which Should I Build, a Plugin Gem or a Theme Gem?
You should absolutely release this as a Plugin Gem.
Here is exactly why, how it fits into the broader Jekyll ecosystem, and what already exists in the wild.
Why It Must Be a Plugin Gem
In Jekyll’s architecture, there is a strict separation of concerns between presentation and build logic:
- Theme Gems (Presentation): These are strictly for the frontend. They provide layouts, includes, CSS, and static assets. They do not execute Ruby code to change how Jekyll parses files.
- Plugin Gems (Build Pipeline): These hook directly into Jekyll’s build lifecycle (like the
:pre_renderand:post_renderhooks you’ve been using). They manipulate the raw text, intercept the DOM via Nokogiri, and compile different formats.
Because almost all of your features (Org Mode drafting, automatic numbering, Kramdown attribute injection, and AES post encryption) require intercepting and rewriting data during the Ruby build phase, a Theme Gem simply cannot do the job.
Can These Features Apply to Other Themes?
Yes, and this is the biggest advantage of packaging it as a Plugin Gem. By decoupling your build logic from Minimal Mistakes, your plugin becomes completely theme-agnostic.
Whether someone is using Minimal Mistakes, Chirpy, or a completely custom layout, your Ruby logic will still compile their Org files, automatically number their headings, and securely encrypt their post content before the theme ever touches the HTML.
The only feature that walks the line is the Dark Mode Toggle and the Encrypted Post Password UI. Because these rely on specific DOM elements and CSS, they are usually bundled into themes. However, you can easily ship the raw JavaScript and baseline CSS within your plugin and let users map it to their theme’s specific classes.
Do Similar Plugins Already Exist?
Some of these features exist in isolation, but your specific implementations are heavily customized to solve edge cases that the standard tools miss:
-
Org Mode Drafting: There is an official jekyll-org plugin. However, it is a very basic wrapper around org-ruby. It does not support Jekyll/Liquid front matter properly without hacks, and it completely lacks your advanced features like Kramdown attribute parsing (
{: .class }), native link abbreviations, and highly optimized furigana parsing. - Post Encryption: There are a few plugins out there like jekyll-encrypt or jekyll-password-protect. Some rely on basic Base64 obfuscation (which isn’t secure), while others rely on external serverless functions. Your approach of handling AES encryption natively in the Ruby build step and decrypting client-side is a very solid, self-contained method.
- Heading Numbering: Most people rely on Kramdown’s auto-TOC generation or frontend JavaScript to number headings. Doing it at the DOM level via Nokogiri in the Ruby pipeline is a much cleaner approach that prevents client-side layout shifts.
- Dark Mode: This is almost universally handled by individual themes nowadays (themes like jekyll-theme-chirpy have it natively). Standalone dark mode plugins are rare because styling is so deeply tied to the theme’s specific Sass variables.
The Procedures
1. Scaffold the Gem
To package this up, I’m essentially building a “Jekyll Power-User Utility Belt.” Based on the previous mental discussions I’ve decided that the best approach for this gem is to create a plugin gem firmly tied with Minimal Mistakes (non-theme-agnostic), hence the name “minimal-mistakes-plus”.
-
Use Bundler to generate the gem skeleton where standard Ruby Gem conventions apply:
bundle gem minimal_mistakes_plusMake sure you use
minimal_mistakes_plusinstead ofminimal-mistakes-plus. With the former Bundler generates a flat file structure, and with the latter it generates a deeply nested file hierarchy. See this section for more details. -
Rename the gem to a hyphenated name, i.e.,
minimal-mistakes-plus:-
Rename these folders and files:
mv minimal_mistakes_plus minimal-mistakes-plus cd minimal-mistakes-plus/lib/ mv minimal_mistakes_plus.rb minimal-mistakes-plus.rb -
Update
spec.namein the fileminimal_mistakes_plus.gemspec:spec.name = "minimal-mistakes-plus" spec.summary = "Write a short summary, because RubyGems requires one." spec.description = "Write a longer description or delete this line." spec.homepage = "https://github.com/josephtesfaye/minimal-mistakes-plus/" spec.required_ruby_version = ">= 3.1.0" spec.metadata["allowed_push_host"] = "https://rubygems.org" spec.metadata["source_code_uri"] = "https://github.com/josephtesfaye/minimal-mistakes-plus/" spec.metadata["changelog_uri"] = "https://github.com/josephtesfaye/minimal-mistakes-plus/CHANGELOG.md"The value of
spec.namedetermines the final name of the gem as released to the public and required in other projects. There must be a.rbfile of the same name underlib/, e.g.,lib/minimal-mistakes-plus.rb.
-
-
Create the file
lib/minimal_mistakes_plus/hello.rbwith the content:puts "Hello World from Minimal Mistakes Plus!"Then add the require statement in
lib/minimal-mistakes-plus.rb:require_relative "minimal_mistakes_plus/hello" -
Add the files to Git:
cd ~/projects/minimal-mistakes-plus/ git add .Because the gemspec uses
git ls-filesto determine which files belong to the gem, any file that has not been tracked by Git is completely invisible to Bundler. See this section for more details. -
Add the gem to the
Gemfileof your blog:group :jekyll_plugins do gem "minimal-mistakes-plus", path: "~/projects/minimal-mistakes-plus" end -
Run the commands:
bundle install bundle exec jekyll serveThe gem is loaded properly if you see the message:
Hello World from Minimal Mistakes Plus!
Your final file structure will look like this beautiful, professional setup:
minimal-mistakes-plus/
├── minimal_mistakes_plus.gemspec
├── lib/
│ ├── minimal-mistakes-plus.rb # The Bridge File Jekyll looks for
│ ├── minimal_mistakes_plus # Folder for your actual logic
│ │ ├── version.rb
│ │ └── hello.rb1.1. What Happens Upon First Run
If this is the first time you have ever run the bundle gem command, it will enter an interactive session where you are asked to choose certain features for the project. For example, here’s the output of an initial run:
~/projects/temp $ bundle gem minimal-mistakes-plus
Creating gem 'minimal-mistakes-plus'...
Do you want to generate tests with your gem?
Future `bundle gem` calls will use your choice. This setting can be changed anytime with `bundle config gem.test`.
Enter a test framework. rspec/minitest/test-unit/(none): rspec
Do you want to set up continuous integration for your gem? Supported services:
* CircleCI: https://circleci.com/
* GitHub Actions: https://github.com/features/actions
* GitLab CI: https://docs.gitlab.com/ee/ci/
* Travis CI: https://travis-ci.org/
Future `bundle gem` calls will use your choice. This setting can be changed anytime with `bundle config gem.ci`.
Enter a CI service. github/travis/gitlab/circle/(none): github
Do you want to license your code permissively under the MIT license?
This means that any other developer or company will be legally allowed to use your code for free as long as they admit you created it. You can read more about the MIT license at https://choosealicense.com/licenses/mit. y/(n): y
MIT License enabled in config
Do you want to include a code of conduct in gems you generate?
Codes of conduct can increase contributions to your project by contributors who prefer collaborative, safe spaces. You can read more about the code of conduct at contributor-covenant.org. Having a code of conduct means agreeing to the responsibility of enforcing it, so be sure that you are prepared to do that. Be sure that your email address is specified as a contact in the generated code of conduct so that people know who to contact in case of a violation. For suggestions about how to enforce codes of conduct, see https://bit.ly/coc-enforcement. y/(n): y
Code of conduct enabled in config
Do you want to include a changelog?
A changelog is a file which contains a curated, chronologically ordered list of notable changes for each version of a project. To make it easier for users and contributors to see precisely what notable changes have been made between each release (or version) of the project. Whether consumers or developers, the end users of software are human beings who care about what's in the software. When the software changes, people want to know why and how. see https://keepachangelog.com y/(n): y
Changelog enabled in config
Do you want to add a code linter and formatter to your gem? Supported Linters:
* RuboCop: https://rubocop.org
* Standard: https://github.com/testdouble/standard
Future `bundle gem` calls will use your choice. This setting can be changed anytime with `bundle config gem.linter`.
Enter a linter. rubocop/standard/(none): rubocop
RuboCop enabled in config
Initializing git repo in /Users/josephtesfaye/projects/temp/minimal-mistakes-plus
create minimal-mistakes-plus/Gemfile
create minimal-mistakes-plus/lib/minimal/mistakes/plus.rb
create minimal-mistakes-plus/lib/minimal/mistakes/plus/version.rb
create minimal-mistakes-plus/sig/minimal/mistakes/plus.rbs
create minimal-mistakes-plus/minimal-mistakes-plus.gemspec
create minimal-mistakes-plus/Rakefile
create minimal-mistakes-plus/README.md
create minimal-mistakes-plus/bin/console
create minimal-mistakes-plus/bin/setup
create minimal-mistakes-plus/.gitignore
create minimal-mistakes-plus/.rspec
create minimal-mistakes-plus/spec/spec_helper.rb
create minimal-mistakes-plus/spec/minimal/mistakes/plus_spec.rb
create minimal-mistakes-plus/.github/workflows/main.yml
create minimal-mistakes-plus/LICENSE.txt
create minimal-mistakes-plus/CODE_OF_CONDUCT.md
create minimal-mistakes-plus/CHANGELOG.md
create minimal-mistakes-plus/.rubocop.yml
Gem 'minimal-mistakes-plus' was successfully created. For more information on making a RubyGem visit https://bundler.io/guides/creating_gem.htmlBundler saves your responses as global preferences in the configuration file ~/.bundle/config:
---
BUNDLE_GEM__TEST: "rspec"
BUNDLE_GEM__CI: "github"
BUNDLE_GEM__MIT: "true"
BUNDLE_GEM__COC: "true"
BUNDLE_GEM__CHANGELOG: "true"
BUNDLE_GEM__LINTER: "rubocop"This suppresses the interactive session for future bundle gem commands and passes those preferences as the default.
To update a global preference, say, for the test framework to use minitest, run:
bundle config [set] gem.test minitest
But it will not retroactively generate the test files for your already-created project. Bundler only generates the boilerplate files at the exact moment you run bundle gem.
1.2. File Structure & Ruby Naming Convention
If you run bundle gem against the name minimal-mistakes-plus:
bundle gem minimal-mistakes-plusYou will notice that the hyphens in the name are treated as directory hierarchy indicators for some files in the generated file structure:
...
minimal-mistakes-plus/lib/minimal/mistakes/plus.rb
minimal-mistakes-plus/lib/minimal/mistakes/plus/version.rb
minimal-mistakes-plus/sig/minimal/mistakes/plus.rbs
...
minimal-mistakes-plus/spec/minimal/mistakes/plus_spec.rb
...Why this happens: The Ruby Naming Convention
In the Ruby and RubyGems ecosystem, there is a strict naming convention that translates gem names into module hierarchies and directory structures. It all comes down to the difference between hyphens (-) and underscores (_):
-
Hyphens indicate namespace nesting. Bundler interprets hyphens as a signal to create nested directories and modules.
- Your input:
minimal-mistakes-plus - Bundler generates:
lib/minimal/mistakes/plus.rb - Ruby Module created:
Minimal::Mistakes::Plus
- Your input:
-
Underscores indicate word boundaries in a single namespace.
- Your input:
minimal_mistakes_plus - Bundler generates:
lib/minimal_mistakes_plus.rb - Ruby Module created:
MinimalMistakesPlus
- Your input:
Because you used hyphens, Bundler scaffolded a deeply nested directory structure assuming you wanted three levels of module hierarchy.
Should you commit these files?
These files form the foundational boilerplate of your gem. They set up the versioning, the base module, and the test environment. You will commit them once you are satisfied with the structure.
1.2.1. The Jekyll Plugin Catch
There is an important detail to consider because you are building a Jekyll plugin. When you add a plugin to Jekyll’s _config.yml like this:
plugins:
- minimal-mistakes-plusJekyll will literally attempt to execute ~require “minimal-mistakes-plus”~. By default, Ruby looks inside the lib/ folder for a file named exactly minimal-mistakes-plus.rb. Because Bundler only created the nested folder structure, Jekyll won’t find the file and will crash.
To fix this, you have two options:
-
Option 1: The “Flat” Approach
Delete the current folder and regenerate the gem using underscores. This keeps the file structure flat, which is generally easier to manage for simple Jekyll plugins.
rm -rf minimal-mistakes-plus bundle gem minimal_mistakes_plusYour gem will be named
minimal_mistakes_plus, and you will list it exactly like that in your_config.yml. -
Option 2: The “Hyphenated” Approach
While there are some popular Ruby gems with underscores (like
awesome_printoractive_record), in the Jekyll ecosystem, hyphens are the standard convention (e.g.,jekyll-paginate,jekyll-sitemap,minimal-mistakes-jekyll).If you want the gem to be published and referenced as
minimal-mistakes-plus, you can keep your current nested folder. However, you must manually create a new file namedlib/minimal-mistakes-plus.rbthat simply requires the nested file so Jekyll can find it (the “Bridge File” pattern):# Require the auto-generated version and namespace file require "minimal/mistakes/plus" # Require all of your custom Jekyll plugin scripts require "minimal_mistakes_plus/org_converter"To keep things clean, you don’t actually have to bury your custom Jekyll scripts three folders deep in
lib/minimal/mistakes/plus/. You can create a single folder insidelib/namedminimal_mistakes_plus/(using underscores for the folder name is a standard Ruby practice for housing secondary files belonging to a hyphenated gem).Your final file structure will look like this beautiful, professional setup:
minimal-mistakes-plus/ ├── minimal-mistakes-plus.gemspec # The official hyphenated name ├── lib/ │ ├── minimal-mistakes-plus.rb # The Bridge File Jekyll looks for │ ├── minimal/ │ │ └── mistakes/ │ │ └── plus.rb # Auto-generated base module │ └── minimal_mistakes_plus/ # Folder for your actual logic │ ├── org_converter.rb │ ├── link_abbr.rb │ └── encrypt_post.rbThis strategy gives you the best of both worlds: a standard hyphenated public gem name, strict compliance with Ruby’s module naming conventions, and perfect compatibility with Jekyll’s plugin loader.
2. Restructure the Ruby Code
Jekyll plugins distributed as Gems need to be loaded into the Jekyll environment.
- Move the contents of your
org_converter.rbandencrypt_post.rbinto thelib/minimal_mistakes_plus/directory. -
In the root
lib/minimal-mistakes-plus.rbfile, add standardrequirestatements to load your specific plugin files:require "minimal_mistakes_plus/org_converter" require "minimal_mistakes_plus/encrypt_post"
3. Handle the UI Assets (The Tricky Part)
Currently, encrypt_post.rb looks for _includes/secure_ui.html inside the user’s Jekyll source directory. When users install your Gem, they won’t naturally have this file. You have two elegant options to handle this:
-
Option A (The Zero-Config Approach): Refactor the Ruby script to read
secure_ui.htmldirectly from the Gem’s internal directory structure using__dir__. The user doesn’t have to copy or manage any HTML files. -
Option B (The Overridable Approach): Keep the logic that checks the user’s
_includesdirectory first. If the file doesn’t exist, fall back to the Gem’s internal default UI. This gives users the option to customize the prompt while keeping it plug-and-play.
4. Define Dependencies in the Gemspec
You need to ensure that anyone installing your Gem automatically gets the necessary underlying libraries. Open your minimal-mistakes-plus.gemspec and add the required dependencies:
spec.required_ruby_version = ">= 3.1.0"
spec.add_dependency "jekyll", "~> 3.9" # or your target version
spec.add_dependency "minimal-mistakes-jekyll", "~> 4.26"
spec.add_dependency "nokogiri", "~> 1.16"
spec.add_dependency "org-ruby", "~> 0.9"
spec.add_dependency "rouge", "~> 3.30"To set the Ruby version dependency, you use the required_ruby_version attribute.
Instead of locking the gem strictly to, say, 3.1.3 (which would prevent anyone with newer versions like 3.2.0 or 3.3.0 from installing it), the standard best practice in the Ruby community is to set a minimum required version using the >= operator. If your code doesn’t rely on a feature specifically introduced in patch .3, it is very common to round down to the minor version to maximize compatibility for other users, hence the chosen version 3.1.0.
The ~~>~ operator in RubyGems is known as the pessimistic version constraint. The =”~> 3.9”= constraint means the gem requires a Jekyll version that is greater than or equal to 3.9.0, but strictly less than 4.0.0. This safely allows your gem to automatically accept newer minor and patch releases (like 3.9.3 or 3.9.5) which contain backward-compatible bug fixes and security updates, while explicitly blocking major updates (like 4.0.0) that could introduce breaking API changes.
You can omit the version string entirely, which tells RubyGems to accept any available version of the gem. While this works in the short term, it is highly discouraged for a published plugin. For example, if Jekyll releases version 4.0 or 5.0 with completely different internal methods, users installing your gem will automatically pull the newest incompatible Jekyll version, instantly breaking your plugin. Defining a version constraint ensures long-term stability.
Adding the theme gem minimal-mistakes-jekyll as a dependency is a deliberate architectural decision: it permanently locks this plugin so that it can only be used by people who also have the Minimal Mistakes theme installed. My goal is to build an exclusive “add-on” specifically for that theme, and this is the correct approach.
Can you omit the jekyll dependency declaration when you have declared dependency on minimal-mistakes-jekyll, which should have also declared dependency on it?
—No, you should never omit your direct dependency on jekyll, even if minimal-mistakes-jekyll already requires it. Here is why you must declare both:
-
The Rule of Direct Dependencies
In package management, there is a golden rule: If your code directly calls a library, you must explicitly declare that library as a dependency. Because your Ruby scripts explicitly use constants like
Jekyll::Hooksand inherit fromJekyll::Converter, your code has a hard, direct dependency on thejekyllgem. Relying on a secondary gem (the theme) to quietly provide your primary framework is known as relying on a “transitive dependency.” This is highly discouraged because it obscures what your code actually needs to run. -
Version Constraint Control
If you rely on the theme to install Jekyll, you are surrendering control over which versions of Jekyll your plugin supports.
If
minimal-mistakes-jekyllupdates to require Jekyll 4.3, but your Nokogiri/Org-Ruby logic breaks on Jekyll 4.3, your plugin will break for users.By explicitly declaring spec.add_dependency “jekyll”, “~> 3.9”, you tell Bundler exactly what version your specific code is guaranteed to work with, preventing conflicting version installations.
Why use >= instead of ~~>~ for Ruby versions?
While the pessimistic operator (~~>~) is highly recommended for gem dependencies (like Jekyll or Nokogiri), it is generally discouraged for the Ruby language itself. If you were to use ~~> 3.1~, your gem would completely reject Ruby 3.2 and 3.3. Because newer Ruby versions are highly backwards-compatible, using >= 3.1.0 ensures your gem continues to work seamlessly as users upgrade their base environments over the years.
5. Documentation
5.1. README
Because this plugin tightly couples with Minimal Mistakes, your README will be the most critical part of the repository. You will need to explicitly document:
- How to add the Gem to their
Gemfileand_config.yml. - How to use
encrypted: trueandencrypted: buttitle, including setting theBLOG_PASSWORDenvironment variable. - How to use the Org-mode features:
{: .notice--warning},ordered: true,{: .ordered}, andCUSTOM_ID.
5.2. User Manual and Demos
The detailed user manual and demos are written in a Minimal Mistakes themed site hosted on Github Pages, and the contents reside in minimal-mistakes-plus/docs/. The site is also using the gem minimal-mistakes-plus. Here’s how to do it from scratch step by step.
To set up a documentation site in the docs/ folder that uses both the Minimal Mistakes theme and your unpublished minimal-mistakes-plus gem, you need to configure a local Jekyll environment and deploy it using GitHub Actions. Standard GitHub Pages builds block custom plugins for security, so a custom Action is required.
Here is the step-by-step process from scratch:
-
Scaffold the directory
-
Navigate to the root of your
minimal-mistakes-plusrepository and create thedocs/folder.cd ~/projects/minimal-mistakes-plus mkdir docs cd docs -
Create the
GemfileInside the
docs/directory, create aGemfile. This file must explicitly link your local gem using the ~path: “../”~ parameter so Jekyll knows where to find your plugin code during local development and the GitHub Actions build.source "https://rubygems.org" gem "jekyll", "~> 3.9" gem "minimal-mistakes-jekyll" # Load your local plugin directly from the parent directory gem "minimal-mistakes-plus", path: "../"In GitHub Actions, the home directory (
~) or relative paths pointing outside the repository are not accessible during CI runs. Thus a path like~/projects/minimal-mistakes-plus/can be used here. -
Create the
_config.ymlCreate your Jekyll configuration file in
docs/_config.ymlto set the theme and activate your plugin.title: Minimal Mistakes Plus Documentation url: "https://<your-github-username>.github.io" baseurl: "/minimal-mistakes-plus" # Crucial for project pages on GitHub theme: minimal-mistakes-jekyll plugins: - minimal-mistakes-plus
-
-
Create the landing page
-
In
_config.ymladd:include: - _pages -
In
_pages/create a new file namedhome.md:--- layout: splash permalink: / hidden: true header: overlay_color: "#5e616c" actions: - label: "<i class='fas fa-download'></i> Install now" url: "/docs/quick-start-guide/" excerpt: > A flexible two-column Jekyll theme. Perfect for building personal sites, blogs, and portfolios.<br /> <small><a href="https://github.com/mmistakes/minimal-mistakes/releases/tag/4.26.0">Latest release v4.26.0</a></small> --- This is the landing page...
-
-
Create the “User Guide” documentation
-
In
_config.ymldefine a collection calleddocsand set the default values for pages in the collection:collections: docs: output: true permalink: /:collection/:path/ defaults: - scope: path: "" type: docs values: layout: single read_time: false author_profile: false share: false comments: false toc_sticky: true sidebar: nav: "docs" -
Create the main menu and sidebar menus in
_data/navigation.yml:main: - title: "User Guide" url: /docs/quick-start-guide/ docs: - title: Getting Started children: - title: "Quick-Start Guide" url: /docs/quick-start-guide/ -
Create the page
_docs/01-quick-start-guide.md:--- title: "Quick-Start Guide" permalink: /docs/quick-start-guide/ excerpt: "How to quickly install and setup Minimal Mistakes Plus for use with GitHub Pages." last_modified_at: 2026-03-15 redirect_from: - /theme-setup/ toc: true --- Minimal Mistakes Plus ...
-
-
Test Locally
While still inside the
docs/directory, install the dependencies and start the local server.bundle install bundle exec jekyll serve -P 4001Visit http://127.0.0.1:4000/minimal-mistakes-plus/ to ensure the site renders with the Minimal Mistakes layout and that your plugin loads without errors.
-
Configure GitHub Actions for Deployment
Because standard GitHub Pages will silently disable
minimal-mistakes-plusas an “unrecognized plugin,” you must use GitHub Actions to build the site.-
At the root of your repository (not in
docs/), create the file.github/workflows/pages.ymland paste this workflow:name: Build and Deploy Docs on: push: branches: - main - master permissions: contents: read pages: write id-token: write jobs: build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Ruby uses: ruby/setup-ruby@v1 with: ruby-version: '3.1' bundler-cache: true working-directory: docs - name: Setup Pages uses: actions/configure-pages@v4 - name: Build with Jekyll working-directory: docs run: bundle exec jekyll build --destination ../_site env: JEKYLL_ENV: production - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: path: _site deploy: environment: name: github-pages url: $ runs-on: ubuntu-latest needs: build steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 -
To install dependencies correctly on GitHub Actions you need to add the platform
x86_64-linuxto your lockfiles (Gemfile.lock). Run the following command under both root anddocs/:bundle lock --add-platform x86_64-linuxThen commit the changes:
git add Gemfile.lock docs/Gemfile.lock git commit -m "Add x86_64-linux platform to Gemfile.lock for CI compatibility" git pushError when failing to do this step:
Your bundle only supports platforms ["arm64-darwin-23"] but your local platform is x86_64-linux.
-
-
Enable GitHub Actions Deployment
- Go to your repository on GitHub.
- Click Settings > Pages.
- Under Build and deployment, change the Source dropdown from “Deploy from a branch” to GitHub Actions.
- Commit all your files and push to GitHub. The action will trigger, build your site from the
docs/directory using your parent gem, and publish it.
5.2.1. Two Gemfiles Trap
Github Actions continuously reports the following error even though you have made the changes:
Your bundle only supports platforms ["arm64-darwin-23"] but your local platform
is x86_64-linux. Add the current platform to the lockfile with
`bundle lock --add-platform x86_64-linux` and try again.
Error: The process '/opt/hostedtoolcache/Ruby/3.1.3/x64/bin/bundle' failed with exit code 16It seems to be loading the old Gemfile.lock file because there’s no x86_64-linux in the “Set up Ruby > Print lockfile” in the job log.
This almost always happens due to the *”Two Gemfiles Trap”* that occurs when you set up a nested documentation site, or a simple missed push.
Since GitHub Actions checks out your exact repository state on every run, it never “holds onto” old files in the working directory. If the “Print lockfile” step shows the old version, the remote repository genuinely does not have the updated file in the location the Action is looking.
Here are the three steps to fix this, in order of most likely to least likely:
-
The
docs/Directory Trap (Most Likely)Because we created a completely separate Jekyll environment inside the
docs/folder, your repository now has twoGemfile.lockfiles:-
~/projects/minimal-mistakes-plus/Gemfile.lock(The plugin’s lockfile) -
~/projects/minimal-mistakes-plus/docs/Gemfile.lock(The documentation site’s lockfile)
Because the GitHub Action is configured with
working-directory: docs, it is completely ignoring the root lockfile.The Fix: Ensure you run the platform command inside the
docs/directory, commit that specific file, and push:cd ~/projects/minimal-mistakes-plus/docs bundle lock --add-platform x86_64-linux git add Gemfile.lock git commit -m "Add linux platform to docs lockfile" git push -
-
Verify via the GitHub Web UI
Sometimes local Git staging can be tricky. To guarantee the file actually made it to GitHub’s servers:
- Open your repository in your web browser.
- Navigate to the docs/ folder.
- Click on
Gemfile.lockand inspect the code. - If you do not see
x86_64-linuxunderPLATFORMS, the file was either not committed or pushed to a different branch than the one triggering the Action.
-
Clear the GitHub Actions Cache
If you absolutely verified that the
docs/Gemfile.lockon the main branch has the correct platforms via the web UI, theruby/setup-rubydependency cache might be aggressively restoring an old bundler state based on a broken cache key.The Fix:
- Go to your repository on GitHub.
- Click the Actions tab.
- On the left sidebar, under the “Management” section, click Caches.
- Click the trash can icon next to any caches listed there to force a completely fresh install on the next run.
- Re-run all jobs for your latest workflow.
The issue isn’t that GitHub is caching an old file. The issue is that the job failing is a completely different workflow, and your documentation deployment workflow isn’t actually running at all.
If you look closely at your repository structure, there are two distinct problems causing this:
-
The Failing Job is the Auto-Generated Ruby Test Suite
When you first ran
bundle gem, it automatically generated a workflow file at.github/workflows/main.yml. This job triggers on every push, runs at the root of your repository, and attempts to execute your test suite (bundle exec rake).Because your root
Gemfile.lockcurrently only supportsarm64-darwin-23, this test job immediately crashes on GitHub’s Linux runners. It is completely ignoring thedocs/Gemfile.lockwhere you successfully added the platform. -
The
pages.ymlWorkflow is Misplaced and MisspelledYour deployment workflow is currently located at
docs/.github/worklows/pages.yml.GitHub Actions strictly requires all workflow files to be located in the top-level
.github/workflows/directory of the repository. It will not scan subdirectories likedocs/. Additionally, the folder nameworklowsis missing the “f”. -
The Fix: To solve this efficiently, open your terminal at the root of your project (
~/projects/minimal-mistakes-plus) and run these exact commands:-
Fix the root lockfile so the test suite passes
bundle lock --add-platform x86_64-linux -
Move the pages workflow to the correct top-level directory
mv docs/.github/worklows/pages.yml .github/workflows/pages.yml -
Clean up the empty folder and push
rm -rf docs/.github git add . git commit -m "Fix workflow locations and root lockfile platforms" git push
Once pushed, if you check the Actions tab, you will now see two jobs running simultaneously: the default Ruby test suite (which will now pass) and your new
Build and Deploy Docsworkflow. -
6. Build and Test
There are two ways to use your plugin locally, depending on whether you are still actively editing the plugin code or just want to install the finished package.
6.1. Method 1: The path approach (Best for active development)
If you are still tweaking the Ruby code inside minimal-mistakes-plus and want to test it in your blog immediately, you do not need to build the gem at all. You can tell Bundler to load the code directly from your local folder.
- Open the
Gemfileof the target blog project (not the plugin). -
Add the gem using the path option, pointing to the local directory where your plugin lives:
# Point this to the actual relative or absolute path on your machine gem "minimal-mistakes-plus", path: "~/projects/minimal-mistakes-plus"If you add it inside the
group :jekyll_plugins doblock, Jekyll automatically detects it and loads it during the build process. Otherwise, Jekyll treats it like a standard Ruby library (likenokogiriororg-ruby). It doesn’t know it is supposed to hook into the build process unless you explicitly add it to theplugins:array in_config.yml:plugins: - minimal-mistakes-plus - Run
bundle installin your blog directory.
With this method, any changes you make to the Ruby files in the plugin directory are instantly picked up the next time you run bundle exec jekyll serve on your blog.
6.1.1. Codes in lib/ Not Loaded
You have added the gem using the path option, but the codes in lib/ aren’t loaded.
This issue is extremely common when developing new gems locally. It usually comes down to a combination of how Bundler tracks files and how Jekyll loads plugins.
Here are the three things you need to check to fix this:
-
The Git Tracking Trap (Most Likely)
If you look closely at your
minimal-mistakes-plus.gemspecfile, you will see this block of code:spec.files = Dir.chdir(__dir__) do `git ls-files -z`.split("\x0").reject do |f|Because the gemspec uses
git ls-filesto determine which files belong to the gem, any file that has not been tracked by Git is completely invisible to Bundler, even when using thepath:approach.Since you just created
lib/minimal-mistakes-plus.rbandlib/minimal_mistakes_plus/org_converter.rb, they are likely untracked.The Fix: Open your terminal, go into your plugin directory, and simply add the files to Git:
cd ~/projects/minimal-mistakes-plus git add .(You don’t even need to commit them yet; they just need to be staged/tracked so
git ls-filescan see them). -
Is it in Jekyll’s
_config.yml?Adding the gem to your
Gemfiletells Ruby to install the code, but it does not tell Jekyll to execute it during the site build.For your
org_converter.rblogic to actually run, you must explicitly declare the gem as a Jekyll plugin.The Fix: Open
_config.ymlin yourjosephs-blogdirectory and ensure the gem is listed under thepluginsarray:plugins: - jekyll-paginate - jekyll-sitemap - minimal-mistakes-plus # Add this line! -
Verify the Bridge File
Finally, ensure that your bridge file (
lib/minimal-mistakes-plus.rb) is successfully requiring your custom converter. When Jekyll reads_config.yml, it looks for this exact file and evaluates it.The Fix: Ensure
lib/minimal-mistakes-plus.rblooks exactly like this:# Require the default auto-generated module require_relative "minimal/mistakes/plus" # Require your custom logic require_relative "minimal_mistakes_plus/org_converter"Once you have staged the files in Git, added the name to
_config.yml, and verified the bridge file, runbundle exec jekyll servein your blog directory. Jekyll will now successfully load and apply your Org Mode converter.
6.1.2. Validation Error: Gem contains itself
~/projects/josephs-blog $ bundle exec jekyll serve
bundler: failed to load command: jekyll (/Users/josephtesfaye/.gem/ruby/3.1.3/bin/jekyll)
/Users/josephtesfaye/.rubies/ruby-3.1.3/lib/ruby/3.1.0/bundler/rubygems_integration.rb:72:in `rescue in validate': The gemspec at /Users/josephtesfaye/projects/minimal-mistakes-plus/minimal-mistakes-plus.gemspec is not valid. Please fix this gemspec. (Gem::InvalidSpecificationException)
The validation error was 'minimal-mistakes-plus-0.1.0 contains itself (minimal-mistakes-plus-0.1.0.gem), check your files list'
from /Users/josephtesfaye/.rubies/ruby-3.1.3/lib/ruby/3.1.0/bundler/rubygems_integration.rb:67:in `validate'The error message “minimal-mistakes-plus-0.1.0 contains itself” is caused by a very specific sequence of events that just occurred.
Earlier, when you ran gem build, it created the file minimal-mistakes-plus-0.1.0.gem in your project folder. Then, when you ran git
add ., Git started tracking that .gem file alongside your Ruby scripts.
Because your .gemspec uses git ls-files to decide what goes into the package, it is currently trying to pack the built .gem file inside of the new gem! RubyGems detects this infinite loop and immediately throws a validation error.
Here is how to fix it and prevent it from happening again:
-
Remove the
.gemfile from Git trackingOpen your terminal, navigate to your plugin directory, and tell Git to stop tracking any
.gemfiles:cd ~/projects/minimal-mistakes-plus git rm --cached *.gem(This removes it from Git’s index, but keeps the file on your hard drive).
-
Add
.gemfiles to.gitignoreTo make sure this never happens again when you run
git add ., open the.gitignorefile in yourminimal-mistakes-plusdirectory and add*.gemto the bottom of it:# .gitignore # ... existing ignores ... *.gem -
Try Jekyll again
Now that the
.gemfile is no longer ingit ls-files, your gemspec is valid again. Go back to your blog directory and start your server:cd ~/projects/josephs-blog bundle exec jekyll serveIt should now successfully boot up and load your local plugin!
6.1.3. Load Error: uri 1.1.1 already activated
For some reason when I run bundle exec jekyll serve the error occurs:
~/projects/josephs-blog $ bundle exec jekyll serve
bundler: failed to load command: jekyll (/Users/josephtesfaye/.gem/ruby/3.1.3/bin/jekyll)
/Users/josephtesfaye/.rubies/ruby-3.1.3/lib/ruby/3.1.0/bundler/runtime.rb:308:in `check_for_activated_spec!': You have already activated uri 1.1.1, but your Gemfile requires uri 0.13.0. Prepending `bundle exec` to your command may solve this. (Gem::LoadError)
from /Users/josephtesfaye/.rubies/ruby-3.1.3/lib/ruby/3.1.0/bundler/runtime.rb:25:in `block in setup'
from /Users/josephtesfaye/.rubies/ruby-3.1.3/lib/ruby/3.1.0/bundler/spec_set.rb:155:in `each'This is a notorious Bundler conflict involving Ruby’s “default gems.”
When Ruby boots up, it automatically activates its built-in, newer version of the uri gem (1.1.1). However, your blog’s Gemfile.lock is strictly demanding an older version (0.13.0). Bundler crashes because it cannot load two different versions of the same gem simultaneously, even when using bundle exec.
Here are the most efficient ways to resolve this, starting with the fastest approach:
-
Update the locked gem (Fastest)
You can tell Bundler to update the
Gemfile.lockso it accepts the newer, already-activated version of theurigem. Run this in your blog directory:bundle update uri bundle exec jekyll serve -
Rebuild the lockfile (Most Comprehensive)
Sometimes, updating
urijust reveals a daisy-chain of other default gem conflicts (likeloggerordate). If the first command fails with a similar error for a different gem, the best approach is to let Bundler recalculate the entire dependency tree from scratch based on your current Ruby environment.rm Gemfile.lock bundle install bundle exec jekyll serve
6.2. Method 2: Build and Install locally (Best for final testing)
If you want to simulate exactly how it will behave once published, you can compile the source code into a .gem file and install it directly into your local machine’s Ruby environment.
- Open your terminal and navigate to your plugin directory.
-
Build the gem using the gemspec file:
gem build minimal-mistakes-plus.gemspecThis will generate a compiled file in that folder, e.g.,
minimal-mistakes-plus-0.1.0.gem. -
Install that specific file locally:
gem install ./minimal-mistakes-plus-0.1.0.gem -
Now, go to your target blog project. You can add it to the Gemfile exactly as if you had downloaded it from the internet:
gem "minimal-mistakes-plus" - Run
bundle installto link it.
Note: If you use Method 2 and later change the plugin’s code, you must increment the version number in lib/minimal/mistakes/plus/version.rb, run gem build again, and run gem install again for the changes to take effect.
6.3. The Command bundle exec rake
The command bundle exec rake is essentially the master execution switch for running your project’s automated tests and code quality checks.
To understand exactly what it does, we can break it down into two parts:
-
bundle execThis prefix tells your terminal, “Do not use the global Ruby gems installed on my computer. Instead, strictly execute the following command using the exact gem versions locked inside this project’s
Gemfile.lock.” This prevents version conflicts and ensures your code runs exactly the same way on your local machine as it does on the GitHub Actions servers. -
rakeRake is Ruby’s built-in task runner (similar to
makein C ornpm runin Node.js). When you type rake without any arguments, it automatically looks for a file namedRakefilein your project’s root directory and runs whatever is set as the default task.
What it does in your specific project
If we look at the Rakefile generated for your minimal-mistakes-plus gem, you will see this line at the very bottom:
task default: %i[spec rubocop]This means that when GitHub Actions (or you locally) runs bundle exec rake, it is sequentially executing two distinct tasks:
-
spec(RSpec): It boots up the RSpec testing framework, looks inside yourspec/directory, and runs all the automated Ruby tests you have written to ensure your plugin logic (likeorg_converter.rb) works correctly. -
rubocop: If all the tests pass, it then boots up RuboCop (a Ruby static code analyzer). RuboCop scans every.rbfile in your project and checks it against the style rules defined in your.rubocop.ymlfile to ensure your code is cleanly formatted.
If either the RSpec tests fail or RuboCop finds formatting violations, the rake command returns an error code, which causes the GitHub Actions main.yml workflow to display a red “Failed” badge.
6.4. How to disable rubocop for the whole project?
To completely disable RuboCop for the whole project and stop it from failing your GitHub Actions build, you need to remove it from your default test execution and remove its dependencies.
Here is the fastest and most comprehensive approach to rip it out of the project entirely:
-
Remove it from the
RakefileThe reason
bundle exec rakeruns RuboCop is because yourRakefileexplicitly defines it as a default task alongside your RSpec tests.Open your
Rakefileand delete the RuboCop configuration:@@ -5,8 +5,4 @@ require "rspec/core/rake_task" RSpec::Core::RakeTask.new(:spec) -require "rubocop/rake_task" - -RuboCop::RakeTask.new - -task default: %i[spec rubocop] +task default: :spec -
Remove the Gem Dependency
Open your
Gemfileand delete this line at the bottom:gem "rubocop", "~> 1.21"After saving the file, run this command in your terminal to update your lockfile and clean up the dependencies:
bundle install -
Delete the Configuration File
Finally, delete the
.rubocop.ymlfile from the root of your project since it is no longer needed. -
Commit and Push
Commit these changes and push them to GitHub.
Now, when GitHub Actions runs bundle exec rake, it will only execute your RSpec tests and will completely ignore style checking.
6.5. Use Your Unpublished Gem on GitHub Actions
To use your unpublished gem across different repositories on GitHub Actions, you must stop using the local path: declaration. The GitHub runner building your blog will not have access to your local hard drive or parent directories.
Instead, you can instruct Bundler to download the plugin directly from its Git repository during the build process.
Here is the exact workflow:
-
Update your Blog’s Gemfile
Open the
Gemfilein yourjosephs-blogrepository. Replace the local path reference with thegithub:keyword, pointing to your plugin’s repository.-gem "minimal-mistakes-plus", path: "~/projects/minimal-mistakes-plus" +gem "minimal-mistakes-plus", github: "josephtesfaye/minimal-mistakes-plus", branch: "main"Note: You can also lock it to a specific release tag if you prefer stability over bleeding-edge updates by using ~tag: “v0.1.0”~ instead of ~branch: “main”~.
-
Update the Lockfile Locally
Because you changed the source from a local file path to a remote Git repository, you must recalculate the lockfile before pushing.
Run this in your
josephs-blogterminal:bundle installYou will see output indicating that Bundler is fetching the gem from GitHub.
-
Commit and Push
Commit the updated
GemfileandGemfile.lockto your blog’s repository and push to GitHub.git add Gemfile Gemfile.lock git commit -m "Use minimal-mistakes-plus directly from GitHub" git push
6.5.1. How GitHub Actions Handles This
As long as your minimal-mistakes-plus repository is public, your existing GitHub Actions workflow for josephs-blog does not need to change. When the ruby/setup-ruby step runs bundle install, it will read the GitHub source from the Gemfile, seamlessly clone the plugin repository into its cache, and execute your custom Ruby logic.
If your plugin repository is currently set to Private, the Action will fail with an authentication error. You would either need to make the plugin repository public, or generate a GitHub Personal Access Token and pass it to the Action via the BUNDLE_GITHUB__COM environment variable.
6.5.2. What to Do When the Plugin is Updated
It seems bundle install in GitHub Actions’ build_and_deploy workflow of josephs-blog doesn’t pull the latest code from minimal-mistakes-plus automatically with the setup:
gem "minimal-mistakes-plus", github: "josephtesfaye/minimal-mistakes-plus", branch: "main"It still uses the old commit as indicated by the line:
Using minimal-mistakes-plus 0.1.0 from https://github.com/josephtesfaye/minimal-mistakes-plus.git (at main@70a94f2)which is the same as shown in Gemfile.lock of josephs-blog. Does this mean I have to run bundle install and commit the updated Gemfile.lock in order to use the latest code? Can you make it pull in the latest code automatically?
—Yes, you hit the nail on the head. This is your Gemfile.lock doing exactly what it was designed to do: guarantee completely deterministic, repeatable builds.
When you first added the github: path and ran bundle install locally, Bundler reached out to GitHub, found the latest commit on main (which happened to be 70a94f2), and hardcoded that exact SHA hash into your Gemfile.lock.
Because GitHub Actions strictly respects the lockfile, it will download that exact commit every single time, completely ignoring any new code you push to the plugin’s repository.
-
Option 1: The “Standard” Way (Manual Update)
In the Ruby ecosystem, the standard best practice is indeed to manually update the lockfile and commit it. This ensures that if you introduce a breaking change to your plugin, it doesn’t spontaneously crash your blog’s automated daily build.
To do this locally, you don’t just run
bundle install. You must explicitly tell Bundler to update that specific gem’s locked commit:bundle update minimal-mistakes-plus git add Gemfile.lock git commit -m "Bump plugin to latest commit" git push -
Option 2: The “Bleeding Edge” Way (Automated CI Update)
If you want your blog to always build with the absolute latest commit from the
mainbranch of your plugin without you having to touch the lockfile, you can force GitHub Actions to update it on the fly.You simply need to add a
bundle updatestep to yourjekyll.ymlworkflow right after the Ruby setup step.Here is what you need to change in
josephs-blog/.github/workflows/jekyll.yml:--- .github/workflows/jekyll.yml +++ .github/workflows/jekyll.yml @@ -25,6 +25,10 @@ ruby-version: '3.1' bundler-cache: true cache-version: 0 + + - name: Force pull latest plugin code + run: bundle update minimal-mistakes-plus + - name: Setup Pages id: pages uses: actions/configure-pages@v4How this works: The
ruby/setup-rubystep will quickly install all your standard gems using the cachedGemfile.lock. Immediately after, your new step runs. Bundler will reach out to GitHub, see thatminimal-mistakes-plushas a new commit onmain, download the fresh code, and temporarily overwrite the lockfile in the GitHub runner’s memory just in time for the Jekyll build step.
But why can the latest code be pulled in and the Gemfile.lock be updated automatically when running bundle install locally? What’s the difference of the command between running locally and running on GitHub Actions? How is it done?
—Your premise is actually experiencing a slight illusion! Running bundle install locally does not automatically pull the latest GitHub commit if a lockfile already exists.
Here is exactly what is happening under the hood, and the strict difference between your local machine and GitHub Actions.
-
The Local Illusion
If you push a new commit to your plugin repository right now, and then run
bundle installlocally without touching yourGemfile, Bundler will not pull the new code. It will stubbornly stay locked to the old commit.The reason it seemed to update automatically for you locally recently is due to two specific events:
-
The Source Switch: When you edited your
Gemfileto changepath:togithub:, Bundler detected that the file was modified. Whenever theGemfilechanges, Bundler is forced to recalculate the dependencies. It reached out to GitHub, grabbed the latest commit on themainbranch at that exact second, and wrote it toGemfile.lock. -
The Local Override: Earlier, you configured your local environment to route all requests for the gem to your local folder (see Local Git Overrides). Because of this, your local Bundler is completely ignoring the GitHub commit hash inside your
Gemfile.lockand reading the live files straight from your hard drive. This makes it feel like it is instantly updating, but it isn’t even talking to GitHub!
-
The Source Switch: When you edited your
-
The GitHub Actions Reality (Deployment Mode)
In your GitHub Actions workflow (
.github/workflows/jekyll.yml), you are using the officialruby/setup-rubyaction with the settingbundler-cache: true.When that setting is enabled, it completely changes how Bundler operates. It forces Bundler into Deployment Mode (which is the equivalent of running
bundle install --deployment).In Deployment Mode, Bundler operates under strict, immutable rules:
- It is physically forbidden from modifying
Gemfile.lock. - It is forbidden from resolving new dependencies or checking for newer branch commits.
- It strictly reads the exact 40-character SHA commit hash saved in
Gemfile.lockand downloads that exact snapshot from GitHub.
This feature exists because production servers need absolute stability. If you or a contributor accidentally pushed a broken commit to the plugin’s main branch, Deployment Mode ensures your blog’s automated build doesn’t spontaneously download the broken code and crash.
- It is physically forbidden from modifying
The Rule of Thumb
-
Locally:
bundle installonly updates the lockfile if you change the text inside theGemfile. Otherwise, it respects the lockfile (unless bypassed by your local path override). -
GitHub Actions:
bundle installruns in Deployment Mode, which strictly enforces the lockfile and refuses to update anything. -
The Command: If you want to force Bundler to fetch the latest commit on a branch without changing the
Gemfile, the required command in the Ruby ecosystem isbundle update minimal-mistakes-plus.
When I run bundle update minimal-mistakes-plus all the other gems are also updated. Is this right?
—No, that is not the intended behavior for a targeted update, but it is a very common side effect of how Bundler resolves dependency trees.
When you run bundle update minimal-mistakes-plus, Bundler does not just pull the latest commit from GitHub. It also evaluates all the dependencies declared in your plugin’s gemspec (such as jekyll, nokogiri, or rouge). If newer versions of those underlying gems are available and allowed by your version constraints, Bundler will upgrade them, which creates a ripple effect that updates other gems across your entire project.
The fastest, most efficient, and best approach to pull the latest GitHub commit without touching a single other gem is to use the source flag:
bundle update --source minimal-mistakes-plusThis strict command tells Bundler to only fetch the new commits from that specific Git source and deliberately ignore any potential version bumps for its dependencies.
You should also swap this into your GitHub Actions jekyll.yml workflow. Using bundle update --source minimal-mistakes-plus guarantees that your automated builds get your latest plugin code while keeping the rest of your blog’s build environment perfectly locked and stable.
6.5.3. Use Local Git Overrides for Efficient Development
When your Gemfile strictly says ~github: “josephtesfaye/minimal-mistakes-plus”~, Bundler will reach out to GitHub and download the gem to your global cache every time you run bundle install, even when you are working on your local machine.
But you do not need to manually switch back and forth between path: and github:. Bundler has a built-in feature specifically designed to solve this exact problem for active gem development.
It is called Local Git Overrides. You can tell your local Bundler environment to intercept any requests for the GitHub URL and route them to your local folder instead, without ever changing the Gemfile.
Here is how to set this up:
-
Keep the GitHub link in your Gemfile
Leave your
Gemfileexactly like this so GitHub Actions always works:gem "minimal-mistakes-plus", github: "josephtesfaye/minimal-mistakes-plus", branch: "main" -
Configure the Local Override
Open your terminal inside your
josephs-blogdirectory and run this command (adjust the path to point to your actual local plugin directory):bundle config set --local local.minimal-mistakes-plus ~/projects/minimal-mistakes-plusThis configuration is stored in
~/josephs-blog/.bundle/config. You should never commit the.bundle/configfile to your Git repository. Here is exactly why, especially in your current setup:- It contains machine-specific paths: Right now, that file holds the local override you just configured (~~/projects/minimal-mistakes-plus~). That specific folder structure only exists on your personal computer.
- It will immediately break GitHub Actions: If you commit and push that file, the GitHub Actions runner will read it and attempt to install the gem from the local path
~/projects/minimal-mistakes-plusinstead of downloading it from GitHub. Because that directory does not exist on the isolated Linux build server, your deployment workflow will crash. - It overrides standard behavior: The entire purpose of the
.bundle/configfile is to store temporary, developer-specific configurations that should not affect anyone else (or any automated system) cloning the repository.
-
How it Works
Now, when you run
bundle installorbundle exec jekyll servelocally, Bundler sees thegithub:declaration, but your local config immediately overrides it. It will use the live files straight from~/projects/minimal-mistakes-plus.Any changes you make to your local plugin’s Ruby code will instantly be picked up the next time you restart the Jekyll server, just as if you had used
path:.Note: For the override to work, your local plugin repository must be on the same branch specified in the Gemfile, which is
mainin this case.
How to turn it off later
Once the gem is completely finished and you want your local machine to go back to pulling the official code from GitHub, simply run:
bundle config unset local.minimal-mistakes-plus7. Publish
The setting spec.metadata["allowed_push_host"] in the .gemspec file is a security feature built into Bundler and RubyGems. Its purpose is to prevent you from accidentally publishing a private, internal gem to the public internet.
When you run the command rake release or gem push, RubyGems checks this URL. It will strictly refuse to upload your code to any server other than the one listed here.
Here is how you should set it based on your goals for minimal-mistakes-plus:
-
Option 1: You want to publish it publicly
If your goal is to share this plugin with the world so anyone can add it to their
Gemfile, you should set it to the official public RubyGems server.spec.metadata["allowed_push_host"] = "https://rubygems.org" -
Option 2: You want to keep it private (GitHub Packages)
If you only want to use this gem for your own projects and do not want it available to the public, you can host it privately on GitHub.
You would change the URL to point to GitHub’s package registry (replace
YOUR_GITHUB_USERNAMEwith your actual username):spec.metadata["allowed_push_host"] = "https://rubygems.pkg.github.com/YOUR_GITHUB_USERNAME" -
Option 3: You don’t want to publish it anywhere at all
If you only plan to use this gem locally on your own computer (e.g., by pointing to its local folder path in your blog’s
Gemfile), you can safely delete that line.
- Push it to RubyGems:
gem push minimal-mistakes-plus-0.1.0.gem
Feature Migration
Dark Mode
I want to maintain all of the the original dark-mode-related setups used in josephs-blog. For example, dark_mode_toggle: true should control whether dark mode toggle is enabled for the site, and the dark mode styles should be same as defined in _sass/custom/dark_mode_toggle.scss. This article recorded how this feature was implemented previously for this blog. Here are the steps to migrate to minimal-mistakes-plus with minimal changes, with the final result being very performant.
To make this highly performant, we will avoid parsing every single HTML page with Nokogiri (which slows down build times). Instead, we will use Jekyll’s native hooks to seamlessly add your plugin’s _sass folder to the global load path, and use a lightning-fast Regex string replacement to inject the button and script only when dark_mode_toggle: true is enabled.
-
Transfer the Assets to the Plugin
Navigate to your
minimal-mistakes-plusdirectory and recreate the asset structure:-
Create the directories:
cd ~/projects/minimal-mistakes-plus/ mkdir -p _sass/minimal_mistakes_plus mkdir -p assets/js mkdir -p _includes/head -
Copy the files:
cd ~/projects/josephs-blog/ cp _sass/custom/dark_mode_toggle.scss ~/projects/minimal-mistakes-plus/_sass/minimal_mistakes_plus/ cp assets/js/dark_mode_toggle.js ~/projects/minimal-mistakes-plus/assets/js/ cp _includes/masthead.html ~/projects/minimal-mistakes-plus/_includes/ cp _includes/head/custom.html ~/projects/minimal-mistakes-plus/_includes/head/
-
-
Create the Ruby Integrator
Create a new file in your plugin at
lib/minimal_mistakes_plus/dark_mode.rband add this code:module MinimalMistakesPlus # 1. Add the plugin's _sass directory to Jekyll's global Sass load path Jekyll::Hooks.register :site, :after_init do |site| sass_dir = File.expand_path("../../_sass", __dir__) site.config['sass'] ||= {} site.config['sass']['load_paths'] ||= [] site.config['sass']['load_paths'] << sass_dir unless site.config['sass']['load_paths'].include?(sass_dir) # Inject the plugin's _includes directory to natively override the theme includes_dir = File.expand_path("../../_includes", __dir__) site.includes_load_paths.insert(1, includes_dir) unless site.includes_load_paths.include?(includes_dir) end # 2. Register the JS file to be automatically copied to _site/assets/js/ Jekyll::Hooks.register :site, :post_read do |site| next unless site.config['dark_mode_toggle'] gem_dir = File.expand_path("../../", __dir__) site.static_files << Jekyll::StaticFile.new(site, gem_dir, 'assets/js', 'dark_mode_toggle.js') end end -
Require the Integrator
Update your bridge file (
lib/minimal-mistakes-plus.rb) to load the new logic:require_relative "minimal_mistakes_plus/dark_mode" -
Clean up the Blog (
josephs-blog)Now that the plugin intelligently manages the file routing and HTML injection natively, you can clean out the overridden files in your blog repository.
-
Delete these files:
rm ~/projects/josephs-blog/_sass/custom/dark_mode_toggle.scss rm ~/projects/josephs-blog/assets/js/dark_mode_toggle.jsJekyll will now safely fall back to the upstream Minimal Mistakes masthead, and your plugin will magically inject the button.
-
For the
_includes/masthead.htmland_includes/head/custom.htmlfiles:You can still keep them in the project but then they will completely override the same files in both the theme and the plugin. In this case you need to add the dark-mode-related snippets to them to properly enable dark mode toggling. See ~/projects/minimal-mistakes-plus/docs/_docs/02-dark-mode.org.
If they don’t have customizations exclusive to the blog you can delete them. Then the ones in the plugin will be used.
-
Update or Create
assets/css/main.scss: Change the SCSS@importpath to point to your new plugin namespace:--- # Only the main Sass file needs front matter (the dashes are enough) --- @charset "utf-8"; @import "minimal-mistakes/skins/default"; // skin @import "minimal-mistakes"; // main partials @import "minimal_mistakes_plus/dark_mode_toggle";You can delete this file if you’ve also migrated it to the plugin. See Custom Styles.
-
With this setup, your plugin handles 100% of the dark mode logic, you get to keep your exact original source code, and your blog’s source code shrinks down drastically.
Custom Styles
We can hand over the style sheet assets/css/main.scss to the plugin so that we don’t need to copy it around for new projects.
We can use an incredibly performant Jekyll trick: In-Memory SCSS Middleware. Instead of forcing users to copy a physical file, we can write a tiny Ruby script that intercepts the default main.scss file provided by the upstream Minimal Mistakes theme while Jekyll holds it in memory. We can then dynamically inject your variable overrides and SCSS imports right before the Sass compiler runs.
This completely eliminates the need for any physical configuration files in new projects. Here is the exact setup.
-
Create the Plugin’s Main SCSS File
Inside your
minimal-mistakes-plusrepository, create a new file at_sass/minimal_mistakes_plus/main.scss.Copy all of your actual CSS rules from your old file into this one. Do not include the front matter (dashes at the top), the charset, or the Liquid dark mode statements. Remove these lines:
---- -# Only the main Sass file needs front matter (the dashes are enough) ---- - -@charset "utf-8"; - -$code-background-color: mix(#fff, #93E0E3, 50%); - -@import "minimal-mistakes/skins/default"; // skin -@import "minimal-mistakes"; // main partials -
Create a Variables File
Inside your plugin, create a new file named
_sass/minimal_mistakes_plus/_variables.scss. Place your$code-background-coloroverride (and any future variables) inside it:$code-background-color: mix(#fff, #93E0E3, 50%);Whenever you want to override a default Minimal Mistakes variable (like
$primary-color,$text-color, or$border-radius), you simply drop it into_variables.scss. -
Create the Style Injector Hook
Inside your plugin, create a new file at
lib/minimal_mistakes_plus/style_injector.rb. This script will safely mutate the base theme’s SCSS file in memory:module MinimalMistakesPlus Jekyll::Hooks.register :site, :post_read do |site| # Locate the default main.scss file loaded by the Minimal Mistakes theme main_scss = site.pages.find { |page| page.name == 'main.scss' && page.dir == '/assets/css/' } if main_scss # 1. Inject the variables partial right BEFORE the theme imports its skins unless main_scss.content.include?('minimal_mistakes_plus/variables') main_scss.content = main_scss.content.sub( /(@import "minimal-mistakes\/skins)/, "@import \"minimal_mistakes_plus/variables\";\n\1" ) end # 2. Dynamically import dark mode (This replaces your old Liquid tags!) if site.config['dark_mode_toggle'] && !main_scss.content.include?('minimal_mistakes_plus/dark_mode_toggle') main_scss.content = main_scss.content.sub( /(@import "minimal-mistakes";?)/, "\1\n@import \"minimal_mistakes_plus/dark_mode_toggle\";" ) end # 3. Inject our plugin's core layout modifications after the base theme imports unless main_scss.content.include?('minimal_mistakes_plus/main') main_scss.content = main_scss.content.sub( /(@import "minimal-mistakes";?)/, "\1\n@import \"minimal_mistakes_plus/main\";" ) end end end endWe inject the plugin styles (
minimal_mistakes_plus/main) immediately after the base theme imports, leaving the entire rest of your local file (josephs-blog/assets/css/main.scss) untouched so your custom styles cascade last. In this way you can override the plugin styles exclusively for a project. -
Require the Injector
Add the new file to your bridge file
lib/minimal-mistakes-plus.rb:require_relative "minimal_mistakes_plus/style_injector" -
The Final Cleanup
Because the plugin now handles 100% of the CSS rendering and configuration automatically, you can delete the
assets/css/main.scssfile from both yourjosephs-blogrepository and your plugin’sdocs/directory.
Going forward, any Jekyll site that includes ~gem “minimal-mistakes-plus”~ will instantly inherit your grid layouts, rotating ordered lists, TOC mobile styling, and dark mode!
You can still create a local assets/css/main.scss in your project, which overwrites the base theme completely and the plugin styles partially. To ensure the theme and the plugin injector can function properly you must preserve the base theme imports in it:
---
# Only the main Sass file needs front matter (the dashes are enough)
---
@charset "utf-8";
@import "minimal-mistakes/skins/default"; // skin
@import "minimal-mistakes"; // main partialsLink Abbreviations
-
Move
link_abbr.rbto the plugin:mv ~/projects/josephs-blog/_plugins/link_abbr.rb ~/projects/minimal-mistakes-plus/lib/minimal_mistakes_plus/ -
Require the code:
require_relative "minimal_mistakes_plus/link_abbr" -
Optimize code:
Rewrite the whole
link_abbr.rbso that it handling code of Org files and Markdown files are decoupled, rather than being tangled together.To be more specific, if a file is an
.orgfile, only expand the link abbreviations in the standard Org links; if it’s a markdown (.md,.markdown, etc.) only expand the link abbreviations in the standard markdown links. In both cases, always expand the link abbreviations used in a gallery in the front matter.gallery: - url: foo:unsplash-gallery-image-1.jpg image_path: foo:unsplash-gallery-image-1.jpg title: Check this [[foo:unsplash-gallery-image-1.jpg][image]] - image_path: foo:unsplash-gallery-image-1.jpg - foo:unsplash-gallery-image-1.jpg - image_path: [[foo:unsplash-gallery-image-1.jpg]] title: An image referenced through an org link - [[foo:unsplash-gallery-image-1.jpg]] - [[foo:unsplash-gallery-image-1.jpg]]But again, don’t expand links like
[[foo:unsplash-gallery-image-1.jpg]]in a markdown file even if they are in a gallery in the front matter because they are Org Links. Only expand Org links in Org files.In all cases, link abbreviations in literal blocks (including inline blocks) should never be expanded.
The final code should be very performant.
- The code.
Related articles:
- Enabling Link Abbreviations in Minimal Mistakes
- Enabling Drafting in Org Mode in Minimal Mistakes
- User Guide: Link Abbreviations
Custom Galley, Symlinks, Encryption
-
Move the assets to the plugin:
mv ~/projects/josephs-blog/_includes/gallery ~/projects/minimal-mistakes-plus/_includes/ mv ~/projects/josephs-blog/_plugins/liquify.rb ~/projects/minimal-mistakes-plus/lib/minimal_mistakes_plus/ mv ~/projects/josephs-blog/_plugins/symlink_external_assets.rb ~/projects/minimal-mistakes-plus/lib/minimal_mistakes_plus/ mv ~/projects/josephs-blog/_plugins/encrypt_post.rb ~/projects/minimal-mistakes-plus/lib/minimal_mistakes_plus/ -
Require the codes:
require_relative "minimal_mistakes_plus/liquify" require_relative "minimal_mistakes_plus/symlink_external_assets" require_relative "minimal_mistakes_plus/encrypt_post"
Comments