Project-level plugins and config for Apex
If you use Apex for more than one project, you’ve probably hit the point where a single global setup doesn’t quite cut it. Maybe your book project wants a different plugin set than your docs site, or one repo has stricter defaults than everything else on your machine.
As a personal example, most of my app documentation has always been written in MultiMarkdown, where header ids get generated with no spaces or dashes, so all of my cross-references link to #thissection type of anchors. My blog and my Jekyll-based documentation sites have always used Kramdown, so header ids and cross references are #this-section. I needed an easy way to always have Apex use the right header format for the current project.
I also have special plugins for different destinations. For example, my Marked documentation has special Liquid-style tags like prefpane that generates nice HTML for referencing Preference panes with x-marked URLs that will open a preference pane directly in Marked. I don’t need or want a plugin to do that universally, the output it generates is very specific to Marked.
So I added project-scoped plugins and configurations to Apex. This allows me to get the settings just right for a project, then save them into a local directory and be able to just run apex without a bunch of command line flags to remember.
You also get a cleaner way to “shadow” plugins you don’t want with a local noop plugin.
I also added
++insert++syntax for adding<ins>insert</ins>tags, but that’s just a little one-off addition.
Project-scoped plugins in .apex/plugins
Plugins used to be purely global: Apex would only look in your XDG config dir:
$XDG_CONFIG_HOME/apex/plugins, or~/.config/apex/plugins
Now there’s a proper project scope , searched in this order:
./.apex/plugins(current working directory)BASE/.apex/pluginswhen you run with--base-dir BASE<git-root>/.apex/pluginswhen you’re inside a Git work tree- Global:
$XDG_CONFIG_HOME/apex/pluginsor~/.config/apex/plugins
Each of those directories can hold Apex plugins in the usual format:
.apex/ plugins/ my-plugin/ plugin.yml whatever-script-you-like
When Apex builds the plugin list, earlier locations win by id. If a plugin with id footnotes-plus exists in both .apex/plugins and your global config dir, the project version is the one that runs.
No-op shadowing: turning off plugins per project
That id-based precedence also gives you a neat trick: no-op shadowing.
If there’s a global plugin you usually like, but you don’t want it in a specific project, you can “shadow” it by dropping an empty or no-op plugin with the same id into .apex/plugins. For example:
.apex/ plugins/ kbd/ plugin.yml noop.sh
plugin.yml might look like:
id: kbd title: KBD Noop
Because the project copy of kbd is discovered first, it shadows the global one. You can also do the same trick with purely declarative regex plugins: define a plugin with the same id that simply doesn’t match anything meaningful, and the global behavior is effectively disabled for that project.
--list-plugins now understands projects
To make this discoverable, apex --list-plugins was updated to use the same resolution rules as the runtime plugin loader.
When you run:
apex --list-plugins
you’ll see:
- An “Installed Plugins” section that includes plugins from:
./.apex/pluginsBASE/.apex/plugins(if--base-dirwas used)<git-root>/.apex/plugins- global config plugins
- An “Available Plugins” section from the remote directory, filtered so remote entries are hidden when you already have a plugin with the same id installed anywhere (project or global).
If a project plugin shadows a global one, you’ll only see the project entry in the installed list, and the remote listing won’t try to “helpfully” re-offer the same id.
Project-level config in .apex/config.yml
Plugins aren’t the only thing that benefit from scoping. Apex’s configuration system now has an explicit project layer , alongside the existing global and per-document metadata.
Config is now read from these places:
- Global config
$XDG_CONFIG_HOME/apex/config.yml, or~/.config/apex/config.yml
- Project config
./.apex/config.ymlBASE/.apex/config.ymlwhen using--base-dir BASE<git-root>/.apex/config.ymlwhen inside a Git work tree
- Explicit metadata file
- Any file you pass with
--meta-file FILE
- Any file you pass with
- Per-document metadata
- YAML front matter, MultiMarkdown metadata, or Pandoc title blocks
- Command-line metadata and flags
--meta KEY=VALUE,--mode,--pretty,--no-tables, and so on
The merge order matters:
- Global
config.yml(lowest file precedence) - Project
.apex/config.yml --meta-file FILE- Document metadata
--metaand CLI flags (highest precedence)
So if you put this in your global config:
mode: unified pretty: true wikilinks: false
and then in your project .apex/config.yml:
wikilinks: true indices: true
you’ll end up with:
mode: unifiedpretty: truewikilinks: true# project overrides globalindices: true# project-only addition
Any --meta-file you pass on the command line layers on top of both, and document/CLI overrides still win last.
A quick example project layout
Here’s what a repo might look like with all of this wired up:
my-book/ .git/ .apex/ config.yml plugins/ figures/ plugin.yml figures.py kbd/ plugin.yml chapters/ 01-intro.md 02-deep-dive.md
Running:
cd my-book apex chapters/01-intro.md --plugins
will:
- Load config from:
- global
config.yml(if any), - then
./.apex/config.yml, - then any
--meta-fileyou pass,
- global
- Run plugins from:
./.apex/pluginsfirst,- then fall back to global plugins,
- Apply per-document metadata and CLI overrides on top.
You get per-repo behavior without having to constantly remember the right --meta-file or a long list of flags.
A quick note on ++insert++
While we were in here, another small but handy syntax has been added: ++insert++.
++insert++ gives you a lightweight way to add an <ins>text</ins> tag to your document. It’s just a little shorter and easier than typing out the tags manually. I try not to add too much esoteric markup to the syntax, but I’ve seen ++ a couple of places and thought it a worthwhil addition.
Wrapping up
To recap:
- Plugins can now live in
.apex/pluginsat the project level, and they override global plugins by id. --list-pluginsshows the actual set of plugins Apex will run for your current project, including overrides.- Config can now live in
.apex/config.yml, layered on top of your globalconfig.ymland below any explicit--meta-file, document metadata, and flags. ++insertion++gives you<ins>tags.
If you’ve been juggling different shell aliases or wrapper scripts for each project, you can probably simplify a lot of that now by letting Apex’s own project-aware behavior do the heavy lifting.
Like or share this post on Mastodon, Bluesky, or Twitter.
BrettTerpstra.com is supported by readers like you. Click here if you'd like to help out.
Find Brett on Mastodon, Bluesky, GitHub, and everywhere else.
Discussion in the ATmosphere