bx-sites-plugins
Write or install a bx-sites (ortus-boxlang/bx-sites) plugin - the models/BxSitesPlugin.bx build-lifecycle hooks (onConfig, onPageMarkdown, onPageHtml, onNav, onSearchIndex, onSitemap, onBuildComplete), installing a published plugin from ForgeBox, and registering new bxSites CLI verbs via models/BxSitesCliProvider.bx. Use this whenever a user wants to extend bx-sites' build pipeline with custom logic, or add a new bxSites <verb> command.
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:
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).
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 inpluginsisn't an installed/activated BoxLang module.BxSites.InvalidPlugin- the module exists but has nomodels/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.
class {
struct function verbs() {
return {
"cloud:publish" : {
class : "models.cli.cloud.Publish@myBxSitesAddon",
description : "Build (if needed) and publish via the configured deploy target"
}
}
}
}
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
@myModuleMappingsuffix on the dispatch class path is required - a bare dotted path like"models.cli.cloud.Publish"only resolves relative tobx-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 forpost:new/i18n:status);bxSites cloud publish(two argv tokens) andbxSites cloud:publishdispatch 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
pluginswins. A missing/malformed config, noBxSitesCliProvider.bx, or averbs()that throws never breaks core dispatch - it's treated like an unactivated plugin.