Writing a Plugin
Plugins are WebAssembly Component-Model components. You can author them in any
language with a Component-Model toolchain; Accent ships first-class templates for
two: Rust with cargo-component and JavaScript with jco /
componentize-js. Rust produces the smallest, fastest artifact (~50 KB);
JavaScript is the most approachable and bundles its engine (~12 MB).
Scaffold a project
The fastest start is the scaffold command. It writes a complete, buildable content-hook plugin – including the slice of the WIT contract it needs – into a new directory:
accent plugin new my-plugin --lang rust # or: --lang js
The generated plugin reads a value from its configuration, logs through the host, and appends a footer note to every rendered page in development. Replace that logic with your own.
Rust (cargo-component)
Prerequisites
cargo install cargo-component cargo install wasm-tools # optional, to inspect the component
cargo-component manages the WebAssembly target it compiles through, so you do
not add one with rustup.
The code
The scaffold’s src/lib.rs implements the world’s Guest trait. Host
capabilities are plain function calls on the generated bindings:
use bindings::accent::plugin::config; use bindings::accent::plugin::environment; use bindings::accent::plugin::logging::{self, LogLevel}; use bindings::exports::accent::plugin::content_hooks::{ContentInput, Guest}; struct Component; impl Guest for Component { fn on_page_load(input: ContentInput) -> Result<String, String> { Ok(input.content) // pass the raw markdown through unchanged } fn on_render(input: ContentInput) -> Result<String, String> { let note = config::get("footer_note").unwrap_or_else(|| "Built with Accent CMS".into()); logging::log(LogLevel::Debug, &format!("annotating {}", input.page_path)); if environment::get_context().production { return Ok(input.content); } Ok(format!("{}\n<footer>{}</footer>", input.content, note)) } } bindings::export!(Component with_types_in bindings);
The bindings module is generated from wit/ by cargo-component – you never
hand-write it.
Build
cargo component build --release
The component is written to
target/wasm32-wasip1/release/my_plugin.wasm. Inspect its world with
wasm-tools component wit target/wasm32-wasip1/release/my_plugin.wasm.
JavaScript (jco)
Prerequisites
Node.js 18+ and npm, then:
npm install
The code
The scaffold’s plugin.js exports the world’s interface as an object. WIT
kebab-case maps to JS lowerCamelCase, an imported interface is a module
specifier, and a WIT enum is a plain string:
import { get as configGet } from 'accent:plugin/config@0.1.0'; import { log } from 'accent:plugin/logging@0.1.0'; import { getContext } from 'accent:plugin/environment@0.1.0'; export const contentHooks = { onPageLoad(input) { return input.content; }, onRender(input) { const note = configGet('footer_note') ?? 'Built with Accent CMS'; log('debug', `annotating ${input.pagePath}`); if (getContext().production) return input.content; return `${input.content}\n<footer>${note}</footer>`; }, };
Versioned import specifiers are mandatory. Because the WIT package is versioned (
accent:plugin@0.1.0), the import specifiers must carry the version suffix (accent:plugin/config@0.1.0). The unversioned form fails the build with a misleading “No such file or directory”.
Build
npm run build
This runs jco componentize and writes the component to plugin.wasm. Every JS
component bundles the StarlingMonkey JavaScript engine, so even a trivial plugin
is ~12 MB. This is fine on a server; prefer Rust for latency-sensitive plugins.
Install
A plugin is a directory containing plugin.wasm and plugin.toml. Copy the
built component and the manifest into your site’s plugins directory:
mkdir -p ~/my-site/plugins/my-plugin cp target/wasm32-wasip1/release/my_plugin.wasm ~/my-site/plugins/my-plugin/plugin.wasm # Rust # cp plugin.wasm ~/my-site/plugins/my-plugin/plugin.wasm # JavaScript cp plugin.toml ~/my-site/plugins/my-plugin/plugin.toml
Enable plugins in config.yaml:
plugins: enabled: true directory: ./plugins
Verify
Start the server and check the logs:
accent serve
INFO accent: Loaded plugin: my-plugin
INFO accent: Plugin registry loaded: 1 plugin(s)
With dev.hot_reload enabled, rebuilding the component and copying it over
plugin.wasm reloads the plugin in place – no server restart.
Building a different kind of plugin
The scaffold starts you on a content hook. To build a filter, route, media, or
diagram plugin, implement the matching world: change the world selection (the
world field in Cargo.toml for Rust, --world-name in package.json for
JavaScript) and copy the interface files that world uses into wit/. See
The Plugin Contract for the full list of
worlds and their signatures.