SkillsGarden-plugin-authorAuthoring a Digital Garden plugin

Authoring a Digital Garden plugin

A garden plugin is a directory that extends a Digital Garden site: markup injected into named layout slots, site-wide styles/scripts, and build-time Eleventy/markdown-it hooks. Plugins are distributed as public GitHub repos with a garden-plugin.json manifest at the repo root, and installed into src/plugins/<id>/ of a garden repo. Follow this skill end to end; do not invent extension points that are not listed here.

Where to work

  • Inside a garden or template checkout (a repo with .eleventy.js and src/site/): create your plugin directly at src/plugins/<id>/ and it runs immediately.
  • Standalone plugin repo: develop the plugin as its own directory, and for testing clone https://github.com/oleeskild/digitalgarden, run npm install, and symlink or copy your plugin into src/plugins/<id>/.

The plugin id must match ^[a-z0-9][a-z0-9-]*$, must equal the directory name, and must NOT start with dg- (reserved for first-party plugins).

Minimal viable plugin

src/plugins/reading-time/
  garden-plugin.json
  templates/badge.njk
{
  "id": "reading-time",
  "name": "Reading Time",
  "version": "1.0.0",
  "description": "Shows an estimated reading time under the note title.",
  "author": "Your Name",
  "slots": { "notes.header": "templates/badge.njk" },
  "noteSettings": ["dgReadingTime"]
}
{% if settings.dgReadingTime === true %}
<div class="reading-time">{{ ((content | striptags).split(" ").length / 200) | round }} min read</div>
{% endif %}

id, name, version, description, author are required. Everything else is optional. All declared paths must be relative, use /, and contain no .. — a violation makes the loader (and installers) reject the plugin.

Slots

