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/**.mdis the one artifact routinely edited by many/external/less-trusted contributors (a docs PR).docs/functions.bxsis the one artifact the project owner explicitly authors. Compiling every.mdfile 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/::: ifkeep 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 scopefunctions.bxsalready has. Every version/locale tree sees the identicaldata; there's no per-version or per-locale override or merge in this first version. Don't duplicatedocs/data/intodocs/versions/<name>/ordocs/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" shapedocs/blog/authors.ymlalready has. datais a reserved{{ }}name, the same waypagealready is (see Reserved names) - abxsites.yamlvariables.dataentry, if a project somehow declared one, is shadowed bydocs/data/'s own struct rather than winning.docs/functions.bxscan't declare a function nameddataeither, for the same reason.
Errors
BxSites.InvalidDataFile- adocs/data/*.yaml/.yml/.jsonfile failed to parse (a YAML/JSON syntax error), naming the offending file.BxSites.UnknownVariable- a{{ data.x.y }}(or a::: for/::: ifpath) doesn't resolve against what's actually indocs/data/.BxSites.InvalidForTarget- a::: for's own path resolved to something that's neither an array nor a struct (can't be looped).