BoxLang 🚀 A New JVM Dynamic Language Learn More...
|:------------------------------------------------------: |
| ⚡︎ B o x L a n g ⚡︎
| Dynamic : Modular : Productive |
| :----------------------------: |
Copyright Since 2023 by Ortus Solutions, Corp
www.boxlang.io | www.ortussolutions.com
Write your content in Markdown. BX Sites turns it into a complete, themed, searchable website - ready to deploy.
It's a general-purpose static site generator, not just for
documentation - docs, a blog, a marketing site, a knowledge base,
anything Markdown can express, all built the same way, with the same
themes, search and i18n. Built in BoxLang on top of bx-markdown,
in the spirit of mkdocs: point
it at a docs/ folder (or src/, if that reads
better for your project) and get a themed, searchable static site out.
Already on GitBook, mkdocs, or Notion, or just have a folder of
Markdown? bxSites migrate
--from=gitbook|mkdocs|markdown-zip|notion converts it straight
into a bx-sites project - see Migrating from
GitBook or Migrating from mkdocs.
bootstrap
(default), material, tailwind, and seven
more inspired by Docsy, Stripe, Docusaurus, Just the Docs, VuePress,
GitBook and Notion, all overridable via a project's own
theme/ folder. See Themes.{{ variables }} and magic
functions - reference bxsites.yaml-defined
values or call a small BoxLang helper function straight from
Markdown, no plugin needed. See Variables &
Magic Functions.docs/blog/posts/ for authors/categories/RSS/drafts,
version:new to snapshot a release,
docs/i18n/<code>/ to translate. See Blog, Versioning, i18n.bxSites deploy to
S3 (and any S3-compatible service), Azure Blob Storage, GCS,
Firebase Hosting, FTP/SFTP, rsync, Netlify, Vercel, Cloudflare
Pages, a local directory, or GitHub Pages - one shared build, every
deployments/*.json target at once if you want, or
bxSites package for a plain zip instead. See Deployment.install:plugin/install:theme pull
published ones from ForgeBox, and theme:import converts
an existing mkdocs/jekyll/hugo theme into a starting point. See Plugins.bxSites
skills:install (or npx skills add
ortus-boxlang/bx-sites-skills, or coldbox ai skills
install) teaches Claude Code, Cursor, Codex, and other AI
coding assistants bx-sites' own conventions - content blocks, config
keys, deployment, and more. See AI Agent Skills.sitemap.xml, robots.txt (or your own
hand-authored one), canonical links, llms.txt, and
redirects that keep a moved/renamed page's old URL working instead
of 404ing.Needs the BoxLang runtime installed
first - either the quick installer (curl -fsSL
https://install.boxlang.io/ | bash) or BVM
(curl -fsSL https://install-bvm.boxlang.io/ | bash).
See Getting
Started for details.
# OS Binary
install-bx-module bx-sites
# CommandBox
box install bx-sites
# Scaffold a new docs project (docs/ + bxsites.yaml)
bxSites new my-docs
cd my-docs
# Build the static site to site/
bxSites build
# Or build and serve locally with live reload while you write
bxSites serve
See Getting Started for the full walkthrough.
box install drops a standalone bxSites
script on your PATH (via box.json's
boxlang.executable), so every verb can be run either that
short way, or as boxlang bxSites <verb> - both run
the exact same thing; use the longer form anywhere the
PATH shim isn't set up (a CI runner, a module registered
by hand):
bxSites <verb> [options]
# or, equivalently:
boxlang bxSites <verb> [options]
| Verb | Purpose |
|---|---|
new
| Scaffold a docs project (docs/ +
bxsites.yaml, defaulting to the
bootstrap theme; --format=json for
bxsites.json instead) |
build
| Render docs/**.md into a static site in
site/, including the search index,
sitemap.xml, llms.txt and assets.
Needs bx-markdown, bx-esapi, bx-yaml and bx-image installed |
serve
| Build and serve the site locally with live reload (needs bx-markdown, bx-esapi, bx-yaml and bx-image too) |
search-index
| Rebuild site/search-index.json standalone
(also runs automatically during build) |
clean
| Remove site/ and any build cache |
migrate
| Convert an existing GitBook export or mkdocs project into
docs/ + nav.json
(--from=gitbook, the default, or --from=mkdocs) |
stats
| Read-only summary report on a built site/:
page/word counts, versions/locales, blog, tags, search index,
site size |
doctor
| Environment/config health check: JVM, docs/
(or src/), config validity, required modules, theme override |
post:new
| Scaffold a new blog post at docs/blog/posts/<slug>.md
|
version:new
| Snapshot docs/ into a new docs/versions/<name>/
|
i18n:status
| Per-locale translation coverage report against the default tree |
i18n:new
| Scaffold a new docs/i18n/<code>/ locale |
page:new
| Scaffold a single docs page at an arbitrary path |
plugin:new
| Scaffold a plugin module skeleton |
install:plugin
| Download a plugin from ForgeBox into project-local boxlang_modules/
|
theme:new
| Eject a built-in theme into project theme/
for customizing |
install:theme
| Download a theme from ForgeBox into project-local themes/<name>/
|
skills:install
| Install the official AI agent skill
pack (ortus-boxlang/bx-sites-skills) via
npx skills add
|
theme:import
| Best-effort convert a mkdocs/jekyll/hugo theme's own
template files into a themes/<name>/ scaffold |
page:rename
| Move a docs page and rewrite every relative link that pointed at it |
blog:drafts
| List every blog post whose frontmatter sets draft: true
|
blog:find
| Filter blog posts by author/category/tag/date range |
search:query
| Query a built search-index.json and rank results |
lint
| Pre-build content checks: heading level skips, blog posts missing a valid date |
deploy
| Build and ship site/ to a real target (s3,
azure, gcs, firebase, ftp, sftp, rsync, netlify, vercel,
cloudflare-pages, local, github-pages); no args deploys every
deployments/*.json entry; --verbose
|
package
| Build and zip site/ into a single archive
(--output=<path>, defaults to site.zip) |
Every verb accepts --projectRoot=<path> (or a bare
positional path) to target a project other than the current directory.
Run bxSites --help for full usage.
This repository documents itself with BX Sites - see
bxsites.yaml and docs/ at the repo root,
published at:
main
development
Both are built with all ten built-in themes side by side
(bootstrap at the root, every other theme under
theme/<name>/) - this doubles as a theme gallery -
use the switcher next to the dark-mode toggle to flip between them on
any page. CI builds each theme as its own parallel job (see
.github/workflows/pages.yml);
./buildMultiTheme.sh reproduces the same gallery layout
locally for a quick preview.
Or read the source directly:
bxsites.yaml, the default, or bxsites.json)See MODULE_SPEC.md for the design spec driving this module's development.
.github/workflows - CI: tests (tests.yml),
PR checks (pr.yml), snapshot/release builds
(snapshot.yml, release.yml), and
publishing this repo's own docs to GitHub Pages (pages.yml)models - the module's own source:
models/cli (one dispatcher per bxSites
verb), models/config (site config loader/validator -
bxsites.yaml/.yml/.json),
models/build (project scaffolding + the
docs/nav/markdown/theme/search/sitemap build pipeline),
models/deploy (the deploy verb's pluggable
targets - S3, Azure, GCS, Firebase, FTP/SFTP, rsync, Netlify,
Vercel, Cloudflare Pages, local, GitHub Pages)resources/themes - built-in themes (native BoxLang
.bxm templates + assets), ten in total:
bootstrap (default), material,
tailwind, plus seven material-forked
themes (docsy, slate,
docusaurus, justthedocs,
vuepress, gitbook, notion) -
all with the BoxLang brand palette, dark mode, breadcrumbs and
code-copy buttons applied out of the box. A project can override any
of them via its own theme/ folder (same
layout.bxm + page.bxm contract - see Themes)resources/assets - module-wide shared client-side
assets: the search widget (search.js, MiniSearch-backed
with a Cmd/Ctrl+K palette), the copy-code button, and per-feature
init scripts (tabs, Mermaid, math, OpenAPI, prompts, conditional
content, ...), plus every vendored third-party library under
vendor/ (Bootstrap, highlight.js, Alpine.js,
MiniSearch, Mermaid, Swagger UI) - no CDN, no outbound requests from
a built sitedocs / bxsites.yaml - this repository's
own docs, built by BX Sites itself (boxlang bxSites build)tests/specs - TestBox specs, one bundle per class under models/
bifs, components,
interceptors - unused by this module today, kept for
BoxLang module conventionbox.json - package metadata used to publish to ForgeBoxModuleConfig.bx - this module's configuration/CLI entry point
Build.bx packages this module for distribution to
ForgeBox: it produces a zip in build/ containing
everything needed to run the module (box.json,
ModuleConfig.bx, and the rest of the module's own files).
It also produces a second, self-contained artifact -
build/artifacts/bx-sites-<version>-with-deps.zip -
that additionally bundles every runtime dependency from
box.json's dependencies block
(bx-markdown, bx-esapi,
bx-yaml, bx-image) inside a
modules/ folder alongside the module itself, using
BoxLang's module
inception: a module's own modules/ folder is
discovered and activated before the module itself, so this artifact
needs nothing pre-installed to run standalone - just drop it into a
modules/ (or boxlang_modules/) folder on its
own. The primary bx-sites-<version>.zip artifact -
and the box forgebox publish step, which publishes from
build/module - are unaffected; the bundled dependencies
only ever land in the -with-deps artifact, built from a
separate build/module-with-deps copy.
boxlang Build.bx --version=1.1.0
| Option | Required | Default Value | Description |
|---|---|---|---|
version
| No | 1.0.0
| The version of the module. |
branch
| No | development
| The branch being built. |
buildId
| No | UUID (generated) | A unique identifier for the build. |
box install
bxsites.* source classes: symlink this repo into your
BoxLang home's modules/ folder - that's
~/.boxlang/modules unless $BOXLANG_HOME is
set to something else (CI pins it to the checkout's own
.boxlang/, per
.github/workflows/tests.yml): mkdir -p "${BOXLANG_HOME:-$HOME/.boxlang}/modules"
ln -s "$(pwd)" "${BOXLANG_HOME:-$HOME/.boxlang}/modules/$(basename "$(pwd)")"
./testbox/run
This module uses SemVer for
versioning. The version is stored in box.json, used to
publish to ForgeBox, and bumped automatically by the release workflow,
which also tags the repo for each release.
pr.yml - runs the test suite against every pull requesttests.yml - the reusable test-suite workflow
(./testbox/run), called by both pr.yml and snapshot.yml
snapshot.yml - on every push to
development: runs tests, then builds and publishes a
snapshot releaserelease.yml - builds and publishes a release to
ForgeBox (a stable release from main, or a snapshot
when called from snapshot.yml); requires a
FORGEBOX_API_TOKEN secretpages.yml - builds and publishes this repo's own
dogfooded docs to GitHub Pages (see Deploying to GitHub Pages)cron.yml - runs the test suite daily against development
BoxLang is a professional open-source project and it is completely funded by the community and Ortus Solutions, Corp. Ortus Patreons get many benefits like a cfcasts account, a FORGEBOX Pro account and so much more. If you are interested in becoming a sponsor, please visit our patronage page: https://patreon.com/ortussolutions
"I am the way, and the truth, and the life; no one comes to the Father, but by me (JESUS)" Jn 14:1-12
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
A page's description (its og:description/<meta name="description"> source) is now auto-derived when frontmatter doesn't set one, instead of jumping straight to the site-wide description. New fallback chain (BuildPipeline.bx's new assignDescriptions(), run for every tree - main, versions, locales): a page's own frontmatter description always wins when set; otherwise its summary (the on-page blurb text - already description-shaped prose, just never previously reused for this); otherwise a plain-text excerpt of the page's own rendered content, HTML-stripped and truncated to 160 characters at a word boundary - the same fallback chain BlogBuilder.bx's computeExcerpt() already gave every blog post, extended to every regular doc page. The site-wide description in bxsites.yaml/bxsites.json is now truly a last resort, only reached by a page with no description, no summary, and no plain text in its own content at all (e.g. a page that's just an embed or a single image). See Getting Started for the full fallback order.
Fixed every page on this repository's own dogfooded docs sharing the same generic social-card preview (site-wide description + one static banner image) when a link to it was pasted into Slack/X/etc., instead of a preview specific to that page. Root cause: description (the frontmatter key that actually feeds og:description/<meta name="description">) and summary (a different key, only ever rendered as the visible one-line blurb under a page's own title - see docs/getting-started.md's own frontmatter reference) are deliberately separate fields, and not one page in this repo had ever set description - every page fell back to bxsites.yaml's site-wide description, and (since no page set its own ogImage either, and generateOgImages was off) every page's preview image fell back to the same static assets/og-image.png banner too. Added description: frontmatter (reusing each page's existing summary: text, which already reads like a description) to every page that had a summary but no description of its own - 52 files, English plus all four translated locales - and turned on generateOgImages: true, so every page now also renders its own real 1200x630 branded social-card PNG (SocialCardGenerator.bx) at build time instead of sharing the one static image.
Added skills:install - a one-shot CLI verb that installs the official ortus-boxlang/bx-sites-skills AI agent skill pack (thirteen SKILL.md files covering everything from scaffolding a project to troubleshooting this repo's own GitHub Actions) straight into a project. bxSites skills:install (or the space-separated bxSites skills install) shells out to npx skills add ortus-boxlang/bx-sites-skills -y (a new SkillsInstaller.bx, same ProcessBuilder shell-out pattern PagefindIndexer.bx uses for the pagefind CLI) against the project root, so any AI coding assistant already configured there (Claude Code, Cursor, Codex, ...) picks up bx-sites-specific knowledge immediately - no more re-explaining ::: card ::: syntax or bxsites.yaml keys in every conversation. --skill=<name> installs just one skill instead of the full set. Requires Node.js/npx on PATH, the same requirement npx skills add has on its own. New docs page documents this alongside the two skill-pack-native install paths (npx skills add/coldbox ai skills install) and the full skill list - see AI Agent Skills and CLI Reference.
Fixed a ::: cards card whose body text auto-links to the same place its own href already points (e.g. a "Get in touch" card linking to mailto:[email protected] whose body also mentions that address) rendering as a broken, split-apart box. A card with an href renders as <a class="bxsites-card" href="...">...</a>; when its body markdown contains a bare URL/email, bx-markdown's own GFM auto-linking wraps that in a second <a> nested inside the first, which is invalid HTML - browsers "fix" it by force-closing the outer <a> the instant they hit the inner one, splitting the card's box in half and stranding the rest of its content outside it. DirectiveBlockProcessor.bx's renderCards() now strips any inner <a> tags from a linked card's body before rendering it, leaving the text intact (the whole card already links to the same destination a click on that text would).
Gave in-page table-of-contents ("On this page") entries breathing room between items, across all ten built-in themes. The list items had no margin or line-height of their own, relying entirely on the browser's UA default - on a long heading that wraps to two lines (e.g. "Prerequisite: install BoxLang"), the wrapped second line sat flush against the next entry with no visual gap, making it look like its own list item rather than a continuation of the one above it. Added margin: 0.35rem 0; line-height: 1.4; to each theme's TOC list item (bootstrap/tailwind via a shared selector since they don't render a per-item class; the other eight via their existing <prefix>-toc__item class) - the same fix applies to both the sticky sidebar TOC and its collapsible mobile variant, since both render from the same markup.
Fixed the Cmd/Ctrl+K command-palette backdrop showing stuck open (a dimmed, empty overlay blocking the whole page) on every page load, in every built-in theme. .bxsites-command-palette { display: flex; ... } was the only rule governing its visibility - CSS's own cascade rules mean an author stylesheet rule always wins over the browser's own [hidden] { display: none } default regardless of specificity, so the backdrop rendered display: flex (and therefore visible, dimming and blocking the page) even while search.js's own buildPalette() had already set backdrop.hidden = true right after creating it. Added an explicit .bxsites-command-palette[hidden] { display: none; } override to all ten built-in themes' assets/style.css - the palette now stays correctly invisible until Cmd/Ctrl+K (or the search palette trigger) actually opens it.
Moved this repository's own dogfooded docs to the bxsites.io custom domain, off ortus-boxlang.github.io/bx-sites. bxsites.yaml's committed baseURL and .github/workflows/pages.yml's own per-branch patch (main at the site root, development at /development/) both now point at https://bxsites.io/ - previously stale references here would have kept every internal link/asset path pointing at the old /bx-sites/... sub-path once the custom domain actually took over serving the site, breaking every asset (wrong absolute path, not a missing file). The main deploy step also now passes cname: bxsites.io to peaceiris/actions-gh-pages, writing CNAME at the true gh-pages root on every deploy so the custom domain survives a gh-pages wipe instead of depending solely on the one-time manual Settings -> Pages -> Custom domain click. readme.md's own doc links updated to match.
Added a pageActions config flag - a GitBook-style "Copy" dropdown on every page offering Copy page (raw Markdown to clipboard), Copy link (the page's canonical URL, right after Copy page since both are clipboard actions), View as Markdown (opens the raw .md source), Open in ChatGPT/Open in Claude (a new chat prefilled with the page's URL), Export as PDF (window.print(), scoped to the page - pairs with the new ::: pagebreak block below for controlling where it breaks), Report an issue (a pre-filled "new issue" form on repo.url), and Share on X/Share on LinkedIn/Share on Facebook (that platform's own share-intent page). false by default; theme.options.pageActionsPosition ("top", the default, or "bottom") picks where it renders, and a bare c keydown toggles the menu open/closed from anywhere on the page (no modifier, same "not already typing" guard as the / search shortcut). A plain <details>/<summary> disclosure needs no JS to open/close; a new shared page-actions-init.js (only shipped when pageActions is on) wires up the copy/print/keyboard-shortcut behavior, styled consistently across all ten built-in themes. A thin divider sets the "Open in..."/"Share on..." pairs apart from the rest of the menu, so those two groups read as their own sections instead of running together with everything else. See docs/configuration.md#pageactions
Added a ::: pagebreak content block - forces a page break wherever it's placed, for a project that prints or exports its built site to PDF (e.g. a browser's own "Print to PDF") and wants a section to always start on a fresh page. No attributes needed (::: pagebreak ... :::); renders as a plain, subtle divider on screen so it's still visible while editing/previewing, and each of the ten built-in themes' own CSS only turns it into an actual forced break inside @media print - it has no other effect on normal browsing. See docs/guides/content-blocks.md#page-break
Added a unified bxSites deploy CLI command, shipping the built site/ directly to a real target: S3 (and any S3-compatible service - DigitalOcean Spaces, Cloudflare R2, Backblaze B2, MinIO), Azure Blob Storage, Google Cloud Storage, Firebase Hosting, FTP, SFTP, rsync-over-SSH, Netlify, Vercel, Cloudflare Pages, a local directory, or GitHub Pages (the same push the older gh-deploy verb already did, now reachable from this one command too). A deployments/<name>.json file per target (bxSites deploy --entry=<name>) declares which target it is plus that target's own config; local/github-pages also work with no deployments/ folder at all via --target/flags. Running bxSites deploy with neither flag deploys every deployments/*.json entry in turn, off one shared build - one target failing doesn't stop the rest, and the command only exits non-zero if at least one did. --verbose prints a progress line as the build and each target start/finish. Secrets are always resolved from a named environment variable at deploy time, never stored as a literal in deployments/*.json, so that file is always safe to commit. New models/deploy/ package: an IDeploymentTarget interface, a DeploymentConfigResolver, and one class per target, plus a hand-rolled AWS Signature Version 4 signer and a Google service-account OAuth2/JWT helper shared by the Google-backed targets - no cloud SDK dependencies. See docs/guides/deployment.md and docs/cli-reference.md#deploy. Adds bx-ftp as a new box.json dependency (for the ftp/sftp targets' real bx:ftp component).
Added a bxSites package CLI command - builds the site, then zips it into a single distributable archive whose root is the built site's own contents (not a wrapping site/ folder), for attaching to a release or handing to any host that only accepts a zip upload. --output defaults to <projectRoot>/site.zip. See docs/guides/deployment.md#the-package-command and docs/cli-reference.md#package.
Swapped the local search provider's engine from lunr.js to MiniSearch, and expanded docs/guides/search.md into a fuller guide covering all three built-in providers plus a worked walkthrough for wiring up a fourth (Meilisearch). Same zero-dependency, fully static/client-side shape as before (still vendored locally, no CDN, no server) - resources/assets/search.js now builds a MiniSearch index instead of a lunr one, gaining prefix matching and typo-tolerant fuzzy matching for free (no more separate trailing-wildcard-query retry hack lunr needed for partial words). The vendored bundle moved from assets/vendor/lunr/lunr.min.js to assets/vendor/minisearch/minisearch.js accordingly - a project loading it directly (rather than through the built-in themes) needs to update that path. New "Choosing a provider" comparison table and "Building your own provider" section (bxsites.yaml's freeform searchProvider.<name> sub-block, ejecting a theme, search.bxm/layout.bxm branches, and onBuildComplete for a provider needing its own indexing step) added to docs/guides/search.md. See docs/guides/search.md
Documented markdown tables (already fully supported, previously undocumented), and added three GitBook-inspired extensions on top: automatic responsive scroll + a sticky header for every table, a "visualizer recipes" catalog of magic functions for rating/badge/progress-bar cells, and an Alpine.js recipe for a client-side sortable/filterable table. GFM pipe tables have always been on by default via bx-markdown/Flexmark, with a markdown.tableOptions passthrough already documented in docs/configuration.md, but the markdown guide itself never covered the syntax - added a ## Tables section to docs/guides/markdown.md (alignment colons, tableOptions cross-link). On top of that: a new TableWrapProcessor.bx (a post-Markdown() pass, same shape as CodeAnnotationProcessor.bx) wraps every rendered <table> in a .bxsites-table-wrap div, since a sticky <thead> needs a separate, actually-bounded scrolling ancestor to stick within - overflow-x on the <table> element itself doesn't work, because the table's own height always exactly fits its own content, leaving position: sticky nothing to stick against (confirmed by reproducing the broken behavior against a real 40-row table before fixing it). All ten built-in themes' assets/style.css give .bxsites-table-wrap a max-height/overflow: auto, so a short table never grows a scrollbar while a long one scrolls internally with its header pinned. Since a magic function (see below) can return arbitrary HTML, docs/functions.bxs (a real, live file for this project's own docs now) gained four example "visualizer" functions - $stars(), $badge(), $progress(), $trend() - documented in a new "Visualizer recipes" section of docs/guides/variables-and-functions.md, rendering live on that page (including inside a real table's cells, the closest equivalent here to GitBook's database-backed Select/Rating table columns, which this module's git-based Markdown source has no direct equivalent of). Finally, docs/guides/interactivity.md gained "A sortable, filterable table" - since a native pipe table is static once built, this recipe has Alpine own the rows (x-data/x-for) instead, for a client-side sortable/filterable table with no new bx-sites code required. All mirrored into all four locales (de/es/it/ja). See docs/guides/markdown.md#tables, docs/guides/variables-and-functions.md#visualizer-recipes, docs/guides/interactivity.md#a-sortable-filterable-table
Added ::: button/::: buttons content blocks - GitBook-style call-to-action buttons, styled consistently across all ten built-in themes. ::: button "Label" href="..." renders a single link-styled button; wrap several in ::: buttons to lay them out in a row. Each button has its own set of optional abilities: style="primary"/"secondary" (solid-accent vs. outline, default secondary), size="small"/"medium"/"large", icon="..." (resolved the same way a card's own icon is), target="_blank" to open in a new tab, and disabled="true" to render an inert, unclickable button for a "coming soon" call to action. Every theme's own CSS also styles GitBook's own raw exported button markup (<a class="button primary">) identically, so a migrated GitBook button needs no conversion at all. See docs/guides/content-blocks.md#buttons
Split ::: openapi's own documentation out of docs/guides/content-blocks.md into its own first-level guide, docs/guides/openapi.md - the OpenAPI/Swagger widget (full-spec and single-operation forms) now gets its own nav entry instead of living as one section among many in Content Blocks, plus a new "Documenting an API without a spec file" note (there's no manual, spec-less equivalent of the block - GitBook's own "API method" block was deprecated in February 2024 for the same reason - the alternatives are a minimal spec covering just the one endpoint, or plain Markdown). Every cross-reference across the docs (configuration.md#openapi, MODULE_SPEC.md, the example spec's own description) and all four translated locales (de/es/it/ja) were updated to match, mirroring the earlier markdown.md → content-blocks.md split.
Added a ::: prompt content block - bx-sites' own equivalent of GitBook's Prompt block, a styled container for a reusable AI prompt. The block's body is the prompt text itself, rendered as markdown (headings/lists/code inside it keep their own formatting); description (an optional one-line summary) and icon (resolved the same way ::: card's own icon is, defaulting to a sparkle glyph when omitted) are both optional. A new shared prompt-init.js (unconditional, alongside conditional-content-init.js) wires up a "Copy" button that copies the block's own raw markdown source rather than its rendered HTML, and an expanded="hidden"/"preview" toggle for a long prompt (default "full", matching GitBook's own default) - styled with a distinct violet accent and top gradient bar, consistent across all ten built-in themes regardless of each theme's own brand palette, so a prompt block always reads as a prompt block. bxSites migrate's GitBook importer now converts {% prompt %} into ::: prompt directly (previously left unconverted, needing a manual look) - openInAIProviders is dropped and reported as a warning when GitBook had it turned on, since this module never talks to a third-party AI provider. See docs/guides/content-blocks.md#prompt
Fixed fenced code blocks rendering with a light (white) background/palette in dark mode, across all ten built-in themes. The vendored github.min.css (highlight.js's syntax-highlighting stylesheet) only ever defined its own light-mode colors, with no dark counterpart - every fenced code example (JS, YAML, bash, ...) rendered as a jarring white box against an otherwise dark page, the syntax token colors unreadable against it. Each theme's assets/style.css already had a [data-theme="dark"] override for the diff-fence-only .hljs-addition/.hljs-deletion classes (reusing the site's own insert/delete colors), but nothing covered the rest of highlight.js's own token classes (keywords, strings, comments, titles, ...) or the base .hljs background/text color itself. Added a [data-theme="dark"] override for the full token set, using highlight.js's own bundled "GitHub Dark" theme values (@highlightjs/cdn-assets' styles/github-dark.min.css - the same package github.min.css itself is vendored from), applied identically across all ten built-in themes. Verified with a real build, screenshotted in both modes before and after the fix.
Added reusable variables and magic functions. bxsites.yaml's new variables object (any shape, flat or nested) is referenced from any Markdown page as {{ dotted.path }}, resolved once at build time. A new project-wide docs/functions.bxs (src/functions.bxs) - a plain BoxLang script, no plugin/wiring needed - lets a project declare small helper functions; any function named with a leading $ becomes a "magic function", callable the same way as {{ $name(arg1, arg2) }} from Markdown, and callable bare (no prefix) directly from a project's own theme/ .bxm overrides. Both share one {{ }} syntax (VariablesProcessor.bx, a new fence-aware markdown-to-markdown pass in convertMarkdown(), right after ::: include splicing and before every other content pre-processor) - a {{ }} shown inside a fenced code example is left untouched, and one that doesn't look like either a variable path or a $name(...) call is left alone too, only a token that looks like one but fails to resolve throws (BxSites.UnknownVariable/BxSites.UnknownFunction). functions.bxs is loaded once per build (FunctionsLoader.bx) and shared across the main tree and every version/locale tree - no need to duplicate it. A magic function's own body can also bare-reference a shared "supporting variables" context - siteConfig, page, nav, basePath, versions, currentVersion, locales, currentLocale, currentLocaleDir - identically whether it's called from {{ }} or bare from a theme override (a new FunctionScope.bx binds this context alongside the loaded functions for the Markdown path, the same names a theme override already has bare in scope); page is the one exception, only as fully populated as DocsLoader.bx's own load-time shape when called from Markdown, fully enriched by the time the same function runs from a theme's page.bxm. See docs/guides/variables-and-functions.md and docs/configuration.md#variables
Build.bx now also produces a self-contained bx-sites-<version>-with-deps.zip artifact that bundles every runtime dependency from box.json's dependencies block (bx-markdown, bx-esapi, bx-yaml, bx-image) inside a modules/ folder alongside the module itself, using BoxLang's module inception (a module's own modules/ folder is discovered and activated before the module itself, recursively) - the result needs nothing pre-installed to run standalone. Built via box install <slug> modules --!save per dependency into a separate build/module-with-deps copy of the already-built module, so the existing bx-sites-<version>.zip artifact and the box forgebox publish step (which still publishes from build/module) are unaffected. devDependencies (testbox, commandbox-*) are dev-only tooling and are never bundled. See readme.md#local-building
RobotsTxtGenerator now honors a project's own hand-authored robots.txt - drop one at docs/robots.txt (or src/robots.txt for a src/-based project, see SourceDirResolver.bx) and it's copied through byte-for-byte on every build instead of the generated one, bxsites.json's robots key ignored entirely once it exists - the only way to get anything beyond the on/off toggle (specific disallowed paths, multiple Sitemap: lines, Crawl-delay, per-user-agent rules). See docs/configuration.md#robotstxt
Added ::: conditional/::: audience-switcher content blocks - reader-chosen, locally-remembered content variants (e.g. "Free" vs. "Pro" instructions inline on the same page), the static-site-appropriate answer to what other docs platforms call adaptive/conditional content (those need a real backend evaluating visitor identity per request; this module has neither). ::: conditional key="plan" value="pro" tags one variant; every variant always renders in the HTML (hidden client-side via a new shared conditional-content-init.js, never omitted, so JS-disabled readers and crawlers still see everything). The reader's own choice is stored in localStorage under bxsites-pref-<key>, so it's remembered across every later page too; a ?plan=pro-style query-string param sets the same preference automatically on load, for sharing a direct link to one variant. ::: audience-switcher key="plan" options="free:Free,pro:Pro" is an optional ready-made button-group control; a project can just as well drive window.bxSitesSetPreference( key, value ) from its own theme override instead. See docs/guides/content-blocks.md#conditional-content
Added a single-operation ::: openapi ... operation="METHOD /path" form - drops one endpoint from an OpenAPI spec inline into a regular page (a tutorial showing just the one relevant call) instead of only ever embedding the whole spec's reference. Still the identical client-side-only Swagger UI render as the full-spec block (no OpenAPI parsing on our side either way) - openapi-init.js's new scopeToOperation() hides every other operation and auto-expands the matching one after Swagger UI finishes rendering, by reading its own already-rendered DOM (.opblock-summary-path's data-path attribute, the stable opblock-<method> class) rather than parsing the spec a second time. See docs/guides/content-blocks.md#one-operation-inline
Added robots.txt generation and a <link rel="canonical"> tag on every page. A new RobotsTxtGenerator.bx writes a permissive robots.txt (Allow: / + a Sitemap: line, when baseURL is a full URL) by default; bxsites.json's new robots: false flips it to Disallow: / instead - the common "don't index this staging/internal deploy at all" need (a crawler opt-out only, never access control - see docs/guides/deployment.md#restricting-who-can-reach-your-site for that). Every built page's own <head> now also gets a correct <link rel="canonical"> (a version/locale tree's own page points at that tree's own URL, not the main site's) whenever baseURL is a full URL - previously only redirect stubs carried one. See docs/configuration.md#robotstxt and #baseurl
Added bxSites migrate --from=markdown-zip and --from=notion alongside the existing gitbook/mkdocs sources. markdown-zip (a new MarkdownZipMigrator.bx) takes a plain .zip of Markdown files - no proprietary export format to translate, so it's mostly a straight copy, relocating non-.md assets to docs/assets/imported/ and rewriting references to match. notion (a new NotionMigrator.bx) additionally cleans up Notion's own two export quirks: every page/folder's own trailing 32-character id suffix is stripped and the remaining name slugified for the output path, and a page's own leading # Heading (Notion's stand-in for a real title) is promoted into a real title frontmatter field instead of staying a duplicate first line - every link/image target (which Notion writes URL-encoded, pointing at the original id-suffixed names) is rewritten to match throughout. Both accept an already-extracted folder too, not just a .zip. See docs/cli-reference.md#migrate
Added a Cmd/Ctrl+K command-palette overlay to search - a centered modal over a backdrop (arrow-key result navigation, Enter to go, Escape/backdrop-click to close), the same "Quick Find" convention Algolia DocSearch/Pagefind/VitePress/Docusaurus/GitBook all share, built entirely in JS and shared across every built-in theme (no template changes needed). Cmd/Ctrl+K previously just focused the sidebar search box (still true of the plain / shortcut); the sidebar widget's own results dropdown is unchanged. Found and fixed a real bug in the existing local-provider search widget along the way: search.js's own trailing-wildcard query (idx.search( query + "*" ), meant to match partial words while typing) silently returns zero hits instead of throwing whenever lunr's wildcard matching doesn't line up with its own stemmed index terms (e.g. querying "advanced*" against a document indexed by its stem "advanc") - the existing try/catch fallback to a plain query never triggered because nothing ever threw, so a real, working query could still show "No results found." search() now falls back to the plain query whenever the wildcard attempt comes back empty, not just on a thrown error - fixing this for the sidebar widget too, not just the new palette that reuses it. See docs/guides/search.md#keyboard-shortcuts
Added two homepage feature cards the earlier theme-gallery/plugin-installer work never got its own callout for: "Plugins & themes, published on ForgeBox" (linking to guides/plugins.md#installing-a-published-plugin, naming the new bxsites-plugins/bxsites-themes ForgeBox categories) and "Import an existing theme" (linking to guides/theme-import.md, the theme:import verb). Also linked those same two ForgeBox categories directly from guides/plugins.md's/guides/themes.md's own "Installing a published X" sections. Mirrored into all four locales (de/es/it/ja), including verifying each locale's real translated heading anchor before linking to it
Rewrote readme.md's opening paragraph - it led with "Static documentation site generator for BoxLang," which read as BoxLang-docs-only to a first-time reader (feedback from an actual first read), even though BX Sites has been a general-purpose static site generator (docs, blogs, marketing sites, anything Markdown) since early on, and the module's own docs/index.md homepage already said so. Now opens with a plain one-line description before any jargon ("Write your content in Markdown. BX Sites turns it into a complete, themed, searchable website - ready to deploy."), then states the general-purpose framing explicitly before getting into docs//bx-markdown/mkdocs specifics. Also added an mkdocs-migration link alongside the existing GitBook one (the intro only mentioned GitBook, despite migrate --from=mkdocs existing), and fixed the migrate verb's own table row, which still only described the GitBook path
serve now reacts to a saved change instantly and rebuilds incrementally, instead of a 1-second poll loop (checkAndRebuild()) that fingerprinted every file under docs//bxsites.json/theme/ on every tick and, once it noticed anything, deleted and rebuilt the entire site regardless of what actually changed - on this repo's own dogfooded docs (21 pages × 5 locale/version trees) that full rebuild alone took 40-50 seconds, an unusable dev loop. Two independent fixes: (1) DevServer.bx's new startWatching()/stopWatching() use BoxLang's own native watcherNew() (docs/, a project theme/ override) plus a schedulerNew() task (its own withNoOverlaps()) to coalesce a burst of rapid saves into exactly one rebuild rather than one per file - bxsites.json/.yaml/.yml gets a plain mtime check on that same scheduled tick instead, since watcherNew() only ever registers directories, and site/ itself can never safely be watched (rebuild() writes there, which would self-trigger); (2) a new BuildCache.bx - held by BuildPipeline.bx (convertMarkdown()) and SearchIndexer.bx, each keyed by a page's own sourcePath + last-modified time - reuses a page's already-converted HTML/search-index entry across repeated build() calls on the same instance (exactly DevServer.bx's own shape) whenever that page's source file hasn't changed, skipping straight back to today's exact from-scratch behavior for a brand-new BuildPipeline/SearchIndexer (every plain bxSites build CLI run), so neither a one-shot build's output nor its cost changes at all. A config/theme change (which a per-page mtime check can't see on its own) clears the whole cache via a new BuildPipeline.clearDevCache() before rebuilding. Together, editing one page in serve now rebuilds in roughly 1-2 seconds instead of 40-50, dominated by that one page's own conversion cost rather than every page in every tree - see docs/getting-started.md#serve-locally and docs/cli-reference.md#serve
English docs no longer default to showing bxsites.json example snippets when bxsites.yaml is the actual default/preferred format (bxSites new scaffolds .yaml unless --format=json is passed). docs/configuration.md's per-key reference - the file this mismatch was worst in, ~19 individual json title="bxsites.json" snippets throughout, one per config key, despite its own intro correctly calling out .yaml as the default - is now YAML throughout (its own "shows JSON snippets for brevity" note flipped to say YAML), keeping only the deliberate top-of-page side-by-side yaml+json comparison and the always-JSON docs/nav.json example (a real, separate file that's genuinely JSON-only regardless of the main config's own format) as JSON. Same sweep across every other guide (themes.md, plugins.md, redirects.md, i18n.md, images.md, markdown.md, blog.md, search.md, deployment.md, content-blocks.md, versioning.md, interactivity.md, migrating-from-gitbook.md) and all three blog posts with config snippets (search-that-just-works.md, never-lose-a-reader-versions-and-translations.md, pick-your-theme.md) - converting each isolated example snippet to YAML and retargeting prose like "in bxsites.json"/"no bxsites.json change needed" to bxsites.yaml, while leaving genuinely format-specific mentions alone (--format=json, the dual-format comparisons, docs/nav.json). Mirrored the same fix into all four locales (de/es/it/ja) - same conversions, prose adapted to each locale's own grammar/translation. Along the way, found (but left alone, out of scope for this pass) a few pre-existing, unrelated content-drift spots surfaced while reviewing these files closely: docs/i18n/it/configuration.md's ## nav section is missing the "title-only entry with children" explanatory paragraph current English has, and docs/i18n/ja/guides/search.md is a much shorter partial translation missing the Algolia/Pagefind sections entirely
docs/getting-started.md and readme.md now say explicitly that the BoxLang runtime itself has to be installed before install-bx-module bx-sites/box install bx-sites will work - both of those commands assume a BoxLang engine (or CommandBox) is already on the machine; neither one installs BoxLang itself. Added a "Prerequisite: install BoxLang" section to getting-started.md, up front of the existing "Install" section, with commands for both of BoxLang's own install paths - the quick installer (curl -fsSL https://install.boxlang.io/ | bash, single version) and BVM, the BoxLang Version Manager (curl -fsSL https://install-bvm.boxlang.io/ | bash then bvm install latest && bvm use latest, for switching between versions) - sourced from BoxLang's own installation docs, plus a pointer there for the Windows/Homebrew variants this guide doesn't otherwise cover. readme.md's Quick Start gained a short one-line version of the same note. Translated into all four locales (de/es/it/ja); doing so also surfaced that docs/i18n/it/getting-started.md's own Install section had drifted out of sync with a previous fix (English had already been consolidated to a single install-bx-module bx-sites/box install bx-sites command, dependencies-auto-install-via-box.json and all - Italian was still showing the old pre-fix instructions, separate commands per dependency and missing bx-image entirely) - fixed to match English along the way
pages.yml's 10-theme gallery build is now 10 separate GitHub Actions matrix jobs (strategy.matrix.theme) instead of one job juggling all ten concurrently via buildMultiTheme.sh's git worktree + bash job control. The earlier single-job concurrent rewrite (previous entry) introduced a real bug that broke the very next development deploy: all ten worktrees plus all ten site/ outputs stayed on disk simultaneously until every variant finished, and on a standard-size GitHub-hosted runner that was enough added peak disk usage for a variant to silently lose a write partway through while still exiting 0 (mv: cannot stat '.../bootstrap/site/*': No such file or directory, after every variant had already logged its own "Built") - a class of failure real per-job OS-level isolation (a separate runner per theme, no shared disk/BOXLANG_HOME/working tree at all) sidesteps entirely rather than papering over. Each matrix job builds one theme normally and uploads its own site/ as an artifact (actions/upload-artifact); a new assemble-and-deploy job downloads all ten (actions/download-artifact) and assembles them into one site/ tree (bootstrap at the root, every other theme under theme/<name>/) before deploying, same as before. Every matrix job's own "Verify the build actually produced output" step still checks for a non-empty site/ before uploading - boxlang bxSites <verb>'s own exit code doesn't reliably surface a real internal crash (ModuleConfig.bx's top-level try/catch), so this isn't optional insurance, whichever architecture runs the build. buildMultiTheme.sh itself still exists, unchanged in behavior from the fixes below, purely as an opt-in local-preview tool for reproducing the same gallery layout in one command without waiting on ten CI jobs - it no longer runs in CI at all
Fixed two bugs in buildMultiTheme.sh found while still using it in CI (now superseded by the matrix-job rewrite above for CI itself, but still real fixes for its own local-preview use): (1) every worktree's own checked-out bxsites.yaml reflected whatever's plain-committed, never pages.yml's own throwaway, uncommitted per-branch patch (pointing baseURL/repo.editUri at that branch's own sub-path) done to the main checkout's copy just before this script runs - a non-bootstrap theme's own sub-path override happened to still compute correctly (it's derived from the correctly-patched top-level file, not the worktree's own copy), but bootstrap itself was never patched at all, so a development (or any non-main) branch build would have silently deployed bootstrap with main's own root URLs baked in. (2) all ten worktrees plus all ten site/ outputs stayed on disk simultaneously until every variant finished and one final pass moved/removed them all at once - the exact mechanism behind the crash the matrix-job rewrite above sidesteps. Each variant now copies the correctly-patched config into its own worktree before applying its own sub-path override, and moves its own output into its final destination and removes its own worktree the moment its own build finishes (capping peak disk usage at BXSITES_BUILD_JOBS variants' worth instead of all ten) - and a build that exits 0 but writes nothing now fails loudly, attributed to that one variant's own name and log, instead of surfacing later as an unattributed glob-not-matched error
Fixed pages.yml's Setup BoxLang step never actually installing bx-esapi - box.json declares exactly four dependencies (bx-markdown/bx-esapi/bx-yaml/bx-image), and tests.yml's own modules: input already lists all four, but pages.yml's only ever listed three, missing bx-esapi (which provides encodeForJavaScript/encodeForHTML/etc. - used all over every theme, not just the markdown/YAML/image pipeline). An earlier pass on this same branch had removed with-commandbox: true as apparently-unused overhead (nothing in this workflow's own steps runs a bare box command) - it wasn't unused: it was apparently the thing silently auto-installing box.json's own declared dependencies, papering over the missing bx-esapi this whole time. Removing it surfaced the real gap the moment a real Publish Docs run exercised it: every one of the ten theme builds crashed immediately (Error: Function [encodeForJavaScript] not found, on the very first page rendered) while still exiting 0, producing a completely empty site/ - caught by the matrix rewrite's own "Verify the build actually produced output" step, not silently deployed. Fixed by both listing all four dependencies explicitly (matching tests.yml's already-proven list) and restoring with-commandbox: true, matching tests.yml's/docbox.yml's own already-working combination exactly rather than re-guessing at exactly what setup-boxlang does or doesn't need CommandBox for
Added a theme:import verb - best-effort conversion of a theme from another static site generator's ecosystem (mkdocs/jekyll/hugo) into a bx-sites theme scaffold under themes/<name>/. JinjaLikeTranslator.bx mechanically translates the Jinja2 (mkdocs)/Liquid (jekyll) shared template subset ({{ expr }} output, if/elif/elsif/else/endif, for/endfor, comments) against a fixed per-ecosystem field-name map; GoTemplateTranslator.bx does the same for Hugo's structurally different Go templates (one delimiter for everything, a generic {{ end }} that closes whichever block is open, dot-prefixed fields) - including a safety net Jinja2/Liquid didn't need: an unmapped Go .Field reference left raw inside a generated condition="#...#" isn't valid BoxLang syntax at all (unlike an unmapped bare word, which is at worst an undefined-variable runtime error), so it's substituted with a syntactically-safe placeholder instead, and a Go condition that isn't a single field reference (Go writes boolean logic as prefix function calls - and .A .B, eq .Type "post" - with no BoxLang infix equivalent) is placeholder'd wholesale rather than partially substituted into broken text. Anything outside what these translators handle (filters, template inheritance/includes, Hugo's with) is left as a visible <!--- TODO: ... ---> marker - a scaffold, not a lossless converter. ThemeImporter.bx ties it together per ecosystem: finds the first matching conventional layout/content filename, copies asset folders verbatim, and reports exactly what was found/converted/still needs a manual look. See docs/guides/theme-import.md
buildMultiTheme.sh (the 10-theme dogfooded-docs build) now builds every theme concurrently instead of one at a time - going from 3 themes to 10 made the old fully-sequential version too slow. Each variant builds in its own throwaway git worktree (git worktree add --detach, the same technique GhPagesDeployer.bx already uses for its own throwaway gh-deploy checkout) rather than all ten fighting over one shared bxsites.yaml/site/ - a plain directory copy or a bare docs/ symlink with no real .git context behind it would have silently broken git log (lastUpdated's per-file history lookup, GitRevisionDate.bx); a real worktree keeps that working while still sharing the main repo's object database. Each worktree's own checked-out docs/ (frozen at HEAD) is immediately swapped for a live symlink back to the real working tree, so uncommitted local edits still show up in the preview build exactly as before. Concurrency is capped at BXSITES_BUILD_JOBS (default: nproc), and a failed variant reports only its own log rather than dumping all ten
Added redirects, so a moved/renamed page's old URL keeps working instead of 404ing - a static HTML stub (<meta http-equiv="refresh"> + <link rel="canonical">, no theme/chrome, no server-side rewrite rule needed) gets written at the old path. Two ways to declare one: a page's own frontmatter redirect_from: [old-path, ...] (scoped to whichever tree - main/version/locale - the page itself belongs to, same as its own real URL already is), or bxsites.json's new redirects: [{ "from", "to" }] array (only ever applied to the main tree, since a bare to is only unambiguous as a root-relative path at the site root - to also accepts a full https:// URL for redirecting off-site entirely). page:rename now auto-stamps the moved page's own redirect_from with its old path on top of the link-rewriting it already did, so a rename's old URL keeps answering with zero extra steps. A new RedirectGenerator.bx does the actual writing, called once per tree the same shape writeTagsPage()/writeSearchIndex() already are; a build fails outright (BxSites.RedirectConflict) rather than silently overwriting real content if a redirect's own from path collides with a real page or another redirect - see docs/guides/redirects.md and docs/configuration.md#redirects
Theme chrome (the surrounding UI text every built-in theme renders - search placeholder, "On this page," "Edit this page," "Last updated," the 404 page, the tags page title, "Page navigation," and the "hasn't been translated yet" notice itself) is now translated per locale, instead of always English regardless of bxsites.json's own i18n config. A new StringsResolver.bx resolves each string in three layers (English default → a built-in translation, if this module ships one for that locale's code → the project's own override), and ThemeRenderer.bx's renderPage() now resolves it once per render and hands it to every theme as variables.strings. Four locales ship a built-in translation out of the box - de, es, it, ja (the same four this repo's own docs are dogfooded in) - so building any of those locale codes gets fully translated chrome with nothing to configure; any other locale code falls back to English, overridable key-by-key via a new strings struct on i18n.defaultLocale/i18n.locales[] (ConfigLoader.bx already validated its shape, from earlier work, but nothing consumed it until now). Deliberately out of scope for this pass: the blog subsystem's own UI strings ("Categories," "Archive," "Read more," ...) and lastUpdated's own date value (its label translates; the date/time formatting itself doesn't yet) - see docs/guides/i18n.md#theme-chrome-ui-strings and docs/configuration.md#i18n
Shipped a 10-theme gallery - 7 new built-in themes alongside the existing bootstrap/material/tailwind: docsy (Read the Docs/Docsy-inspired navy-blue reference-manual look), slate (Stripe/Slate-inspired, a sidebar that stays dark regardless of light/dark mode), docusaurus (Docusaurus-inspired bold full-width navbar), justthedocs (Just the Docs-inspired minimalism, search box relocated to the top of the sidebar), vuepress (VuePress-inspired green accent, soft rounded corners), gitbook (GitBook-inspired centered reading column, serif headings - also a thematic fit given migrate --from=gitbook), and notion (Notion-inspired borderless sidebar, near-grayscale UI). All seven are forked from material rather than written from scratch - byte-identical layout.bxm/page.bxm/search.bxm scripting logic (verified via diff against a scoped md-→<prefix>- class rename for six of them; justthedocs additionally relocates one <bx:include> line moving the search box into the sidebar), only assets/style.css genuinely rewritten per theme - so every one of the seven inherits material's full feature coverage and (system-font-stack-only, no external CDN) air-gapped-offline capability. This repo's own dogfooded docs (buildMultiTheme.sh, docs/assets/theme-switcher.js) now build and switch between all ten, doubling as the gallery. Also corrected a pre-existing inaccuracy in docs/guides/themes.md's "Customizing colors" section - material/tailwind (and now docsy/docusaurus/justthedocs/vuepress/gitbook) never actually exposed the full documented --bxsites-bg/-text/-sidebar-bg/-sidebar-text/-border/-link/-link-hover/-code-bg set under those names (only --bxsites-gradient-start/-end/-accent/-step-* are guaranteed everywhere); bootstrap, slate and notion are the three that do
Added an install:theme verb and a new themes/<name>/ theme-resolution tier - downloads a published theme's zip from ForgeBox and extracts it into the project's own themes/<name>/, validating the result against the ThemeProvider contract (layout.bxm + page.bxm) before finishing so a broken package fails at install time rather than at the next build. ThemeRenderer.resolveThemeDir()'s resolution order is now project theme/ override → themes/theme.name/ installed theme → built-in theme, letting a project carry several installed themes side by side and switch between them purely by setting theme.name - no separate activation step the way install:plugin needs, since a theme is pure files with no BoxLang module/class-loader involvement at all. See docs/guides/themes.md#installing-a-published-theme and docs/cli-reference.md#installtheme
Added an install:plugin verb - downloads a published plugin from ForgeBox (ForgeBoxClient.bx's GET {baseUrl}/entry/{slug}[/{version}]) and extracts it straight into the project's own boxlang_modules/<slug>/, BoxLang's own documented auto-loaded-modules convention for a local CLI app - deliberately project-local, not a global BOXLANG_HOME install, so two bx-sites projects on one machine never fight over the same plugin version. PluginInstaller.bx loads the freshly-extracted module into the running runtime (ModuleService.loadModule()) and reports back its real registered mapping name (read off ModuleService.getModuleList()'s own registry, since a module's ForgeBox slug and its BoxLang mapping name aren't always the same - bx-markdown registers as bxMarkdown) so it's immediately ready to add to bxsites.json's plugins array - installing still never activates a plugin on its own, same existing rule plugin:new already documents. See docs/guides/plugins.md#installing-a-published-plugin and docs/cli-reference.md#installplugin
Added a ::: openapi src="assets/openapi/spec.yaml" title="My API" content block - an interactive Swagger UI widget for an OpenAPI/Swagger spec (JSON or YAML, parsed entirely client-side - no server-side OpenAPI parsing anywhere in this module). Requires bxsites.json's new openapi: true key; unset, the block renders but stays inert and the Swagger UI JS/CSS is never copied into site/ at all, so every other project's build stays exactly as small as before. Only SwaggerUIBundle's own base layout is vendored (resources/assets/vendor/swagger-ui/) - deliberately not the topbar/"Explore" URL-switcher preset, since a ::: openapi block should always show the one spec its author pointed it at, never let a reader type in a different one. This repo's own docs/guides/content-blocks.md now dogfoods a live example against a small hand-authored spec at docs/assets/openapi/example.yaml - see docs/guides/content-blocks.md#openapi--swagger and docs/configuration.md#openapi
A project's source content folder can now be named src/ instead of docs/ - every verb that reads project content (build, serve, check, stats, lint, doctor, migrate, and every page:new/post:new/version:new/i18n:new/page:rename/blog:drafts/blog:find scaffolder) auto-detects whichever of the two actually exists at the project root, via a new SourceDirResolver.bx (docs/ still wins when a project somehow has both, so every existing project - this repo's own dogfooded docs included - keeps working with zero changes). bxSites new still always scaffolds docs/; src/ is purely a "bring your own existing folder" option for a project that isn't really "docs" in spirit. Build output is unaffected either way - it always lands in site/, which deliberately is never itself a valid source-folder name (a build deletes and rewrites site/ on every run, so letting it double as a source name would have made a build delete its own source content) - see docs/getting-started.md#add-pages
Standardized on boxlang bxSites <verb> as the documented/printed long-form invocation, dropping the module: prefix everywhere (docs/, readme.md, MODULE_SPEC.md, AGENTS.md, the CLI's own --help usage line in ModuleConfig.bx, and the CI scripts that build this repo's own docs, buildMultiTheme.sh/.github/workflows/pages.yml) - boxlang module:bxSites <verb> still resolves identically (BoxLang's own module dispatch doesn't require the module: prefix), but bxSites/boxlang bxSites is the pair actually meant to be taught. Also simplified several plain usage examples (the homepage's live stepper demo, content-blocks.md, migrating-from-mkdocs.md's worked example, themes.md) to the plain bxSites <verb> shortcut, reserving the long form for the two places that actually explain/contrast both invocation styles (cli-reference.md, getting-started.md)
Localized the full docs site into German, Spanish, Italian and Japanese - all four locales now sit at 20/20 (100%) page coverage (bxSites i18n:status), up from 13/19 (68%). Translated the 6 pages every locale was missing (services.md, guides/images.md, guides/interactivity.md, guides/blog.md, guides/migrating-from-mkdocs.md, and the new guides/content-blocks.md), split each locale's own guides/markdown.md to match the earlier English markdown.md → content-blocks.md restructuring, retargeted every stale cross-reference, and backfilled several already-translated pages that had fallen behind current English content (search provider options, searchProvider/assets/blog config keys, i18n's versioned-and-translated-docs section, missing icon frontmatter). Found and fixed a real, cross-locale bug along the way: a ## Heading {#custom-id} syntax used to pin a translated heading's anchor to its English slug isn't actually supported by this project's markdown renderer - it was leaking as literal {#custom-id} text into rendered headings (German, and one pre-existing instance in Italian) - fixed by stripping the broken syntax and retargeting every cross-reference to the real auto-generated slug instead
Fixed tags/index.html never being built for any version or locale tree - only the main site ever got one, so every tag badge on every version/locale page linked to a tags page that didn't exist at that path. writeTagsPage() now runs once per tree (main, each version, each locale, each version×locale), the same shape search-index.json generation already uses, so each tree gets its own properly-scoped tags index instead of a 404 (or, if pointed at the main tree's own English one instead, bouncing a locale's reader into the wrong language). Also fixed tagSlug() (duplicated in TagsIndexBuilder.bx and every theme's page.bxm) stripping any non-ASCII letter from a tag before slugging it - harmless-looking for an accented tag (Configuración → configuraci-n, wrong but still unique) but silently collided every Japanese tag onto the same empty ##tag- anchor, since CJK text has no ASCII letters left after stripping. Together these two bugs accounted for 156 of the 161 broken links bxSites check reported against this repo's own localized docs
Replaced the homepage hero banner image (docs/assets/home-banner.jpg) with an updated bx-sites-branded graphic
Fixed 5 blog post cover images (docs/assets/blog/*-cover.svg) still reading "BX DOCS" as literal baked-in SVG text - the earlier bx-docs → bx-sites rebrand sweep only matched hyphenated/camelCase/PascalCase variants of the old name in text files, missing this space-separated all-caps form embedded in an image
Gave a blog post's list-card thumbnail its own blog-post-card-image class (previously unclassed) and added border-radius: 10px to it and the post detail page's own blog-post-featured-image in the bootstrap theme, matching the rounded-corner treatment .bxsites-card__image already gets elsewhere
Documented the homepage hero banner pattern (.bxsites-hero/.bxsites-hero__banner/.bxsites-hero__actions/.bxsites-hero__btn--primary/--secondary) in guides/themes.md - this site's own docs/index.md has used it from the start, but it was otherwise only discoverable by reading that page's raw Markdown source
Added a dedicated Versioning guide - docs/versions/<name>/ was previously only documented piecemeal across configuration.md, i18n.md and cli-reference.md. Covers cutting a version with version:new, how versions sort/build/switch, and what's out of scope (search is scoped per tree - each version gets its own search-index.json during a real build, and the standalone search-index/search:query verbs only ever cover the main docs/ tree; no deprecated/EOL flag or custom label). configuration.md's own Versioning section is trimmed to a short pointer at the new guide, matching how i18n's config section already does
Fixed the Quick Start/Install instructions (readme.md, docs/getting-started.md, and the "Get Your Docs Live in 10 Minutes" blog post) listing box install/install-bx-module separately for every one of bx-sites's own dependencies (bx-markdown/bx-esapi/bx-yaml/bx-image) - those are declared as box.json dependencies and install automatically, so installing bx-sites alone (via either install-bx-module bx-sites or box install bx-sites) has always been sufficient
Rebranded from bx-docs to bx-sites. Every user-facing name changed: the CLI (boxlang module:bxSites <verb>/standalone bxSites <verb>), the project config convention (bxdocs.json/.yaml → bxsites.json/.yaml, no fallback for the old name), the generated HTML/CSS class prefix (.bxdocs-* → .bxsites-* in all three built-in themes), the module's own box.json identity (name/slug/moduleName all now bx-sites/bxsites), the plugin convention (models/BxDocsPlugin.bx → models/BxSitesPlugin.bx), every BxDocs.* exception type (now BxSites.*), and this repository's own dogfooded config (bxdocs.yaml → bxsites.yaml). Breaking on every count above - an existing project needs its own config file renamed to bxsites.json/.yaml, any custom extraCss/ejected theme override targeting .bxdocs-* classes updated to .bxsites-*, and any installed plugin's models/BxDocsPlugin.bx renamed to models/BxSitesPlugin.bx
Added a ::: link-preview url="..." title="..." description="..." image="..." content block - a rich preview card for an external URL, the same card shape as ::: page-link but for a link that isn't one of this site's own pages. Only url is required (title falls back to it, description/image are optional); every field comes straight from the directive's own attributes with no build-time fetch of the target URL's metadata, matching bxSites check's own long-standing internal-links-only design (a live unfurl is exactly the kind of slow, flaky-on-a-bad-day request that has no place in a build) - see docs/guides/content-blocks.md#link-preview
Fixed PageLinkResolver.bx rendering a ::: page-link's target-page icon as raw, literally-visible text (e.g. phosphor-duotone:rocket-launch) instead of its real SVG whenever the icon was a named library reference rather than a plain emoji - it was escaping the icon string with encodeForHTML() instead of resolving it through IconResolver.bx, the same resolver every other icon (frontmatter, nav) already goes through. Invisible in the existing test suite because an emoji icon happens to produce identical output whichever path renders it
Fixed ::: file's own download link never actually getting the root-relative basePath prefix its own docs already promised ("resolved relative to docs/assets/, the same convention theme.logo/ogImage use") - on any page other than the site root, the raw src="assets/whatever.pdf" was used verbatim as the href, 404ing the moment the page wasn't the literal site root or wasn't served with an empty baseURL. DirectiveBlockProcessor.bx gained a resolveFileHrefs() pass (mirroring ResponsiveImageProcessor.bx's own asset-path resolution), run right after restore() in BuildPipeline.bx, once basePath is in scope
Split the ~700-line docs/guides/markdown.md guide: its GitBook-style ::: block family (expandable, cards, columns, stepper, file, embed, page-link, updates, includes) moved to a new docs/guides/content-blocks.md, and its manual image-captioning/framing section merged into the existing docs/guides/images.md, leaving markdown.md focused on native Markdown syntax extensions alone (admonitions, footnotes, definition lists, content tabs, code blocks, diagrams, math). Every cross-reference to a moved section across the docs was updated to match. Also fixed a pre-existing bug found along the way: the "Page link" section's own [page link](#linking-between-pages) reference was a same-page anchor pointing at a heading that only ever existed on getting-started.md, not on markdown.md itself - it never actually linked anywhere
Fixed LinkChecker.bx crashing with Count cannot be zero (a bare BoxLang right() BIF rejecting a zero count) whenever a built page linked to exactly the site's own bare baseURL path (e.g. a theme's own logo/site-title link to /bx-sites/) - bxSites check couldn't run at all against this repo's own real built site until this was fixed
docs/includes/** (and the same folder inside any version/locale tree - docs/versions/<name>/includes/**, docs/i18n/<code>/includes/**) is now the reserved convention for a ::: included partial - DocsLoader.bx excludes it from every tree's own page walk the same way it already excludes assets//versions//i18n//blog/, so a partial lives there without being built as its own standalone page or leaking into nav/search/sitemap/tags. Previously nothing excluded a partial's own file from being built as a real page unless its frontmatter set hidden: true by hand - easy to forget, and the guide's own example didn't do it. A bare ::: include src="..." (no leading .//../) now also always resolves against the current tree's own includes/, regardless of how deeply nested the including page is - the same ./foo vs. bare-specifier disambiguation Node's require() uses. Prefix src with .//../ to instead reach a page-adjacent fragment file-relatively, the old default behavior. Potentially breaking on both counts: a project with real content already living in a folder literally named includes/ will see it silently drop out of the build, and an existing ::: include src="somefile.md" with no dot prefix now looks in includes/ instead of next to the including page - see docs/guides/content-blocks.md's "Reusable content (includes)" section
Added 13 new CLI verbs: doctor (environment/config health check - JVM, docs/, config validity, required modules, theme override), post:new/page:new/version:new/i18n:new/plugin:new/theme:new (scaffold a blog post/page/docs version/locale/plugin module/ejected theme override, respectively), page:rename (move a docs page and rewrite every relative Markdown link across docs/** that pointed at the old path), i18n:status (per-locale translation coverage report), blog:drafts/blog:find (list/filter blog posts without a full build), search:query (run a keyword query against a built search-index.json, ranked with the same field weighting the client-side search widget uses), and lint (pre-build content checks on raw docs/ Markdown source: heading level skips, blog posts missing a valid frontmatter date - distinct from check, which only inspects an already-built site/) - see docs/cli-reference.md
Fixed a build failing outright when the environment's JVM has no registered WebP ImageIO writer (a bx-image/environment packaging detail, not a per-image problem) - assets.images.formats: ["webp"] now warns once and falls back to original-format-only variants for the rest of the build instead of aborting it
Fixed a crash in renderDocTree()'s check for an already-placed "Blog" nav entry - arguments.blogNavUrl was referenced inside a .filter() arrow function, whose own arguments scope shadowed the enclosing function's, throwing on every real build with both blog posts and a non-empty nav. The blog build itself still completed, but /blog/archive/<year>/ and /blog/stats/ were silently never generated, and the main /blog/ list's "Categories"/"View blog stats" links never rendered - both now work correctly
Reworked the blog author page's own "Posts" section into a compact title/date/reading-time list with a client-side Alpine.js search filter, instead of repeating every post's full card a second time
Added small icons to the blog's "Categories"/"Archive" headings, "View blog stats" link, and each post's date/reading-time on an author page. Also fixed vendorIcons.mjs's own attribute-stripping regex incorrectly stripping width/height from inner SVG shape elements (a <rect>'s own geometry, not presentational sizing) instead of just the outer <svg> tag - re-vendoring corrected 413 previously-broken Lucide icons (lucide:calendar, lucide:square, ...)
Added a bxSites stats verb - a read-only summary report on an already-built site/: page/word counts (overall and per version/locale tree), versions/locales present, blog post/category/author/year-active counts (straight off site/blog/'s own folder shape, so always drafts-excluded), distinct tag count, search index entry count/file size, and the built site's own total file count/disk size. Always exits 0 - purely informational, unlike check - see docs/cli-reference.md#stats
Vendored Mermaid too, gated behind bxsites.json's mermaid key the same way lunr.js is gated behind the local search provider - mermaid.min.js is copied into site/assets/vendor/mermaid/ at build time and every built-in theme loads it from there instead of jsDelivr, so a project with mermaid on still ships zero outbound requests for it. Only Mermaid's own elk-api.js layout-engine chunk (used by diagram types that opt into the elk layout algorithm) still resolves against a CDN - see docs/guides/themes.md#air-gapped-offline-sites
Added support for an explicit url on a nav/docs/nav.json entry ({ "title": "Blog", "url": "blog/index.html" }) - a synthetic destination that isn't a real docs/ page, so it skips the usual path-must-match-a-page rule. Lets an author place the auto-generated "Blog" nav entry anywhere instead of always last, or retitle/re-icon it; an explicit entry pointing at the blog index suppresses the auto-appended one entirely, so there's never a duplicate - see docs/guides/blog.md's "Categories, archives, pagination, and the Blog nav entry" section
Fixed a blog post's own featured-image header, its list/card thumbnail, and an author's avatar bypassing the responsive image pipeline entirely - these are generated by BlogBuilder.bx after the asset manifest is built, so they never passed through the page-body <img> rewrite. ResponsiveImageProcessor.bx gained a pictureTag() entry point for exactly this case, and BlogBuilder.bx now renders every post/author image through it, so blog images get the same <picture>/srcset/WebP treatment as any other page's images
Fixed assets.fingerprint: false being validated in config but never actually wired into AssetBundler.bx/ImageVariantGenerator.bx - it now really does drop the content-hash from bundle/variant filenames when turned off
Fixed a portrait-oriented source image producing a narrower-than-labeled variant - imageRead().scaleToFit(width) fits the longest dimension within a width x width box, so a taller-than-wide source undershot the requested width. Variant generation now passes an explicit target height computed from the source's own aspect ratio, so the output always matches its w srcset descriptor exactly
Fixed several ResponsiveImageProcessor.bx rewrite bugs: a lazy-loading data-src attribute was mistaken for src; the <img>/src match was case-sensitive (missed <IMG SRC=...>); single-quoted attributes weren't matched at all; a hand-authored srcset/sizes was left duplicated alongside the generated ones instead of replaced; the webp <source> tag was missing its own sizes attribute; and a src with a ?query or #fragment failed to resolve against the manifest
Fixed AssetBundler.bx allowing a ../-traversing extraCss/extraJs entry to read a file outside docs/, and a missing trailing semicolon between concatenated JS files silently merging two statements into one under automatic-semicolon-insertion - both now fall back to today's per-URL/per-file behavior (traversal) or insert an explicit separator (JS)
Fixed .github/workflows/pages.yml (this repo's own GitHub Pages deploy) missing the bx-image module dependency the asset pipeline requires
Fixed every YAML array (bxsites.yaml's social/nav/extraJs/etc., mkdocs.yml's nav during bxSites migrate --from=mkdocs, and any other value parsed via bx-yaml) coming back wrapped in a spurious extra array layer - a bx-yaml BoxLangConstructor bug (Array.of( list ) only has an Object... overload, so a parsed List never gets spread across it). YamlRenderer.bx now repairs the parsed tree before handing it back, so ConfigLoader.bx/MkdocsMigrator.bx/BlogAuthorsLoader.bx all see the plain struct/array shape the YAML text actually describes
First iteration of this module
Added a blog, by convention - docs/blog/posts/*.md (required date, plus authors/categories/tags/summary/image/draft/slug), an optional docs/blog/authors.yml roster (by-convention avatar lookup under docs/assets/blog/authors/<id>.*), a paginated /blog/ list, one /blog/category/<slug>/ per category, one /blog/authors/<id>/ per credited author, an RSS 2.0 /blog/feed.xml, an <!-- more -->-aware excerpt (falling back to summary, then a plain-text truncation), and og:type: article/article:published_time/article:author meta tags on every post across all three built-in themes - see docs/guides/blog.md. Posts fold into the existing tags index, search index, sitemap.xml and llms.txt unchanged, and a "Blog" nav entry appears automatically once there's at least one published post
Added automatic year archives to the blog - one paginated /blog/archive/<year>/ page per calendar year with at least one post, derived purely from each post's own date frontmatter (never from where its .md file happens to live under docs/blog/posts/, so posts can be filed into your own subfolders for easier editing at scale without affecting anything built). The main /blog/ list gets a "Browse by year" links block, with a post count per year, automatically once posts span more than one year. Also added blog.feedLimit (default 25, 0 for unlimited) to cap /blog/feed.xml to the N most-recent posts instead of every post ever written - see docs/guides/blog.md#categories-archives-pagination-and-the-blog-nav-entry and #feed
docs/blog/posts/ discovery is now recursive - a post can be filed under any subfolder scheme (docs/blog/posts/2026/, docs/blog/posts/2026/03/, ...) purely for keeping a large collection browsable in an editor, with zero effect on sort order, its year archive, or its URL - all still resolved from frontmatter alone, exactly as if the file sat flat in posts/
Every blog category now gets its own filtered RSS feed at /blog/category/<slug>/feed.xml (docs previously described this as query-string-based ?category= filtering, which was never actually implemented - fixed to match the real, path-based feed each category page already links from), and the main /blog/ list gets a "Browse by category" links block (post count per category), the same shape as the "Browse by year" archive block, once posts span more than one category
Every post's own meta line now shows an estimated reading time (word count / 200wpm) next to its date
bxSites serve now previews a draft: true blog post locally - with a visible "🚧 Draft" banner on its own page and a dashed-border card wherever it's listed - so a draft can be proofread before publishing, without it ever leaking into a real bxSites build
Added /blog/stats/ - total posts/words, average reading time, category/contributor/year counts, and linked "spotlight" cards for the longest post and most active category/author, computed purely from already-loaded posts (no tracking/analytics) - see docs/guides/blog.md#stats
bxsites.yaml (or .yml) is now the default/preferred site config format, via a new bx-yaml dependency - bxSites new scaffolds it unless --format=json is passed, bxSites migrate --from=mkdocs now writes it too, and ConfigLoader resolves whichever of bxsites.yaml/.yml/.json is present (in that order) so existing bxsites.json projects keep working with zero changes required
Added a pluggable searchProvider setting in bxsites.json - "local" (bx-sites' own static/lunr search) stays the default, "algolia" wires up Algolia DocSearch, "pagefind" wires up Pagefind (shells out to the pagefind CLI after build), and any other provider name can be wired up by a project's own theme override
Added a Cmd/Ctrl+K keyboard shortcut (with a platform-detected Ctrl K/⌘K hint) to focus/open search, alongside the existing / shortcut, for the local and pagefind providers (algolia already gets it for free from DocSearch)
Made the ::: stepper directive block's colors themeable via three new CSS custom properties (--bxsites-step-marker-bg, --bxsites-step-marker-text, --bxsites-step-line), overridable via extraCss like the rest of each built-in theme's palette - no more hardcoded stepper colors
Added a color attribute to ::: step (success/warning/danger) so an author can flag individual steps semantically, each backed by its own themeable --bxsites-step-<color>-bg/-text CSS custom properties
Extended bxSites migrate with --from=mkdocs - converts an mkdocs project (mkdocs.yml + its docs/ folder) into a complete bx-sites project in one command (mkdocs.yml → bxsites.json + docs/nav.json, pages copied through unchanged since mkdocs-material's own extended Markdown syntax already is bx-sites' native syntax, assets relocated to docs/assets/mkdocs/ with references rewritten). --from=gitbook (unchanged) stays the default
Added a bxSites check verb - a CI-grade content quality gate over an already-built site/: broken internal links/images and <img> tags with no alt attribute at all fail the check (exit 1); pages unreachable from any tree's own homepage nav are reported as orphaned, informationally, without failing the check
Added docs/guides/interactivity.md documenting Alpine.js (already bundled on every page to power the dark-mode toggle and language dropdown) as a first-class way to add reactive content - x-data/x-show/@click/etc. attributes on raw HTML in markdown, worked examples for a copy-to-clipboard button and a live client-side filter
Vendored Bootstrap's own CSS/JS, highlight.js, Alpine.js and lunr.js (resources/assets/vendor/, vendorAssets.mjs) and switched every built-in theme's layout.bxm to reference them locally instead of a CDN - a site built with the bootstrap/material theme and the default local search provider now works with zero outbound network requests, no configuration needed. math/Algolia search/Google Analytics (opt-in) and the tailwind theme's own CDN JIT compiler still require a CDN/hosted API - see docs/guides/themes.md#air-gapped-offline-sites
Versions and locales now compose one level - a docs/versions/<name>/i18n/<code>/ folder (the same by-convention rule as top-level docs/i18n/<code>/, one level down) builds site/versions/<name>/<code>/, and a version's own default-locale pages get a language switcher listing only the locales that version itself translates. Switching version always drops back to the target version's own default locale; switching locale always stays on the current version - see docs/guides/i18n.md's "Versioned and translated docs" section
Added insert/delete (spelled out in full, not abbreviated) and frame="terminal" fenced-code-block attributes, alongside the existing hl_lines/linenums/title - insert/delete mark added/removed lines with a tinted row and a +/– gutter marker; frame="terminal" swaps the plain title bar for a macOS-style terminal window. No bxsites.json config needed, in all three built-in themes - see docs/guides/markdown.md#diff-markers-and-terminal-frames
Fixed a dark-mode bug in real, pasted git diff/git show fences (tagged ```diff): highlight.js's vendored diff grammar was still using GitHub's light-mode .hljs-addition/.hljs-deletion colors with no dark counterpart, so a real diff in dark mode rendered pale, near-unreadable boxes on a near-black background. Each built-in theme now overrides both under [data-theme="dark"], reusing the same tokens as the new insert/delete markers above
Added an asset pipeline, on by default: every eligible docs/assets/** image (.png/.jpg/.jpeg) gets resized/WebP variants generated via bx-image (now a required dependency alongside bx-markdown/bx-esapi), and every matching <img> is rewritten into a responsive <picture> - no new Markdown syntax, resolved file-relative to the authoring page the same way .md page links already are. SVGs and animated GIFs are deliberately left unprocessed - see docs/guides/images.md's "What doesn't get resized" section. extraCss/extraJs get bundled into one fingerprinted file each too (assets.bundle), falling back to today's exact per-URL behavior the moment any entry is an external URL. Every generated variant/bundle is content-hash-fingerprinted (assets.fingerprint) and cached under a project's own .cache/images/ (removed by bxSites clean, same as site/), so repeated builds across versions/locales - and bxSites serve rebuilds - don't re-process an unchanged image. New bxsites.json assets/assets.images keys - see docs/configuration.md#assets and docs/guides/images.md
$
box install bx-sites