Data sources let a name's content be produced by a script at request time,
instead of being hard-coded in YAML. They are declared with the $ prefix and
behave just like a content definition once the script has run: the JSON
returned by the script becomes the value of that name and flows through the
normal rendering pipeline (including any matching ^name format).
This is what powers the built-in $navlinks data source, which scans the
vhost directory and produces the nav bar from whatever pages it finds.
$mydata:
script: javascript
code: |
print(JSON.stringify([
{key: "/", value: "Home"},
{key: "/about", value: "About"}
]));
After this runs, the name mydata has the content
{"/": "Home", "/about": "About"} — identical to writing it out by hand:
mydata:
"/": Home
"/about": About
So any format that consumes mydata (such as ^links, ^ulist, or a custom
^mydata) sees the script's output as if it were YAML.
| Field | Description |
|---|---|
script |
Language: javascript (alias js, node), python, php, or sh/bash |
code |
Inline script source |
file |
Path to a script file, relative to the document root (alternative to code) |
The script's stdout must be a JSON document. Objects become OrderedMaps (insertion order is preserved), arrays become lists.
^name)| Aspect | Format script (^name) |
Data source ($name) |
|---|---|---|
| Purpose | Render a value as HTML | Produce content for a name |
| Output | HTML fragment | JSON |
| Called | Once per record (iterated) | Once total |
Sees record |
Yes, per iteration | No |
A common pattern is to pair them: a $name data source produces structured
data, and a ^name format renders it.
JavaScript runs in an embedded goja interpreter — no Node.js process is forked. Available host builtins:
| Helper | Description |
|---|---|
env |
Object of CGI environment variables: env.REQUEST_URI, env.DOCUMENT_ROOT, etc. |
print(...args) |
Append a space-joined line to captured output |
listdir(path) |
List entries in a directory (must be under DOCUMENT_ROOT) |
readFile(path) |
Read an entire file as a string |
readFileHead(path, n) |
Read the first n bytes of a file |
joinPath(a, b, ...) |
Path concatenation (like path.join) |
splitExt(name) |
[basename, ext] split |
File access is restricted to paths under the vhost's DOCUMENT_ROOT,
plus up to parent-levels directories above it — the same scope used by
YAML name resolution. Traversal beyond that ceiling via .. or absolute
paths is rejected.
For these languages bserver forks the system interpreter and runs your
code with the standard CGI-style environment variables set (see
Server-Side Scripts for the full list). Write JSON to stdout.
$navlinksThe default www/navlinks.yaml auto-discovers nav items by scanning the
vhost root for pages:
$navlinks:
script: javascript
code: |
var docroot = env.DOCUMENT_ROOT || '.';
var results = [{key: "/", value: "Home"}];
var files = listdir(docroot);
files.sort();
for (var i = 0; i < files.length; i++) {
var f = files[i];
var parts = splitExt(f);
var name = parts[0], ext = parts[1];
if (name.charAt(0) === '.' || name.charAt(0) === '_' || name === 'index') continue;
var title = name.replace(/[-_]/g, ' ').replace(/\b\w/g, function(c) { return c.toUpperCase(); });
if (ext === '.md') {
results.push({key: "/" + name, value: title});
} else if (ext === '.yaml') {
try {
var head = readFileHead(joinPath(docroot, f), 500);
if (/^main:/m.test(head)) {
results.push({key: "/" + name, value: title});
}
} catch (e) {}
}
}
print(JSON.stringify(results));
Every .md file and every .yaml file that defines main: shows up
automatically in the navbar. Drop in pricing.md and a "Pricing" link
appears with no other changes.
To replace this with a hand-written list, just define navlinks: (plain
content) in your site — your definition wins because page-level content is
loaded before parent definitions:
navlinks:
"/": Home
"/about": About
"/contact": Contact
requestDir), so
relative file paths resolve from there.^name format-script counterpart$name with a ^name to render it$navlinks and ^navlinks working together