Content Blocks

On this page

Content Blocks

On top of everything in Markdown Extensions, BxSites supports a family of rich content blocks for things plain CommonMark has no concept of - cards, tabs of steps, downloads, embeds, and more. Every one uses the same ::: name ... ::: container syntax (a bare ::: on its own line closes whichever block is currently open, or write the closing ::: right on the same line for a block with no body of its own - ::: file src="assets/spec.pdf" ::: works exactly the same as the two-line form) - no bxsites.yaml config needed, always available. A block can nest inside another (an expandable containing a cards group, for instance) - each is scanned again for further blocks inside its own content. Migrating from GitBook? Every block here maps directly onto its GitBook counterpart of the same name - see Migrating from GitBook.

Expandable

A plain collapsible section - no callout icon/color, unlike a collapsible admonition (???, see Admonitions):

::: expandable "Is this different from a collapsible admonition?"
Yes - this has no type/icon/color, just a plain expand/collapse section.
Add `open="true"` to start it expanded.
:::
Is this different from a collapsible admonition?

Yes - this has no type/icon/color, just a plain expand/collapse section. Add open="true" to start it expanded.

Cards

A grid of link cards, each its own ::: card inside a ::: cards wrapper - title, icon, image and href are all optional (a card with no href renders as a plain, non-clickable card). icon is resolved the same way frontmatter/nav icon values are - a plain emoji, or a named icon from a bundled library (icon="phosphor-duotone:rocket-launch", icon="lucide:rocket", ...) - see Icons:

::: cards
::: card title="Getting Started" icon="phosphor-duotone:rocket-launch" href="../getting-started.md"
Install, scaffold and build your first site.
:::
::: card title="Themes" icon="phosphor-duotone:palette" href="themes.md"
Customize a built-in theme or write your own.
:::
:::

Columns

A side-by-side layout - ::: column accepts an optional width (a plain CSS length/percentage, e.g. "40%"); columns with no explicit width share the row equally:

::: columns
::: column width="60%"
The wider column.
:::
::: column
The narrower one.
:::
:::

The wider column.

The narrower one.

Stepper

A numbered, connected sequence of steps:

::: stepper
::: step "Install"
`install-bx-module bx-sites`
:::
::: step "Scaffold"
`bxSites new`
:::
:::
1
Install

install-bx-module bx-sites

2
Scaffold

bxSites new

A step's own optional color attribute flags its marker with one of four semantic colors - the default (no color), success, warning or danger - independent of the step's position in the sequence:

::: stepper
::: step "Back up your data" color="success"
Routine, safe to run any time.
:::
::: step "Optional: enable telemetry" color="warning"
Skip this one if you're not sure.
:::
::: step "Delete the old install" color="danger"
Irreversible - make sure the backup above finished first.
:::
:::
1
Back up your data

Routine, safe to run any time.

2
Optional: enable telemetry

Skip this one if you're not sure.

3
Delete the old install

Irreversible - make sure the backup above finished first.

The numbered marker, connecting line, and each of the three color palettes above are themeable independently of the rest of the site's palette, via CSS custom properties - see Customizing colors.

A responsive grid of images. The optional columns attribute is 2, 3 or 4 (default 3); any other value falls back to 3. The grid itself needs no configuration, but the click-to-enlarge lightbox needs bxsites.yaml's imageGallery set to true - unset, the grid still renders, just without the lightbox's JS. There are two ways to build one:

List each image explicitly with ::: image children - src is required, alt and caption are both optional:

::: image-gallery columns="3"
::: image src="../assets/favicon.png" alt="BoxLang icon" caption="Icon"
:::
::: image src="../assets/home-banner.jpg" alt="Home banner"
:::
::: image src="../assets/og-image.png" caption="Social preview"
:::
:::

Or let bxSites discover the images for you by convention: drop files named {name}-{order}.{jpg,jpeg,png,webp,gif} into docs/assets/gallery/{name}/ and reference them by name alone - no children needed. Files are sorted numerically by {order}:

