Data Files

On this page

Data Files

Reusable variables are great for a flat, one-off fact (company, supportEmail) but awkward for anything with real shape - a team roster, a pricing table, a feature matrix. Data files fill that gap: drop a docs/data/*.yaml/.yml/ .json file in your project, and its whole content - any shape you like, an object or an array - becomes reachable as data.<file> from every page, the same {{ }} syntax variables/page already use.

The convention

Add a docs/data/ folder. Each file's basename (extension stripped) becomes one top-level key under data:

docs/
├── index.md
└── data/
    ├── team.yaml
    └── pricing.json
- name: Luis Majano
  role: CEO
- name: Jon Clausen
  role: CTO
{
	"free": { "price": 0, "seats": 3 },
	"pro": { "price": 29, "seats": 20 }
}

data.team is now that array, data.pricing.pro.price that nested number - a file's parsed root is used exactly as parsed, object or array alike, no fixed shape to conform to. No docs/data/ folder at all simply means no data - the same opt-in-by-presence shape docs/functions.bxs/ docs/blog/authors.yml already use.

Reference any of it in ordinary Markdown, by dotted path:

The Pro plan is **${{ data.pricing.pro.price }}/mo** for up to
{{ data.pricing.pro.seats }} seats.

builds to:

<p>The Pro plan is <strong>$29/mo</strong> for up to 20 seats.</p>

If more than one file shares a basename across extensions (both products.yaml and products.json present), .yaml wins, then .yml, then .json - pick one format per basename rather than relying on that order in practice.

Consuming data

A scalar {{ data.x.y }} reference works anywhere {{ }} already does, but real content - a team grid, a pricing table - usually means looping over data.*. There are three ways to do that, depending on where the loop belongs:

In a theme override

Once a project has a theme/ override (see Themes), data is bound bare into layout.bxm/page.bxm the same way page/siteConfig already are - no {{ }}, just real BoxLang:

<ul class="footer-sponsors">
<bx:loop array="#data.sponsors#" index="sponsor">
	<li>#encodeForHTML( sponsor )#</li>
</bx:loop>
</ul>

This is the natural home for data that belongs on every page (a footer sponsor list, a site-wide nav badge) rather than one specific page's content.

From a magic function

A magic function can read data bare too (it's one of the same "supporting variables" page/ siteConfig/etc. already are), and loop/branch over it with real BoxLang, returning a Markdown/HTML fragment:

function $team() {
	var html = ""
	for ( item, idx in data.team ) {
		html &= "- **" & encodeForHTML( item.name ) & "** - " & encodeForHTML( item.role ) & char( 10 )
	}
	return html
}
## Our team

{{ $team() }}

This renders server-side, at build time - visible to a search crawler with no JavaScript needed, unlike the Alpine recipe below.

Directly in Markdown, with ::: for/::: if

For a loop or a simple truthy check that doesn't need a magic function at all, ::: for/::: if work straight from Markdown:

::: for member, idx in data.team
{{ idx }}. **{{ member.name }}** - {{ member.role }}
:::

::: for <item>, <index> in <dotted.path> binds <item>/<index> using BoxLang's own native two-variable for loop semantics for whatever <dotted.path> resolves to - item + 1-based index for an array, or key + value for a struct, the identical syntax either way (no array-vs-struct branching to write yourself):

::: for name, enabled in data.flags
- {{ name }}: {{ enabled }}
:::

::: if <dotted.path> renders its own 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.
:::

Chain ::: elseif <dotted.path> (any number) and a trailing bare ::: else right after a ::: if for real if/elseif/else semantics - the first truthy condition wins, ::: else catches whatever's left, and a later branch's own condition is never even resolved until its own turn comes. One trailing ::: closes the whole chain - ::: elseif/::: else mark where the previous branch ends, no ::: needed before each of them (though it still works if you'd rather write it that way):

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

Both bodies can contain ordinary Markdown and even other content blocks, including a nested ::: for/::: if. Deliberately narrow grammar, matching {{ }} itself - a dotted path only, no comparison operators (==, &&, ...) in this first version. A real comparison need routes to a magic function instead (above), which already has full BoxLang at its disposal.

In Alpine, client-side (x-data)

Interactivity already covers dropping raw x-data/x-for HTML into Markdown; feeding it from data.* instead of a hand-typed JS array just needs data.* turned into a safe HTML attribute value. jsonSerialize() alone isn't enough - the result still needs HTML-attribute encoding to sit safely inside a "..."-quoted attribute (the same two-step recipe ColdBox's own attribute()/forAttribute() helper uses) - so define a one-line helper once, in your own functions.bxs:

function $jsonAttr( required any value ) {
	return encodeForHtmlAttribute( jsonSerialize( arguments.value ) )
}

encodeForHtmlAttribute() comes from bx-esapi, already a dependency of every bx-sites project - no new dependency, just this one recipe. Then, in Markdown:

<div x-data="{ team: {{ $jsonAttr(data.team) }} }">
  <template x-for="member in team" :key="member.name">
    <li x-text="member.name + ' - ' + member.role"></li>
  </template>
</div>

Plain double quotes work safely around x-data - encodeForHtmlAttribute() already handles the conflict, no single-quote workaround needed. This is the one path that renders client-side only (nothing for a JS-disabled reader or a search crawler) - reach for a magic function or ::: for instead when the content should be visible without JavaScript.

Why data files, not BoxLang templates in Markdown?

A related, bigger question came up while designing this: why not let Markdown itself become a real BoxLang template (loops, conditionals, arbitrary logic), instead of adding a narrow ::: for/::: if and leaning on magic functions for anything more? Two reasons:

  • Trust boundary. docs/**.md is the one artifact routinely edited by many/external/less-trusted contributors (a docs PR). docs/functions.bxs is the one artifact the project owner explicitly authors. Compiling every .md file as a real BoxLang template would collapse that boundary - any contributor able to open a docs PR would gain arbitrary BoxLang execution (file I/O, environment access) rather than just Markdown text.
  • Failure mode. An unmatched {{ }} today is left as literal text - a typo never breaks a build. A BoxLang template compile error is a hard failure. ::: for/::: if keep that same forgiving shape (an unresolvable path throws a clear, typo-catching error - see Errors - rather than silently miscompiling).

Data files close the actual gap (structured content, and loops/ conditionals over it) without either tradeoff: Markdown itself stays inert-until-{{ }}-substituted, and functions.bxs remains the one explicitly-trusted escape hatch into real BoxLang logic.

Scope

  • docs/data/ is project-wide, loaded once - the same single-load scope functions.bxs already has. Every version/locale tree sees the identical data; there's no per-version or per-locale override or merge in this first version. Don't duplicate docs/data/ into docs/versions/<name>/ or docs/i18n/<code>/ - it isn't read from there.
  • Flat directory only - no subfolder recursion into docs/data/ in this first version, the same "exactly one file" shape docs/blog/authors.yml already has.
  • data is a reserved {{ }} name, the same way page already is (see Reserved names) - a bxsites.yaml variables.data entry, if a project somehow declared one, is shadowed by docs/data/'s own struct rather than winning. docs/functions.bxs can't declare a function named data either, for the same reason.

Errors

  • BxSites.InvalidDataFile - a docs/data/*.yaml/.yml/.json file failed to parse (a YAML/JSON syntax error), naming the offending file.
  • BxSites.UnknownVariable - a {{ data.x.y }} (or a ::: for/::: if path) doesn't resolve against what's actually in docs/data/.
  • BxSites.InvalidForTarget - a ::: for's own path resolved to something that's neither an array nor a struct (can't be looped).
Edit this page Download Markdown Last updated Sep 11, 2026, 7:11:15 PM