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
Â
Static documentation site generator for BoxLang, built on bx-markdown
- in the spirit of mkdocs: write
Markdown in docs/ (or src/, if that reads
better for your project), get a themed, searchable static site out.
Already on GitBook? bxSites migrate --source=... converts
an export straight into a bx-sites project - see Migrating from GitBook.
# 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 a GitBook export (SUMMARY.md +
.md files) into docs/ + nav.json
|
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 |
theme:new
| Eject a built-in theme into project theme/
for customizing |
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 |
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 three built-in themes side by side
(bootstrap at the root,
material/tailwind under
theme/material//theme/tailwind/) via
buildMultiTheme.sh - use the switcher next to the
dark-mode toggle to flip between them on any page.
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)resources/themes - built-in themes (native BoxLang
.bxm templates + assets): bootstrap
(default), material, tailwind - 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) and the copy-code
button (copy-code.js)docs / 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).
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.
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-pagesboxlang 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)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 insteadtags/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 docsdocs/assets/home-banner.jpg) with an updated bx-sites-branded graphicdocs/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 imageblog-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.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 sourcedocs/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 doesreadme.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 sufficientboxlang 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::: 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-previewPageLinkResolver.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::: 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 scopedocs/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 anywhereLinkChecker.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 fixeddocs/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)" sectiondoctor (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.mdImageIO 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 itrenderDocTree()'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 correctlyvendorIcons.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, ...)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#statsbxsites.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-sitesurl 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" sectionBlogBuilder.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 imagesassets.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 offimageRead().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 exactlyResponsiveImageProcessor.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 manifestAssetBundler.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).github/workflows/pages.yml (this repo's own GitHub Pages deploy) missing the bx-image module dependency the asset pipeline requiresbxsites.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 describesdocs/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/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 #feeddocs/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//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 categorybxSites 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/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#statsbxsites.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 requiredsearchProvider 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 overrideCtrl 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)::: 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 colorscolor 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 propertiesbxSites 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 defaultbxSites 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 checkdocs/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 filterresources/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-sitesdocs/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" sectioninsert/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-framesgit 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 abovedocs/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