{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreidgtqkmskt7l2wsjosymczfx2feasdaxmq33x7ryl6a2dfs7xnuwm",
    "uri": "at://did:plc:t4aigbwuwix7x3q42qzjc6mn/app.bsky.feed.post/3mdjzlag4i4t2"
  },
  "coverImage": {
    "$type": "blob",
    "ref": {
      "$link": "bafkreiew27detoyodsy3ohpic4cwxygkd5gyphislif7y7yrj6mtjsvqs4"
    },
    "mimeType": "image/jpeg",
    "size": 22267
  },
  "path": "/link/535/17265388/project-level-plugins-and-config-for-apex",
  "publishedAt": "2026-01-28T18:14:00.000Z",
  "site": "https://brett.trpstra.net",
  "tags": [
    "on Mastodon",
    "Bluesky",
    "Twitter",
    "Click here if you'd like to help out.",
    "Mastodon",
    "Bluesky",
    "GitHub",
    "everywhere else"
  ],
  "textContent": "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.\n\nAs 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.\n\nI 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.\n\nSo 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.\n\nYou also get a cleaner way to “shadow” plugins you don’t want with a local noop plugin.\n\n> I also added `++insert++` syntax for adding `<ins>insert</ins>` tags, but that’s just a little one-off addition.\n\n### Project-scoped plugins in `.apex/plugins`\n\nPlugins used to be purely global: Apex would only look in your XDG config dir:\n\n  * `$XDG_CONFIG_HOME/apex/plugins`, or\n  * `~/.config/apex/plugins`\n\n\n\nNow there’s a proper **project scope** , searched in this order:\n\n  1. `./.apex/plugins` (current working directory)\n  2. `BASE/.apex/plugins` when you run with `--base-dir BASE`\n  3. `<git-root>/.apex/plugins` when you’re inside a Git work tree\n  4. Global: `$XDG_CONFIG_HOME/apex/plugins` or `~/.config/apex/plugins`\n\n\n\nEach of those directories can hold Apex plugins in the usual format:\n\n\n    .apex/ plugins/ my-plugin/ plugin.yml whatever-script-you-like\n\nWhen 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.\n\n### No-op shadowing: turning off plugins per project\n\nThat id-based precedence also gives you a neat trick: **no-op shadowing**.\n\nIf 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:\n\n\n    .apex/ plugins/ kbd/ plugin.yml noop.sh\n\n`plugin.yml` might look like:\n\n\n    id: kbd title: KBD Noop\n\nBecause 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.\n\n### `--list-plugins` now understands projects\n\nTo make this discoverable, `apex --list-plugins` was updated to use the **same resolution rules** as the runtime plugin loader.\n\nWhen you run:\n\n\n    apex --list-plugins\n\nyou’ll see:\n\n  * An “Installed Plugins” section that includes plugins from:\n    * `./.apex/plugins`\n    * `BASE/.apex/plugins` (if `--base-dir` was used)\n    * `<git-root>/.apex/plugins`\n    * global config plugins\n  * 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).\n\n\n\nIf 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.\n\n### Project-level config in `.apex/config.yml`\n\nPlugins 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.\n\nConfig is now read from these places:\n\n  1. **Global config**\n     * `$XDG_CONFIG_HOME/apex/config.yml`, or\n     * `~/.config/apex/config.yml`\n  2. **Project config**\n     * `./.apex/config.yml`\n     * `BASE/.apex/config.yml` when using `--base-dir BASE`\n     * `<git-root>/.apex/config.yml` when inside a Git work tree\n  3. **Explicit metadata file**\n     * Any file you pass with `--meta-file FILE`\n  4. **Per-document metadata**\n     * YAML front matter, MultiMarkdown metadata, or Pandoc title blocks\n  5. **Command-line metadata and flags**\n     * `--meta KEY=VALUE`, `--mode`, `--pretty`, `--no-tables`, and so on\n\n\n\nThe merge order matters:\n\n  * Global `config.yml` (lowest file precedence)\n  * Project `.apex/config.yml`\n  * `--meta-file FILE`\n  * Document metadata\n  * `--meta` and CLI flags (highest precedence)\n\n\n\nSo if you put this in your **global** config:\n\n\n    mode: unified pretty: true wikilinks: false\n\nand then in your **project** `.apex/config.yml`:\n\n\n    wikilinks: true indices: true\n\nyou’ll end up with:\n\n  * `mode: unified`\n  * `pretty: true`\n  * `wikilinks: true` # project overrides global\n  * `indices: true` # project-only addition\n\n\n\nAny `--meta-file` you pass on the command line layers on top of both, and document/CLI overrides still win last.\n\n### A quick example project layout\n\nHere’s what a repo might look like with all of this wired up:\n\n\n    my-book/ .git/ .apex/ config.yml plugins/ figures/ plugin.yml figures.py kbd/ plugin.yml chapters/ 01-intro.md 02-deep-dive.md\n\nRunning:\n\n\n    cd my-book apex chapters/01-intro.md --plugins\n\nwill:\n\n  * Load config from:\n    * global `config.yml` (if any),\n    * then `./.apex/config.yml`,\n    * then any `--meta-file` you pass,\n  * Run plugins from:\n    * `./.apex/plugins` first,\n    * then fall back to global plugins,\n  * Apply per-document metadata and CLI overrides on top.\n\n\n\nYou get per-repo behavior **without** having to constantly remember the right `--meta-file` or a long list of flags.\n\n### A quick note on `++insert++`\n\nWhile we were in here, another small but handy syntax has been added: `++insert++`.\n\n`++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.\n\n### Wrapping up\n\nTo recap:\n\n  * Plugins can now live in `.apex/plugins` at the project level, and they override global plugins by id.\n  * `--list-plugins` shows the **actual** set of plugins Apex will run for your current project, including overrides.\n  * Config can now live in `.apex/config.yml`, layered on top of your global `config.yml` and below any explicit `--meta-file`, document metadata, and flags.\n  * `++insertion++` gives you `<ins>` tags.\n\n\n\nIf 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.\n\nLike or share this post on Mastodon, Bluesky, or Twitter.\n\n* * *\n\nBrettTerpstra.com is supported by readers like you. Click here if you'd like to help out.\n\nFind Brett on Mastodon, Bluesky, GitHub, and everywhere else.",
  "title": "Project-level plugins and config for Apex",
  "updatedAt": "2026-01-28T18:14:00.000Z"
}