๐Ÿ”ท Core

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.

$ npx skills add ortus-boxlang/bx-sites-skills/skills/bx-sites-plugins
$ coldbox ai skills install ortus-boxlang/bx-sites-skills/skills/bx-sites-plugins
๐Ÿ”— https://skills.boxlang.io/skills/raw/ortus-boxlang/bx-sites-skills/skills~bx-sites-plugins

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 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.

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 @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.