::: image-gallery name="showcase" columns="3"
:::

File

A download card for a PDF, video, or any other project asset - src is resolved the same way theme.logo/frontmatter ogImage already are (relative to docs/assets/):

::: file src="assets/spec.pdf" title="API Specification" :::
Site Preview Image

Page break

Forces a page break wherever it's placed - handy for printing the built site, or exporting it to PDF (e.g. via a browser's own "Print to PDF"), and want a section to always start on a fresh page. No attributes:

::: pagebreak :::

It renders as a plain, subtle divider on screen (so it's still visible while editing or previewing), and only forces an actual page break inside print/PDF output - it has no other effect on normal browsing.

Buttons

A call-to-action button - ::: button on its own, or several laid out in a row inside a ::: buttons wrapper. The leading "Label" and href are the only pieces most buttons need:

::: button "Get Started" href="../getting-started.md" style="primary" :::
Get Started

A few optional attributes give each button its own abilities:

  • style="primary" or style="secondary" (the default) - solid-accent vs. outline.
  • size="small", "medium" (the default) or "large".
  • icon="..." - resolved the same way a card's own icon is (a plain emoji, or a named icon like icon="phosphor-duotone:rocket-launch" - see Themes: Icons).
  • target="_blank" - opens the link in a new tab instead of the same one (rel="noopener noreferrer" is added automatically).
  • disabled="true" - renders an inert, unclickable button (no href needed) for a "coming soon" call to action.
::: buttons
::: button "Read the docs" href="../getting-started.md" icon="phosphor-duotone:book-open" size="large" :::
::: button "Star on GitHub" href="https://github.com/ortus-boxlang/bx-sites" style="secondary" target="_blank" :::
::: button "Coming soon" disabled="true" :::
:::

Embed

A responsive iframe embed for a recognized provider - currently YouTube, Vimeo, CodePen, Spotify, Loom and Figma. A URL from anywhere else falls back to a plain "visit ↗" link card instead of an iframe that would just refuse to render (most sites block being framed):

::: embed url="https://www.youtube.com/watch?v=dQw4w9WgXcQ" title="A demo" :::
A demo

A rich preview card linking to another page - href follows the same file-relative convention as an ordinary page link. Unlike a card, its title/icon/summary are pulled automatically from the target page's own frontmatter, so it stays in sync if that page is renamed or its summary changes:

::: page-link href="../getting-started.md" :::
Getting StartedInstall the module, scaffold a project, and build your first site.

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, so there's no page to pull a title/summary from automatically. Every field comes from the directive's own attributes: only url is required, title falls back to the bare URL when omitted, and description/image are both optional. There's no build-time fetch of the target URL to auto-fill these

  • the same reasoning that keeps audit internal-links-only applies here too, so a slow or unreachable third-party site never affects build time:
::: link-preview url="https://boxlang.io" title="BoxLang" description="A dynamic, multi-paradigm JVM language." :::

AI Prompt

A styled container for a reusable AI prompt. The block's body is the prompt text, written as ordinary Markdown (so headings, lists, and code inside it still get their own formatting); every prompt gets a "Copy" button that copies that exact source text, formatting markup included, ready to paste into whatever AI tool you're using it with. description (an optional one-line summary) and icon (resolved the same way ::: card's own icon is - defaults to a sparkle glyph when omitted) are both optional:

::: prompt description="Summarizes a pull request for a changelog entry" icon="phosphor-duotone:git-pull-request"
Summarize the following pull request diff as a single changelog entry,
written for an end user rather than a developer. Group related changes
together and skip anything purely internal (refactors, tests, CI).
:::
PromptSummarizes a pull request for a changelog entry

Summarize the following pull request diff as a single changelog entry, written for an end user rather than a developer. Group related changes together and skip anything purely internal (refactors, tests, CI).

Add expanded="preview" to clamp a long prompt to a short, fade-out preview until the reader clicks "Show more", or expanded="hidden" to start it fully collapsed behind a "Show prompt" button - handy for a page that lists several prompts back to back. Omit expanded (or set it to "full", the default) to always show the whole prompt:

::: prompt description="A longer, multi-step prompt" expanded="preview"
1. Read the attached error log line by line.
2. For each stack trace, identify the failing module.
3. Group failures by root cause, not by timestamp.
4. Propose one fix per root cause, not per failure.
5. Skip anything that already has an open issue - list those separately.
:::
PromptA longer, multi-step prompt
  1. Read the attached error log line by line.
  2. For each stack trace, identify the failing module.
  3. Group failures by root cause, not by timestamp.
  4. Propose one fix per root cause, not per failure.
  5. Skip anything that already has an open issue - list those separately.

There's no "Open in AI providers" menu here - bx-sites never talks to a third-party AI provider, so a prompt's own "Copy" button is the one way to get it into whatever tool you're using.

Updates (changelog)

A dated, taggable changelog list - ::: update accepts date="YYYY-MM-DD" and an optional comma-separated tags:

::: updates
::: update date="2026-01-15" tags="feature,fix"
Added dark mode and fixed a footer alignment bug.
:::
::: update date="2026-01-01"
Initial release.
:::
:::
featurefix

Added dark mode and fixed a footer alignment bug.

Initial release.

A page with an ::: updates block also gets its own feed.xml (RSS 2.0) written alongside it once bxsites.yaml's baseURL is a full URL - same requirement as sitemap.xml - so readers can subscribe to just that page's changelog.

Reusable content (includes)

::: include src="..." splices another file's raw Markdown in at that point. Unlike every block above, this becomes real page content (headings, paragraphs, its own nested blocks), not something wrapped in a widget - useful for a warning/notice repeated across several pages. Put the partial itself under docs/includes/ - the same reserved-folder convention as assets//versions//i18n//blog/. A file under includes/ is never built as its own page and never appears in nav/search/sitemap/tags - it only exists to be spliced into other pages:

docs/
├── index.md
├── includes/
│   ├── beta-notice.md
│   └── legal/
│       └── terms.md
└── guides/
    └── deep/
        └── setup.md

A bare src (no leading ./ or ../) always resolves against the current tree's own docs/includes/, no matter how deeply nested the including page is - guides/deep/setup.md above reaches the same file index.md does, both with the exact same src:

::: include src="beta-notice.md"

A bare src can also point into a subfolder of includes/ itself:

::: include src="legal/terms.md"

Prefix src with ./ or ../ instead to reach a page-adjacent fragment that isn't meant to live in the centralized includes/ folder - that form resolves file-relative to the including page's own directory, the same convention as an ordinary page link:

::: include src="../local-note.md"

A version/locale tree gets its own includes/ the same way - a page under docs/versions/2.0/ resolves a bare src against docs/versions/2.0/includes/, and one under docs/i18n/es/ against docs/i18n/es/includes/ - each tree's partials are its own, not shared with the main tree's docs/includes/.

An included file can itself include another (a circular chain throws BxSites.CircularInclude at build time rather than looping forever).

Conditional content

Shows one of several variants of a block based on a reader's own choice - "Free" vs. "Pro" instructions on the same page, say. This is a fully static site with no visitor identity of any kind, so unlike a platform with a real backend, there's no server-evaluated "who is this reader" - the reader picks for themself, and their choice is simply remembered in their own browser (localStorage) for every later page too:

::: audience-switcher key="plan" options="free:Free,pro:Pro" :::

::: conditional key="plan" value="free"
The Free plan includes basic search.
:::

::: conditional key="plan" value="pro"
The Pro plan adds AI-assisted search and unlimited team seats.
:::

The Free plan includes basic search.

The Pro plan adds AI-assisted search and unlimited team seats.

::: conditional key="..." value="..." marks one variant; key is whatever preference name you're switching on ("plan" above, but it could just as well be "os", "language", anything), and value is the one setting this particular block should show for. Every variant always renders in the HTML - hidden client-side, never omitted - so a reader with JavaScript disabled (or a search crawler) still sees every variant rather than none.

::: audience-switcher key="..." options="value:Label,value:Label,..." is an optional, ready-made control - one button per option, switching every ::: conditional block sharing that same key immediately, anywhere on the page. You don't need it at all: a link ending in ?plan=pro sets the same preference automatically on load (handy for sharing a direct link to "the Pro version of this page"), and a project's own theme override can call window.bxSitesSetPreference( key, value ) directly to drive it from custom UI instead.

Contact forms

Premium feature. A ::: contact-form block always renders as a real, complete form - inputs, labels, a submit button, all of it - but submitting it only works once the project's own bxSites Cloud plan includes working forms. On a plan that doesn't, a reader who submits anyway sees a friendly "upgrade to enable this form" message instead of their message going anywhere. There's nothing to configure here to turn that on or off - it's entirely a property of the account's plan.

A labeled contact/lead-gen form, submitted with fetch() to bxSites Cloud rather than a page reload:

::: contact-form id="demo-request" to="sales@acme.com" fields="name:text,email*:email,message:textarea" submitLabel="Send" :::
  • id - this form's own slug. Matches a form configuration (who it notifies, spam filtering, and so on) that you set up in bxSites Cloud, not in this markdown - this build never checks that the id you write here actually exists there, it's just passed straight through. Defaults to "contact" when omitted.
  • to - optional and purely informational (real delivery routing is configured server-side, by the account admin) - handy as a reminder of where a given form's submissions end up when you're skimming the page source later.
  • fields - required; a small DSL, comma-separated name:type pairs: add a trailing * right after a field's own name (before its :) to mark it required, e.g. email*:email. Supported types are text, email, and textarea; any other/unrecognized type falls back to a plain text input rather than failing the build. Each field's own label is derived from its name (full-name becomes "Full Name").
  • submitLabel - the submit button's own text; defaults to "Send".

Every form also carries a hidden honeypot field a real visitor never sees or fills in - bxSites Cloud's own spam filtering uses it, no configuration needed here.

Cloud content

Premium feature. ::: cloud bakes a bxSites Cloud content block into the page at build time - it requires a bxSites Cloud account with Content Blocks on your plan, cloud.apiUrl/ cloud.contentBlocks: true set in bxsites.yaml, and a BXSITES_CLOUD_TOKEN environment variable holding a valid API token (never written to bxsites.yaml itself - same convention publish already uses for its own token). Without cloud.contentBlocks: true, a ::: cloud block always fails the build outright.

slug is the content block's own slug (set when it was created in bxSites Cloud's portal, or via its headless API) - it can itself contain / for a hierarchical slug like press/release1:

::: cloud slug="press/release1" :::

At build time, this fetches that block's current published content, renders it through this site's own Markdown pipeline exactly like any other page (so this project's own plugins/syntax highlighting/nested content blocks all apply), sanitizes the resulting HTML, and bakes it straight into the page - instant paint, fully readable with no JavaScript, real indexable text for search engines. A draft or expired block is treated the same as a missing one.

What happens when that fetch fails (unknown slug, not published, or a request failure) is configurable per project via cloud.contentBlocksOnError in bxsites.yaml:

  • "fail" (the default) - fails the whole build, the same posture as a broken internal link.
  • "skip" - logs a build warning and renders an empty placeholder instead.

Either way, once the page is live, a small client-side script (shipped only when cloud.contentBlocks: true - the same explicit opt-in mermaid/openapi already use) checks bxSites Cloud again on page load and swaps in the latest content if it's changed since the build - so a content edit shows up for visitors without waiting for the next deploy. That check is a plain fetch(), so it honors the delivery endpoint's own Cache-Control: private, max-age=60 header automatically - a browser that's already checked within the last minute skips the network call entirely on its next page view. A first-time visitor (or one past that window) always gets the current version; an edit is visible to everyone within about a minute, not on every single page view.

Comments

Premium feature. ::: comments mounts a live, moderated visitor comment thread on the page - it requires a bxSites Cloud account with Comments on your plan and cloud.comments: true set in bxsites.yaml. Without it, a ::: comments block always fails the build outright, the same posture as ::: cloud without cloud.contentBlocks.

::: comments :::

No attributes are required - the thread id defaults to the page's own URL path, so every page gets its own comment thread for free. An optional id="..." lets two blocks on one page (or blocks on different pages) share a single thread instead:

::: comments id="general-feedback" :::

Unlike ::: cloud, nothing is fetched or baked in at build time - a comment thread keeps growing after every deploy, so a build-time snapshot would go stale the moment a new comment arrives. The block renders an empty placeholder; a small client-side script (shipped only when cloud.comments: true, the same explicit opt-in cloud.contentBlocks already uses) loads the current, already-moderated comments every time the page loads and paginates through older ones on request, then renders a form for posting a new one. A newly-submitted comment shows locally, in that one visitor's own browser, labeled as pending moderation - it never appears for anyone else until a moderator approves it.

That form also carries a hidden honeypot field a real visitor never sees or fills in - bxSites Cloud's own spam filtering uses it, no configuration needed here.

Loop and conditional (data-driven)

::: for and ::: if render their own content against reusable data - a docs/data/*.yaml/.json file's own value, addressed by dotted path. Unlike every block above, these two take a bare expression instead of key="value" attributes - deliberately narrow, the same dotted-path-only philosophy {{ }} itself already uses (no comparison operators in this first version):

::: for member, idx in data.team
{{ idx }}. **{{ member.name }}** - {{ member.role }}
:::
  1. Luis Majano - CEO
  1. Jon Clausen - CTO

::: for <item>, <index> in <dotted.path> binds <item>/<index> the same way BoxLang's own two-variable for loop does for whatever the path resolves to - item + 1-based index for an array (as above), or key + value for a struct, the identical syntax either way:

::: for name, enabled in data.flags
- {{ name }}: {{ enabled }}
:::
  • betaBanner: true
  • darkModeDefault: false

::: if <dotted.path> renders its content only when the resolved value is truthy - an empty array/struct/string, 0 and false all count as falsy:

::: if data.flags.betaBanner
Beta features are enabled on this build.
:::

Beta features are enabled on this build.

Chain ::: elseif <dotted.path> (any number of them) and a trailing bare ::: else after a ::: if for real if/elseif/else semantics - the first truthy condition wins, ::: else (no condition of its own) catches whatever's left, and a condition after the winning one is never even resolved, so a typo'd ::: elseif path only breaks the build once its own branch is actually reached. The whole chain closes with one trailing ::: - ::: elseif/::: else themselves mark where the previous branch ends, so there's no ::: needed before each of them:

::: if data.flags.darkModeDefault
Dark mode is on by default.
::: elseif data.flags.betaBanner
Beta features are enabled, though dark mode isn't on by default.
::: else
Nothing special about this build.
:::

Beta features are enabled, though dark mode isn't on by default.

A ::: before an ::: elseif/::: else still works too, if you'd rather close each branch explicitly - both forms parse identically.

Both bodies can contain ordinary Markdown and even other content blocks - including another ::: for/::: if, nested exactly like any block above. See Data Files: Consuming data for the full loop/conditional story, including the two other ways to work with data.* - a theme override, or a magic function.

Course index

::: course id="..." ::: renders a whole course's lessons as one numbered, linked index - a big "1. Introduction, 2. Windows Installation, 3. Mac Installation..." list, each number a real link, built from a docs/data/courses.yaml manifest rather than hand-authored:

::: course id="getting-started" :::

Unlike every block above, this one takes only a bare id, no href/body content of its own - the lessons themselves, and their order, come entirely from the manifest. See Courses for the manifest format, scoped lesson-to-lesson navigation, and how reader progress gets tracked once the index is on the page.

Edit this page Download Markdown Last updated Oct 7, 2026, 10:34:03 AM