Plugin Structure and Metadata
Every plugin is a pair of files in its own directory: a compiled WASM component
and a TOML manifest that tells Accent CMS what the plugin can do. This page
covers the directory layout and every section of the plugin.toml file.
Plugin Directory Structure
Plugins live in site/plugins/, with each plugin in its own subdirectory:
site/
plugins/
syntax-highlight/
plugin.toml # Metadata (required)
plugin.wasm # Compiled WASM module (required)
contact-form/
plugin.toml
plugin.wasm
search/
plugin.toml
plugin.wasm
Each plugin directory must contain both files:
plugin.toml- Declares the plugin’s name, version, and capabilitiesplugin.wasm- The compiled WebAssembly component built with a Component-Model toolchain (cargo-componentorjco)
If either file is missing, the plugin is skipped with a warning. Invalid plugins do not prevent the server from starting.
Plugin Metadata (plugin.toml)
The plugin.toml file declares what a plugin can do. Here is a complete example:
[plugin]
name = "syntax-highlight"
version = "0.1.0"
api_version = "0.2.0"
description = "Syntax highlighting for code blocks"
[hooks]
on_render = true # Called after markdown-to-HTML rendering
on_page_load = false # Called before markdown processing
on_media_discover = false # Called during content scanning for media files
on_media_process = false # Called after image processing
[filters]
# filter_name = "exported_function_name"
highlight = "apply_highlight"
lang_name = "get_language_name"
[shortcodes]
# Tags this plugin renders; each needs a manifest in shortcodes/
codes = ["code", "highlight"]
[routes]
# path = "HTTP method"
"/api/highlight" = "POST"
"/api/languages" = "GET"
[config]
# Plugin-specific settings (read via the config capability)
theme = "monokai"
line_numbers = true
Required Fields
The [plugin] section is required:
| Field | Type | Description |
|---|---|---|
name | string | Unique plugin identifier (used as registry key) |
version | string | Semantic version of the plugin |
api_version | string | Host API version the plugin targets (checked against the host’s supported range; see Security) |
description | string | Human-readable description (optional) |
requires_fs | bool | Request scoped WASI filesystem access (optional, default: false) |
allowed_hosts | array | Hosts the plugin may reach over outbound HTTP (optional; see Security) |
host_services | array | Host services the plugin may call through the typed host-services capability: "mail", "data" (optional; see Security) |
Hooks
The [hooks] section declares which lifecycle hooks the plugin listens to:
| Hook | When it fires | Input | Output |
|---|---|---|---|
on_page_load | Before markdown processing | Raw markdown | Modified markdown |
on_render | After HTML rendering | Rendered HTML | Modified HTML |
on_media_discover | During content scanning | File path, MIME type | Metadata key-value pairs |
on_media_process | After image processing | Raw image bytes, dimensions | Modified bytes, metadata |
Multiple plugins can register for the same hook. They are called in the order they were loaded.
The on_media_discover and on_media_process hooks are described in detail in the Media Hooks section.
Filters
The [filters] section declares the template filter names the plugin provides:
[filters]
highlight = "apply" # {{ code | highlight("rust") }}
uppercase = "apply" # {{ text | uppercase }}
The key is the filter name used in templates. The host dispatches every filter
to the component’s single apply export, keyed on the filter name, so the value
is simply that export.
Shortcodes
The [shortcodes] section lists the tags your plugin renders. The plugin exports a render function from the shortcodes interface, and the host calls it with the tag name, the tag’s attributes, its body, and the page path.
[shortcodes]
codes = ["note", "warning"]
Each tag also needs a component manifest in the plugin’s shortcodes/ directory, in the same format a theme uses. The manifest is the tag’s schema: content that names the tag is validated against it.
my-plugin/
├── plugin.toml
├── plugin.wasm
└── shortcodes/
├── note.yaml
└── warning.yaml
# shortcodes/note.yaml name: note description: A highlighted note. params: - name: title type: string body: markdown
Attribute values reach the plugin as strings: a number or a boolean as you wrote it, and a list or an object as JSON. The body is the tag’s content as Markdown, with any tags inside it already expanded. If the plugin fails to render a tag, the tag stays in the page as you wrote it, and the server log says why.
accent validate reports a tag listed in codes with no manifest, and a manifest for a tag codes does not list. Until you fix it, none of the plugin’s tags register.
What a plugin tag may do
Installing a plugin does not by itself put markup in your pages. Two settings in config.yaml decide what plugin tags may do:
| Setting | Default | Effect |
|---|---|---|
plugins.components.schemas | true | Registers the manifests plugins ship, so content using their tags is validated. |
plugins.components.render | false | Lets plugins write the markup for their tags. |
With the defaults, content that uses a plugin tag is validated, and the tag stays in the page as you wrote it. The server log names the plugin and the setting that turns rendering on. To let plugins render their tags:
plugins: components: render: true
Rendering is off by default because a plugin’s markup goes into every page that uses its tag, and the plugin sandbox limits what a plugin can compute, not what markup it returns. Turn it on for plugins you trust.
The two settings are independent. With schemas: false and render: true, plugins render their tags, and content using them is not validated. accent validate lists each use at Info level, so the choice stays visible without failing the run.
A built-in tag or a theme component with the same name as a plugin tag wins, and accent validate reports the plugin’s tag as shadowed.
Plugin tags in accent validate
- Content that uses a plugin tag incorrectly is an error, like any other tag.
- A tag declared by a plugin that did not load names the plugin and the reason, for example that plugins are disabled.
- With plugin schemas off and rendering on, each use is reported at Info level, because its content is not validated. It does not fail the run.
- A plugin tag whose manifest does not register is reported as not available, with the reason. The plugin’s other tags, other plugins’ tags, and every other tag on the site are still served and checked.
While a plugin is missing or still in development, run accent validate --lax. The run reports these findings as warnings, prints how many it relaxed, and does not fail on them.
To see which tags come from which plugin, and whether each one renders, run accent query components.
Routes
The [routes] section registers custom HTTP endpoints:
[routes]
"/api/search" = "GET"
"/api/contact" = "POST"
Plugin Configuration
The [config] section holds plugin-specific settings. A plugin reads them at
runtime through the config capability (config::get(key) /
config::get_all()); they merge under any overrides from plugins.config in
config.yaml:
[config]
theme = "monokai"
line_numbers = true
max_results = 50
A minimal plugin.toml only needs the [plugin] section:
[plugin]
name = "my-plugin"
version = "0.1.0"
api_version = "0.2.0"