Slot templates are plain Nunjucks fragments (no front matter). They are rendered with the full Eleventy data cascademeta, settings, title, tags, created, updated, content, collections, graph, filetree, page — plus pluginSettings (this plugin's resolved settings). Always self-gate on your own setting: every enabled plugin's slots render unconditionally.

SlotRenders
common.head / notes.head / index.headin <head> (all pages / notes / home page)
common.header / notes.header / index.headerin <header> after title and tags
common.beforeContent / notes.beforeContent / index.beforeContentin <main> before the content
common.afterContent / notes.afterContent / index.afterContentin <main> after the content
common.footer / notes.footer / index.footerend of <body> (scripts, overlays, tooltips)
navbar.actionsnavbar, where the search button sits
filetree.actionsfiletree sidebar below the site title
filetree.beforeTitle / filetree.afterTitlearound the filetree site title
sidebar.top / sidebar.bottomright-hand sidebar
floating.bottomRightfixed stack in the bottom-right corner, filled from the corner upward; core's page panel toggle sits nearest the corner. Render one element per template (a button or link), it becomes a flex item and stacks above the previous one

Manifest form: "slots": { "<slot>": "file.njk" } or a list of files. Use common.footer for anything with a <script> or overlay markup.

Render order within a slot is the site owner's call, not the plugin's: plugins render by the order number on their entry in src/plugins/plugins.json (lower first, default 0), then by id. In floating.bottomRight a lower order sits nearer the corner. Example: {"plugins": {"theme-switcher": {"order": 10}, "random-note": {"order": 20}}}.

Regions (exclusive replacement)

A region replaces a core UI area instead of adding to it — at most one enabled plugin provides each region (first by id wins; conflicts warn). Currently: navigation (default: the core navbar; the shipped dg-filetree plugin claims it to render the folder-tree sidebar).

"regions": { "navigation": "templates/my-nav.njk" }

A custom navigation plugin gets the full data cascade, including the core-computed filetree data structure, and may include core components (e.g. fall back to {% include "components/navbar.njk" %}) and render sub-slots via components/pluginSlot.njk. Users switch navigations by disabling dg-filetree and enabling yours. Read src/plugins/dg-filetree/ in the template as the reference.

A navigation plugin MUST render the surfaces other extensions rely on, in its own markup: the navbar.actions and filetree.actions plugin slots (the search button lives there), and the site owner's custom filetree components — {% for imp in dynamics.filetree.beforeTitle %}{% include imp %}{% endfor %} plus the same for dynamics.filetree.afterTitle. dg-filetree does both; copy its template as the starting point.

Build hooks ("hooks": "index.js")

module.exports = {
  // Both optional. context = { settings, manifest, pluginDir }
  setupMarkdown(md, context) { md.use(require("markdown-it-footnote")); },
  setupEleventy(eleventyConfig, context) {
    eleventyConfig.addFilter("myFilter", (v) => v);
    // To emit a page (needs collections), register a virtual template:
    // eleventyConfig.addTemplate("my-page.njk",
    //   require("fs").readFileSync(require("path").join(context.pluginDir, "templates/page.njk"), "utf8"),
    //   { permalink: "/my-page.json", eleventyExcludeFromCollections: true });
  },
};

Rules: hooks run at config time in Node; they may only require packages the template already ships (check the template's package.json) or pure JS files vendored inside the plugin dir — plugins cannot add npm dependencies. Hook changes need a dev-server restart. Thrown errors are caught and the hook is skipped with a [plugins] warning.

Settings and per-note flags

"settings": [
  { "key": "label", "name": "Label", "description": "…",
    "type": "text",              // text | boolean | number | select
    "default": "Hello",
    "options": ["a", "b"],       // select only
    "env": "MY_ENV_VAR" }        // optional env fallback
],
"noteSettings": ["dgMyFlag"]
  • Setting resolution: stored value in src/plugins/plugins.json → env var (env or the key) from the garden's .envdefault. Read them as pluginSettings.<key> in templates, context.settings.<key> in hooks.
  • noteSettings keys (camelCase, dg-prefixed by convention) resolve like core note settings: env var of the same name is the global default, a note's frontmatter dg-my-flag: true/false overrides per note. Read as settings.<key> in templates.

Styles, scripts, assets

  • "styles": ["styles/x.scss"].scss compiled per file (_partials skipped) to /plugins/<id>/styles/x.css and linked in <head>; plain .css copied and linked as-is. Prefer the garden's --dg-* CSS variables for colors/spacing so themes keep working.
  • "scripts": ["assets/client.js"] — copied and loaded via <script defer> in <head>.
  • "assets": ["assets/"] — copied to /plugins/<id>/…, referenced only by your own code.

Test before publishing

From the template/garden checkout:

  1. npm install once, then npm run dev — slot template, style, and plugins.json edits hot-reload; restart after hook or manifest changes.
  2. Watch for [plugins] … warnings in the build log — each one is a manifest or runtime problem in your plugin.
  3. Build once with the plugin disabled (src/plugins/plugins.json{"plugins": {"<id>": {"enabled": false}}}) and confirm the site is unchanged.
  4. If the template has a test suite, npm test must stay green.

Publish

  1. Put the plugin at the root of a public GitHub repo (garden-plugin.json at top level). Suggested repo name: garden-plugin-<id>.
  2. Add a README (what it does, a screenshot, settings) and a screenshot file — the community directory shows it.
  3. Tag a release matching the manifest version: git tag v1.0.0 && git push --tags. Installers fetch the latest release tag, falling back to the default branch only when no release exists. Every future change: bump version, tag again.
  4. Users can now install it by pasting the repo URL, or manually by copying the directory into src/plugins/<id>/.
  5. To list it in the community directory, open a PR against oleeskild/digitalgarden-plugins adding one entry to community-plugins.json: { "id": "<id>", "name": "…", "author": "…", "description": "…", "repo": "you/garden-plugin-<id>", "screenshot": "screenshot.png" }.

Hard rules

  • Never require install-time execution: no postinstall steps, no "run this script to finish setup". Installation is file copying only.
  • A plugin must never break the build when misconfigured — the loader warns and skips, but keep your own code defensive too.
  • Don't touch files outside your plugin directory or claim another plugin's id. Don't use the dg- prefix.
  • Plugin code runs in the site build and in visitors' browsers — keep it small, readable, and dependency-light so users can audit it.

For deeper reference, read the first-party plugins under src/plugins/ in the template repo — dg-link-preview is the smallest, dg-search the most complete, and dg-filetree the navigation reference.

Built with LogoFlowershow