Data-Driven Sections

A data-driven section is a section whose content comes from data — records in YAML or JSON, an API's response — and is instantiated via Loom expressions. Instead of writing static text, the content author writes a template with {placeholders} that resolve against live data at runtime.

This guide builds progressively: simple substitution first, then aggregation, then repeated iteration, then filtering.

Prerequisite: Your foundation must declare a content handler. The cv-loom template is a complete working example. See Content Handlers for the setup.


The Setup

Four things connect a section to its data:

1. Records in records/, reached by a query — every file in records/profile/ is a @/profile record:

# queries.yml — a query names a schema; the site's records of that schema are its rows
profile:
  schema: '@/profile'

2. The query, named in page.yml:

# page.yml
title: Curriculum Vitae
query: profile

3. The key the handler reads, declared in the foundation's main.js:

// src/main.js
export default {
  handlers: createLoomHandlers({ vars: (data) => data?.profile?.[0] }),
  data: { profile: {} },   // every section receives `profile`
}

A section receives only the data keys its component declares, and a content handler runs for every section — so the keys a handler reads are declared once, for all of them, in main.js. Without this line {first_name} stays unresolved. See Content Handlers.

4. Loom expressions in the markdown:

---
type: Header
---
# {first_name} {family_name}
## {role}
{title} -- {affiliation}

The profile query delivers the site's records of @/profile — here, the one file in records/profile/ — under the profile key. The content handler resolves {first_name} against that record before the component sees the content.


Simple Substitution

Any {field_name} in the markdown is replaced with the corresponding value from the data. Given this data:

# records/profile/darwin.yml
first_name: Charles
family_name: Darwin
role: Naturalist
affiliation: Down House, Downe, Kent

This markdown:

# {first_name} {family_name}
## {role}
Based at **{affiliation}**.

Produces content equivalent to:

# Charles Darwin
## Naturalist
Based at **Down House, Downe, Kent**.

The component receives this as ordinary parsed content — content.title is "Charles Darwin", content.subtitle is "Naturalist". It doesn't know Loom was involved.


Aggregation

Loom expressions can count, total, and average over arrays in the data:

**{COUNT OF publications}** published works, including
{COUNT OF publications WHERE type = 'book'} books and
{COUNT OF publications WHERE type = 'article-journal'} journal articles.

Total funding: £{TOTAL OF funding.amount}
(average £{AVERAGE OF funding.amount} per grant).

With 17 publications and 5 grants totalling £1,730, this renders as:

17 published works, including 14 books and 3 journal articles. Total funding: £1,730 (average £346 per grant).

Aggregate expressions work anywhere in the markdown — titles, paragraphs, list items. The WHERE clause filters which items are counted.

Other aggregation forms: {SHOW research_areas JOINED BY ', '} concatenates a list into a comma-separated string. {SHOW publications.title WHERE year > 1870 JOINED BY ', '} filters and joins a specific field.


The Repeat Pattern

When a section needs to iterate over a data array — education entries, publications, grants — use the source frontmatter param and --- dividers to split the markdown into regions:

---
type: CvEntry
source: education
---
# Education
{COUNT OF education} degrees.
---
## {degree}
{institution} -- {field} ({start}--{end})

The three regions are:

  1. Header (before the first ---): rendered once, with expressions resolved against the full data. Good for headings and summary statistics.
  2. Body (between dividers): repeated once per item in the source array. {degree}, {institution}, etc. resolve against each item.
  3. Footer (after the second ---): rendered once, against the full data. Optional — omit the second divider if you don't need it.

Given two education entries, the component receives content with a title ("Education"), a paragraph ("2 degrees."), and two items — each with its own title and paragraph.

The funding section adds a total at the bottom:

---
type: CvEntry
source: funding
---
# Research Funding
{COUNT OF funding} grants.
---
## {title}
{agency} -- £{amount} ({year})
---
**Total: £{TOTAL OF funding.amount}**

Filtering with where

Add where in frontmatter to iterate over a subset of the source array:

---
type: CvEntry
source: publications
where: "type = 'book'"
---
# Books ({COUNT OF publications})
---
**{title}** ({year}), {publisher}

Only publications where type equals 'book' are iterated. The {COUNT OF publications} in the header reflects the filtered set — it counts books, not all publications.

where expressions use Loom Plain form:

ExpressionMeaning
type = 'book'Equality
year > 1870Comparison
refereedTruthy check
type = 'book' AND refereedBoolean combination

Data Conventions

A few conventions make expressions work predictably:

  • Years as numbers. Write year: 1859, not year: '1859'. Loom skips locale grouping for 4-digit integers, so 1859 renders as "1859" not "1,859". Comparisons like WHERE year > 1870 work naturally.
  • Money as numbers. Write amount: 1000, not amount: '£1,000'. This lets TOTAL OF and AVERAGE OF aggregate correctly — Loom adds locale grouping to the output.
  • Pre-sorted lists. Loom iterates in array order. Sort your data in the YAML file the way you want it displayed. Reverse chronological is common for academic CVs.
  • Dot notation for nested fields. {funding.0.amount} accesses the first funding item's amount. {TOTAL OF funding.amount} maps over all items.

Putting It Together

The cv-loom template demonstrates the full pattern — a complete academic CV driven by a single YAML profile. Create one with:

npx uniweb@latest create --template cv-loom

The template includes: a profile record (records/profile/darwin.yml), a foundation with createLoomHandlers and data: { profile: {} }, and sections for education, employment, publications, funding, teaching, service, and awards — each using the header/body/footer pattern with aggregation and filtering.


See Also