BoxLang 🚀 A New JVM Dynamic Language Learn More...

BX Sites

v1.0.0-snapshot BoxLang Modules

⚡︎ BX Sites

|:------------------------------------------------------: |
| ⚡︎ 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.


Quick Start

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

Usage

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.

Documentation

This repository documents itself with BX Sites - see bxsites.yaml and docs/ at the repo root, published at:

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:

See MODULE_SPEC.md for the design spec driving this module's development.

Directory Structure

  • .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 convention
  • box.json - package metadata used to publish to ForgeBox
  • ModuleConfig.bx - this module's configuration/CLI entry point

Local Building

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 No1.0.0 The version of the module.
branch Nodevelopment The branch being built.
buildId NoUUID (generated)A unique identifier for the build.

Running Tests

  1. With CommandBox installed, install TestBox: box install
  2. Register the module so BoxLang can resolve its 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)")"
    
  3. With the BoxLang CLI installed, run tests using ./testbox/run

Version Management

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.

GitHub Actions Automation

  • pr.yml - runs the test suite against every pull request
  • tests.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 release
  • release.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 secret
  • pages.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

Ortus Sponsors

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

THE DAILY BREAD

"I am the way, and the truth, and the life; no one comes to the Father, but by me (JESUS)" Jn 14:1-12

Changelog

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.


[Unreleased]

  • 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

No collaborators yet.
     
  • {{ getFullDate("2026-08-22T23:05:07Z") }}
  • {{ getFullDate("2026-08-23T02:21:25Z") }}
  • 10
  • 0