BxSites Plugins Reference
A BxSites plugin is nothing more than another BoxLang module - its own box.json + ModuleConfig.bx, installed as a sibling of bx-sites in the same runtime. No plugin API to import, no separate registry - BoxLang's own module system is the plugin system. Installing a module never activates it as a plugin on its own:
```yaml title="bxsites.yaml" plugins: [ myBxSitesPlugin ]
(see `bx-sites-configuration` for the `plugins` key)
## Installing a published plugin
bxSites install:plugin --name=bx-sites-plugin-analytics [--version=1.2.0]
Downloads from ForgeBox and extracts into
`boxlang_modules/bx-sites-plugin-analytics/` at the project root (BoxLang's
auto-loaded local-module convention - no `box`/CommandBox needed, no
`BOXLANG_HOME`/global install step). Loads it into the runtime immediately
and prints the real registered module mapping name (not always the same as
the ForgeBox slug) - add *that* name to `plugins`. Browse published plugins
under ForgeBox's `bxsites-plugins` category.
## Writing a plugin
One class beyond the usual module scaffolding: `models/BxSitesPlugin.bx`.
Every method is optional - bx-sites checks for each before calling it.
bxSites plugin:new --name=my-analytics-plugin [--dest=]
scaffolds `box.json`, `ModuleConfig.bx`, and a fully-stubbed
`models/BxSitesPlugin.bx` mirroring `examples/hello-plugin/` in the bx-sites
repo (a complete, working reference implementation - adds an HTML comment to
every page and appends a build summary via `onPageHtml`/`onBuildComplete`).
```bx title="models/BxSitesPlugin.bx"
class {
struct function onConfig( required struct config ) {
// Mutate/return the site config, right after bxsites.yaml is loaded.
return arguments.config
}
string function onPageMarkdown( required string markdown, required struct page, required struct config ) {
// Mutate a page's raw markdown before conversion.
return arguments.markdown
}
string function onPageHtml( required string html, required struct page, required struct config ) {
// Mutate a page's rendered HTML after conversion.
return arguments.html
}
array function onNav( required array nav, required struct config ) {
// Mutate the nav tree: array of { title, url, order, children } nodes.
return arguments.nav
}
array function onSearchIndex( required array entries, required struct config ) {
// Mutate search-index.json entries before it's written - each is
// { title, url, headings, body, tags }. Runs once per index actually
// written (main + each version/locale tree using a local index).
return arguments.entries
}
array function onSitemap( required array pages, required struct config ) {
// Mutate the accumulated page list right before BOTH sitemap.xml and
// llms.txt are built from it. Only urlPath/title/hidden need be set.
return arguments.pages
}
void function onBuildComplete( required string siteDir, required struct config ) {
// Fires once, after everything is written to siteDir. No return value.
}
}
Hooks run in plugins array order; each hook's return value (except onBuildComplete) replaces the value the next hook (or bx-sites itself) sees - return the input unchanged if there's nothing to modify. onPageMarkdown/onPageHtml run once per page, per doc tree (main + every docs/versions/<name>/ - see bx-sites-blog-versioning-i18n). onSearchIndex/onSitemap exist specifically for content living outside docs/ altogether (e.g. a dynamically-served page a CLI-provider addon adds) that would otherwise be invisible to search/sitemap/llms.txt.
Order of hook calls
onConfig(config)
-> build the nav tree
onNav(nav, config)
-> for every page: onPageMarkdown(markdown, page, config) -> Markdown() -> onPageHtml(html, page, config)
-> write site/
onSearchIndex(entries, config)
-> write search-index.json
onSitemap(pages, config)
-> write sitemap.xml + llms.txt
onBuildComplete(siteDir, config)
Errors
BxSites.PluginNotFound - a name in plugins isn't an installed/activated
BoxLang module.
BxSites.InvalidPlugin - the module exists but has no
models/BxSitesPlugin.bx.
Registering new bxSites CLI verbs (CLI providers)
models/BxSitesPlugin.bx only covers the build lifecycle. A module can also register its own bxSites <verb> commands (e.g. a commercial deploy/ cloud publish addon) via a sibling models/BxSitesCliProvider.bx class exposing one verbs() method - same plugins array activation, a different file. A module can implement BxSitesPlugin.bx, BxSitesCliProvider.bx, both, or neither.
```bx title="models/BxSitesCliProvider.bx" class { struct function verbs() { return { "cloud:publish" : { class : "models.cli.cloud.Publish@myBxSitesAddon", description : "Build (if needed) and publish via the configured deploy target" } } } }
```bx title="models/cli/cloud/Publish.bx"
class {
struct function run( struct options ) {
// arguments.options carries the same parsed-flags-plus-projectRoot
// shape every core verb receives.
return { exitCode : 0, message : "Published #arguments.options.projectRoot#" }
}
}
- The
@myModuleMapping suffix on the dispatch class path is required -
a bare dotted path like "models.cli.cloud.Publish" only resolves relative to bx-sites' own module root; a provider's own verb classes must self-supply their module's @<mapping> suffix to be reachable at all.
- Verb names are one colon-joined string (
"cloud:publish", the same
convention core uses for post:new/i18n:status); bxSites cloud publish (two argv tokens) and bxSites cloud:publish dispatch identically once that name is registered - no separate two-word registration needed.
- Core always wins on a name collision with a built-in verb (the
provider's entry is silently ignored - a provider can add commands, never shadow one). Between two providers, first listed in plugins wins. A missing/malformed config, no BxSitesCliProvider.bx, or a verbs() that throws never breaks core dispatch - it's treated like an unactivated plugin.