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.