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-loomtemplate 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: profile3. 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, KentThis 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:
- Header (before the first
---): rendered once, with expressions resolved against the full data. Good for headings and summary statistics. - Body (between dividers): repeated once per item in the
sourcearray.{degree},{institution}, etc. resolve against each item. - 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.
A footer example
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:
| Expression | Meaning |
|---|---|
type = 'book' | Equality |
year > 1870 | Comparison |
refereed | Truthy check |
type = 'book' AND refereed | Boolean combination |
Data Conventions
A few conventions make expressions work predictably:
- Years as numbers. Write
year: 1859, notyear: '1859'. Loom skips locale grouping for 4-digit integers, so 1859 renders as "1859" not "1,859". Comparisons likeWHERE year > 1870work naturally. - Money as numbers. Write
amount: 1000, notamount: '£1,000'. This letsTOTAL OFandAVERAGE OFaggregate 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-loomThe 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
- Content Handlers -- Setting up the transform layer in main.js
- Working with Data -- Queries, and how their records reach a section
- Records -- Records in YAML, JSON and markdown