Configuration
Every site config key, what it defaults to, and what it does.
On this page
Configuration
Every project has one site config at its root - bxsites.yaml (or .yml),
the default/preferred format, or bxsites.json for a project that wants to
stay on it. Both are fully supported and produce the exact same result;
bxSites new scaffolds bxsites.yaml unless --format=json is passed (see
Getting Started). If a project
somehow has more than one, bxsites.yaml wins, then bxsites.yml, then
bxsites.json.
name: "My Docs"
description: ""
baseURL: "/"
theme:
name: bootstrap
options: {}
logo: ""
favicon: ""
search: true
searchProvider:
provider: local
algolia: { appId: "", apiKey: "", indexName: "", insights: false }
nav: []
markdown:
enableAdmonition: true
repo:
url: ""
editUri: ""
social: []
footer: false
lastUpdated: false
mermaid: false
math: false
analytics:
provider: ""
id: ""
ogImage: ""
generateOgImages: false
extraCss: []
extraJs: []
assets:
fingerprint: true
bundle: true
images: { enabled: true, widths: [400, 800, 1200, 1600], formats: [original, webp] }
plugins: []
i18n:
defaultLocale: { code: en, label: English }
locales: []
blog:
postsPerPage: 10
feed: true
The equivalent bxsites.json, for a project that prefers it:
{
"name": "My Docs",
"description": "",
"baseURL": "/",
"theme": {
"name": "bootstrap",
"options": {},
"logo": "",
"favicon": ""
},
"search": true,
"searchProvider": {
"provider": "local",
"algolia": { "appId": "", "apiKey": "", "indexName": "", "insights": false }
},
"nav": [],
"markdown": { "enableAdmonition": true },
"repo": {
"url": "",
"editUri": ""
},
"social": [],
"footer": false,
"lastUpdated": false,
"mermaid": false,
"math": false,
"analytics": {
"provider": "",
"id": ""
},
"ogImage": "",
"generateOgImages": false,
"extraCss": [],
"extraJs": [],
"assets": {
"fingerprint": true,
"bundle": true,
"images": { "enabled": true, "widths": [400, 800, 1200, 1600], "formats": ["original", "webp"] }
},
"plugins": [],
"i18n": {
"defaultLocale": { "code": "en", "label": "English" },
"locales": []
},
"blog": {
"postsPerPage": 10,
"feed": true
}
}
Only name is required - everything else falls back to the defaults shown
above. A partial theme object is merged one level deep, so
{theme: {name: material}} alone still keeps the default (empty)
options. Every key below is named/shaped identically in both formats -
the rest of this page just shows JSON snippets for brevity, but every one
of them reads the same in YAML.
name
The site name, shown in the header/brand mark and page titles. Required.
description
An optional site description, used as the fallback <meta name="description">
and og:description for any page that doesn't set its own description
frontmatter (see Getting Started).
baseURL
Controls how every internal link, asset path and nav entry is prefixed, and
doubles as the site's canonical URL for sitemap.xml and llms.txt.
- Left blank or
"/"(the default) - links stay root-relative (/page/), and neithersitemap.xmlnor an absolute-URLllms.txtis generated (there's no canonical domain to build them from). - A bare path, e.g.
"my-docs"or"/my-docs/"- the site is assumed to be served from that sub-path, and every internal link, nav entry and asset is prefixed with it (/my-docs/page/). Still nositemap.xml, since there's still no absolute domain. - A full URL, e.g.
"https://docs.example.com/"- the path portion (/here) is used the same way a bare path would be, andsitemap.xmlis written at build time with every non-hidden page's absolute URL under that domain.
llms.txt (see below) is always written; it just prefers an
absolute URL when baseURL provides one.
llms.txt
Every build writes a llms.txt to the site root - a plain Markdown index of
every non-hidden page, following the emerging
llms.txt convention for helping LLM-based tools
navigate a site without crawling its rendered HTML. There's no config key
for this; it's generated automatically, using an absolute URL per link when
baseURL is a full URL, or a basePath-relative one otherwise.
sitemap.xml
Written at the site root, but only when baseURL is a full URL (see
above) - a sitemap needs an absolute domain to be meaningful. Lists every
non-hidden page per the sitemaps.org protocol.
theme
theme.name- one of the built-in themes (bootstrap,material,tailwind), or the name of a custom theme you provide via atheme/folder at the project root (see Themes)theme.logo- path/URL to an image shown next to the site name in the header brand mark (in place of the default "âš¡" glyph) - a relative path (e.g."assets/logo.svg", resolved againstdocs/assets/) is prefixed withbaseURLlike any other internal asset; an absolute URL is used as-is. Left blank (the default), the header shows "âš¡ <site name>".theme.favicon- path/URL to a favicon, resolved the same way astheme.logo. Left blank (the default), no<link rel="icon">is rendered at all (falling back to the browser's own default behavior).theme.options- theme-specific options, read by every built-in theme:-
theme.options.colorMode-"auto"(the default),"light"or"dark". Controls which mode a first-time visitor sees before they've picked one themselves via the header's dark/light toggle -"auto"follows their OS preference,"light"/"dark"pins a fixed default. Once a visitor toggles the switch, their own choice (stored inlocalStorage) always wins on later visits, regardless of this setting.{ "theme": { "options": { "colorMode": "dark" } } } -
theme.options.navCollapsible-false(the default) renders every nav section always expanded, as today.truegives every section with children a toggle button the visitor can click to collapse/expand it - whether that section is a bare group heading (a folder with noindex.md) or links to its own page. The section containing the page you're currently on always starts open, regardless ofnavExpandAll, so navigating there never buries the very link you're on. -
theme.options.navExpandAll- only relevant whennavCollapsibleistrue.true(the default) starts every section open;falsestarts every section collapsed, except the one containing the current page.{ "theme": { "options": { "navCollapsible": true, "navExpandAll": false } } } -
theme.options.tocPosition- where a page's own "On this page" table of contents renders."top"(the default) renders it inline, at the top of the article, same as today."sticky"moves it into its own right-hand column that stays in view while the article scrolls underneath it - the same "On this page" list, just pinned, which helps on long pages. The pinned column only fits on wide viewports (it's hidden below the point a 3-column layout would get cramped); below that widthstickymode instead renders a collapsible "On this page" bar pinned to the top of the viewport while scrolling - tap to expand the list, same treatment VitePress/GitBook use on mobile - so the TOC stays reachable at every viewport width, it just changes shape depending on how much room there is.{ "theme": { "options": { "tocPosition": "sticky" } } } -
theme.options.pageMetaPosition- where the edit-this-page/download- markdown/last-updated row renders relative to a page's own content."bottom"(the default) renders it as a small footer note right before the article ends."top"renders it up near the title instead, the same place it always rendered before this option existed.{ "theme": { "options": { "pageMetaPosition": "top" } } }
-
search
true (the default) builds a static search index and wires up the search
box; false skips both entirely - no search-index.json, no search UI, no
extra JS shipped. See Search.
searchProvider
Which search UI search: true wires up:
provider-"local"(the default) is bx-sites' own static/client-side search (search-index.json+ lunr.js, see Search)."algolia"wires up Algolia DocSearch instead, and"pagefind"wires up Pagefind. Any other value is a project's own custom provider, wired up by atheme/override - see Search.algolia- required whenprovideris"algolia":appId,apiKey(the search-only public API key, not an admin key) andindexName, exactly as Algolia's own DocSearch client expects them.insights(falseby default) turns on DocSearch's click/conversion analytics.
{
"search": true,
"searchProvider": {
"provider": "algolia",
"algolia": {
"appId": "ABC123",
"apiKey": "a1b2c3d4e5f6...",
"indexName": "my-docs"
}
}
}
pagefind- both keys optional whenprovideris"pagefind":bin(default"pagefind") is the CLI executable name/path, resolved againstPATHwhen it's a bare name;optionsis an array of extra raw CLI flags passed straight through. ThepagefindCLI itself must already be installed and onPATH- BX Sites shells out to it (likegitforlastUpdated/gh-deploy), it doesn't install it for you.
{
"search": true,
"searchProvider": {
"provider": "pagefind",
"pagefind": { "bin": "pagefind", "options": [] }
}
}
nav
By default, nav is inferred from docs/'s own folder/file structure (with
order/hidden frontmatter) - fine for small sites, but a large one can
outgrow it: an explicit nav lets you title, group and order pages however
you want, independent of where their files actually live.
An empty array (the default) means "infer from folder structure". A
non-empty array replaces that inference entirely - array order becomes nav
order, and a page not referenced anywhere in it is still built, just not
linked from the nav (same as hidden: true). Each entry is either:
- a bare docs/-relative path string, e.g.
"guides/setup.md"- title comes from that page's own frontmatter/filename, same as folder-inference would give it - an object
{ "title", "path", "icon", "children" }-path,iconandchildrenare all optional; atitle-only entry with nopathis an unlinked group heading (like a folder with noindex.mdtoday), and an explicittitle/iconalways overrides the linked page's own title/icon in the nav (the page's real<h1>/<title>is untouched - only the nav label/icon changes) - see Themes: Icons for what aniconvalue can be
A title-only entry with children and no path is exactly a menu
container/section label - a non-clickable heading that just groups its
children, the same role GitBook's "MAIN COMPONENTS" plays in its own
sidebar:
{
"nav": [
"index.md",
{
"title": "Main Components",
"children": [
{ "title": "Quick Start", "path": "guides/setup.md" },
"guides/deployment.md"
]
}
]
}
Give that same group entry a path instead and it becomes a normal linked
section (its own landing page, plus children) rather than a bare label - both
shapes nest under theme.options.navCollapsible the same way (see above).
For a nav large enough that it clutters bxsites.json, move it to its own
docs/nav.json file instead - same array shape, just as the whole file's
top-level content:
[
"index.md",
{ "title": "Guides", "children": [ "guides/setup.md" ] }
]
bxsites.json's own nav, when non-empty, always wins over docs/nav.json.
Only the main tree honors either - a docs/versions/<name>/ tree always
infers its nav from its own folder structure, even when the main tree has
an explicit one.
markdown
Forwarded as-is to bx-markdown's
own module settings before each page renders. BX Sites doesn't redefine or
validate these keys; whatever you put here is bx-markdown's own option set,
straight through - so this list can drift from bx-markdown's own as it
evolves. Tables, ~~strikethrough~~, - [ ] task-list checkboxes and the
in-page table of contents are always on, with no toggle. The one exception
is enableAdmonition - bx-markdown itself defaults it to false, but BX
Docs defaults it to true (see the Markdown Extensions guide).
| Key | Default | Effect |
|---|---|---|
enableAdmonition | true (BX Sites default; bx-markdown's own default is false) | !!!/???/???+ callout blocks - see the Markdown Extensions guide |
enableFootnotes | false | [^label] footnote references - see the Markdown Extensions guide |
enableDefinitionLists | false | Term\n: Definition lists - see the Markdown Extensions guide |
autoLinkUrls | true | Auto-links bare URLs and email addresses |
anchorLinks | true | Adds a clickable anchor link to every heading |
anchorSetId | true | Stamps an id attribute onto every heading |
achorSetName (sic) | true | Stamps a name attribute onto every heading |
anchorWrapText | false | Wraps the whole heading text in the anchor link, instead of just a bare marker |
anchorClass | "anchor" | CSS class on the anchor <a> |
anchorPrefix / anchorSuffix | "" | Raw HTML injected immediately before/after the heading text |
enableYouTubeTransformer | false | Auto-embeds bare YouTube links as a player |
codeStyleHTMLOpen / codeStyleHTMLClose | "<code>" / "</code>" | Wrapper HTML around inline code spans |
fencedCodeLanguageClassPrefix | "language-" | Class prefix bx-sites's client-side syntax highlighter (and Mermaid, see below) key off of, e.g. ```js -> class="language-js" |
tableOptions.columnSpans | true | Honors colspan-style merged table cells |
tableOptions.appendMissingColumns | true | Pads a short row out to the header's column count |
tableOptions.discardExtraColumns | true | Drops extra cells in an over-long row |
tableOptions.className | "table" | CSS class on every rendered <table> |
tableOptions.headerSeparationColumnMatch | true | Requires the --- separator row to match the header's column count |
{
"markdown": {
"enableFootnotes": true,
"enableDefinitionLists": true,
"anchorLinks": false,
"enableYouTubeTransformer": true
}
}
repo
Adds a repository icon link to the header (all three built-in themes) and, when both keys are set, an "Edit this page" link on every page.
repo.url- your repo's URL, e.g."https://github.com/acme/docs". Renders the header icon link on its own; leave blank to omit it entirely.repo.editUri- the path segment between the repo URL and a page's own source path, e.g."edit/main/docs/"(GitHub's own "edit" URL convention). Combined withrepo.urland a page'sdocs/-relative source path to build its edit link - e.g. with the example above,docs/guides/setup.mdgetshttps://github.com/acme/docs/edit/main/docs/guides/setup.md. Requiresrepo.urltoo; leave blank to omit edit links while still showing the header icon.
{ "repo": { "url": "https://github.com/acme/docs", "editUri": "edit/main/docs/" } }
social
An array of social/external links rendered in the footer (see
footer - has no effect unless it's also turned on). Each entry
needs a url; icon selects from a small built-in icon set (github,
twitter/x, youtube, linkedin, facebook, bluesky, threads,
slack, patreon, rss, email, falling back to a generic link glyph
for anything else), and label sets the link's accessible name/tooltip
(defaults to icon, then "Link").
{
"social": [
{ "url": "https://twitter.com/acme", "icon": "twitter", "label": "Twitter" },
{ "url": "https://acme.com/rss.xml", "icon": "rss", "label": "RSS" }
]
}
footer
false (the default) - no footer at all. true adds one to every page:
a copyright line (© <year> <site name>), the social links (if any),
and a "Built with BX Sites" credit.
{ "footer": true }
lastUpdated
false (the default) - no last-updated date. true adds a "Last updated"
line next to the edit link (or on its own, if repo.editUri isn't set),
sourced from git log on each page's own Markdown file at build time.
Silently omitted for a page git has no history for - a fresh git init
with no commits yet, a build running from a downloaded zip with no .git
at all, or git not being installed on the build machine - rather than
breaking the build.
{ "lastUpdated": true }
analytics
Wires up pageview analytics. Currently supports Google Analytics
(gtag.js) only:
analytics.provider-"google"to enable it; left blank (the default), no analytics script is shipped at all.analytics.id- the Google Analytics measurement ID (e.g."G-ABC123"). Required whenprovideris"google".
{ "analytics": { "provider": "google", "id": "G-ABC123" } }
ogImage
Path/URL to a default social-card image, rendered as og:image (and paired
with a twitter:card of summary_large_image) on every page that doesn't
override it - resolved the same way as theme.logo (relative paths are
prefixed with baseURL, absolute URLs are used as-is). Left blank (the
default) and generateOgImages off, no og:image/twitter:card tags are
rendered.
{ "ogImage": "assets/social-card.png" }
A page's own frontmatter ogImage (see Getting Started)
always wins over this site-wide default for that one page.
generateOgImages
false (the default) - no per-page cards. true renders a real 1200x630
PNG social card for every page that doesn't already have its own
frontmatter ogImage - the page's title on the brand gradient, written to
site/assets/og/<page>.png - instead of every page sharing one generic
site-wide image. Pure java.awt/javax.imageio under the hood (part of
any JVM BoxLang runs on), so this needs no headless browser, external
service, or network access at build time.
{ "generateOgImages": true }
extraCss / extraJs
Arrays of extra stylesheet/script URLs to include on every page, appended
after the theme's own assets - each entry is resolved the same way as
theme.logo (a relative path is prefixed with baseURL; an absolute URL
is used as-is). extraJs entries are loaded with defer.
{
"extraCss": [ "assets/custom.css" ],
"extraJs": [ "assets/custom.js" ]
}
When assets.bundle is on (the default), a local extraCss/extraJs
list like the one above is bundled into one fingerprinted file each,
instead of one <link>/<script> tag per entry - see assets
below.
assets
{
"assets": {
"fingerprint": true,
"bundle": true,
"images": {
"enabled": true,
"widths": [ 400, 800, 1200, 1600 ],
"formats": [ "original", "webp" ]
}
}
}
The asset pipeline - image resizing/WebP via
bx-image (a required
dependency, installed alongside bx-markdown/bx-esapi) and CSS/JS
bundling. Everything here is on by default with reasonable settings - a
fresh bxSites new project needs to touch none of this. See
Responsive Images for the full picture, including
what deliberately isn't covered (AVIF, animated GIFs, SVGs).
assets.fingerprint-true(the default). Content-hash-names every generated image variant and CSS/JS bundle (e.g.screenshot-800w.a3f9c2e1.webp,bundle.a3f9c2e1.css) so they can be served with safe, far-future cache headers - a project's build changes the file's own name only when its content actually changes. Does not rename a project's own original files underdocs/assets/- only pipeline-generated output gets fingerprinted, so anything else that references an asset by its plain filename (a::: filedownload card, a raw markdown link) keeps working unchanged.assets.bundle-true(the default). ConcatenatesextraCss/extraJsinto one fingerprinted file each - pure BoxLang/JVM, no Node/esbuild toolchain. Falls back to today's exact per-URL<link>/<script>behavior, untouched, the moment any entry in the list is an external URL (a CDN link) or names a file that doesn't exist - see Responsive Images.assets.images.enabled-true(the default). Every eligibledocs/assets/**image (.png/.jpg/.jpeg) gets resized/WebP variants generated via bx-image, and every matching<img>gets rewritten into a<picture>withsrcset. Setfalseto fall back to plain, unprocessed image copying, exactly as before this feature existed.assets.images.widths- breakpoints to generate, in pixels. A width at or above a given image's own width is skipped automatically for that image - nothing is ever upscaled.assets.images.formats-"original"keeps the source format as the<img>fallback;"webp"adds a same-size<source type="image/webp">variant. Both on by default.
mermaid
false (the default) - no Mermaid diagram
support shipped at all. true loads mermaid.js client-side and renders
every ```mermaid fenced code block as a diagram. See
Markdown Extensions for the syntax.
{ "mermaid": true }
math
false (the default) - no KaTeX shipped at all.
true loads it client-side and typesets $...$/$$...$$ written directly
into a page's markdown. See
Markdown Extensions for the syntax.
{ "math": true }
Admonitions (note/warning/tip-style callout boxes), content tabs, and
fenced-code hl_lines/linenums/title annotations are always available
in every page's markdown, no config needed - see
Markdown Extensions.
plugins
[] (the default) - an array of BoxLang module names to activate as
plugins. Installing a plugin module (box install) never activates it on
its own; it has to be named here too. See Plugins
for how to write one.
{ "plugins": [ "myBxSitesPlugin" ] }
i18n
Metadata for the docs/i18n/<code>/ locale-folder
convention - a locale builds automatically once its folder exists;
i18n just supplies its display label/direction for the language
switcher.
i18n.defaultLocale-{ "code", "label", "flag" }for the project's own regulardocs/tree, defaulting to{ "code": "en", "label": "English" }. Only needs setting when your default locale isn't English.i18n.locales-[](the default) - an array of{ "code", "label", "dir", "flag" }for every other locale.codedoubles as thedocs/i18n/<code>/folder name and the built URL prefix - letters/digits/hyphens only (es,pt-BR,zh-Hans).diris"ltr"(the default) or"rtl".flagis an optional emoji override for the language switcher's flag icon - most common codes already resolve to a sensible flag on their own.
{
"i18n": {
"defaultLocale": { "code": "en", "label": "English" },
"locales": [
{ "code": "es", "label": "Español" },
{ "code": "ar", "label": "العربية", "dir": "rtl" }
]
}
}
See Internationalization for the full picture - untranslated-page fallback, the language switcher, and what isn't translated yet.
blog
Options for the blog - itself a by-convention feature
(docs/blog/posts/), no key here required to turn it on.
blog.postsPerPage-10(the default) - how many posts per page on/blog/, every category page, and every/blog/archive/<year>/page before it moves to.../page/2/.blog.feed-true(the default) - whether/blog/feed.xml(RSS 2.0) is written. Only meaningful with an absolutebaseURL, same requirement assitemap.xml.blog.feedLimit-25(the default) - caps/blog/feed.xmlto this many most-recent posts.0means unlimited (every post, in full). Most feed readers only care about what's new, so an unbounded feed on a blog with hundreds of posts just wastes bandwidth on every poll - see Blog: Feed.
{ "blog": { "postsPerPage": 10, "feed": true, "feedLimit": 25 } }
See Blog for post/author frontmatter, categories, featured images, and SEO/social metadata.
Versioning
Versioned docs are convention over configuration - there's no bxsites.json
key for it. Add a docs/versions/<name>/ folder and it builds automatically
as its own doc tree, with a version switcher every theme renders for free
once more than one version exists. See Versioning
for the full picture - cutting a new version with version:new, how
versions sort and build, and what's out of scope (per-tree search
scoping, no deprecated/EOL flag).